Skip to content

Commit

Permalink
feat: introducing chain processors
Browse files Browse the repository at this point in the history
  • Loading branch information
chr-hertel committed Sep 25, 2024
1 parent 1db39a3 commit f64bb0e
Show file tree
Hide file tree
Showing 20 changed files with 285 additions and 44 deletions.
4 changes: 3 additions & 1 deletion examples/structured-output-math.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\StructuredOutput\ChainProcessor;
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
use PhpLlm\LlmChain\StructuredOutput\SchemaFactory;
use PhpLlm\LlmChain\Tests\StructuredOutput\Data\MathReasoning;
Expand All @@ -21,7 +22,8 @@
$responseFormatFactory = new ResponseFormatFactory(SchemaFactory::create());
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);

$chain = new Chain($llm, responseFormatFactory: $responseFormatFactory, serializer: $serializer);
$processor = new ChainProcessor($responseFormatFactory, $serializer);
$chain = new Chain($llm, [$processor], [$processor]);
$messages = new MessageBag(
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
Message::ofUser('how can I solve 8x + 7 = -23'),
Expand Down
4 changes: 3 additions & 1 deletion examples/toolbox-clock.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\ToolBox\Tool\Clock;
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\ToolBox\ToolBox;
Expand All @@ -19,7 +20,8 @@

$clock = new Clock(new SymfonyClock());
$toolBox = new ToolBox(new ToolAnalyzer(), [$clock]);
$chain = new Chain($llm, $toolBox);
$processor = new ChainProcessor($toolBox);
$chain = new Chain($llm, [$processor], [$processor]);

$messages = new MessageBag(Message::ofUser('What date and time is it?'));
$response = $chain->call($messages);
Expand Down
4 changes: 3 additions & 1 deletion examples/toolbox-serpapi.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\ToolBox\Tool\SerpApi;
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\ToolBox\ToolBox;
Expand All @@ -19,7 +20,8 @@

$serpApi = new SerpApi($httpClient, getenv('SERP_API_KEY'));
$toolBox = new ToolBox(new ToolAnalyzer(), [$serpApi]);
$chain = new Chain($llm, $toolBox);
$processor = new ChainProcessor($toolBox);
$chain = new Chain($llm, [$processor], [$processor]);

$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
$response = $chain->call($messages);
Expand Down
4 changes: 3 additions & 1 deletion examples/toolbox-weather.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\ToolBox\Tool\OpenMeteo;
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\ToolBox\ToolBox;
Expand All @@ -19,7 +20,8 @@

$wikipedia = new OpenMeteo($httpClient);
$toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]);
$chain = new Chain($llm, $toolBox);
$processor = new ChainProcessor($toolBox);
$chain = new Chain($llm, [$processor], [$processor]);

$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?'));
$response = $chain->call($messages);
Expand Down
4 changes: 3 additions & 1 deletion examples/toolbox-wikipedia.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\ToolBox\Tool\Wikipedia;
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\ToolBox\ToolBox;
Expand All @@ -19,7 +20,8 @@

$wikipedia = new Wikipedia($httpClient);
$toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]);
$chain = new Chain($llm, $toolBox);
$processor = new ChainProcessor($toolBox);
$chain = new Chain($llm, [$processor], [$processor]);

$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
$response = $chain->call($messages);
Expand Down
4 changes: 3 additions & 1 deletion examples/toolbox-youtube.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
use PhpLlm\LlmChain\ToolBox\Tool\YouTubeTranscriber;
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
use PhpLlm\LlmChain\ToolBox\ToolBox;
Expand All @@ -19,7 +20,8 @@

$transcriber = new YouTubeTranscriber($httpClient);
$toolBox = new ToolBox(new ToolAnalyzer(), [$transcriber]);
$chain = new Chain($llm, $toolBox);
$processor = new ChainProcessor($toolBox);
$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);
Expand Down
5 changes: 5 additions & 0 deletions src/Anthropic/Model/Claude.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public function supportsToolCalling(): bool
return false; // it does, but implementation here is still open.
}

public function supportsImageInput(): bool
{
return false; // it does, but implementation here is still open.
}

