Skip to content

Commit 4fbbb14

Browse files
committed
feat(platform): Ollama structured ouput support
1 parent 11ef98f commit 4fbbb14

File tree

4 files changed

+142
-13
lines changed

4 files changed

+142
-13
lines changed
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\StructuredOutput\AgentProcessor;
14+
use Symfony\AI\Fixtures\StructuredOutput\MathReasoning;
15+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
16+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
17+
use Symfony\AI\Platform\Message\Message;
18+
use Symfony\AI\Platform\Message\MessageBag;
19+
20+
require_once dirname(__DIR__).'/bootstrap.php';
21+
22+
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client());
23+
$model = new Ollama();
24+
25+
$processor = new AgentProcessor();
26+
$agent = new Agent($platform, $model, [$processor], [$processor], logger());
27+
$messages = new MessageBag(
28+
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
29+
Message::ofUser('how can I solve 8x + 7 = -23'),
30+
);
31+
$result = $agent->call($messages, ['output_structure' => MathReasoning::class]);
32+
33+
dump($result->getContent());

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Ollama extends Model
4545
'/./' => [
4646
Capability::INPUT_MESSAGES,
4747
Capability::OUTPUT_TEXT,
48+
Capability::OUTPUT_STRUCTURED,
4849
],
4950
'/^llama\D*3(\D*\d+)/' => [
5051
Capability::TOOL_CALLING,

src/platform/src/Bridge/Ollama/OllamaClient.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,33 @@ public function request(Model $model, array|string $payload, array $options = []
5858
* @param array<string|int, mixed> $payload
5959
* @param array<string, mixed> $options
6060
*/
61-
private function doCompletionRequest(array|string $payload, array $options = []): RawHttpResult
61+
public function doEmbeddingsRequest(Model $model, array|string $payload, array $options = []): RawHttpResult
6262
{
63-
// Revert Ollama's default streaming behavior
64-
$options['stream'] ??= false;
65-
66-
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/chat', $this->hostUrl), [
67-
'headers' => ['Content-Type' => 'application/json'],
68-
'json' => array_merge($options, $payload),
63+
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/embed', $this->hostUrl), [
64+
'json' => array_merge($options, [
65+
'model' => $model->getName(),
66+
'input' => $payload,
67+
]),
6968
]));
7069
}
7170

7271
/**
7372
* @param array<string|int, mixed> $payload
7473
* @param array<string, mixed> $options
7574
*/
76-
public function doEmbeddingsRequest(Model $model, array|string $payload, array $options = []): RawHttpResult
75+
private function doCompletionRequest(array|string $payload, array $options = []): RawHttpResult
7776
{
78-
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/embed', $this->hostUrl), [
79-
'json' => array_merge($options, [
80-
'model' => $model->getName(),
81-
'input' => $payload,
82-
]),
77+
// Revert Ollama's default streaming behavior
78+
$options['stream'] ??= false;
79+
80+
if (\array_key_exists('response_format', $options) && \array_key_exists('json_schema', $options['response_format'])) {
81+
$options['format'] = $options['response_format']['json_schema']['schema'];
82+
unset($options['response_format']);
83+
}
84+
85+
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/chat', $this->hostUrl), [
86+
'headers' => ['Content-Type' => 'application/json'],
87+
'json' => array_merge($options, $payload),
8388
]));
8489
}
8590
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 Bridge\Ollama;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\UsesClass;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
18+
use Symfony\AI\Platform\Bridge\Ollama\OllamaClient;
19+
use Symfony\AI\Platform\Model;
20+
use Symfony\Component\HttpClient\MockHttpClient;
21+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
22+
23+
#[CoversClass(OllamaClient::class)]
24+
#[UsesClass(Ollama::class)]
25+
#[UsesClass(Model::class)]
26+
final class OllamaClientTest extends TestCase
27+
{
28+
public function testSupportsModel()
29+
{
30+
$client = new OllamaClient(new MockHttpClient(), 'http://localhost:1234');
31+
32+
$this->assertTrue($client->supports(new Ollama()));
33+
$this->assertFalse($client->supports(new Model('any-model')));
34+
}
35+
36+
public function testOutputStructureIsSupported()
37+
{
38+
$httpClient = new MockHttpClient([
39+
new JsonMockResponse([
40+
'capabilities' => ['completion', 'tools'],
41+
]),
42+
new JsonMockResponse([
43+
'model' => 'foo',
44+
'response' => [
45+
'age' => 22,
46+
'available' => true,
47+
],
48+
'done' => true,
49+
]),
50+
], 'http://127.0.0.1:1234');
51+
52+
$client = new OllamaClient($httpClient, 'http://127.0.0.1:1234');
53+
$response = $client->request(new Ollama(), [
54+
'messages' => [
55+
[
56+
'role' => 'user',
57+
'content' => 'Ollama is 22 years old and is busy saving the world. Respond using JSON',
58+
],
59+
],
60+
'model' => 'llama3.2',
61+
], [
62+
'response_format' => [
63+
'type' => 'json_schema',
64+
'json_schema' => [
65+
'name' => 'clock',
66+
'strict' => true,
67+
'schema' => [
68+
'type' => 'object',
69+
'properties' => [
70+
'age' => ['type' => 'integer'],
71+
'available' => ['type' => 'boolean'],
72+
],
73+
'required' => ['age', 'available'],
74+
'additionalProperties' => false,
75+
],
76+
],
77+
],
78+
]);
79+
80+
$this->assertSame(2, $httpClient->getRequestsCount());
81+
$this->assertSame([
82+
'model' => 'foo',
83+
'response' => [
84+
'age' => 22,
85+
'available' => true,
86+
],
87+
'done' => true,
88+
], $response->getData());
89+
}
90+
}

0 commit comments

Comments
 (0)