Skip to content

Commit e37acda

Browse files
authored
Merge pull request #28 from utopia-php/feat-better-sse
refactor: enhance stream processing in Adapter class
2 parents 06064fd + af4ead3 commit e37acda

30 files changed

+2108
-451
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ tests/chunk.php
55
.idea/
66
.env
77
example.php
8-
example.md
8+
example.md
9+
/blueprints/

README.md

Lines changed: 178 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP ve
2323

2424
- **Multiple AI Providers** - Support for OpenAI, Anthropic, Deepseek, Perplexity, XAI, Gemini, and OpenRouter APIs
2525
- **Flexible Message Types** - Support for text and structured content in messages
26+
- **Message Attachments** - Attach files (for example images) directly to conversation turns
2627
- **Conversation Management** - Easy-to-use conversation handling between agents and users
2728
- **Model Selection** - Choose from various AI models (GPT-4, Claude 3, Deepseek Chat, Sonar, Grok, etc.)
2829
- **Parameter Control** - Fine-tune model behavior with temperature and token controls
30+
- **Streaming Output** - Consume incremental model output through callback-driven Server-Sent Events (SSE) streams
2931

3032
## Usage
3133

@@ -35,8 +37,8 @@ Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP ve
3537
<?php
3638

3739
use Utopia\Agents\Agent;
40+
use Utopia\Agents\Message;
3841
use Utopia\Agents\Roles\User;
39-
use Utopia\Agents\Messages\Text;
4042
use Utopia\Agents\Conversation;
4143
use Utopia\Agents\Adapters\OpenAI;
4244

@@ -50,7 +52,7 @@ $user = new User('user-1', 'John');
5052
// Start a conversation
5153
$conversation = new Conversation($agent);
5254
$conversation
53-
->message($user, new Text('What is artificial intelligence?'))
55+
->message($user, new Message('What is artificial intelligence?'))
5456
->send();
5557
```
5658

@@ -182,7 +184,7 @@ $openrouter = new OpenRouter(
182184
```php
183185
use Utopia\Agents\Roles\User;
184186
use Utopia\Agents\Roles\Assistant;
185-
use Utopia\Agents\Messages\Text;
187+
use Utopia\Agents\Message;
186188

187189
// Create a conversation with system instructions
188190
$agent = new Agent($adapter);
@@ -197,26 +199,190 @@ $assistant = new Assistant('assistant-1');
197199

198200
$conversation = new Conversation($agent);
199201
$conversation
200-
->message($user, new Text('Hello!'))
201-
->message($assistant, new Text('Hi! How can I help you today?'))
202-
->message($user, new Text('What is the capital of France?'));
202+
->message($user, new Message('Hello!'))
203+
->message($assistant, new Message('Hi! How can I help you today?'))
204+
->message($user, new Message('What is the capital of France?'));
205+
206+
// Add a user message with attachments
207+
$conversation->message(
208+
$user,
209+
new Message('Please summarize this screenshot'),
210+
[new Message($imageBinaryContent)]
211+
);
203212

204213
// Send and get response
205214
$response = $conversation->send();
206215
```
207216

217+
### Streaming Responses (SSE)
218+
219+
The conversation layer supports incremental output streaming through `Conversation::listen(callable $listener)`.
220+
The callback receives each text delta as it arrives from the provider's SSE stream, while `send()` still returns the final aggregated `Message`.
221+
222+
#### Streaming in CLI / Worker Contexts
223+
224+
```php
225+
use Utopia\Agents\Agent;
226+
use Utopia\Agents\Conversation;
227+
use Utopia\Agents\Adapters\OpenAI;
228+
use Utopia\Agents\Message;
229+
use Utopia\Agents\Roles\User;
230+
231+
$agent = new Agent(new OpenAI('your-api-key', OpenAI::MODEL_GPT_4O));
232+
$conversation = new Conversation($agent);
233+
$user = new User('user-1', 'John');
234+
235+
$conversation
236+
->listen(function (string $chunk): void {
237+
echo $chunk; // render partial output as soon as it is received
238+
})
239+
->message($user, new Message('Explain vector databases in one paragraph.'));
240+
241+
$final = $conversation->send(); // final, complete assistant message
242+
```
243+
244+
#### Exposing Model Output as HTTP SSE
245+
246+
```php
247+
use Utopia\Agents\Agent;
248+
use Utopia\Agents\Conversation;
249+
use Utopia\Agents\Adapters\OpenAI;
250+
use Utopia\Agents\Message;
251+
use Utopia\Agents\Roles\User;
252+
253+
header('Content-Type: text/event-stream');
254+
header('Cache-Control: no-cache');
255+
header('Connection: keep-alive');
256+
257+
$agent = new Agent(new OpenAI('your-api-key', OpenAI::MODEL_GPT_4O));
258+
$conversation = new Conversation($agent);
259+
$user = new User('user-1', 'John');
260+
261+
$conversation
262+
->listen(function (string $chunk): void {
263+
// Send each token delta as an SSE frame
264+
echo 'data: '.json_encode(['delta' => $chunk], JSON_UNESCAPED_UNICODE)."\n\n";
265+
266+
if (function_exists('ob_flush')) {
267+
@ob_flush();
268+
}
269+
flush();
270+
})
271+
->message($user, new Message('Write a short release note for today''s deployment.'));
272+
273+
$final = $conversation->send();
274+
275+
// Optional terminal event with complete text
276+
echo 'event: done'."\n";
277+
echo 'data: '.json_encode(['message' => $final->getContent()], JSON_UNESCAPED_UNICODE)."\n\n";
278+
echo 'data: [DONE]'."\n\n";
279+
flush();
280+
```
281+
282+
#### Operational Notes
283+
284+
- Streaming is adapter-dependent and available for chat-capable providers that expose incremental output.
285+
- The listener is optional; if omitted, responses are still collected and returned as a single final message.
286+
- Keep callbacks non-blocking and lightweight to avoid slowing downstream token delivery.
287+
- When serving SSE over HTTP, send `Content-Type: text/event-stream`, flush frequently, and disable intermediary buffering where applicable.
288+
- Usage metrics (input/output tokens and cache counters, where supported) remain available after `send()` completes.
289+
208290
### Working with Messages
209291

210292
```php
211-
use Utopia\Agents\Messages\Text;
212-
use Utopia\Agents\Messages\Image;
293+
use Utopia\Agents\Message;
213294

