Skip to content

Commit 8201f6b

Browse files
JoshuaBehrenschr-hertel
authored andcommitted
[Platform] Add tooling support for ollama to allow agent and toolbox usage
1 parent ab882c8 commit 8201f6b

File tree

12 files changed

+595
-9
lines changed

12 files changed

+595
-9
lines changed

examples/ollama/chat-llama.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
*/
1111

1212
use Symfony\AI\Agent\Agent;
13-
use Symfony\AI\Platform\Bridge\Meta\Llama;
13+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
1414
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
1515
use Symfony\AI\Platform\Message\Message;
1616
use Symfony\AI\Platform\Message\MessageBag;
1717

1818
require_once dirname(__DIR__).'/bootstrap.php';
1919

2020
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client());
21-
$model = new Llama('llama3.2');
21+
$model = new Ollama();
2222

2323
$agent = new Agent($platform, $model, logger: logger());
2424
$messages = new MessageBag(

examples/ollama/toolcall.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
14+
use Symfony\AI\Agent\Toolbox\Tool\Clock;
15+
use Symfony\AI\Agent\Toolbox\Toolbox;
16+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
17+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
18+
use Symfony\AI\Platform\Message\Message;
19+
use Symfony\AI\Platform\Message\MessageBag;
20+
21+
require_once dirname(__DIR__).'/bootstrap.php';
22+
23+
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client());
24+
$model = new Ollama();
25+
26+
$toolbox = new Toolbox([new Clock()], logger: logger());
27+
$processor = new AgentProcessor($toolbox);
28+
$agent = new Agent($platform, $model, [$processor], [$processor], logger());
29+
30+
$messages = new MessageBag(Message::ofUser('What time is it?'));
31+
$result = $agent->call($messages);
32+
33+
echo $result->getContent().\PHP_EOL;

src/platform/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ CHANGELOG
5757
* Add support for embeddings generation across multiple providers
5858
* Add response promises for async operations
5959
* Add InMemoryPlatform and InMemoryRawResult for testing Platform without external Providers calls
60+
* Add tool calling support for Ollama platform
6061

