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 26, 2024
1 parent 3ab2764 commit ff58c37
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 38 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 @@ -23,7 +24,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 @@ -21,7 +22,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 @@ -21,7 +22,8 @@

$serpApi = new SerpApi($httpClient, $_ENV['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 @@ -21,7 +22,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 @@ -21,7 +22,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 @@ -21,7 +22,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
50 changes: 19 additions & 31 deletions src/Chain.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@

namespace PhpLlm\LlmChain;

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\Message;
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 @@ -26,39 +29,24 @@ public function __construct(
*/
public function call(MessageBag $messages, array $options = []): string|object
{
$llmOptions = $options;
$input = new Input($this->llm, $messages, $options);
array_map(fn (InputProcessor $processor) => $processor->processInput($input), $this->inputProcessor);

if ($messages->containsImage() && !$this->llm->supportsImageInput()) {
throw MissingModelSupport::forImageInput($this->llm::class);
}

if (!array_key_exists('tools', $llmOptions) && null !== $this->toolBox && $this->llm->supportsToolCalling()) {
$llmOptions['tools'] = $this->toolBox->getMap();
}

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']);
}
$response = $this->llm->call($messages, $input->getOptions());

$response = $this->llm->call($messages, $llmOptions);
$output = new Output($this->llm, $response, $messages, $options);
foreach ($this->outputProcessor as $outputProcessor) {
$result = $outputProcessor->processOutput($output);

while ($response->hasToolCalls()) {
$clonedMessages = clone $messages;
$clonedMessages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());

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;
}
10 changes: 10 additions & 0 deletions src/Exception/MissingModelSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ 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: 1 addition & 1 deletion src/OpenAI/Model/Gpt/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static function gpt4(): self

public static function gpt4Turbo(): self
{
return new self('gpt-4-turbo', true, false);
return new self('gpt-4-turbo', true);
}

public static function gpt4o(): self
Expand Down
43 changes: 43 additions & 0 deletions src/StructuredOutput/ChainProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\StructuredOutput;

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 Symfony\Component\Serializer\SerializerInterface;

final class ChainProcessor implements InputProcessor, OutputProcessor
{
private string $outputStructure;

public function __construct(
private readonly ResponseFormatFactory $responseFormatFactory,
private readonly SerializerInterface $serializer,
) {
}

public function processInput(Input $input): void
{
if (!$input->llm->supportsStructuredOutput()) {
throw MissingModelSupport::forStructuredOutput($input->llm::class);
}

$options = $input->getOptions();
$options['response_format'] = $this->responseFormatFactory->create($options['output_structure']);

$this->outputStructure = $options['output_structure'];
unset($options['output_structure']);

$input->setOptions($options);
}

public function processOutput(Output $output): object
{
return $this->serializer->deserialize($output->response->getContent(), $this->outputStructure, 'json');
}
}
49 changes: 49 additions & 0 deletions src/ToolBox/ChainProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\ToolBox;

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\Message;

final readonly class ChainProcessor implements InputProcessor, OutputProcessor
{
public function __construct(
private ToolBox $toolBox,
) {
}

public function processInput(Input $input): void
{
if (!$input->llm->supportsToolCalling()) {
throw MissingModelSupport::forToolCalling($input->llm::class);
}

$options['tools'] = $this->toolBox->getMap();
$input->setOptions($options);
}

public function processOutput(Output $output): mixed
{
$response = $output->response;
$messages = clone $output->messages;

while ($response->hasToolCalls()) {
$messages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());

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

$response = $output->llm->call($messages, $output->options);
}

return $response->getContent();
}
}

0 comments on commit ff58c37

Please sign in to comment.