Skip to content

Commit 3dd2e6c

Browse files
authored
Merge pull request #43 from moassaad/feature-function-calling
Feature / Function Calling
2 parents eeaeaa5 + 218ea3f commit 3dd2e6c

File tree

6 files changed

+318
-3
lines changed

6 files changed

+318
-3
lines changed

src/DeepSeekClient.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
use DeepSeek\Enums\Requests\QueryFlags;
1414
use DeepSeek\Enums\Configs\TemperatureValues;
1515
use DeepSeek\Traits\Resources\{HasChat, HasCoder};
16+
use DeepSeek\Traits\Client\HasToolsFunctionCalling;
1617

1718
class DeepSeekClient implements ClientContract
1819
{
1920
use HasChat, HasCoder;
21+
use HasToolsFunctionCalling;
2022

2123
/**
2224
* PSR-18 HTTP client for making requests.
@@ -58,6 +60,12 @@ class DeepSeekClient implements ClientContract
5860

5961
protected ?string $endpointSuffixes;
6062

63+
/**
64+
* Array of tools for using function calling.
65+
* @var array|null $tools
66+
*/
67+
protected ?array $tools;
68+
6169
/**
6270
* Initialize the DeepSeekClient with a PSR-compliant HTTP client.
6371
*
@@ -71,6 +79,7 @@ public function __construct(ClientInterface $httpClient)
7179
$this->requestMethod = 'POST';
7280
$this->endpointSuffixes = EndpointSuffixes::CHAT->value;
7381
$this->temperature = (float) TemperatureValues::GENERAL_CONVERSATION->value;
82+
$this->tools = null;
7483
}
7584

7685
public function run(): string
@@ -80,9 +89,9 @@ public function run(): string
8089
QueryFlags::MODEL->value => $this->model,
8190
QueryFlags::STREAM->value => $this->stream,
8291
QueryFlags::TEMPERATURE->value => $this->temperature,
92+
QueryFlags::TOOLS->value => $this->tools,
8393
];
84-
// Clear queries after sending
85-
$this->queries = [];
94+
8695
$this->setResult((new Resource($this->httpClient, $this->endpointSuffixes))->sendRequest($requestData, $this->requestMethod));
8796
return $this->getResult()->getContent();
8897
}
@@ -120,6 +129,17 @@ public function query(string $content, ?string $role = "user"): self
120129
$this->queries[] = $this->buildQuery($content, $role);
121130
return $this;
122131
}
132+
133+
/**
134+
* Reset a queries list to empty.
135+
*
136+
* @return self The current instance for method chaining.
137+
*/
138+
public function resetQueries()
139+
{
140+
$this->queries = [];
141+
return $this;
142+
}
123143

124144
/**
125145
* get list of available models .
@@ -173,7 +193,7 @@ public function buildQuery(string $content, ?string $role = null): array
173193

174194
/**
175195
* set result model
176-
* @param \DeepseekPhp\Contracts\Models\ResultContract $result
196+
* @param \DeepSeek\Contracts\Models\ResultContract $result
177197
* @return self The current instance for method chaining.
178198
*/
179199
public function setResult(ResultContract $result)

src/Enums/Queries/QueryRoles.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ enum QueryRoles: string
66
{
77
case USER = 'user';
88
case SYSTEM = 'system';
9+
case ASSISTANT = 'assistant';
10+
case TOOL = 'tool';
911
}

src/Enums/Requests/QueryFlags.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ enum QueryFlags: string
88
case MODEL = 'model';
99
case STREAM = 'stream';
1010
case TEMPERATURE = 'temperature';
11+
case TOOLS = 'tools';
1112
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace DeepSeek\Traits\Client;
4+
5+
use DeepSeek\Enums\Queries\QueryRoles;
6+
7+
trait HasToolsFunctionCalling
8+
{
9+
/**
10+
* @param array $tools A list of tools the model may call.
11+
* @return self The current instance for method chaining.
12+
*/
13+
public function setTools(array $tools): self
14+
{
15+
$this->tools = $tools;
16+
return $this;
17+
}
18+
19+
/**
20+
* Add a query tool calls to the accumulated queries list.
21+
*
22+
* @param array $toolCalls The tool calls generated by the model, such as function calls.
23+
* @param string $content
24+
* @param string|null $role
25+
* @return self The current instance for method chaining.
26+
*/
27+
public function queryToolCall(array $toolCalls, string $content, ?string $role = null): self
28+
{
29+
$this->queries[] = $this->buildToolCallQuery($toolCalls, $content, $role);
30+
return $this;
31+
}
32+
33+
public function buildToolCallQuery(array $toolCalls, string $content, ?string $role = null): array
34+
{
35+
$query = [
36+
'role' => $role ?: QueryRoles::ASSISTANT->value,
37+
'tool_calls' => $toolCalls,
38+
'content' => $content,
39+
];
40+
return $query;
41+
}
42+
43+
/**
44+
* Add a query tool to the accumulated queries list.
45+
*
46+
* @param string $toolCallId
47+
* @param string $content
48+
* @param string|null $role
49+
* @return self The current instance for method chaining.
50+
*/
51+
public function queryTool(string $toolCallId, string $content , ?string $role = null): self
52+
{
53+
$this->queries[] = $this->buildToolQuery($toolCallId, $content, $role);
54+
return $this;
55+
}
56+
57+
public function buildToolQuery(string $toolCallId, string $content, ?string $role): array
58+
{
59+
$query = [
60+
'role' => $role ?: QueryRoles::TOOL->value,
61+
'tool_call_id' => $toolCallId,
62+
'content' => $content,
63+
];
64+
return $query;
65+
}
66+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Tests\Feature\ClientDependency;
4+
5+
class FakeResponse
6+
{
7+
public function toolFunctionCalling()
8+
{
9+
return <<<responseAPI
10+
{
11+
"id": "930c60df-bf64-41c9-a88e-3ec75f81e00e",
12+
"choices": [
13+
{
14+
"finish_reason": "stop",
15+
"index": 0,
16+
"message": {
17+
"content": "What is the weather like in Cairo?",
18+
"tool_calls": [
19+
{
20+
"id": "930c60df-3ec75f81e00e",
21+
"type": "function",
22+
"function": {
23+
"name": "get_weather",
24+
"arguments": {"city": "Cairo"}
25+
}
26+
}
27+
],
28+
"role": "assistant"
29+
}
30+
}
31+
],
32+
"created": 1705651092,
33+
"model": "deepseek-chat",
34+
"object": "chat.completion",
35+
"usage": {
36+
"completion_tokens": 10,
37+
"prompt_tokens": 16,
38+
"total_tokens": 26
39+
}
40+
}
41+
responseAPI;
42+
}
43+
public function resultToolFunctionCalling()
44+
{
45+
return <<<responseAPI
46+
{
47+
"id": "930c60df-bf64-41c9-a88e-3ec75f81e00e",
48+
"choices": [
49+
{
50+
"finish_reason": "stop",
51+
"index": 0,
52+
"message": {
53+
"content": "The weather in Cairo is sunny with a temperature of 22 degrees.",
54+
"role": "assistant"
55+
}
56+
}
57+
],
58+
"created": 1705651092,
59+
"model": "deepseek-chat",
60+
"object": "chat.completion",
61+
"usage": {
62+
"completion_tokens": 10,
63+
"prompt_tokens": 16,
64+
"total_tokens": 26
65+
}
66+
}
67+
responseAPI;
68+
}
69+
}

tests/Feature/FunctionCallingTest.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
namespace Tests\Feature;
3+
4+
use DeepSeek\DeepSeekClient;
5+
use DeepSeek\Enums\Requests\HTTPState;
6+
use Mockery;
7+
use Mockery\{LegacyMockInterface,MockInterface};
8+
use Tests\Feature\ClientDependency\FakeResponse;
9+
10+
11+
function get_weather($city)
12+
{
13+
$city = strtolower($city);
14+
$city = match($city){
15+
"cairo" => ["temperature"=> 22, "condition" => "Sunny"],
16+
"gharbia" => ["temperature"=> 23, "condition" => "Sunny"],
17+
"sharkia" => ["temperature"=> 24, "condition" => "Sunny"],
18+
"beheira" => ["temperature"=> 21, "condition" => "Sunny"],
19+
default => "not found city name."
20+
};
21+
return json_encode($city);
22+
}
23+
24+
test('Test function calling with fake responses.', function () {
25+
// Arrange
26+
$fake = new FakeResponse();
27+
28+
/** @var DeepSeekClient&LegacyMockInterface&MockInterface */
29+
$mockClient = Mockery::mock(DeepSeekClient::class);
30+
31+
$mockClient->shouldReceive('build')->andReturn($mockClient);
32+
$mockClient->shouldReceive('setTools')->andReturn($mockClient);
33+
$mockClient->shouldReceive('query')->andReturn($mockClient);
34+
$mockClient->shouldReceive('run')->once()->andReturn($fake->toolFunctionCalling());
35+
36+
// Act
37+
$response = $mockClient::build('your-api-key')
38+
->query('What is the weather like in Cairo?')
39+
->setTools([
40+
[
41+
"type" => "function",
42+
"function" => [
43+
"name" => "get_weather",
44+
"description" => "Get the current weather in a given city",
45+
"parameters" => [
46+
"type" => "object",
47+
"properties" => [
48+
"city" => [
49+
"type" => "string",
50+
"description" => "The city name",
51+
],
52+
],
53+
"required" => ["city"],
54+
],
55+
],
56+
],
57+
]
58+
)->run();
59+
60+
// Assert
61+
expect($fake->toolFunctionCalling())->toEqual($response);
62+
63+
//------------------------------------------
64+
65+
// Arrange
66+
$response = json_decode($response, true);
67+
$message = $response['choices'][0]['message'];
68+
69+
$firstFunction = $message['tool_calls'][0];
70+
if ($firstFunction['function']['name'] == "get_weather")
71+
{
72+
$weather_data = get_weather($firstFunction['function']['arguments']['city']);
73+
}
74+
75+
$mockClient->shouldReceive('queryCallTool')->andReturn($mockClient);
76+
$mockClient->shouldReceive('queryTool')->andReturn($mockClient);
77+
$mockClient->shouldReceive('run')->andReturn($fake->resultToolFunctionCalling());
78+
79+
// Act
80+
$response2 = $mockClient->queryCallTool(
81+
$message['tool_calls'],
82+
$message['content'],
83+
$message['role']
84+
)->queryTool(
85+
$firstFunction['id'],
86+
$weather_data,
87+
'tool'
88+
)->run();
89+
90+
// Assert
91+
expect($fake->resultToolFunctionCalling())->toEqual($response2);
92+
});
93+
94+
test('Test function calling use base data with real responses.', function () {
95+
// Arrange
96+
$client = DeepSeekClient::build('your-api-key')
97+
->query('What is the weather like in Cairo?')
98+
->setTools([
99+
[
100+
"type" => "function",
101+
"function" => [
102+
"name" => "get_weather",
103+
"description" => "Get the current weather in a given city",
104+
"parameters" => [
105+
"type" => "object",
106+
"properties" => [
107+
"city" => [
108+
"type" => "string",
109+
"description" => "The city name",
110+
],
111+
],
112+
"required" => ["city"],
113+
],
114+
],
115+
],
116+
]
117+
);
118+
119+
// Act
120+
$response = $client->run();
121+
$result = $client->getResult();
122+
123+
// Assert
124+
expect($response)->not()->toBeEmpty($response)
125+
->and($result->getStatusCode())->toEqual(HTTPState::OK->value);
126+
127+
//-----------------------------------------------------------------
128+
129+
// Arrange
130+
$response = json_decode($response, true);
131+
132+
$message = $response['choices'][0]['message'];
133+
$firstFunction = $message['tool_calls'][0];
134+
if ($firstFunction['function']['name'] == "get_weather")
135+
{
136+
$args = json_decode($firstFunction['function']['arguments'], true);
137+
$weather_data = get_weather($args['city']);
138+
}
139+
140+
$client2 = $client->queryToolCall(
141+
$message['tool_calls'],
142+
$message['content'],
143+
$message['role']
144+
)->queryTool(
145+
$firstFunction['id'],
146+
$weather_data,
147+
'tool'
148+
);
149+
150+
// Act
151+
$response2 = $client2->run();
152+
$result2 = $client2->getResult();
153+
154+
// Assert
155+
expect($response2)->not()->toBeEmpty($response2)
156+
->and($result2->getStatusCode())->toEqual(HTTPState::OK->value);
157+
});

0 commit comments

Comments
 (0)