-
Notifications
You must be signed in to change notification settings - Fork 87
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
87115db
71ff869
8a21822
f633f1b
1d03f00
252be37
6d41c14
2fa2740
76e5f6c
ff80a36
842f447
d12e439
8811c05
e581fec
8fbd253
8ac19d2
fe6cbbb
b0e4e04
048bdd2
098cc8c
6fb0de3
d1e339c
beefcee
988d830
4151366
b9e990b
a9e6ae9
47da82a
54514e5
f4d1f5b
a71e90b
b023fa8
cdf2b04
155aa46
f6080a9
321706c
4eb6c5c
dbaf89b
78e8b74
f585db2
b5996be
29bf3a9
5ac692b
292bbc6
5bd1da7
284256c
2e257bc
2a92d88
3f5037d
c9815f3
21288f3
9293c92
8c61415
1519f96
390191f
a1d0086
17ddb02
37e8f33
d1ec578
6d59175
c0aa717
9e55904
366c6a1
136fa7d
67803d5
0475fb6
a1b107f
03f934e
306b4c3
c2b0771
4e7197b
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 |
|---|---|---|
|
|
@@ -10,6 +10,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; | ||
|
|
@@ -25,11 +26,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; | ||
|
|
@@ -861,6 +864,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
+884
to
+891
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.