Skip to content

Commit 2e8f434

Browse files
authored
feat: introducing chain processors (#47)
1 parent 3ab2764 commit 2e8f434

15 files changed

+220
-38
lines changed

examples/structured-output-math.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\StructuredOutput\ChainProcessor;
910
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
1011
use PhpLlm\LlmChain\StructuredOutput\SchemaFactory;
1112
use PhpLlm\LlmChain\Tests\StructuredOutput\Data\MathReasoning;
@@ -23,7 +24,8 @@
2324
$responseFormatFactory = new ResponseFormatFactory(SchemaFactory::create());
2425
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
2526

26-
$chain = new Chain($llm, responseFormatFactory: $responseFormatFactory, serializer: $serializer);
27+
$processor = new ChainProcessor($responseFormatFactory, $serializer);
28+
$chain = new Chain($llm, [$processor], [$processor]);
2729
$messages = new MessageBag(
2830
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
2931
Message::ofUser('how can I solve 8x + 7 = -23'),

examples/toolbox-clock.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
910
use PhpLlm\LlmChain\ToolBox\Tool\Clock;
1011
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
1112
use PhpLlm\LlmChain\ToolBox\ToolBox;
@@ -21,7 +22,8 @@
2122

2223
$clock = new Clock(new SymfonyClock());
2324
$toolBox = new ToolBox(new ToolAnalyzer(), [$clock]);
24-
$chain = new Chain($llm, $toolBox);
25+
$processor = new ChainProcessor($toolBox);
26+
$chain = new Chain($llm, [$processor], [$processor]);
2527

2628
$messages = new MessageBag(Message::ofUser('What date and time is it?'));
2729
$response = $chain->call($messages);

examples/toolbox-serpapi.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
910
use PhpLlm\LlmChain\ToolBox\Tool\SerpApi;
1011
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
1112
use PhpLlm\LlmChain\ToolBox\ToolBox;
@@ -21,7 +22,8 @@
2122

2223
$serpApi = new SerpApi($httpClient, $_ENV['SERP_API_KEY']);
2324
$toolBox = new ToolBox(new ToolAnalyzer(), [$serpApi]);
24-
$chain = new Chain($llm, $toolBox);
25+
$processor = new ChainProcessor($toolBox);
26+
$chain = new Chain($llm, [$processor], [$processor]);
2527

2628
$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
2729
$response = $chain->call($messages);

examples/toolbox-weather.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
910
use PhpLlm\LlmChain\ToolBox\Tool\OpenMeteo;
1011
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
1112
use PhpLlm\LlmChain\ToolBox\ToolBox;
@@ -21,7 +22,8 @@
2122

2223
$wikipedia = new OpenMeteo($httpClient);
2324
$toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]);
24-
$chain = new Chain($llm, $toolBox);
25+
$processor = new ChainProcessor($toolBox);
26+
$chain = new Chain($llm, [$processor], [$processor]);
2527

2628
$messages = new MessageBag(Message::ofUser('How is the weather currently in Berlin?'));
2729
$response = $chain->call($messages);

examples/toolbox-wikipedia.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
910
use PhpLlm\LlmChain\ToolBox\Tool\Wikipedia;
1011
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
1112
use PhpLlm\LlmChain\ToolBox\ToolBox;
@@ -21,7 +22,8 @@
2122

2223
$wikipedia = new Wikipedia($httpClient);
2324
$toolBox = new ToolBox(new ToolAnalyzer(), [$wikipedia]);
24-
$chain = new Chain($llm, $toolBox);
25+
$processor = new ChainProcessor($toolBox);
26+
$chain = new Chain($llm, [$processor], [$processor]);
2527

2628
$messages = new MessageBag(Message::ofUser('Who is the current chancellor of Germany?'));
2729
$response = $chain->call($messages);

examples/toolbox-youtube.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
77
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
88
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\ToolBox\ChainProcessor;
910
use PhpLlm\LlmChain\ToolBox\Tool\YouTubeTranscriber;
1011
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
1112
use PhpLlm\LlmChain\ToolBox\ToolBox;
@@ -21,7 +22,8 @@
2122

2223
$transcriber = new YouTubeTranscriber($httpClient);
2324
$toolBox = new ToolBox(new ToolAnalyzer(), [$transcriber]);
24-
$chain = new Chain($llm, $toolBox);
25+
$processor = new ChainProcessor($toolBox);
26+
$chain = new Chain($llm, [$processor], [$processor]);
2527

