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