Skip to content

Commit 387fb33

Browse files
Merge pull request #2 from utopia-php/feat-deepseek-adapter
Feat deepseek adapter
2 parents 0a01f3e + cee55ac commit 387fb33

File tree

2 files changed

+293
-2
lines changed

2 files changed

+293
-2
lines changed

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP ve
2121

2222
## Features
2323

24-
- **Multiple AI Providers** - Support for OpenAI and Anthropic APIs
24+
- **Multiple AI Providers** - Support for OpenAI, Anthropic, and Deepseek APIs
2525
- **Flexible Message Types** - Support for text and structured content in messages
2626
- **Conversation Management** - Easy-to-use conversation handling between agents and users
27-
- **Model Selection** - Choose from various AI models (GPT-4, Claude 3, etc.)
27+
- **Model Selection** - Choose from various AI models (GPT-4, Claude 3, Deepseek Chat, etc.)
2828
- **Parameter Control** - Fine-tune model behavior with temperature and token controls
2929

3030
## Usage
@@ -93,6 +93,23 @@ Available Anthropic Models:
9393
- `MODEL_CLAUDE_3_HAIKU`: Fast and efficient
9494
- `MODEL_CLAUDE_2_1`: Previous generation
9595

96+
#### Deepseek
97+
98+
```php
99+
use Utopia\Agents\Adapters\Deepseek;
100+
101+
$deepseek = new Deepseek(
102+
apiKey: 'your-api-key',
103+
model: Deepseek::MODEL_DEEPSEEK_CHAT,
104+
maxTokens: 2048,
105+
temperature: 0.7
106+
);
107+
```
108+
109+
Available Deepseek Models:
110+
- `MODEL_DEEPSEEK_CHAT`: General-purpose chat model
111+
- `MODEL_DEEPSEEK_CODER`: Specialized for code-related tasks
112+
96113
### Managing Conversations
97114