public function supportsStructuredOutput(): bool
{
return false;
Expand Down
51 changes: 22 additions & 29 deletions src/Chain.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@

namespace PhpLlm\LlmChain;

use PhpLlm\LlmChain\Message\Message;
use PhpLlm\LlmChain\Chain\Input;
use PhpLlm\LlmChain\Chain\InputProcessor;
use PhpLlm\LlmChain\Chain\Output;
use PhpLlm\LlmChain\Chain\OutputProcessor;
use PhpLlm\LlmChain\Exception\MissingModelSupport;
use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
use PhpLlm\LlmChain\ToolBox\ToolBoxInterface;
use Symfony\Component\Serializer\SerializerInterface;

final readonly class Chain
{
/**
* @param InputProcessor[] $inputProcessor
* @param OutputProcessor[] $outputProcessor
*/
public function __construct(
private LanguageModel $llm,
private ?ToolBoxInterface $toolBox = null,
private ?ResponseFormatFactory $responseFormatFactory = null,
private ?SerializerInterface $serializer = null,
private array $inputProcessor = [],
private array $outputProcessor = [],
) {
}

Expand All @@ -25,35 +29,24 @@ public function __construct(
*/
public function call(MessageBag $messages, array $options = []): string|object
{
$llmOptions = $options;

if (!array_key_exists('tools', $llmOptions) && null !== $this->toolBox && $this->llm->supportsToolCalling()) {
$llmOptions['tools'] = $this->toolBox->getMap();
}
$input = new Input($this->llm, $messages, $options);
array_map(fn (InputProcessor $processor) => $processor->processInput($input), $this->inputProcessor);

if (array_key_exists('output_structure', $llmOptions) && null !== $this->responseFormatFactory && $this->llm->supportsStructuredOutput()) {
$llmOptions['response_format'] = $this->responseFormatFactory->create($llmOptions['output_structure']);
unset($llmOptions['output_structure']);
if ($messages->containsImage() && !$this->llm->supportsImageInput()) {
throw MissingModelSupport::forImageInput($this->llm::class);
}

$response = $this->llm->call($messages, $llmOptions);
$response = $this->llm->call($messages, $input->getOptions());

while ($response->hasToolCalls()) {
$clonedMessages = clone $messages;
$clonedMessages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());
$output = new Output($this->llm, $response, $messages, $options);
foreach ($this->outputProcessor as $outputProcessor) {
$result = $outputProcessor->processOutput($output);

foreach ($response->getToolCalls() as $toolCall) {
$result = $this->toolBox->execute($toolCall);
$clonedMessages[] = Message::ofToolCall($toolCall, $result);
if (null !== $result) {
return $result;
}

$response = $this->llm->call($clonedMessages, $llmOptions);
}

if (!array_key_exists('output_structure', $options) || null === $this->serializer) {
return $response->getContent();
}

return $this->serializer->deserialize($response->getContent(), $options['output_structure'], 'json');
return $response->getContent();
}
}
37 changes: 37 additions & 0 deletions src/Chain/Input.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

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

final class Input
{
/**
* @param array<string, mixed> $options
*/
public function __construct(
public readonly LanguageModel $llm,
public readonly MessageBag $messages,
private array $options,
) {
}

/**
* @return array<string, mixed>
*/
public function getOptions(): array
{
return $this->options;
}

/**
* @param array<string, mixed> $options
*/
public function setOptions(array $options): void
{
$this->options = $options;
}
}
10 changes: 10 additions & 0 deletions src/Chain/InputProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

interface InputProcessor
{
public function processInput(Input $input): void;
}
23 changes: 23 additions & 0 deletions src/Chain/Output.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

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

final readonly class Output
{
/**
* @param array<string, mixed> $options
*/
public function __construct(
public LanguageModel $llm,
public Response $response,
public MessageBag $messages,
public array $options,
) {
}
}
10 changes: 10 additions & 0 deletions src/Chain/OutputProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

interface OutputProcessor
{
public function processOutput(Output $output): mixed;
}
28 changes: 28 additions & 0 deletions src/Exception/MissingModelSupport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Exception;

final class MissingModelSupport extends \RuntimeException
{
private function __construct(string $model, string $support)
{
parent::__construct(sprintf('Model "%s" does not support "%s".', $model, $support));
}

public static function forToolCalling(string $model): self
{
return new self($model, 'tool calling');
}

public static function forImageInput(string $model): self
{
return new self($model, 'image input');
}

public static function forStructuredOutput(string $model): self
{
return new self($model, 'structured output');
}
}
2 changes: 2 additions & 0 deletions src/LanguageModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ public function call(MessageBag $messages, array $options = []): Response;

public function supportsToolCalling(): bool;

public function supportsImageInput(): bool;

public function supportsStructuredOutput(): bool;
}
11 changes: 11 additions & 0 deletions src/Message/MessageBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ public function prepend(MessageInterface $message): self
return $messages;
}

public function containsImage(): bool
{
foreach ($this as $message) {
if ($message instanceof UserMessage && $message->hasImageContent()) {
return true;
}
}

return false;
}

/**
* @return MessageInterface[]
*/
Expand Down
12 changes: 12 additions & 0 deletions src/Message/UserMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpLlm\LlmChain\Message;

use PhpLlm\LlmChain\Message\Content\ContentInterface;
use PhpLlm\LlmChain\Message\Content\Image;
use PhpLlm\LlmChain\Message\Content\Text;

final readonly class UserMessage implements MessageInterface
Expand All @@ -25,6 +26,17 @@ public function getRole(): Role
return Role::User;
}

public function hasImageContent(): bool
{
foreach ($this->content as $content) {
if ($content instanceof Image) {
return true;
}
}

return false;
}

/**
* @return array{
* role: Role::User,
Expand Down
5 changes: 5 additions & 0 deletions src/OpenAI/Model/Gpt.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public function supportsToolCalling(): bool
return true;
}

public function supportsImageInput(): bool
{
return $this->version->supportImageInput;
}

public function supportsStructuredOutput(): bool
{
return $this->version->supportStructuredOutput;
Expand Down
Loading

0 comments on commit f64bb0e

Please sign in to comment.