Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

feat: introducing chain processors #47

Merged
merged 1 commit into from
Sep 26, 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
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();
}
}