Skip to content
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

feat: support streaming of responses #58

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/stream-claude-anthropic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use PhpLlm\LlmChain\Anthropic\Model\Claude;
use PhpLlm\LlmChain\Anthropic\Platform\Anthropic;
use PhpLlm\LlmChain\Chain;
use PhpLlm\LlmChain\Message\Message;
use PhpLlm\LlmChain\Message\MessageBag;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpClient\HttpClient;

require_once dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

if (empty($_ENV['ANTHROPIC_API_KEY'])) {
echo 'Please set the ANTHROPIC_API_KEY environment variable.'.PHP_EOL;
exit(1);
}

$platform = new Anthropic(HttpClient::create(), $_ENV['ANTHROPIC_API_KEY']);
$llm = new Claude($platform);

$chain = new Chain($llm);
$messages = new MessageBag(
Message::forSystem('You are a thoughtful philosopher.'),
Message::ofUser('What is the purpose of an ant?'),
);
$response = $chain->call($messages, [
'stream' => true, // enable streaming of response text
]);

assert($response instanceof Generator);

foreach ($response as $word) {
echo $word;
}
echo PHP_EOL;
37 changes: 37 additions & 0 deletions examples/stream-gpt-openai.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use PhpLlm\LlmChain\Chain;
use PhpLlm\LlmChain\Message\Message;
use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpClient\EventSourceHttpClient;

require_once dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

if (empty($_ENV['OPENAI_API_KEY'])) {
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
exit(1);
}

$platform = new OpenAI(new EventSourceHttpClient(), $_ENV['OPENAI_API_KEY']);
$llm = new Gpt($platform, Version::gpt4oMini());

$chain = new Chain($llm);
$messages = new MessageBag(
Message::forSystem('You are a thoughtful philosopher.'),
Message::ofUser('What is the purpose of an ant?'),
);
$response = $chain->call($messages, [
'stream' => true, // enable streaming of response text
]);

assert($response instanceof Generator);

foreach ($response as $word) {
echo $word;
}
echo PHP_EOL;
11 changes: 9 additions & 2 deletions examples/toolbox-youtube.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
$chain = new Chain($llm, [$processor], [$processor]);

$messages = new MessageBag(Message::ofUser('Please summarize this video for me: https://www.youtube.com/watch?v=6uXW-ulpj0s'));
$response = $chain->call($messages);
$response = $chain->call($messages, [
'stream' => true, // enable streaming of response text
]);

echo $response.PHP_EOL;
assert($response instanceof Generator);

foreach ($response as $word) {
echo $word;
}
echo PHP_EOL;
19 changes: 18 additions & 1 deletion src/Anthropic/Model/Claude.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\Response\Choice;
use PhpLlm\LlmChain\Response\Response;
use PhpLlm\LlmChain\Response\ResponseInterface;
use PhpLlm\LlmChain\Response\StreamResponse;