6162

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Ollama\Contract;
13+
14+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
15+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
16+
use Symfony\AI\Platform\Message\AssistantMessage;
17+
use Symfony\AI\Platform\Message\Role;
18+
use Symfony\AI\Platform\Model;
19+
use Symfony\AI\Platform\Result\ToolCall;
20+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
21+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
22+
23+
/**
24+
* @author Joshua Behrens <[email protected]>
25+
*/
26+
final class AssistantMessageNormalizer extends ModelContractNormalizer implements NormalizerAwareInterface
27+
{
28+
use NormalizerAwareTrait;
29+
30+
protected function supportedDataClass(): string
31+
{
32+
return AssistantMessage::class;
33+
}
34+
35+
protected function supportsModel(Model $model): bool
36+
{
37+
return $model instanceof Ollama;
38+
}
39+
40+
/**
41+
* @param AssistantMessage $data
42+
*
43+
* @return array{
44+
* role: Role::Assistant,
45+
* tool_calls: list<array{
46+
* type: 'function',
47+
* function: array{
48+
* name: string,
49+
* arguments: array<string, mixed>
50+
* }
51+
* }>
52+
* }
53+
*/
54+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
55+
{
56+
return [
57+
'role' => Role::Assistant,
58+
'tool_calls' => array_values(array_map(function (ToolCall $message): array {
59+
return [
60+
'type' => 'function',
61+
'function' => [
62+
'name' => $message->name,
63+
// stdClass forces empty object
64+
'arguments' => [] === $message->arguments ? new \stdClass() : $message->arguments,
65+
],
66+
];
67+
}, $data->toolCalls ?? [])),
68+
];
69+
}
70+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Ollama\Contract;
13+
14+
use Symfony\AI\Platform\Contract;
15+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
16+
17+
/**
18+
* @author Joshua Behrens <[email protected]>
19+
*/
20+
final readonly class OllamaContract extends Contract
21+
{
22+
public static function create(NormalizerInterface ...$normalizer): Contract
23+
{
24+
return parent::create(
25+
new AssistantMessageNormalizer(),
26+
...$normalizer,
27+
);
28+
}
29+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Ollama;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Model;
16+
17+
/**
18+
* @author Joshua Behrens <[email protected]>
19+
*/
20+
class Ollama extends Model
21+
{
22+
public const DEEPSEEK_R_1 = 'deepseek-r1';
23+
public const GEMMA_3_N = 'gemma3n';
24+
public const GEMMA_3 = 'gemma3';
25+
public const QWEN_3 = 'qwen3';
26+
public const QWEN_2_5_VL = 'qwen2.5vl';
27+
public const LLAMA_3_1 = 'llama3.1';
28+
public const LLAMA_3_2 = 'llama3.2';
29+
public const MISTRAL = 'mistral';
30+
public const QWEN_2_5 = 'qwen2.5';
31+
public const LLAMA_3 = 'llama3';
32+
public const LLAVA = 'llava';
33+
public const PHI_3 = 'phi3';
34+
public const GEMMA_2 = 'gemma2';
35+
public const QWEN_2_5_CODER = 'qwen2.5-coder';
36+
public const GEMMA = 'gemma';
37+
public const QWEN = 'qwen';
38+
public const QWEN_2 = 'qwen2';
39+
public const LLAMA_2 = 'llama2';
40+
41+
private const TOOL_PATTERNS = [
42+
'/./' => [
43+
Capability::INPUT_MESSAGES,
44+
Capability::OUTPUT_TEXT,
45+
],
46+
'/^llama\D*3(\D*\d+)/' => [
47+
Capability::TOOL_CALLING,
48+
],
49+
'/^qwen\d(\.\d)?(-coder)?$/' => [
50+
Capability::TOOL_CALLING,
51+
],
52+
'/^(deepseek|mistral)/' => [
53+
Capability::TOOL_CALLING,
54+
],
55+
];
56+
57+
/**
58+
* @param array<string, mixed> $options
59+
*/
60+
public function __construct(string $name = self::LLAMA_3_2, array $options = [])
61+
{
62+
$capabilities = [];
63+
64+
foreach (self::TOOL_PATTERNS as $pattern => $possibleCapabilities) {
65+
if (1 === preg_match($pattern, $name)) {
66+
foreach ($possibleCapabilities as $capability) {
67+
$capabilities[] = $capability;
68+
}
69+
}
70+
}
71+
72+
parent::__construct($name, $capabilities, $options);
73+
}
74+
}

src/platform/src/Bridge/Ollama/LlamaModelClient.php renamed to src/platform/src/Bridge/Ollama/OllamaModelClient.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Ollama;
1313

14-
use Symfony\AI\Platform\Bridge\Meta\Llama;
1514
use Symfony\AI\Platform\Model;
1615
use Symfony\AI\Platform\ModelClientInterface;
1716
use Symfony\AI\Platform\Result\RawHttpResult;
@@ -20,7 +19,7 @@
2019
/**
2120
* @author Christopher Hertel <[email protected]>
2221
*/
23-
final readonly class LlamaModelClient implements ModelClientInterface
22+
final readonly class OllamaModelClient implements ModelClientInterface
2423
{
2524
public function __construct(
2625
private HttpClientInterface $httpClient,
@@ -30,7 +29,7 @@ public function __construct(
3029

3130
public function supports(Model $model): bool
3231
{
33-
return $model instanceof Llama;
32+
return $model instanceof Ollama;
3433
}
3534

3635
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult

src/platform/src/Bridge/Ollama/LlamaResultConverter.php renamed to src/platform/src/Bridge/Ollama/OllamaResultConverter.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Ollama;
1313

14-
use Symfony\AI\Platform\Bridge\Meta\Llama;
1514
use Symfony\AI\Platform\Exception\RuntimeException;
1615
use Symfony\AI\Platform\Model;
1716
use Symfony\AI\Platform\Result\RawResultInterface;
1817
use Symfony\AI\Platform\Result\ResultInterface;
1918
use Symfony\AI\Platform\Result\TextResult;
19+
use Symfony\AI\Platform\Result\ToolCall;
20+
use Symfony\AI\Platform\Result\ToolCallResult;
2021
use Symfony\AI\Platform\ResultConverterInterface;
2122

2223
/**
2324
* @author Christopher Hertel <[email protected]>
2425
*/
25-
final readonly class LlamaResultConverter implements ResultConverterInterface
26+
final readonly class OllamaResultConverter implements ResultConverterInterface
2627
{
2728
public function supports(Model $model): bool
2829
{
29-
return $model instanceof Llama;
30+
return $model instanceof Ollama;
3031
}
3132

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

45+
$toolCalls = [];
46+
47+
foreach ($data['message']['tool_calls'] ?? [] as $id => $toolCall) {
48+
$toolCalls[] = new ToolCall($id, $toolCall['function']['name'], $toolCall['function']['arguments']);
49+
}
50+
51+
if ([] !== $toolCalls) {
52+
return new ToolCallResult(...$toolCalls);
53+
}
54+
4455
return new TextResult($data['message']['content']);
4556
}
4657
}

src/platform/src/Bridge/Ollama/PlatformFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Ollama;
1313

14+
use Symfony\AI\Platform\Bridge\Ollama\Contract\OllamaContract;
1415
use Symfony\AI\Platform\Contract;
1516
use Symfony\AI\Platform\Platform;
1617
use Symfony\Component\HttpClient\EventSourceHttpClient;
@@ -28,6 +29,6 @@ public static function create(
2829
): Platform {
2930
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
3031

31-
return new Platform([new LlamaModelClient($httpClient, $hostUrl)], [new LlamaResultConverter()], $contract);
32+
return new Platform([new OllamaModelClient($httpClient, $hostUrl)], [new OllamaResultConverter()], $contract ?? OllamaContract::create());
3233
}
3334
}

0 commit comments

Comments
 (0)