From 6a9eb7056145e0cab915c2a1d7866e1304871d60 Mon Sep 17 00:00:00 2001 From: Dominic Wagner Date: Thu, 23 Oct 2025 15:27:22 +0200 Subject: [PATCH] [Platform] add ollama toolcall support for streaming --- examples/ollama/stream-toolcall.php | 36 +++++++++++++++ .../Bridge/Ollama/OllamaResultConverter.php | 44 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 examples/ollama/stream-toolcall.php diff --git a/examples/ollama/stream-toolcall.php b/examples/ollama/stream-toolcall.php new file mode 100644 index 000000000..da709829c --- /dev/null +++ b/examples/ollama/stream-toolcall.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\Toolbox\AgentProcessor; +use Symfony\AI\Agent\Toolbox\Tool\Clock; +use Symfony\AI\Agent\Toolbox\Toolbox; +use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client()); + +$toolbox = new Toolbox([new Clock()], logger: logger()); +$processor = new AgentProcessor($toolbox); +$agent = new Agent($platform, env('OLLAMA_LLM'), [$processor], [$processor]); + +$messages = new MessageBag(Message::ofUser('What time is it?')); + +$result = $agent->call($messages, ['stream' => true]); + +foreach ($result->getContent() as $chunk) { + echo $chunk->getContent(); +} + +echo \PHP_EOL; diff --git a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php index ba5b8f896..9870f7f4c 100644 --- a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php +++ b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php @@ -95,6 +95,7 @@ public function doConvertEmbeddings(array $data): ResultInterface private function convertStream(ResponseInterface $result): \Generator { + $toolCalls = []; foreach ((new EventSourceHttpClient())->stream($result) as $chunk) { if ($chunk instanceof FirstChunk || $chunk instanceof LastChunk) { continue; @@ -106,6 +107,14 @@ private function convertStream(ResponseInterface $result): \Generator throw new RuntimeException('Failed to decode JSON: '.$e->getMessage()); } + if ($this->streamIsToolCall($data)) { + $toolCalls = $this->convertStreamToToolCalls($toolCalls, $data); + } + + if ([] !== $toolCalls && $this->isToolCallsStreamFinished($data)) { + yield new ToolCallResult(...$toolCalls); + } + yield new OllamaMessageChunk( $data['model'], new \DateTimeImmutable($data['created_at']), @@ -114,4 +123,39 @@ private function convertStream(ResponseInterface $result): \Generator ); } } + + /** + * @param array $toolCalls + * @param array $data + * + * @return array + */ + private function convertStreamToToolCalls(array $toolCalls, array $data): array + { + if (!isset($data['message']['tool_calls'])) { + return $toolCalls; + } + + foreach ($data['message']['tool_calls'] ?? [] as $id => $toolCall) { + $toolCalls[] = new ToolCall($id, $toolCall['function']['name'], $toolCall['function']['arguments']); + } + + return $toolCalls; + } + + /** + * @param array $data + */ + private function streamIsToolCall(array $data): bool + { + return isset($data['message']['tool_calls']); + } + + /** + * @param array $data^ + */ + private function isToolCallsStreamFinished(array $data): bool + { + return isset($data['done']) && true === $data['done']; + } }