2628
$messages = new MessageBag(Message::ofUser('Please summarize this video for me: https://www.youtube.com/watch?v=6uXW-ulpj0s'));
2729
$response = $chain->call($messages);

src/Chain.php

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44

55
namespace PhpLlm\LlmChain;
66

7+
use PhpLlm\LlmChain\Chain\Input;
8+
use PhpLlm\LlmChain\Chain\InputProcessor;
9+
use PhpLlm\LlmChain\Chain\Output;
10+
use PhpLlm\LlmChain\Chain\OutputProcessor;
711
use PhpLlm\LlmChain\Exception\MissingModelSupport;
8-
use PhpLlm\LlmChain\Message\Message;
912
use PhpLlm\LlmChain\Message\MessageBag;
10-
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
11-
use PhpLlm\LlmChain\ToolBox\ToolBoxInterface;
12-
use Symfony\Component\Serializer\SerializerInterface;
1313

1414
final readonly class Chain
1515
{
16+
/**
17+
* @param InputProcessor[] $inputProcessor
18+
* @param OutputProcessor[] $outputProcessor
19+
*/
1620
public function __construct(
1721
private LanguageModel $llm,
18-
private ?ToolBoxInterface $toolBox = null,
19-
private ?ResponseFormatFactory $responseFormatFactory = null,
20-
private ?SerializerInterface $serializer = null,
22+
private array $inputProcessor = [],
23+
private array $outputProcessor = [],
2124
) {
2225
}
2326

@@ -26,39 +29,24 @@ public function __construct(
2629
*/
2730
public function call(MessageBag $messages, array $options = []): string|object
2831
{
29-
$llmOptions = $options;
32+
$input = new Input($this->llm, $messages, $options);
33+
array_map(fn (InputProcessor $processor) => $processor->processInput($input), $this->inputProcessor);
3034

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

35-
if (!array_key_exists('tools', $llmOptions) && null !== $this->toolBox && $this->llm->supportsToolCalling()) {
36-
$llmOptions['tools'] = $this->toolBox->getMap();
37-
}
38-
39-
if (array_key_exists('output_structure', $llmOptions) && null !== $this->responseFormatFactory && $this->llm->supportsStructuredOutput()) {
40-
$llmOptions['response_format'] = $this->responseFormatFactory->create($llmOptions['output_structure']);
41-
unset($llmOptions['output_structure']);
42-
}
39+
$response = $this->llm->call($messages, $input->getOptions());
4340

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

46-
while ($response->hasToolCalls()) {
47-
$clonedMessages = clone $messages;
48-
$clonedMessages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());
49-
50-
foreach ($response->getToolCalls() as $toolCall) {
51-
$result = $this->toolBox->execute($toolCall);
52-
$clonedMessages[] = Message::ofToolCall($toolCall, $result);
45+
if (null !== $result) {
46+
return $result;
5347
}
54-
55-
$response = $this->llm->call($clonedMessages, $llmOptions);
56-
}
57-
58-
if (!array_key_exists('output_structure', $options) || null === $this->serializer) {
59-
return $response->getContent();
6048
}
6149

62-
return $this->serializer->deserialize($response->getContent(), $options['output_structure'], 'json');
50+
return $response->getContent();
6351
}
6452
}

src/Chain/Input.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain;
6+
7+
use PhpLlm\LlmChain\LanguageModel;
8+
use PhpLlm\LlmChain\Message\MessageBag;
9+
10+
final class Input
11+
{
12+
/**
13+
* @param array<string, mixed> $options
14+
*/
15+
public function __construct(
16+
public readonly LanguageModel $llm,
17+
public readonly MessageBag $messages,
18+
private array $options,
19+
) {
20+
}
21+
22+
/**
23+
* @return array<string, mixed>
24+
*/
25+
public function getOptions(): array
26+
{
27+
return $this->options;
28+
}
29+
30+
/**
31+
* @param array<string, mixed> $options
32+
*/
33+
public function setOptions(array $options): void
34+
{
35+
$this->options = $options;
36+
}
37+
}

src/Chain/InputProcessor.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain;
6+
7+
interface InputProcessor
8+
{
9+
public function processInput(Input $input): void;
10+
}

src/Chain/Output.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain;
6+
7+
use PhpLlm\LlmChain\LanguageModel;
8+
use PhpLlm\LlmChain\Message\MessageBag;
9+
use PhpLlm\LlmChain\Response\Response;
10+
11+
final readonly class Output
12+
{
13+
/**
14+
* @param array<string, mixed> $options
15+
*/
16+
public function __construct(
17+
public LanguageModel $llm,
18+
public Response $response,
19+
public MessageBag $messages,
20+
public array $options,
21+
) {
22+
}
23+
}

src/Chain/OutputProcessor.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain;
6+
7+
interface OutputProcessor
8+
{
9+
public function processOutput(Output $output): mixed;
10+
}

src/Exception/MissingModelSupport.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ private function __construct(string $model, string $support)
1111
parent::__construct(sprintf('Model "%s" does not support "%s".', $model, $support));
1212
}
1313

