From ea8faeebda21892eb32d66cda23b8b80ac308549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81my=20DECOOL?= Date: Wed, 5 Nov 2025 22:56:39 +0100 Subject: [PATCH 1/3] Add tool support --- .gitignore | 1 + examples/chat-with-tools.php | 81 ++++++++++++++++++++++++++++++ src/Client/Message.php | 35 ++++++++++++- src/Client/Request/ChatRequest.php | 27 ++++++++++ src/Client/Tool.php | 30 +++++++++++ src/Client/ToolCall.php | 25 +++++++++ src/Client/ToolCallFunction.php | 28 +++++++++++ src/Client/ToolFunction.php | 31 ++++++++++++ 8 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 examples/chat-with-tools.php create mode 100644 src/Client/Tool.php create mode 100644 src/Client/ToolCall.php create mode 100644 src/Client/ToolCallFunction.php create mode 100644 src/Client/ToolFunction.php diff --git a/.gitignore b/.gitignore index 28e760b..48bc965 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ .php-cs-fixer.cache +.phpunit.result.cache composer.lock diff --git a/examples/chat-with-tools.php b/examples/chat-with-tools.php new file mode 100644 index 0000000..a3eb048 --- /dev/null +++ b/examples/chat-with-tools.php @@ -0,0 +1,81 @@ +create(); + +$request = new ChatRequest( + model: 'llama3.1', + messages: $messages = [ + new Message('user', 'What is the weather in San Francisco?'), + ], + tools: [ + new Tool( + type: Tool::TYPE_FUNCTION, + function: new ToolFunction( + name: 'get_current_weather', + description: 'Get the current weather for a location', + parameters: [ + 'type' => 'object', + 'properties' => [ + 'location' => [ + 'type' => 'string', + 'description' => 'The city and state, e.g., San Francisco, CA', + ], + 'unit' => [ + 'type' => 'string', + 'enum' => ['celsius', 'fahrenheit'], + 'description' => 'The unit of temperature', + ], + ], + 'required' => ['location'], + ], + ), + ), + ], +); + +$response = $client->chat($request); +var_dump($response); + +// Check if the model wants to use a tool +if ($response->message->toolCalls !== null) { + foreach ($response->message->toolCalls as $toolCall) { + echo "Function: {$toolCall->function->name}\n"; + echo "Arguments: " . json_encode($toolCall->function->arguments, JSON_PRETTY_PRINT) . "\n"; + + // Simulate executing the function + $functionResult = match($toolCall->function->name) { + 'get_current_weather' => json_encode([ + 'temperature' => 72, + 'unit' => $toolCall->function->arguments['unit'] ?? 'fahrenheit', + 'condition' => 'sunny', + ]), + default => json_encode(['error' => 'Unknown function']), + }; + + // Add assistant message with tool calls + $messages[] = $response->message; + + // Add tool response message + $messages[] = new Message( + role: 'tool', + content: (string) $functionResult, + ); + } + + // Send the conversation back with tool results + $finalRequest = new ChatRequest( + model: 'llama3.1', + messages: $messages, + ); + + var_dump($client->chat($finalRequest)); +} diff --git a/src/Client/Message.php b/src/Client/Message.php index 1c2d2e0..c31dc91 100644 --- a/src/Client/Message.php +++ b/src/Client/Message.php @@ -6,13 +6,46 @@ class Message extends Request { public static function fromArray(array $data): self { - return new self($data['role'], $data['content'], /* $data['images'] */); + $toolCalls = null; + if (isset($data['tool_calls'])) { + $toolCalls = array_map( + fn (array $toolCall) => ToolCall::fromArray($toolCall), + $data['tool_calls'] + ); + } + + return new self( + role: $data['role'], + content: $data['content'] ?? '', + toolCalls: $toolCalls, + ); } + /** + * @param ToolCall[]|null $toolCalls + */ public function __construct( public readonly string $role, public readonly string $content, + public readonly ?array $toolCalls = null, // public readonly array $images = [], ) { } + + public function toArray(): array + { + $data = [ + 'role' => $this->role, + 'content' => $this->content, + ]; + + if ($this->toolCalls !== null) { + $data['tool_calls'] = array_map( + fn (ToolCall $toolCall) => $toolCall->toArray(), + $this->toolCalls + ); + } + + return $data; + } } diff --git a/src/Client/Request/ChatRequest.php b/src/Client/Request/ChatRequest.php index 597d750..b6f43b6 100644 --- a/src/Client/Request/ChatRequest.php +++ b/src/Client/Request/ChatRequest.php @@ -3,6 +3,7 @@ namespace JDecool\OllamaClient\Client\Request; use JDecool\OllamaClient\Client\Request; +use JDecool\OllamaClient\Client\Tool; class ChatRequest extends Request { @@ -11,14 +12,40 @@ public static function fromArray(array $data): self return new self( model: $data['model'], messages: $data['messages'] ?? [], + tools: isset($data['tools']) ? array_map(Tool::fromArray(...), $data['tools']) : null, format: $data['format'] ?? null, ); } + /** + * @param Tool[]|null $tools + */ public function __construct( public readonly string $model, public readonly array $messages = [], + public readonly ?array $tools = null, public readonly ?string $format = null, ) { } + + public function toArray(): array + { + $data = [ + 'model' => $this->model, + 'messages' => $this->messages, + ]; + + if ($this->tools !== null) { + $data['tools'] = array_map( + static fn (Tool $tool) => $tool->toArray(), + $this->tools + ); + } + + if ($this->format !== null) { + $data['format'] = $this->format; + } + + return $data; + } } diff --git a/src/Client/Tool.php b/src/Client/Tool.php new file mode 100644 index 0000000..447dcb3 --- /dev/null +++ b/src/Client/Tool.php @@ -0,0 +1,30 @@ + $this->type, + 'function' => $this->function->toArray(), + ]; + } +} diff --git a/src/Client/ToolCall.php b/src/Client/ToolCall.php new file mode 100644 index 0000000..ec803df --- /dev/null +++ b/src/Client/ToolCall.php @@ -0,0 +1,25 @@ + $this->function->toArray(), + ]; + } +} diff --git a/src/Client/ToolCallFunction.php b/src/Client/ToolCallFunction.php new file mode 100644 index 0000000..2b844ef --- /dev/null +++ b/src/Client/ToolCallFunction.php @@ -0,0 +1,28 @@ + $this->name, + 'arguments' => $this->arguments, + ]; + } +} diff --git a/src/Client/ToolFunction.php b/src/Client/ToolFunction.php new file mode 100644 index 0000000..e8daa14 --- /dev/null +++ b/src/Client/ToolFunction.php @@ -0,0 +1,31 @@ + $this->name, + 'description' => $this->description, + 'parameters' => $this->parameters, + ]; + } +} From 38406dead775d1c72103973855a364e45b3f5211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81my=20DECOOL?= Date: Wed, 5 Nov 2025 22:57:03 +0100 Subject: [PATCH 2/3] Fix phpstan.neon configuration deprecation --- phpstan.neon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 4c4617a..cf7b17e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,5 +4,5 @@ parameters: - examples - src - tests - - checkMissingIterableValueType: false + ignoreErrors: + - identifier: missingType.iterableValue From 3627d34b3cda479602f548086ce13d104799e1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81my=20DECOOL?= Date: Wed, 5 Nov 2025 22:57:35 +0100 Subject: [PATCH 3/3] Fix PHP deprecation --- src/Http.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http.php b/src/Http.php index 5af2038..e48049c 100644 --- a/src/Http.php +++ b/src/Http.php @@ -15,7 +15,7 @@ public function __construct( ) { } - public function request(string $method, string $uri, string $body = null): string + public function request(string $method, string $uri, ?string $body = null): string { $response = $this->executeRequest($method, $uri, $body); @@ -30,7 +30,7 @@ public function request(string $method, string $uri, string $body = null): strin /** * @return Generator */ - public function stream(string $method, string $uri, string $body = null): Generator + public function stream(string $method, string $uri, ?string $body = null): Generator { $response = $this->executeRequest($method, $uri, $body); @@ -49,7 +49,7 @@ public function stream(string $method, string $uri, string $body = null): Genera } } - private function executeRequest(string $method, string $uri, string $body = null): ResponseInterface + private function executeRequest(string $method, string $uri, ?string $body = null): ResponseInterface { $this->logger->debug('HTTP Request: {method} {uri}', [ 'method' => $method,