diff --git a/README-AR.md b/README-AR.md index 8b6ce37..83163f9 100644 --- a/README-AR.md +++ b/README-AR.md @@ -1,4 +1,3 @@ -
🚀 حزمة SDK لـ PHP مدعومة من المجتمع لتكامل واجهة برمجة التطبيقات الذكية DeepSeek
@@ -30,6 +29,7 @@ - [التكوين المتقدم](#التكوين-المتقدم) - [الاستخدام مع عميل HTTP من Symfony](#الاستخدام-مع-عميل-http-من-symfony) - [الحصول على قائمة النماذج](#الحصول-على-قائمة-النماذج) + - [استدعاء الدوال](#استدعاء-الدوال) - [تكامل مع الأطر](#-تكامل-مع-الأطر) - [🆕 دليل الترحيل](#-دليل-الترحيل) - [📝 سجل التغييرات](#-سجل-التغييرات) @@ -97,6 +97,7 @@ $response = $client ->withModel(Models::CODER->value) ->withStream() ->withTemperature(1.2) + ->query('Explain quantum computing in simple terms') ->run(); echo 'API Response:'.$response; @@ -129,6 +130,17 @@ $response = DeepSeekClient::build('your-api-key') echo $response; // {"object":"list","data":[{"id":"deepseek-chat","object":"model","owned_by":"deepseek"},{"id":"deepseek-reasoner","object":"model","owned_by":"deepseek"}]} ``` +### استدعاء الدوال + +يتيح **استدعاء الدوال** للنموذج استدعاء أدوات خارجية لتعزيز قدراته. +يمكنك الرجوع إلى الوثائق الخاصة باستدعاء الدوال في الملف: +[FUNCTION-CALLING.md](docs/FUNCTION-CALLING.md) + +--- + +هل ترغب في أن أضع النسخ الثلاث (الإنجليزية + العربية + الصينية) ضمن ملف Markdown موحد؟ + + ### 🛠 تكامل مع الأطر ### [حزمة Deepseek لـ Laravel](https://github.com/deepseek-php/deepseek-laravel) diff --git a/README-CN.md b/README-CN.md index 8977cf9..df859b4 100644 --- a/README-CN.md +++ b/README-CN.md @@ -25,11 +25,12 @@ - [✨ 特性](#-特性) - [📦 安装](#-安装) - [🚀 快速入门](#-快速入门) - - [基本用法](#基本用法) - - [高级配置](#advanced-configuration) - - [Use with Symfony HttpClient](#use-with-symfony-httpclient) - - [获取模型列表](#获取模型列表) - - [框架集成](#-框架集成) + - [基本用法](#基本用法) + - [高级配置](#高级配置) + - [使用 Symfony HttpClient](#使用-symfony-httpclient) + - [获取模型列表](#获取模型列表) + - [函数调用](#函数调用) + - [框架集成](#-框架集成) - [🆕 迁移指南](#-迁移指南) - [📝 更新日志](#-更新日志) - [🧪 测试](#-测试) @@ -95,6 +96,7 @@ $response = $client ->withModel(Models::CODER->value) ->withStream() ->withTemperature(1.2) + ->query('Explain quantum computing in simple terms') ->run(); echo 'API Response:'.$response; @@ -127,11 +129,32 @@ $response = DeepSeekClient::build('your-api-key') echo $response; // {"object":"list","data":[{"id":"deepseek-chat","object":"model","owned_by":"deepseek"},{"id":"deepseek-reasoner","object":"model","owned_by":"deepseek"}]} ``` +### 函数调用 + +**函数调用**允许模型调用外部工具以增强其功能。 +你可以在文档中查看有关函数调用的详细信息: +[FUNCTION-CALLING.md](docs/FUNCTION-CALLING.md) + + ### 🛠 框架集成 ### [Laravel Deepseek Package](https://github.com/deepseek-php/deepseek-laravel) +# 🐘✨ **DeepSeek PHP Community** ✨🐘 + +Click the button bellow or [join here](https://t.me/deepseek_php_community) to be part of our growing community! + +[](https://t.me/deepseek_php_community) + +### **Channel Structure** 🏗️ +- 🗨️ **General** - Daily chatter +- 💡 **Ideas & Suggestions** - Shape the community's future +- 📢 **Announcements & News** - Official updates & news +- 🚀 **Releases & Updates** - Version tracking & migration support +- 🐞 **Issues & Bug Reports** - Collective problem-solving +- 🤝 **Pull Requests** - Code collaboration & reviews + --- ## 🚧 迁移指南 diff --git a/README.md b/README.md index 1ec6776..7ed8c3e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - [Advanced Configuration](#advanced-configuration) - [Use with Symfony HttpClient](#use-with-symfony-httpclient) - [Get Models List](#get-models-list) + - [Function Calling](#function-calling) - [Framework Integration](#-framework-integration) - [🆕 Migration Guide](#-migration-guide) - [📝 Changelog](#-changelog) @@ -95,7 +96,8 @@ $client = DeepSeekClient::build(apiKey:'your-api-key', baseUrl:'https://api.deep $response = $client ->withModel(Models::CODER->value) ->withStream() - ->withTemperature(1.2) + ->setTemperature(1.2) + ->query('Explain quantum computing in simple terms') ->run(); echo 'API Response:'.$response; @@ -128,6 +130,14 @@ $response = DeepSeekClient::build('your-api-key') echo $response; // {"object":"list","data":[{"id":"deepseek-chat","object":"model","owned_by":"deepseek"},{"id":"deepseek-reasoner","object":"model","owned_by":"deepseek"}]} ``` + +### Function Calling + +Function Calling allows the model to call external tools to enhance its capabilities.[[1]](https://api-docs.deepseek.com/guides/function_calling) + +You Can check the documentation for function calling in [FUNCTION-CALLING.md](docs/FUNCTION-CALLING.md) + + ### 🛠 Framework Integration ### [Laravel Deepseek Package](https://github.com/deepseek-php/deepseek-laravel) diff --git a/composer.json b/composer.json index b3d0459..5f03fd6 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "role": "creator" } ], - "version": "2.0.3", + "version": "2.0.4", "require": { "php": "^8.1.0", "nyholm/psr7": "^1.8", diff --git a/docs/FUNCTION-CALLING.md b/docs/FUNCTION-CALLING.md new file mode 100644 index 0000000..4d115cf --- /dev/null +++ b/docs/FUNCTION-CALLING.md @@ -0,0 +1,192 @@ +## Function Calling + +Function Calling allows the model to call external tools to enhance its capabilities.[[1]](https://api-docs.deepseek.com/guides/function_calling) + +#### 1. Define the tools used by the model and pass them with each message passed to the model, Receive query messages from the end user and pass them to the model with the defined tools. +- example function `get_weather($city)`. +```php +function get_weather($city) +{ + $city = strtolower($city); + $city = match($city){ + "cairo" => ["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); +} +``` +The user requests the weather in Cairo. +```php +$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"], + ], + ], + ], + ] +); + +$response = $client->run(); + +``` + +Output response like. +```json +{ + "id": "chat_12345", + "object": "chat.completion", + "created": 1677654321, + "model": "deepseek-chat", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_12345", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"city\": \"Cairo\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ] +} +``` + +#### 2. Receive the response and check if it has called one or more tools to execute it in the system ,And execute the tool called by the model. +The deepseek api responds to the system and requests the execution of the tool responsible for fetching the weather status. +```php + +$response = $client->run(); + +$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']); +} + +``` + +#### 3. Coordinate the results and send the previous response with the results of the executed tools. +Formats the response, and sends it back to the form. +```php +$response2 = $client->queryToolCall( + $message['tool_calls'], + $message['content'], + $message['role'] + )->queryTool( + $firstFunction['id'], + $weather_data +); +``` + +Request like +```json +{ + "messages": [ + { + "role": "user", + "content": "What is the weather like in Cairo?" + }, + { + "content": "What is the weather like in Cairo?", + "tool_calls": [ + { + "id": "930c60df-3ec75f81e00e", + "type": "function", + "function": { + "name": "get_weather", + "arguments": { + "city": "Cairo" + } + } + } + ], + "role": "assistant" + }, + { + "role": "tool", + "tool_call_id": "930c60df-3ec75f81e00e", + "content": "{\"temperature\":22,\"condition\":\"Sunny\"}" + } + ], + "model": "deepseek-chat", + "stream": false, + "temperature": 1.3, + "tools": [ + { + "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" + ] + } + } + } + ] +} +``` + +#### 4. Receive the final response from the model and pass it to the end user. +The deepseek api responds with the final response, which is the weather status according to the data passed to it in the example. +```php + +$response2 = $response2->run(); +echo $response2; +``` +Output response like :- +```json +{ + "id": "chat_67890", + "object": "chat.completion", + "created": 1677654322, + "model": "deepseek-chat", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The weather in Cairo is 22℃." + }, + "finish_reason": "stop" + } + ] +} +``` + 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); +});