14+
public static function forToolCalling(string $model): self
15+
{
16+
return new self($model, 'tool calling');
17+
}
18+
1419
public static function forImageInput(string $model): self
1520
{
1621
return new self($model, 'image input');
1722
}
23+
24+
public static function forStructuredOutput(string $model): self
25+
{
26+
return new self($model, 'structured output');
27+
}
1828
}

src/OpenAI/Model/Gpt/Version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static function gpt4(): self
3636

3737
public static function gpt4Turbo(): self
3838
{
39-
return new self('gpt-4-turbo', true, false);
39+
return new self('gpt-4-turbo', true);
4040
}
4141

4242
public static function gpt4o(): self
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\StructuredOutput;
6+
7+
use PhpLlm\LlmChain\Chain\Input;
8+
use PhpLlm\LlmChain\Chain\InputProcessor;
9+
use PhpLlm\LlmChain\Chain\Output;
10+
use PhpLlm\LlmChain\Chain\OutputProcessor;
11+
use PhpLlm\LlmChain\Exception\MissingModelSupport;
12+
use Symfony\Component\Serializer\SerializerInterface;
13+
14+
final class ChainProcessor implements InputProcessor, OutputProcessor
15+
{
16+
private string $outputStructure;
17+
18+
public function __construct(
19+
private readonly ResponseFormatFactory $responseFormatFactory,
20+
private readonly SerializerInterface $serializer,
21+
) {
22+
}
23+
24+
public function processInput(Input $input): void
25+
{
26+
if (!$input->llm->supportsStructuredOutput()) {
27+
throw MissingModelSupport::forStructuredOutput($input->llm::class);
28+
}
29+
30+
$options = $input->getOptions();
31+
$options['response_format'] = $this->responseFormatFactory->create($options['output_structure']);
32+
33+
$this->outputStructure = $options['output_structure'];
34+
unset($options['output_structure']);
35+
36+
$input->setOptions($options);
37+
}
38+
39+
public function processOutput(Output $output): object
40+
{
41+
return $this->serializer->deserialize($output->response->getContent(), $this->outputStructure, 'json');
42+
}
43+
}

src/ToolBox/ChainProcessor.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\ToolBox;
6+
7+
use PhpLlm\LlmChain\Chain\Input;
8+
use PhpLlm\LlmChain\Chain\InputProcessor;
9+
use PhpLlm\LlmChain\Chain\Output;
10+
use PhpLlm\LlmChain\Chain\OutputProcessor;
11+
use PhpLlm\LlmChain\Exception\MissingModelSupport;
12+
use PhpLlm\LlmChain\Message\Message;
13+
14+
final readonly class ChainProcessor implements InputProcessor, OutputProcessor
15+
{
16+
public function __construct(
17+
private ToolBox $toolBox,
18+
) {
19+
}
20+
21+
public function processInput(Input $input): void
22+
{
23+
if (!$input->llm->supportsToolCalling()) {
24+
throw MissingModelSupport::forToolCalling($input->llm::class);
25+
}
26+
27+
$options['tools'] = $this->toolBox->getMap();
28+
$input->setOptions($options);
29+
}
30+
31+
public function processOutput(Output $output): mixed
32+
{
33+
$response = $output->response;
34+
$messages = clone $output->messages;
35+
36+
while ($response->hasToolCalls()) {
37+
$messages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());
38+
39+
foreach ($response->getToolCalls() as $toolCall) {
40+
$result = $this->toolBox->execute($toolCall);
41+
$messages[] = Message::ofToolCall($toolCall, $result);
42+
}
43+
44+
$response = $output->llm->call($messages, $output->options);
45+
}
46+
47+
return $response->getContent();
48+
}
49+
}

0 commit comments

Comments
 (0)