diff --git a/src/DeepSeekClient.php b/src/DeepSeekClient.php index a8d7f01..93c989d 100644 --- a/src/DeepSeekClient.php +++ b/src/DeepSeekClient.php @@ -13,10 +13,12 @@ use DeepSeek\Enums\Requests\QueryFlags; use DeepSeek\Enums\Configs\TemperatureValues; use DeepSeek\Traits\Resources\{HasChat, HasCoder}; +use DeepSeek\Traits\Client\HasToolsFunctionCalling; class DeepSeekClient implements ClientContract { use HasChat, HasCoder; + use HasToolsFunctionCalling; /** * PSR-18 HTTP client for making requests. @@ -58,6 +60,12 @@ class DeepSeekClient implements ClientContract protected ?string $endpointSuffixes; + /** + * Array of tools for using function calling. + * @var array|null $tools + */ + protected ?array $tools; + /** * Initialize the DeepSeekClient with a PSR-compliant HTTP client. * @@ -71,6 +79,7 @@ public function __construct(ClientInterface $httpClient) $this->requestMethod = 'POST'; $this->endpointSuffixes = EndpointSuffixes::CHAT->value; $this->temperature = (float) TemperatureValues::GENERAL_CONVERSATION->value; + $this->tools = null; } public function run(): string @@ -80,9 +89,9 @@ public function run(): string QueryFlags::MODEL->value => $this->model, QueryFlags::STREAM->value => $this->stream, QueryFlags::TEMPERATURE->value => $this->temperature, + QueryFlags::TOOLS->value => $this->tools, ]; - // Clear queries after sending - $this->queries = []; + $this->setResult((new Resource($this->httpClient, $this->endpointSuffixes))->sendRequest($requestData, $this->requestMethod)); return $this->getResult()->getContent(); } @@ -120,6 +129,17 @@ public function query(string $content, ?string $role = "user"): self $this->queries[] = $this->buildQuery($content, $role); return $this; } + + /** + * Reset a queries list to empty. + * + * @return self The current instance for method chaining. + */ + public function resetQueries() + { + $this->queries = []; + return $this; + } /** * get list of available models . @@ -173,7 +193,7 @@ public function buildQuery(string $content, ?string $role = null): array /** * set result model - * @param \DeepseekPhp\Contracts\Models\ResultContract $result + * @param \DeepSeek\Contracts\Models\ResultContract $result * @return self The current instance for method chaining. */ public function setResult(ResultContract $result) diff --git a/src/Enums/Queries/QueryRoles.php b/src/Enums/Queries/QueryRoles.php index 4659084..a23925c 100644 --- a/src/Enums/Queries/QueryRoles.php +++ b/src/Enums/Queries/QueryRoles.php @@ -6,4 +6,6 @@ enum QueryRoles: string { case USER = 'user'; case SYSTEM = 'system'; + case ASSISTANT = 'assistant'; + case TOOL = 'tool'; } diff --git a/src/Enums/Requests/QueryFlags.php b/src/Enums/Requests/QueryFlags.php index 19c1cdd..0e2b49a 100644 --- a/src/Enums/Requests/QueryFlags.php +++ b/src/Enums/Requests/QueryFlags.php @@ -8,4 +8,5 @@ enum QueryFlags: string case MODEL = 'model'; case STREAM = 'stream'; case TEMPERATURE = 'temperature'; + case TOOLS = 'tools'; } diff --git a/src/Traits/Client/HasToolsFunctionCalling.php b/src/Traits/Client/HasToolsFunctionCalling.php new file mode 100644 index 0000000..97d53fc --- /dev/null +++ b/src/Traits/Client/HasToolsFunctionCalling.php @@ -0,0 +1,66 @@ +tools = $tools; + return $this; + } + + /** + * Add a query tool calls to the accumulated queries list. + * + * @param array $toolCalls The tool calls generated by the model, such as function calls. + * @param string $content + * @param string|null $role + * @return self The current instance for method chaining. + */ + public function queryToolCall(array $toolCalls, string $content, ?string $role = null): self + { + $this->queries[] = $this->buildToolCallQuery($toolCalls, $content, $role); + return $this; + } + + public function buildToolCallQuery(array $toolCalls, string $content, ?string $role = null): array + { + $query = [ + 'role' => $role ?: QueryRoles::ASSISTANT->value, + 'tool_calls' => $toolCalls, + 'content' => $content, + ]; + return $query; + } + + /** + * Add a query tool to the accumulated queries list. + * + * @param string $toolCallId + * @param string $content + * @param string|null $role + * @return self The current instance for method chaining. + */ + public function queryTool(string $toolCallId, string $content , ?string $role = null): self + { + $this->queries[] = $this->buildToolQuery($toolCallId, $content, $role); + return $this; + } + + public function buildToolQuery(string $toolCallId, string $content, ?string $role): array + { + $query = [ + 'role' => $role ?: QueryRoles::TOOL->value, + 'tool_call_id' => $toolCallId, + 'content' => $content, + ]; + return $query; + } +} diff --git a/tests/Feature/ClientDependency/FakeResponse.php b/tests/Feature/ClientDependency/FakeResponse.php new file mode 100644 index 0000000..3b1b725 --- /dev/null +++ b/tests/Feature/ClientDependency/FakeResponse.php @@ -0,0 +1,69 @@ + ["temperature"=> 22, "condition" => "Sunny"], + "gharbia" => ["temperature"=> 23, "condition" => "Sunny"], + "sharkia" => ["temperature"=> 24, "condition" => "Sunny"], + "beheira" => ["temperature"=> 21, "condition" => "Sunny"], + default => "not found city name." + }; + return json_encode($city); +} + +test('Test function calling with fake responses.', function () { + // Arrange + $fake = new FakeResponse(); + + /** @var DeepSeekClient&LegacyMockInterface&MockInterface */ + $mockClient = Mockery::mock(DeepSeekClient::class); + + $mockClient->shouldReceive('build')->andReturn($mockClient); + $mockClient->shouldReceive('setTools')->andReturn($mockClient); + $mockClient->shouldReceive('query')->andReturn($mockClient); + $mockClient->shouldReceive('run')->once()->andReturn($fake->toolFunctionCalling()); + + // Act + $response = $mockClient::build('your-api-key') + ->query('What is the weather like in Cairo?') + ->setTools([ + [ + "type" => "function", + "function" => [ + "name" => "get_weather", + "description" => "Get the current weather in a given city", + "parameters" => [ + "type" => "object", + "properties" => [ + "city" => [ + "type" => "string", + "description" => "The city name", + ], + ], + "required" => ["city"], + ], + ], + ], + ] + )->run(); + + // Assert + expect($fake->toolFunctionCalling())->toEqual($response); + + //------------------------------------------ + + // Arrange + $response = json_decode($response, true); + $message = $response['choices'][0]['message']; + + $firstFunction = $message['tool_calls'][0]; + if ($firstFunction['function']['name'] == "get_weather") + { + $weather_data = get_weather($firstFunction['function']['arguments']['city']); + } + + $mockClient->shouldReceive('queryCallTool')->andReturn($mockClient); + $mockClient->shouldReceive('queryTool')->andReturn($mockClient); + $mockClient->shouldReceive('run')->andReturn($fake->resultToolFunctionCalling()); + + // Act + $response2 = $mockClient->queryCallTool( + $message['tool_calls'], + $message['content'], + $message['role'] + )->queryTool( + $firstFunction['id'], + $weather_data, + 'tool' + )->run(); + + // Assert + expect($fake->resultToolFunctionCalling())->toEqual($response2); +}); + +test('Test function calling use base data with real responses.', function () { + // Arrange + $client = DeepSeekClient::build('your-api-key') + ->query('What is the weather like in Cairo?') + ->setTools([ + [ + "type" => "function", + "function" => [ + "name" => "get_weather", + "description" => "Get the current weather in a given city", + "parameters" => [ + "type" => "object", + "properties" => [ + "city" => [ + "type" => "string", + "description" => "The city name", + ], + ], + "required" => ["city"], + ], + ], + ], + ] + ); + + // Act + $response = $client->run(); + $result = $client->getResult(); + + // Assert + expect($response)->not()->toBeEmpty($response) + ->and($result->getStatusCode())->toEqual(HTTPState::OK->value); + + //----------------------------------------------------------------- + + // Arrange + $response = json_decode($response, true); + + $message = $response['choices'][0]['message']; + $firstFunction = $message['tool_calls'][0]; + if ($firstFunction['function']['name'] == "get_weather") + { + $args = json_decode($firstFunction['function']['arguments'], true); + $weather_data = get_weather($args['city']); + } + + $client2 = $client->queryToolCall( + $message['tool_calls'], + $message['content'], + $message['role'] + )->queryTool( + $firstFunction['id'], + $weather_data, + 'tool' + ); + + // Act + $response2 = $client2->run(); + $result2 = $client2->getResult(); + + // Assert + expect($response2)->not()->toBeEmpty($response2) + ->and($result2->getStatusCode())->toEqual(HTTPState::OK->value); +});