98115
```php

src/Agents/Adapters/Deepseek.php

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
3+
namespace Utopia\Agents\Adapters;
4+
5+
use Utopia\Agents\Adapter;
6+
use Utopia\Agents\Message;
7+
use Utopia\Agents\Messages\Text;
8+
use Utopia\Fetch\Chunk;
9+
use Utopia\Fetch\Client;
10+
11+
class Deepseek extends Adapter
12+
{
13+
/**
14+
* Deepseek-Chat - Most powerful model
15+
*/
16+
public const MODEL_DEEPSEEK_CHAT = 'deepseek-chat';
17+
18+
/**
19+
* Deepseek-Coder - Specialized for code
20+
*/
21+
public const MODEL_DEEPSEEK_CODER = 'deepseek-coder';
22+
23+
/**
24+
* @var string
25+
*/
26+
protected string $apiKey;
27+
28+
/**
29+
* @var string
30+
*/
31+
protected string $model;
32+
33+
/**
34+
* @var int
35+
*/
36+
protected int $maxTokens;
37+
38+
/**
39+
* @var float
40+
*/
41+
protected float $temperature;
42+
43+
/**
44+
* Create a new Deepseek adapter
45+
*
46+
* @param string $apiKey
47+
* @param string $model
48+
* @param int $maxTokens
49+
* @param float $temperature
50+
*
51+
* @throws \Exception
52+
*/
53+
public function __construct(
54+
string $apiKey,
55+
string $model = self::MODEL_DEEPSEEK_CHAT,
56+
int $maxTokens = 1024,
57+
float $temperature = 1.0
58+
) {
59+
$this->apiKey = $apiKey;
60+
$this->maxTokens = $maxTokens;
61+
$this->temperature = $temperature;
62+
$this->setModel($model);
63+
}
64+
65+
/**
66+
* Send a message to the Deepseek API
67+
*
68+
* @param array<array<string, mixed>> $messages
69+
* @param callable|null $listener
70+
* @return Message
71+
*
72+
* @throws \Exception
73+
*/
74+
public function send(array $messages, ?callable $listener = null): Message
75+
{
76+
if ($this->getAgent() === null) {
77+
throw new \Exception('Agent not set');
78+
}
79+
80+
$client = new \Utopia\Fetch\Client();
81+
$client
82+
->setTimeout(90)
83+
->addHeader('authorization', 'Bearer ' . $this->apiKey)
84+
->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
85+
86+
$formattedMessages = [];
87+
foreach ($messages as $message) {
88+
if (!isset($message['role']) || !isset($message['content'])) {
89+
throw new \Exception('Invalid message format');
90+
}
91+
$formattedMessages[] = [
92+
'role' => $message['role'],
93+
'content' => $message['content'],
94+
];
95+
}
96+
97+
$instructions = [];
98+
foreach ($this->getAgent()->getInstructions() as $name => $content) {
99+
$instructions[] = "# " . $name . "\n\n" . $content;
100+
}
101+
102+
$systemMessage = $this->getAgent()->getDescription() .
103+
(empty($instructions) ? '' : "\n\n" . implode("\n\n", $instructions));
104+
105+
if (!empty($systemMessage)) {
106+
array_unshift($formattedMessages, [
107+
'role' => 'system',
108+
'content' => $systemMessage
109+
]);
110+
}
111+
112+
$payload = [
113+
'model' => $this->model,
114+
'messages' => $formattedMessages,
115+
'max_tokens' => $this->maxTokens,
116+
'temperature' => $this->temperature,
117+
'stream' => true,
118+
];
119+
120+
$content = '';
121+
$response = $client->fetch(
122+
'https://api.deepseek.com/chat/completions',
123+
Client::METHOD_POST,
124+
$payload,
125+
[],
126+
function ($chunk) use (&$content, $listener) {
127+
$content .= $this->process($chunk, $listener);
128+
}
129+
);
130+
131+
if ($response->getStatusCode() >= 400) {
132+
throw new \Exception('Deepseek API error ('.$response->getStatusCode().'): '.$response->getBody());
133+
}
134+
135+
$message = new Text($content);
136+
137+
return $message;
138+
}
139+
140+
/**
141+
* Process a stream chunk from the Deepseek API
142+
*
143+
* @param \Utopia\Fetch\Chunk $chunk
144+
* @param callable|null $listener
145+
* @return string
146+
*
147+
* @throws \Exception
148+
*/
149+
protected function process(Chunk $chunk, ?callable $listener): string
150+
{
151+
$block = '';
152+
$data = $chunk->getData();
153+
$lines = explode("\n", $data);
154+
155+
foreach ($lines as $line) {
156+
if (empty(trim($line))) {
157+
continue;
158+
}
159+
160+
if (! str_starts_with($line, 'data: ')) {
161+
continue;
162+
}
163+
164+
$line = substr($line, 6);
165+
if ($line === '[DONE]') {
166+
continue;
167+
}
168+
169+
$json = json_decode($line, true);
170+
if (! is_array($json)) {
171+
continue;
172+
}
173+
174+
if (isset($json['choices'][0]['delta']['content'])) {
175+
$delta = $json['choices'][0]['delta']['content'];
176+
if (!empty($delta)) {
177+
$block .= $delta;
178+
if ($listener !== null) {
179+
$listener($delta);
180+
}
181+
}
182+
}
183+
184+
if (isset($json['usage'])) {
185+
if (isset($json['usage']['prompt_tokens'])) {
186+
$this->countInputTokens($json['usage']['prompt_tokens']);
187+
}
188+
if (isset($json['usage']['completion_tokens'])) {
189+
$this->countOutputTokens($json['usage']['completion_tokens']);
190+
}
191+
}
192+
}
193+
194+
return $block;
195+
}
196+
197+
/**
198+
* Get available models
199+
*
200+
* @return array<string>
201+
*/
202+
public function getModels(): array
203+
{
204+
return [
205+
self::MODEL_DEEPSEEK_CHAT,
206+
self::MODEL_DEEPSEEK_CODER,
207+
];
208+
}
209+
210+
/**
211+
* Get current model
212+
*
213+
* @return string
214+
*/
215+
public function getModel(): string
216+
{
217+
return $this->model;
218+
}
219+
220+
/**
221+
* Set model to use
222+
*
223+
* @param string $model
224+
* @return self
225+
*
226+
* @throws \Exception
227+
*/
228+
public function setModel(string $model): self
229+
{
230+
if (! in_array($model, $this->getModels())) {
231+
throw new \Exception('Unsupported model: '.$model);
232+
}
233+
234+
$this->model = $model;
235+
236+
return $this;
237+
}
238+
239+
/**
240+
* Set max tokens
241+
*
242+
* @param int $maxTokens
243+
* @return self
244+
*/
245+
public function setMaxTokens(int $maxTokens): self
246+
{
247+
$this->maxTokens = $maxTokens;
248+
249+
return $this;
250+
}
251+
252+
/**
253+
* Set temperature
254+
*
255+
* @param float $temperature
256+
* @return self
257+
*/
258+
public function setTemperature(float $temperature): self
259+
{
260+
$this->temperature = $temperature;
261+
262+
return $this;
263+
}
264+
265+
/**
266+
* Get the adapter name
267+
*
268+
* @return string
269+
*/
270+
public function getName(): string
271+
{
272+
return 'deepseek';
273+
}
274+
}

0 commit comments

Comments
 (0)