8000 WIP Implement Assistants Streaming · openai-php/client@aeee256 · GitHub
[go: up one dir, main page]

Skip to content

Commit aeee256

Browse files
EthanBarlogehrisandro
authored andcommitted
WIP Implement Assistants Streaming
1 parent 0f755fa commit aeee256

11 files changed

+607
-0
lines changed

src/Resources/ThreadsRuns.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
use OpenAI\Contracts\Resources\ThreadsRunsContract;
88
use OpenAI\Contracts\Resources\ThreadsRunsStepsContract;
99
use OpenAI\Responses\Threads\Runs\ThreadRunListResponse;
10+
use OpenAI\Responses\EventStreamResponse;
11+
use OpenAI\Responses\Threads\Runs\StreamedThreadRunResponseFactory;
12+
1013
use OpenAI\Responses\Threads\Runs\ThreadRunResponse;
1114
use OpenAI\ValueObjects\Transporter\Payload;
1215
use OpenAI\ValueObjects\Transporter\Response;
1316

1417
final class ThreadsRuns implements ThreadsRunsContract
1518
{
19+
use Concerns\Streamable;
1620
use Concerns\Transportable;
1721

1822
/**
@@ -32,6 +36,25 @@ public function create(string $threadId, array $parameters): ThreadRunResponse
3236
return ThreadRunResponse::from($response->data(), $response->meta());
3337
}
3438

39+
/**
40+
* Creates a streamed run
41+
*
42+
* @see https://platform.openai.com/docs/api-reference/runs/createRun
43+
*
44+
* @param array<string, mixed> $parameters
45+
* @return EventStreamResponse<mixed>
46+
*/
47+
public function createStreamed(string $threadId, array $parameters): EventStreamResponse
48+
{
49+
$parameters = $this->setStreamParameter($parameters);
50+
51+
$payload = Payload::create('threads/'.$threadId.'/runs', $parameters);
52+
53+
$response = $this->transporter->requestStream($payload);
54+
55+
return new EventStreamResponse(StreamedThreadRunResponseFactory::class, $response);
56+
}
57+
3558
/**
3659
* Retrieves a run.
3760
*

src/Responses/EventStreamResponse.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace OpenAI\Responses;
4+
5+
use Generator;
6+
use OpenAI\Contracts\ResponseHasMetaInformationContract;
7+
use OpenAI\Contracts\ResponseStreamContract;
8+
use OpenAI\Exceptions\ErrorException;
9+
use OpenAI\Responses\Meta\MetaInformation;
10+
use Psr\Http\Message\ResponseInterface;
11+
use Psr\Http\Message\StreamInterface;
12+
13+
/**
14+
* @template TResponse
15+
*
16+
* @implements ResponseStreamContract<TResponse>
17+
*/
18+
final class EventStreamResponse implements ResponseHasMetaInformationContract, ResponseStreamContract
19+
{
20+
/**
21+
* Creates a new Stream Response instance.
22+
*
23+
* @param class-string<TResponse> $responseClass
24+
*/
25+
public function __construct(
26+
private readonly string $responseClass,
27+
private readonly ResponseInterface $response,
28+
) {
29+
//
30+
}
31+
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public function getIterator(): Generator
36+
{
37+
while (! $this->response->getBody()->eof()) {
38+
$line = $this->readLine($this->response->getBody());
39+
40+
if (! str_starts_with($line, 'event:')) {
41+
continue;
42+
}
43+
44+
$event = trim(substr($line, strlen('event:')));
45+
46+
$line = $this->readLine($this->response->getBody());
47+
if (! str_starts_with($line, 'data:')) {
48+
throw new ErrorException('openai-php/client - Unhandled edge case, Event has no associated data. Event: '.$event);
49+
}
50+
51+
$data = trim(substr($line, strlen('data:')));
52+
53+
if ($data === '[DONE]') {
54+
break;
55+
}
56+
57+
/** @var array{error?: array{message: string|array<int, string>, type: string, code: string}} $response */
58+
$response = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
59+
60+
if (isset($response['error'])) {
61+
throw new ErrorException($response['error']);
62+
}
63+
64+
$responseObject = $this->responseClass::from($event, $response, $this->meta());
65+
yield new EventStreamResponseItem($event, $responseObject);
66+
}
67+
}
68+
69+
/**
70+
* Read a line from the stream.
71+
*/
72+
private function readLine(StreamInterface $stream): string
73+
{
74+
$buffer = '';
75+
76+
while (! $stream->eof()) {
77+
if ('' === ($byte = $stream->read(1))) {
78+
return $buffer;
79+
}
80+
$buffer .= $byte;
81+
if ($byte === "\n") {
82+
break;
83+
}
84+
}
85+
86+
return $buffer;
87+
}
88+
89+
public function meta(): MetaInformation
90+
{
91+
// @phpstan-ignore-next-line
92+
return MetaInformation::from($this->response->getHeaders());
93+
}
94+
}

src/Responses/StreamResponseItem.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace OpenAI\Responses;
4+
5+
class EventStreamResponseItem
6+
{
7+
public readonly string $event;
8+
9+
public readonly object $data;
10+
11+
public function __construct(string $event, $data)
12+
{
13+
$this->event = $event;
14+
$this->data = $data;
15+
}
16+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Threads\Messages\Delta;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
use OpenAI\Testing\Responses\Concerns\Fakeable;
10+
11+
/**
12+
* @implements ResponseContract<array{id: string, object: string, created_at: int, thread_id: string, role: string, content: array<int, array{type: string, image_file: array{file_id: string}}|array{type: string, text: array{value: string, annotations: array<int, array{type: string, text: string, file_citation: array{file_id: string, quote: string}, start_index: int, end_index: int}|array{type: string, text: string, file_path: array{file_id: string}, start_index: int, end_index: int}>}}>, assistant_id: ?string, run_id: ?string, file_ids: array<int, string>, metadata: array<string, string>}>
13+
*/
14+
final class ThreadMessageDeltaResponse implements ResponseContract
15+
{
16+
/**
17+
* @use ArrayAccessible<array{id: string, object: string, created_at: int, thread_id: string, role: string, content: array<int, array{type: string, image_file: array{file_id: string}}|array{type: string, text: array{value: string, annotations: array<int, array{type: string, text: string, file_citation: array{file_id: string, quote: string}, start_index: int, end_index: int}|array{type: string, text: string, file_path: array{file_id: string}, start_index: int, end_index: int}>}}>, assistant_id: ?string, run_id: ?string, file_ids: array<int, string>, metadata: array<string, string>}>
18+
*/
19+
use ArrayAccessible;
20+
21+
use Fakeable;
22+
23+
/**
24+
* @param array<int, ThreadMessageResponseContentImageFileObject|ThreadMessageDeltaResponseContentTextObject> $content
25+
* @param array<int, string> $fileIds
26+
*/
27+
private function __construct(
28+
public string $id,
29+
public string $object,
30+
public ?string $role,
31+
public array $content,
32+
public ?array $fileIds,
33+
) {
34+
}
35+
36+
/**
37+
* Acts as static factory, and returns a new Response instance.
38+
*
39+
* @param array{id: string, object: string, created_at: int, thread_id: string, role: string, content: array<int, array{type: 'image_file', image_file: array{file_id: string}}|array{type: 'text', text: array{value: string, annotations: array<int, array{type: 'file_citation', text: string, file_citation: array{file_id: string, quote: string}, start_index: int, end_index: int}|array{type: 'file_path', text: string, file_path: array{file_id: string}, start_index: int, end_index: int}>}}>, assistant_id: ?string, run_id: ?string, file_ids: array<int, string>, metadata: array<string, string>} $attributes
40+
*/
41+
public static function from(array $attributes): self
42+
{
43+
$content = array_map(
44+
fn (array $content): \OpenAI\Responses\Threads\Messages\Delta\ThreadMessageDeltaResponseContentTextObject|\OpenAI\Responses\Threads\Messages\Delta\ThreadMessageDeltaResponseContentImageFileObject => match ($content['type']) {
45+
'text' => ThreadMessageDeltaResponseContentTextObject::from($content),
46+
'image_file' => ThreadMessageDeltaResponseContentImageFileObject::from($content),
47+
},
48+
$attributes['delta']['content'],
49+
);
50+
51+
return new self(
52+
$attributes['id'],
53+
$attributes['object'],
54+
isset($attributes['delta']['role']) ? $attributes['delta']['role'] : null,
55+
$content,
56+
isset($attributes['delta']['file_ids']) ? $attributes['delta']['file_ids'] : null,
57+
);
58+
}
59+
60+
/**
61+
* {@inheritDoc}
62+
*/
63+
public function toArray(): array
64+
{
65+
return [
66+
'id' => $this->id,
67+
'object' => $this->object,
68+
'role' => $this->role,
69+
'content' => array_map(
70+
fn (ThreadMessageResponseContentImageFileObject|ThreadMessageDeltaResponseContentTextObject $content): array => $content->toArray(),
71+
$this->content,
72+
),
73+
'file_ids' => $this->fileIds,
74+
];
75+
}
76+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Threads\Messages\Delta;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
use OpenAI\Testing\Responses\Concerns\Fakeable;
10+
11+
/**
12+
* @implements ResponseContract<array{file_id: string}>
13+
*/
14+
final class ThreadMessageDeltaResponseContentImageFile implements ResponseContract
15+
{
16+
/**
17+
* @use ArrayAccessible<array{file_id: string}>
18+
*/
19+
use ArrayAccessible;
20+
21+
use Fakeable;
22+
23+
private function __construct(
24+
public string $fileId,
25+
) {
26+
}
27+
28+
/**
29+
* Acts as static factory, and returns a new Response instance.
30+
*
31+
* @param array{file_id: string} $attributes
32+
*/
33+
public static function from(array $attributes): self
34+
{
35+
return new self(
36+
$attributes['file_id'],
37+
);
38+
}
39+
40+
/**
41+
* {@inheritDoc}
42+
*/
43+
public function toArray(): array
44+
{
45+
return [
46+
'file_id' => $this->fileId,
47+
];
48+
}
49+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Threads\Messages\Delta;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
use OpenAI\Testing\Responses\Concerns\Fakeable;
10+
11+
/**
12+
* @implements ResponseContract<array{type: string, image_file: array{file_id: string}}>
13+
*/
14+
final class ThreadMessageDeltaResponseContentImageFileObject implements ResponseContract
15+
{
16+
/**
17+
* @use ArrayAccessible<array{type: string, image_file: array{file_id: string}}>
18+
*/
19+
use ArrayAccessible;
20+
21+
use Fakeable;
22+
23+
private function __construct(
24+
public ?int $index,
25+
public string $type,
26+
public ThreadMessageDeltaResponseContentImageFile $imageFile,
27+
) {
28+
}
29+
30+
/**
31+
* Acts as static factory, and returns a new Response instance.
32+
*
33+
* @param array{type: string, image_file: array{file_id: string}} $attributes
34+
*/
35+
public static function from(array $attributes): self
36+
{
37+
return new self(
38+
isset($attributes['index']) ? $attributes['index'] : null,
39+
$attributes['type'],
40+
ThreadMessageDeltaResponseContentImageFile::from($attributes['image_file']),
41+
);
42+
}
43+
44+
/**
45+
* {@inheritDoc}
46+
*/
47+
public function toArray(): array
48+
{
49+
return [
50+
'index' => $this->index,
51+
'type' => $this->type,
52+
'image_file' => $this->imageFile->toArray(),
53+
];
54+
}
55+
}

0 commit comments

Comments
 (0)
0