-
Notifications
You must be signed in to change notification settings - Fork 73
Add support for streaming #255
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
d8141c1
f00f0c8
64844fc
3f75843
998d695
afa0ba0
8751105
31c2170
8bb52fc
4ff589d
cf15f0b
cbb11c2
f67deb4
127ddad
2fc0ab9
a42a4c1
9781d4e
ecd6235
5e3c0d9
bf803a7
90a9cb3
958d30f
d4eb377
76737ee
d7502ed
f169b69
bdda338
94edcd2
2f31263
befc706
8c12cdc
f2aada5
512ce47
96a895b
0e511db
412a813
0450a83
5762881
4422749
baab7a3
a0937e5
e5ddc70
d9e1809
d197a79
a216e62
7822fd9
4a6182b
d32da75
760e95a
4fd90a2
ef2de11
f3f0dc9
b2f9fb7
ba62029
35c8ba7
e55e429
91ae1c4
c96776b
9ee700e
6587a5f
fec38bf
43b15c2
b21bb8f
f05dce8
f866fcf
5f460ac
2fa7159
a3f098e
77c730d
4a345d6
1bd7ca9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| use WordPress\AiClient\Common\Exception\RuntimeException; | ||
| use WordPress\AiClient\Events\AfterGenerateResultEvent; | ||
| use WordPress\AiClient\Events\BeforeGenerateResultEvent; | ||
| use WordPress\AiClient\Events\GenerateResultErrorEvent; | ||
| use WordPress\AiClient\Files\DTO\File; | ||
| use WordPress\AiClient\Files\Enums\FileTypeEnum; | ||
| use WordPress\AiClient\Files\Enums\MediaOrientationEnum; | ||
|
|
@@ -26,11 +27,13 @@ | |
| use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; | ||
| use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface; | ||
| use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; | ||
| use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\StreamingTextGenerationModelInterface; | ||
| use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; | ||
| use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface; | ||
| use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface; | ||
| use WordPress\AiClient\Providers\ProviderRegistry; | ||
| use WordPress\AiClient\Results\DTO\GenerativeAiResult; | ||
| use WordPress\AiClient\Results\StreamedGenerativeAiResult; | ||
| use WordPress\AiClient\Tools\DTO\FunctionDeclaration; | ||
| use WordPress\AiClient\Tools\DTO\FunctionResponse; | ||
| use WordPress\AiClient\Tools\DTO\WebSearch; | ||
|
|
@@ -1048,6 +1051,65 @@ public function generateTextResult(): GenerativeAiResult | |
| return $this->generateResult(CapabilityEnum::textGeneration()); | ||
| } | ||
|
|
||
| /** | ||
| * Streams a text result from the prompt. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return StreamedGenerativeAiResult The streamed result. | ||
| * @throws InvalidArgumentException If the prompt or model validation fails. | ||
| * @throws RuntimeException If the model does not support streaming text generation. | ||
| */ | ||
| public function streamGenerateTextResult(): StreamedGenerativeAiResult | ||
| { | ||
| $this->includeOutputModalities(ModalityEnum::text()); | ||
| $this->validateMessages(); | ||
|
|
||
| $capability = CapabilityEnum::textGeneration(); | ||
| $model = $this->getConfiguredModel($capability); | ||
|
|
||
| if (!$model instanceof StreamingTextGenerationModelInterface) { | ||
| throw new RuntimeException( | ||
| sprintf( | ||
| 'Model "%s" does not support streaming text generation.', | ||
| $model->metadata()->getId() | ||
| ) | ||
| ); | ||
| } | ||
|
Comment on lines
+1071
to
+1078
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a model doesn't implement this interface, it will fail even if the model supports text generation. It fails because streaming is gated behind this opt-in interface that no provider actually implements. We can't discover our way around it either, since streaming isn't a The clean solution is to remove Another way is to add a new capability and wire discovery to find a model with it, but that isn't standard given streaming isn't a model-level capability, it's a networking primitive, not something a model generates. We'd also still need a method on an interface plus every provider opting in, so it's more machinery for something that isn't really model-level. |
||
|
|
||
| $messages = $this->messages; | ||
|
|
||
| return $model->streamGenerateTextResult($messages) | ||
| ->onStart(function () use ($messages, $model, $capability): void { | ||
| $this->dispatchEvent(new BeforeGenerateResultEvent($messages, $model, $capability)); | ||
| }) | ||
| ->onComplete(function (GenerativeAiResult $result) use ($messages, $model, $capability): void { | ||
| $this->dispatchEvent(new AfterGenerateResultEvent($messages, $model, $capability, $result)); | ||
| }) | ||
| ->onError(function (\Throwable $error) use ($messages, $model, $capability): void { | ||
| $this->dispatchEvent(new GenerateResultErrorEvent($messages, $model, $capability, $error)); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Streams generated text from the prompt as it arrives. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return iterable<string> The text deltas, in order. | ||
| * @throws InvalidArgumentException If the prompt or model validation fails. | ||
| * @throws RuntimeException If the model does not support streaming text generation. | ||
| */ | ||
| public function streamGenerateText(): iterable | ||
| { | ||
| foreach ($this->streamGenerateTextResult() as $chunk) { | ||
| $delta = $chunk->getDeltaText(); | ||
| if ($delta !== '') { | ||
| yield $delta; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Generates an image result from the prompt. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace WordPress\AiClient\Events; | ||
|
|
||
| use Throwable; | ||
| use WordPress\AiClient\Messages\DTO\Message; | ||
| use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; | ||
| use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; | ||
|
|
||
| /** | ||
| * Class GenerateResultErrorEvent. | ||
| * | ||
| * @since n.e.x.t | ||
| */ | ||
| class GenerateResultErrorEvent | ||
| { | ||
| /** | ||
| * @var list<Message> The messages that were sent to the model. | ||
| */ | ||
| private array $messages; | ||
|
|
||
| /** | ||
| * @var ModelInterface The model that processed the prompt. | ||
| */ | ||
| private ModelInterface $model; | ||
|
|
||
| /** | ||
| * @var CapabilityEnum|null The capability that was used for generation. | ||
| */ | ||
| private ?CapabilityEnum $capability; | ||
|
|
||
| /** | ||
| * @var Throwable The error that occurred during generation. | ||
| */ | ||
| private Throwable $error; | ||
|
|
||
| /** | ||
| * Constructor. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param list<Message> $messages The messages that were sent to the model. | ||
| * @param ModelInterface $model The model that processed the prompt. | ||
| * @param CapabilityEnum|null $capability The capability that was used for generation. | ||
| * @param Throwable $error The error that occurred during generation. | ||
| */ | ||
| public function __construct( | ||
| array $messages, | ||
| ModelInterface $model, | ||
| ?CapabilityEnum $capability, | ||
| Throwable $error | ||
| ) { | ||
| $this->messages = $messages; | ||
| $this->model = $model; | ||
| $this->capability = $capability; | ||
| $this->error = $error; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the messages that were sent to the model. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return list<Message> The messages. | ||
| */ | ||
| public function getMessages(): array | ||
| { | ||
| return $this->messages; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the model that processed the prompt. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return ModelInterface The model. | ||
| */ | ||
| public function getModel(): ModelInterface | ||
| { | ||
| return $this->model; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the capability that was used for generation. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return CapabilityEnum|null The capability, or null if not specified. | ||
| */ | ||
| public function getCapability(): ?CapabilityEnum | ||
| { | ||
| return $this->capability; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the error that occurred during generation. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return Throwable The error. | ||
| */ | ||
| public function getError(): Throwable | ||
| { | ||
| return $this->error; | ||
| } | ||
|
|
||
| /** | ||
| * Performs a deep clone of the event. | ||
| * | ||
| * @since n.e.x.t | ||
| */ | ||
| public function __clone() | ||
| { | ||
| $clonedMessages = []; | ||
| foreach ($this->messages as $message) { | ||
| $clonedMessages[] = clone $message; | ||
| } | ||
| $this->messages = $clonedMessages; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.