214-
// Text message
215-
$textMessage = new Text('Hello, how are you?');
295+
// Message content is always text
296+
$textMessage = new Message('Hello, how are you?');
216297

217-
// Image message
218-
$imageMessage = new Image($imageBinaryContent);
298+
// Attachments are binary payloads (for example images)
299+
$imageMessage = new Message($imageBinaryContent);
219300
$mimeType = $imageMessage->getMimeType(); // Get the MIME type of the image
301+
302+
// Attach image to a text prompt
303+
$message = (new Message('Describe this image'))->addAttachment($imageMessage);
304+
```
305+
306+
### Attachment Examples
307+
308+
```php
309+
use Utopia\Agents\Conversation;
310+
use Utopia\Agents\Message;
311+
use Utopia\Agents\Roles\User;
312+
313+
$conversation = new Conversation($agent);
314+
$user = new User('user-1', 'John');
315+
316+
// 1) Attach a single image in the same turn
317+
$conversation->message(
318+
$user,
319+
new Message('What is shown here?'),
320+
[new Message(file_get_contents(__DIR__.'/images/screenshot.png'))]
321+
);
322+
323+
// 2) Attach multiple images in one turn
324+
$conversation->message(
325+
$user,
326+
new Message('Compare these two images and list differences.'),
327+
[
328+
new Message(file_get_contents(__DIR__.'/images/before.png')),
329+
new Message(file_get_contents(__DIR__.'/images/after.png')),
330+
]
331+
);
332+
333+
// 3) Build and reuse a message object with attachments
334+
$prompt = (new Message('Extract visible text from this receipt'))
335+
->addAttachment(new Message(file_get_contents(__DIR__.'/images/receipt.jpg')));
336+
337+
$conversation->message($user, $prompt);
338+
```
339+
340+
### Attachment Limits and Validation
341+
342+
Attachment validation is enforced by default in `Conversation::message(...)`.
343+
Guardrail values come from the selected adapter (not from conversation-level user configuration).
344+
345+
Default adapter guardrails:
346+
347+
- Max attachments per message: `10`
348+
- Max binary size per attachment: `5_000_000` bytes (~5 MB)
349+
- Max total attachment payload per turn: `20_000_000` bytes (~20 MB)
350+
- MIME allowlist: `image/png`, `image/jpeg`, `image/webp`, `image/gif`
351+
- Reject empty or unreadable payloads
352+
- Adapter compatibility checks (attachment type must be supported by the selected adapter)
353+
354+
To customize limits, create an adapter subclass and override limit methods:
355+
356+
```php
357+
<?php
358+
359+
use Utopia\Agents\Adapters\OpenAI;
360+
361+
class StrictOpenAI extends OpenAI
362+
{
363+
public function getMaxAttachmentsPerMessage(): ?int
364+
{
365+
return 3;
366+
}
367+
368+
public function getMaxAttachmentBytes(): ?int
369+
{
370+
return 2_000_000;
371+
}
372+
373+
public function getMaxTotalAttachmentBytes(): ?int
374+
{
375+
return 6_000_000;
376+
}
377+
378+
/**
379+
* @return list<string>|null
380+
*/
381+
public function getAllowedAttachmentMimeTypes(): ?array
382+
{
383+
return ['image/png', 'image/jpeg'];
384+
}
385+
}
220386
```
221387

222388
## Schema and Schema Objects

0 commit comments

Comments
 (0)