Skip to content

Commit 5ec9334

Browse files
feat: Add comprehensive Ollama integration with chat and embedding support (#385)
## Summary This PR adds comprehensive Ollama integration to AgentScope, enabling users to leverage local Ollama models with full feature support. ## Features Implemented ### Core Chat Features: - **OllamaOptions**: Complete configuration class supporting 40+ Ollama parameters including advanced model loading options (GPU offloading, memory management) and runtime generation parameters (sampling strategies, penalties) - **OllamaHttpClient**: HTTP client for Ollama API with sync/async and streaming support, proper JSON serialization/deserialization and error handling - **OllamaChatModel**: Full ChatModel implementation with tool usage, streaming, timeout/retry mechanisms, and seamless integration with AgentScope framework ### Message Formatting System: - **OllamaChatFormatter**: Standard chat formatting with proper role mapping - **OllamaMultiAgentFormatter**: Multi-agent conversation formatting with history management - **OllamaMessageConverter**: Message conversion between AgentScope and Ollama formats - **OllamaConversationMerger**: Multi-agent conversation merging with history tags - **OllamaToolsHelper**: Tool configuration and usage support - **OllamaMediaConverter**: Media and image handling for multimodal models ### Text Embedding Features: - **OllamaTextEmbedding**: Complete text embedding implementation with support for Ollama's embedding API - Includes proper error handling, timeout/retry mechanisms, and dimension validation - Supports reactive programming with Project Reactor's Mono ### API DTOs: - Complete set of data transfer objects for Ollama API requests and responses - Support for messages, tools, tool calls, embeddings, and complex conversation structures ## Design Decisions - Used adapter pattern instead of inheritance for OllamaOptions to avoid coupling with generic options - Implemented flexible message formatting system supporting both standard and multi-agent scenarios - Added Chain of Thought (CoT) support through thinking options - Optimized single message handling to avoid unnecessary conversation history wrapping - Proper reactive programming support with timeout and retry mechanisms ## Testing - Complete test coverage for all Ollama components - Unit tests for message formatting, conversion, and API communication - Integration tests for end-to-end functionality - Tests for both chat and embedding functionality ## Additional Notes This integration provides a complete solution for using Ollama models within the AgentScope ecosystem with full feature parity.
1 parent 7616103 commit 5ec9334

42 files changed

Lines changed: 10300 additions & 10 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agentscope-core/src/main/java/io/agentscope/core/formatter/ollama/OllamaChatFormatter.java

Lines changed: 474 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.formatter.ollama;
17+
18+
import io.agentscope.core.formatter.ollama.dto.OllamaMessage;
19+
import io.agentscope.core.message.Base64Source;
20+
import io.agentscope.core.message.ContentBlock;
21+
import io.agentscope.core.message.ImageBlock;
22+
import io.agentscope.core.message.Msg;
23+
import io.agentscope.core.message.TextBlock;
24+
import io.agentscope.core.message.ToolResultBlock;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.function.Function;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Merges multi-agent conversation messages for Ollama API.
33+
*
34+
*/
35+
public class OllamaConversationMerger {
36+
private static final Logger log = LoggerFactory.getLogger(OllamaConversationMerger.class);
37+
private static final String HISTORY_START_TAG = "<history>";
38+
private static final String HISTORY_END_TAG = "</history>";
39+
40+
private final String conversationHistoryPrompt;
41+
42+
public OllamaConversationMerger(String conversationHistoryPrompt) {
43+
this.conversationHistoryPrompt = conversationHistoryPrompt;
44+
}
45+
46+
public OllamaMessage mergeToMessage(
47+
List<Msg> msgs,
48+
Function<Msg, String> nameExtractor,
49+
Function<List<ContentBlock>, String> toolResultConverter,
50+
String historyPrompt) {
51+
52+
StringBuilder textAccumulator = new StringBuilder();
53+
if (historyPrompt != null && !historyPrompt.isEmpty()) {
54+
textAccumulator.append(historyPrompt);
55+
}
56+
textAccumulator.append(HISTORY_START_TAG).append("\n");
57+
58+
List<String> images = new ArrayList<>();
59+
60+
for (Msg msg : msgs) {
61+
String name = nameExtractor.apply(msg);
62+
63+
for (ContentBlock block : msg.getContent()) {
64+
if (block instanceof TextBlock) {
65+
textAccumulator
66+
.append(name)
67+
.append(": ")
68+
.append(((TextBlock) block).getText())
69+
.append("\n");
70+
} else if (block instanceof ImageBlock) {
71+
ImageBlock imageBlock = (ImageBlock) block;
72+
if (imageBlock.getSource() instanceof Base64Source) {
73+
Base64Source source = (Base64Source) imageBlock.getSource();
74+
images.add(source.getData());
75+
textAccumulator.append(name).append(": [Image]\n");
76+
} else {
77+
log.warn(
78+
"URL image source not yet supported for Ollama, skipping image"
79+
+ " block in merger");
80+
textAccumulator.append(name).append(": [Image - processing failed]\n");
81+
}
82+
} else if (block instanceof ToolResultBlock) {
83+
// Tool results in history are usually just appended as text
84+
ToolResultBlock toolResult = (ToolResultBlock) block;
85+
86+
// Simplify: Just append tool result string.
87+
Object output = toolResult.getOutput();
88+
String resultText =
89+
output instanceof String ? (String) output : String.valueOf(output);
90+
91+
textAccumulator
92+
.append(name)
93+
.append(" (")
94+
.append(toolResult.getName())
95+
.append("): ")
96+
.append(resultText)
97+
.append("\n");
98+
}
99+
}
100+
}
101+
102+
textAccumulator.append(HISTORY_END_TAG);
103+
104+
OllamaMessage message = new OllamaMessage();
105+
message.setRole("user"); // Merged history is typically treated as user input context
106+
message.setContent(textAccumulator.toString());
107+
if (!images.isEmpty()) {
108+
message.setImages(images);
109+
}
110+
111+
return message;
112+
}
113+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.formatter.ollama;
17+
18+
import io.agentscope.core.formatter.MediaUtils;
19+
import io.agentscope.core.message.Base64Source;
20+
import io.agentscope.core.message.ImageBlock;
21+
import io.agentscope.core.message.Source;
22+
import io.agentscope.core.message.URLSource;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
/**
27+
* Handles media content conversion for Ollama API.
28+
* Converts ImageBlock to Base64 strings as required by Ollama.
29+
*
30+
*/
31+
public class OllamaMediaConverter {
32+
33+
private static final Logger log = LoggerFactory.getLogger(OllamaMediaConverter.class);
34+
35+
/**
36+
* Convert ImageBlock to Base64 string for Ollama API.
37+
*
38+
* <p>Ollama API expects an array of base64-encoded strings for images.
39+
* This method handles:
40+
* <ul>
41+
* <li>Base64 sources → Returns raw base64 data</li>
42+
* <li>Local file URLs → Reads file and converts to base64</li>
43+
* <li>Remote URLs → Downloads content and converts to base64</li>
44+
* </ul>
45+
*
46+
* @param imageBlock The image block to convert
47+
* @return Base64 encoded string of the image
48+
* @throws Exception If conversion, file reading, or download fails
49+
*/
50+
public String convertImageBlockToBase64(ImageBlock imageBlock) throws Exception {
51+
Source source = imageBlock.getSource();
52+
53+
if (source instanceof Base64Source base64Source) {
54+
// Ollama expects raw base64 string without data URI prefix
55+
return base64Source.getData();
56+
} else if (source instanceof URLSource urlSource) {
57+
String url = urlSource.getUrl();
58+
MediaUtils.validateImageExtension(url);
59+
60+
if (MediaUtils.isLocalFile(url)) {
61+
// Read local file to base64
62+
// Remove file:// prefix if present for local path
63+
String path = url.startsWith("file://") ? url.substring(7) : url;
64+
return MediaUtils.fileToBase64(path);
65+
} else {
66+
// Download remote URL to base64
67+
return MediaUtils.downloadUrlToBase64(url);
68+
}
69+
} else {
70+
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)