Skip to content

Feature Function Calling #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/DeepSeekClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 .
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/Enums/Queries/QueryRoles.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ enum QueryRoles: string
{
case USER = 'user';
case SYSTEM = 'system';
case ASSISTANT = 'assistant';
case TOOL = 'tool';
}
1 change: 1 addition & 0 deletions src/Enums/Requests/QueryFlags.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum QueryFlags: string
case MODEL = 'model';
case STREAM = 'stream';
case TEMPERATURE = 'temperature';
case TOOLS = 'tools';
}
66 changes: 66 additions & 0 deletions src/Traits/Client/HasToolsFunctionCalling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace DeepSeek\Traits\Client;

use DeepSeek\Enums\Queries\QueryRoles;

trait HasToolsFunctionCalling
{
/**
* @param array $tools A list of tools the model may call.
* @return self The current instance for method chaining.
*/
public function setTools(array $tools): self
{
$this->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;
}
}
69 changes: 69 additions & 0 deletions tests/Feature/ClientDependency/FakeResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Tests\Feature\ClientDependency;

class FakeResponse
{
public function toolFunctionCalling()
{
return <<<responseAPI
{
"id": "930c60df-bf64-41c9-a88e-3ec75f81e00e",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "What is the weather like in Cairo?",
"tool_calls": [
{
"id": "930c60df-3ec75f81e00e",
"type": "function",
"function": {
"name": "get_weather",
"arguments": {"city": "Cairo"}
}
}
],
"role": "assistant"
}
}
],
"created": 1705651092,
"model": "deepseek-chat",
"object": "chat.completion",
"usage": {
"completion_tokens": 10,
"prompt_tokens": 16,
"total_tokens": 26
}
}
responseAPI;
}
public function resultToolFunctionCalling()
{
return <<<responseAPI
{
"id": "930c60df-bf64-41c9-a88e-3ec75f81e00e",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "The weather in Cairo is sunny with a temperature of 22 degrees.",
"role": "assistant"
}
}
],
"created": 1705651092,
"model": "deepseek-chat",
"object": "chat.completion",
"usage": {
"completion_tokens": 10,
"prompt_tokens": 16,
"total_tokens": 26
}
}
responseAPI;
}
}
157 changes: 157 additions & 0 deletions tests/Feature/FunctionCallingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php
namespace Tests\Feature;

use DeepSeek\DeepSeekClient;
use DeepSeek\Enums\Requests\HTTPState;
use Mockery;
use Mockery\{LegacyMockInterface,MockInterface};
use Tests\Feature\ClientDependency\FakeResponse;


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);
}

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);
});