diff --git a/examples/ollama/structured-output-math.php b/examples/ollama/structured-output-math.php new file mode 100644 index 00000000..02003e4f --- /dev/null +++ b/examples/ollama/structured-output-math.php @@ -0,0 +1,33 @@ + + * + * 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\StructuredOutput\AgentProcessor; +use Symfony\AI\Fixtures\StructuredOutput\MathReasoning; +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(); + +$processor = new AgentProcessor(); +$agent = new Agent($platform, $model, [$processor], [$processor], logger()); +$messages = new MessageBag( + Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'), + Message::ofUser('how can I solve 8x + 7 = -23'), +); +$result = $agent->call($messages, ['output_structure' => MathReasoning::class]); + +dump($result->getContent()); diff --git a/src/platform/src/Bridge/Ollama/Ollama.php b/src/platform/src/Bridge/Ollama/Ollama.php index cca4c464..c31eef3d 100644 --- a/src/platform/src/Bridge/Ollama/Ollama.php +++ b/src/platform/src/Bridge/Ollama/Ollama.php @@ -45,6 +45,7 @@ class Ollama extends Model '/./' => [ Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, + Capability::OUTPUT_STRUCTURED, ], '/^llama\D*3(\D*\d+)/' => [ Capability::TOOL_CALLING, diff --git a/src/platform/src/Bridge/Ollama/OllamaClient.php b/src/platform/src/Bridge/Ollama/OllamaClient.php index 9fdf7b43..147a6e56 100644 --- a/src/platform/src/Bridge/Ollama/OllamaClient.php +++ b/src/platform/src/Bridge/Ollama/OllamaClient.php @@ -58,14 +58,13 @@ public function request(Model $model, array|string $payload, array $options = [] * @param array $payload * @param array $options */ - private function doCompletionRequest(array|string $payload, array $options = []): RawHttpResult + public function doEmbeddingsRequest(Model $model, array|string $payload, array $options = []): RawHttpResult { - // Revert Ollama's default streaming behavior - $options['stream'] ??= false; - - return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/chat', $this->hostUrl), [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => array_merge($options, $payload), + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/embed', $this->hostUrl), [ + 'json' => array_merge($options, [ + 'model' => $model->getName(), + 'input' => $payload, + ]), ])); } @@ -73,13 +72,19 @@ private function doCompletionRequest(array|string $payload, array $options = []) * @param array $payload * @param array $options */ - public function doEmbeddingsRequest(Model $model, array|string $payload, array $options = []): RawHttpResult + private function doCompletionRequest(array|string $payload, array $options = []): RawHttpResult { - return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/embed', $this->hostUrl), [ - 'json' => array_merge($options, [ - 'model' => $model->getName(), - 'input' => $payload, - ]), + // Revert Ollama's default streaming behavior + $options['stream'] ??= false; + + if (\array_key_exists('response_format', $options) && \array_key_exists('json_schema', $options['response_format'])) { + $options['format'] = $options['response_format']['json_schema']['schema']; + unset($options['response_format']); + } + + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/api/chat', $this->hostUrl), [ + 'headers' => ['Content-Type' => 'application/json'], + 'json' => array_merge($options, $payload), ])); } } diff --git a/src/platform/tests/Bridge/Ollama/OllamaClientTest.php b/src/platform/tests/Bridge/Ollama/OllamaClientTest.php new file mode 100644 index 00000000..2ddc1fb8 --- /dev/null +++ b/src/platform/tests/Bridge/Ollama/OllamaClientTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Bridge\Ollama; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Ollama\Ollama; +use Symfony\AI\Platform\Bridge\Ollama\OllamaClient; +use Symfony\AI\Platform\Model; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; + +#[CoversClass(OllamaClient::class)] +#[UsesClass(Ollama::class)] +#[UsesClass(Model::class)] +final class OllamaClientTest extends TestCase +{ + public function testSupportsModel() + { + $client = new OllamaClient(new MockHttpClient(), 'http://localhost:1234'); + + $this->assertTrue($client->supports(new Ollama())); + $this->assertFalse($client->supports(new Model('any-model'))); + } + + public function testOutputStructureIsSupported() + { + $httpClient = new MockHttpClient([ + new JsonMockResponse([ + 'capabilities' => ['completion', 'tools'], + ]), + new JsonMockResponse([ + 'model' => 'foo', + 'response' => [ + 'age' => 22, + 'available' => true, + ], + 'done' => true, + ]), + ], 'http://127.0.0.1:1234'); + + $client = new OllamaClient($httpClient, 'http://127.0.0.1:1234'); + $response = $client->request(new Ollama(), [ + 'messages' => [ + [ + 'role' => 'user', + 'content' => 'Ollama is 22 years old and is busy saving the world. Respond using JSON', + ], + ], + 'model' => 'llama3.2', + ], [ + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => [ + 'name' => 'clock', + 'strict' => true, + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'age' => ['type' => 'integer'], + 'available' => ['type' => 'boolean'], + ], + 'required' => ['age', 'available'], + 'additionalProperties' => false, + ], + ], + ], + ]); + + $this->assertSame(2, $httpClient->getRequestsCount()); + $this->assertSame([ + 'model' => 'foo', + 'response' => [ + 'age' => 22, + 'available' => true, + ], + 'done' => true, + ], $response->getData()); + } +}