final class Claude implements LanguageModel
{
Expand All @@ -28,7 +30,7 @@ public function __construct(
* @param array<string, mixed> $options The options to be used for this specific call.
* Can overwrite default options.
*/
public function call(MessageBag $messages, array $options = []): Response
public function call(MessageBag $messages, array $options = []): ResponseInterface
{
$system = $messages->getSystemMessage();
$body = array_merge($this->options, $options, [
Expand All @@ -39,6 +41,10 @@ public function call(MessageBag $messages, array $options = []): Response

$response = $this->platform->request($body);

if ($response instanceof \Generator) {
return new StreamResponse($this->convertStream($response));
}

return new Response(new Choice($response['content'][0]['text']));
}

Expand All @@ -56,4 +62,15 @@ public function supportsStructuredOutput(): bool
{
return false;
}

private function convertStream(\Generator $generator): \Generator
{
foreach ($generator as $data) {
if ('content_block_delta' != $data['type'] || !isset($data['delta']['text'])) {
continue;
}

yield $data['delta']['text'];
}
}
}
2 changes: 1 addition & 1 deletion src/Anthropic/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ interface Platform
*
* @return array<string, mixed>
*/
public function request(array $body): array;
public function request(array $body): iterable;
}
25 changes: 23 additions & 2 deletions src/Anthropic/Platform/Anthropic.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@
namespace PhpLlm\LlmChain\Anthropic\Platform;

use PhpLlm\LlmChain\Anthropic\Platform;
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final readonly class Anthropic implements Platform
{
private EventSourceHttpClient $httpClient;

public function __construct(
private HttpClientInterface $httpClient,
HttpClientInterface $httpClient,
private string $apiKey,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
}

/**
* @param array<string, mixed> $body
*
* @return array<string, mixed>
*/
public function request(array $body): array
public function request(array $body): iterable
{
$response = $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [
'headers' => [
Expand All @@ -31,6 +37,21 @@ public function request(array $body): array
'json' => $body,
]);

if ($body['stream'] ?? false) {
return $this->stream($response);
}

return $response->toArray();
}

private function stream(ResponseInterface $response): \Generator
{
foreach ((new EventSourceHttpClient())->stream($response) as $chunk) {
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
continue;
}

yield $chunk->getArrayData();
}
}
}
4 changes: 2 additions & 2 deletions src/Chain/Output.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use PhpLlm\LlmChain\LanguageModel;
use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\Response\Response;
use PhpLlm\LlmChain\Response\ResponseInterface;

final readonly class Output
{
Expand All @@ -15,7 +15,7 @@
*/
public function __construct(
public LanguageModel $llm,
public Response $response,
public ResponseInterface $response,
public MessageBag $messages,
public array $options,
) {
Expand Down
4 changes: 2 additions & 2 deletions src/LanguageModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
namespace PhpLlm\LlmChain;

use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\Response\Response;
use PhpLlm\LlmChain\Response\ResponseInterface;

interface LanguageModel
{
/**
* @param array<string, mixed> $options
*/
public function call(MessageBag $messages, array $options = []): Response;
public function call(MessageBag $messages, array $options = []): ResponseInterface;

public function supportsToolCalling(): bool;

Expand Down
19 changes: 18 additions & 1 deletion src/OpenAI/Model/Gpt.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use PhpLlm\LlmChain\OpenAI\Platform;
use PhpLlm\LlmChain\Response\Choice;
use PhpLlm\LlmChain\Response\Response;
use PhpLlm\LlmChain\Response\ResponseInterface;
use PhpLlm\LlmChain\Response\StreamResponse;
use PhpLlm\LlmChain\Response\ToolCall;

final class Gpt implements LanguageModel
Expand All @@ -30,7 +32,7 @@ public function __construct(
* @param array<mixed> $options The options to be used for this specific call.
* Can overwrite default options.
*/
public function call(MessageBag $messages, array $options = []): Response
public function call(MessageBag $messages, array $options = []): ResponseInterface
{
$body = array_merge($this->options, $options, [
'model' => $this->version->name,
Expand All @@ -39,6 +41,10 @@ public function call(MessageBag $messages, array $options = []): Response

$response = $this->platform->request('chat/completions', $body);

if ($response instanceof \Generator) {
return new StreamResponse($this->convertStream($response));
}

return new Response(...array_map([$this, 'convertChoice'], $response['choices']));
}

Expand All @@ -57,6 +63,17 @@ public function supportsStructuredOutput(): bool
return $this->version->supportStructuredOutput;
}

private function convertStream(\Generator $generator): \Generator
{
foreach ($generator as $data) {
if (!isset($data['choices'][0]['delta']['content'])) {
continue;
}

yield $data['choices'][0]['delta']['content'];
}
}

/**
* @param array{
* index: integer,
Expand Down
2 changes: 1 addition & 1 deletion src/OpenAI/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Platform
*
* @return array<string, mixed>
*/
public function request(string $endpoint, array $body): array;
public function request(string $endpoint, array $body): iterable;

/**
* @param array<array<string, mixed>> $bodies
Expand Down
25 changes: 22 additions & 3 deletions src/OpenAI/Platform/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@

use PhpLlm\LlmChain\Exception\RuntimeException;
use PhpLlm\LlmChain\OpenAI\Platform;
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Contracts\HttpClient\ResponseInterface;

abstract class AbstractPlatform implements Platform
abstract readonly class AbstractPlatform implements Platform
{
public function request(string $endpoint, array $body): array
public function request(string $endpoint, array $body): iterable
{
$response = $this->rawRequest($endpoint, $body);

if ($body['stream'] ?? false) {
return $this->stream($response);
}

try {
return $this->rawRequest($endpoint, $body)->toArray();
return $response->toArray();
} catch (ClientException $e) {
dump($e->getResponse()->getContent(false));
throw new RuntimeException('Failed to make request', 0, $e);
Expand All @@ -33,6 +41,17 @@ public function multiRequest(string $endpoint, array $bodies): \Generator
}
}

private function stream(ResponseInterface $response): \Generator
{
foreach ((new EventSourceHttpClient())->stream($response) as $chunk) {
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
continue;
}

yield $chunk->getArrayData();
}
}

/**
* @param array<string, mixed> $body
*/
Expand Down
16 changes: 10 additions & 6 deletions src/OpenAI/Platform/Azure.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
namespace PhpLlm\LlmChain\OpenAI\Platform;

use PhpLlm\LlmChain\OpenAI\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class Azure extends AbstractPlatform implements Platform
final readonly class Azure extends AbstractPlatform implements Platform
{
private EventSourceHttpClient $httpClient;

public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly string $baseUrl,
private readonly string $deployment,
private readonly string $apiVersion,
private readonly string $key,
HttpClientInterface $httpClient,
private string $baseUrl,
private string $deployment,
private string $apiVersion,
private string $key,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
}

protected function rawRequest(string $endpoint, array $body): ResponseInterface
Expand Down
10 changes: 7 additions & 3 deletions src/OpenAI/Platform/OpenAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
namespace PhpLlm\LlmChain\OpenAI\Platform;

use PhpLlm\LlmChain\OpenAI\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class OpenAI extends AbstractPlatform implements Platform
final readonly class OpenAI extends AbstractPlatform implements Platform
{
private EventSourceHttpClient $httpClient;

public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly string $apiKey,
HttpClientInterface $httpClient,
private string $apiKey,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
}

protected function rawRequest(string $endpoint, array $body): ResponseInterface
Expand Down
Loading