Skip to content

Commit edf2f33

Browse files
committed
feature #211 [Platform] Add tooling support for ollama to allow agent and toolbox usage (JoshuaBehrens)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Add tooling support for ollama to allow agent and toolbox usage | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes? | Docs? | no | License | MIT Running a toolbox agent with ollama was not yet supported. This ensures, that tool messages and responses work with a locally hosted ollama instance. It seems not to be a fix as it was not meant to work and it is not really a mentionable feature as one could expect it to work like the others so I am not really sure to work where to add more content for humans. I tested it with the given new example in `examples/ollama/toolcall.php`. A current design decision had me struggling a bit. I was not yet able to solve in a good way the fact, that ollama itself cannot claim every model to have tool support. So someone who uses a model needs to know before its usage whether it has model support. This can be queried easily but the capabilities are part of the model, that you need to create before using the platform to query in the name of the model. So this is like hen-egg. If the model itself would not know its capabilities but there is a service, that has model and platform at hand, could evaluate it (and cache it). Is this something we want to establish? Possible query: ```shell curl http://localhost:11434/api/chat -d '{ "model": "llama3:8b-instruct-q6_K", "tools": [{}], "stream": false }' {"error":"llama3:8b-instruct-q6_K does not support tools"} ``` Commits ------- 8201f6b [Platform] Add tooling support for ollama to allow agent and toolbox usage
2 parents f1aa4e8 + 8201f6b commit edf2f33

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)