Skip to content

[Platform] Add tooling support for ollama to allow agent and toolbox usage #211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/ollama/chat-llama.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Platform\Bridge\Meta\Llama;
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
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());
$model = new Llama('llama3.2');
$model = new Ollama();

$agent = new Agent($platform, $model, logger: logger());
$messages = new MessageBag(
Expand Down
33 changes: 33 additions & 0 deletions examples/ollama/toolcall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Ollama;
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());
$model = new Ollama();

$toolbox = new Toolbox([new Clock()], logger: logger());
$processor = new AgentProcessor($toolbox);
$agent = new Agent($platform, $model, [$processor], [$processor], logger());

$messages = new MessageBag(Message::ofUser('What time is it?'));
$result = $agent->call($messages);

echo $result->getContent().\PHP_EOL;
1 change: 1 addition & 0 deletions src/platform/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ CHANGELOG
* Add support for embeddings generation across multiple providers
* Add response promises for async operations
* Add InMemoryPlatform and InMemoryRawResult for testing Platform without external Providers calls
* Add tool calling support for Ollama platform


Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Ollama\Contract;

use Symfony\AI\Platform\Bridge\Ollama\Ollama;
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
use Symfony\AI\Platform\Message\AssistantMessage;
use Symfony\AI\Platform\Message\Role;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\Result\ToolCall;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

/**
* @author Joshua Behrens <[email protected]>
*/
final class AssistantMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface
{
use NormalizerAwareTrait;

protected function supportedDataClass(): string
{
return AssistantMessage::class;
}

protected function supportsModel(Model $model): bool
{
return $model instanceof Ollama;
}

/**
* @param AssistantMessage $data
*
* @return array{
* role: Role::Assistant,
* tool_calls: list<array{
* type: 'function',
* function: array{
* name: string,
* arguments: array<string, mixed>
* }
* }>
* }
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
return [
'role' => Role::Assistant,
'tool_calls' => array_values(array_map(function (ToolCall $message): array {
return [
'type' => 'function',
'function' => [
'name' => $message->name,
// stdClass forces empty object
'arguments' => [] === $message->arguments ? new \stdClass() : $message->arguments,
],
];
}, $data->toolCalls ?? [])),
];
}
}
29 changes: 29 additions & 0 deletions src/platform/src/Bridge/Ollama/Contract/OllamaContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Ollama\Contract;

use Symfony\AI\Platform\Contract;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @author Joshua Behrens <[email protected]>
*/
final readonly class OllamaContract extends Contract
{
public static function create(NormalizerInterface ...$normalizer): Contract
{
return parent::create(
new AssistantMessageNormalizer(),
...$normalizer,
);
}
}
74 changes: 74 additions & 0 deletions src/platform/src/Bridge/Ollama/Ollama.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Ollama;

use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\Model;

/**
* @author Joshua Behrens <[email protected]>
*/
class Ollama extends Model
{
public const DEEPSEEK_R_1 = 'deepseek-r1';
public const GEMMA_3_N = 'gemma3n';
public const GEMMA_3 = 'gemma3';
public const QWEN_3 = 'qwen3';
public const QWEN_2_5_VL = 'qwen2.5vl';
public const LLAMA_3_1 = 'llama3.1';
public const LLAMA_3_2 = 'llama3.2';
public const MISTRAL = 'mistral';
public const QWEN_2_5 = 'qwen2.5';
public const LLAMA_3 = 'llama3';
public const LLAVA = 'llava';
public const PHI_3 = 'phi3';
public const GEMMA_2 = 'gemma2';
public const QWEN_2_5_CODER = 'qwen2.5-coder';
public const GEMMA = 'gemma';
public const QWEN = 'qwen';
public const QWEN_2 = 'qwen2';
public const LLAMA_2 = 'llama2';

private const TOOL_PATTERNS = [
'/./' => [
Capability::INPUT_MESSAGES,
Capability::OUTPUT_TEXT,
],
'/^llama\D*3(\D*\d+)/' => [
Capability::TOOL_CALLING,
],
'/^qwen\d(\.\d)?(-coder)?$/' => [
Capability::TOOL_CALLING,
],
'/^(deepseek|mistral)/' => [
Capability::TOOL_CALLING,
],
];

/**
* @param array<string, mixed> $options
*/
public function __construct(string $name = self::LLAMA_3_2, array $options = [])
{
$capabilities = [];

foreach (self::TOOL_PATTERNS as $pattern => $possibleCapabilities) {
if (1 === preg_match($pattern, $name)) {
foreach ($possibleCapabilities as $capability) {
$capabilities[] = $capability;
}
}
}

parent::__construct($name, $capabilities, $options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace Symfony\AI\Platform\Bridge\Ollama;

use Symfony\AI\Platform\Bridge\Meta\Llama;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\ModelClientInterface;
use Symfony\AI\Platform\Result\RawHttpResult;
Expand All @@ -20,7 +19,7 @@
/**
* @author Christopher Hertel <[email protected]>
*/
final readonly class LlamaModelClient implements ModelClientInterface
final readonly class OllamaModelClient implements ModelClientInterface
{
public function __construct(
private HttpClientInterface $httpClient,
Expand All @@ -30,7 +29,7 @@ public function __construct(

public function supports(Model $model): bool
{
return $model instanceof Llama;
return $model instanceof Ollama;
}

public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@

namespace Symfony\AI\Platform\Bridge\Ollama;

use Symfony\AI\Platform\Bridge\Meta\Llama;
use Symfony\AI\Platform\Exception\RuntimeException;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\Result\RawResultInterface;
use Symfony\AI\Platform\Result\ResultInterface;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\AI\Platform\Result\ToolCall;
use Symfony\AI\Platform\Result\ToolCallResult;
use Symfony\AI\Platform\ResultConverterInterface;

/**
* @author Christopher Hertel <[email protected]>
*/
final readonly class LlamaResultConverter implements ResultConverterInterface
final readonly class OllamaResultConverter implements ResultConverterInterface
{
public function supports(Model $model): bool
{
return $model instanceof Llama;
return $model instanceof Ollama;
}

public function convert(RawResultInterface $result, array $options = []): ResultInterface
Expand All @@ -41,6 +42,16 @@ public function convert(RawResultInterface $result, array $options = []): Result
throw new RuntimeException('Message does not contain content.');
}

$toolCalls = [];

foreach ($data['message']['tool_calls'] ?? [] as $id => $toolCall) {
$toolCalls[] = new ToolCall($id, $toolCall['function']['name'], $toolCall['function']['arguments']);
}

if ([] !== $toolCalls) {
return new ToolCallResult(...$toolCalls);
}

return new TextResult($data['message']['content']);
}
}
3 changes: 2 additions & 1 deletion src/platform/src/Bridge/Ollama/PlatformFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\AI\Platform\Bridge\Ollama;

use Symfony\AI\Platform\Bridge\Ollama\Contract\OllamaContract;
use Symfony\AI\Platform\Contract;
use Symfony\AI\Platform\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
Expand All @@ -28,6 +29,6 @@ public static function create(
): Platform {
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);

return new Platform([new LlamaModelClient($httpClient, $hostUrl)], [new LlamaResultConverter()], $contract);
return new Platform([new OllamaModelClient($httpClient, $hostUrl)], [new OllamaResultConverter()], $contract ?? OllamaContract::create());
}
}
Loading
Loading