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

Commit 009d216

Browse files
committed
feat: introduce chain awareness in chain processors to nest them
1 parent 1be8cb2 commit 009d216

File tree

9 files changed

+147
-32
lines changed

9 files changed

+147
-32
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain;
4+
use PhpLlm\LlmChain\Message\Message;
5+
use PhpLlm\LlmChain\Message\MessageBag;
6+
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
7+
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
8+
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
9+
use PhpLlm\LlmChain\StructuredOutput\ChainProcessor as StructuredOutputProcessor;
10+
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
11+
use PhpLlm\LlmChain\ToolBox\ChainProcessor as ToolProcessor;
12+
use PhpLlm\LlmChain\ToolBox\Tool\Clock;
13+
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
14+
use PhpLlm\LlmChain\ToolBox\ToolBox;
15+
use Symfony\Component\Clock\Clock as SymfonyClock;
16+
use Symfony\Component\Dotenv\Dotenv;
17+
use Symfony\Component\HttpClient\HttpClient;
18+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
19+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
20+
use Symfony\Component\Serializer\Serializer;
21+
22+
require_once dirname(__DIR__).'/vendor/autoload.php';
23+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
24+
25+
if (empty($_ENV['OPENAI_API_KEY'])) {
26+
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
27+
exit(1);
28+
}
29+
30+
$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']);
31+
$llm = new Gpt($platform, Version::gpt4oMini());
32+
33+
$clock = new Clock(new SymfonyClock());
34+
$toolBox = new ToolBox(new ToolAnalyzer(), [$clock]);
35+
$toolProcessor = new ToolProcessor($toolBox);
36+
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
37+
$structuredOutputProcessor = new StructuredOutputProcessor(new ResponseFormatFactory(), $serializer);
38+
$chain = new Chain($llm, [$toolProcessor, $structuredOutputProcessor], [$toolProcessor, $structuredOutputProcessor]);
39+
40+
$messages = new MessageBag(Message::ofUser('What date and time is it?'));
41+
$response = $chain->call($messages, ['response_format' => [
42+
'type' => 'json_schema',
43+
'json_schema' => [
44+
'name' => 'clock',
45+
'strict' => true,
46+
'schema' => [
47+
'type' => 'object',
48+
'properties' => [
49+
'date' => ['type' => 'string', 'description' => 'The current date in the format YYYY-MM-DD.'],
50+
'time' => ['type' => 'string', 'description' => 'The current time in the format HH:MM:SS.'],
51+
],
52+
'required' => ['date', 'time'],
53+
'additionalProperties' => false,
54+
],
55+
],
56+
]]);
57+
58+
dump($response->getContent());

src/Chain.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
namespace PhpLlm\LlmChain;
66

7+
use PhpLlm\LlmChain\Chain\ChainAwareProcessor;
78
use PhpLlm\LlmChain\Chain\Input;
89
use PhpLlm\LlmChain\Chain\InputProcessor;
910
use PhpLlm\LlmChain\Chain\Output;
1011
use PhpLlm\LlmChain\Chain\OutputProcessor;
12+
use PhpLlm\LlmChain\Exception\InvalidArgumentException;
1113
use PhpLlm\LlmChain\Exception\MissingModelSupport;
1214
use PhpLlm\LlmChain\Message\MessageBag;
1315
use PhpLlm\LlmChain\Response\ResponseInterface;
@@ -33,8 +35,8 @@ public function __construct(
3335
iterable $inputProcessor = [],
3436
iterable $outputProcessor = [],
3537
) {
36-
$this->inputProcessor = $inputProcessor instanceof \Traversable ? iterator_to_array($inputProcessor) : $inputProcessor;
37-
$this->outputProcessor = $outputProcessor instanceof \Traversable ? iterator_to_array($outputProcessor) : $outputProcessor;
38+
$this->inputProcessor = $this->initializeProcessors($inputProcessor, InputProcessor::class);
39+
$this->outputProcessor = $this->initializeProcessors($outputProcessor, OutputProcessor::class);
3840
}
3941

4042
/**
@@ -52,14 +54,28 @@ public function call(MessageBag $messages, array $options = []): ResponseInterfa
5254
$response = $this->llm->call($messages, $options = $input->getOptions());
5355

5456
$output = new Output($this->llm, $response, $messages, $options);
55-
foreach ($this->outputProcessor as $outputProcessor) {
56-
$result = $outputProcessor->processOutput($output);
57+
array_map(fn (OutputProcessor $processor) => $processor->processOutput($output), $this->outputProcessor);
5758

58-
if (null !== $result) {
59-
return $result;
59+
return $output->response;
60+
}
61+
62+
/**
63+
* @param InputProcessor[]|OutputProcessor[] $processors
64+
*
65+
* @return InputProcessor[]|OutputProcessor[]
66+
*/
67+
private function initializeProcessors(iterable $processors, string $interface): array
68+
{
69+
foreach ($processors as $processor) {
70+
if (!$processor instanceof $interface) {
71+
throw new InvalidArgumentException(sprintf('Processor %s must implement %s interface.', $processor::class, $interface));
72+
}
73+
74+
if ($processor instanceof ChainAwareProcessor) {
75+
$processor->setChain($this);
6076
}
6177
}
6278

63-
return $response;
79+
return $processors instanceof \Traversable ? iterator_to_array($processors) : $processors;
6480
}
6581
}

src/Chain/ChainAwareProcessor.php

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

src/Chain/ChainAwareTrait.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain;
6+
7+
use PhpLlm\LlmChain\Chain;
8+
9+
trait ChainAwareTrait
10+
{
11+
private Chain $chain;
12+
13+
public function setChain(Chain $chain): void
14+
{
15+
$this->chain = $chain;
16+
}
17+
}

src/Chain/Output.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
use PhpLlm\LlmChain\Message\MessageBag;
99
use PhpLlm\LlmChain\Response\ResponseInterface;
1010

11-
final readonly class Output
11+
final class Output
1212
{
1313
/**
1414
* @param array<string, mixed> $options
1515
*/
1616
public function __construct(
17-
public LanguageModel $llm,
17+
public readonly LanguageModel $llm,
1818
public ResponseInterface $response,
19-
public MessageBag $messages,
20-
public array $options,
19+
public readonly MessageBag $messages,
20+
public readonly array $options,
2121
) {
2222
}
2323
}

src/Chain/OutputProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66

77
interface OutputProcessor
88
{
9-
public function processOutput(Output $output): mixed;
9+
public function processOutput(Output $output): void;
1010
}

src/StructuredOutput/ChainProcessor.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,25 @@ public function processInput(Input $input): void
4747
$input->setOptions($options);
4848
}
4949

50-
public function processOutput(Output $output): ?StructuredResponse
50+
public function processOutput(Output $output): void
5151
{
5252
$options = $output->options;
5353

54-
if (!isset($options['output_structure'])) {
55-
return null;
54+
if ($output->response instanceof StructuredResponse) {
55+
return;
56+
}
57+
58+
if (!isset($options['response_format'])) {
59+
return;
60+
}
61+
62+
if (!isset($this->outputStructure)) {
63+
$output->response = new StructuredResponse(json_decode($output->response->getContent(), true));
64+
65+
return;
5666
}
5767

58-
return new StructuredResponse(
68+
$output->response = new StructuredResponse(
5969
$this->serializer->deserialize($output->response->getContent(), $this->outputStructure, 'json')
6070
);
6171
}

src/ToolBox/ChainProcessor.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44

55
namespace PhpLlm\LlmChain\ToolBox;
66

7+
use PhpLlm\LlmChain\Chain\ChainAwareProcessor;
8+
use PhpLlm\LlmChain\Chain\ChainAwareTrait;
79
use PhpLlm\LlmChain\Chain\Input;
810
use PhpLlm\LlmChain\Chain\InputProcessor;
911
use PhpLlm\LlmChain\Chain\Output;
1012
use PhpLlm\LlmChain\Chain\OutputProcessor;
1113
use PhpLlm\LlmChain\Exception\MissingModelSupport;
1214
use PhpLlm\LlmChain\Message\Message;
13-
use PhpLlm\LlmChain\Response\ResponseInterface;
1415
use PhpLlm\LlmChain\Response\ToolCallResponse;
1516

16-
final readonly class ChainProcessor implements InputProcessor, OutputProcessor
17+
final class ChainProcessor implements InputProcessor, OutputProcessor, ChainAwareProcessor
1718
{
19+
use ChainAwareTrait;
20+
1821
public function __construct(
1922
private ToolBoxInterface $toolBox,
2023
) {
@@ -31,23 +34,20 @@ public function processInput(Input $input): void
3134
$input->setOptions($options);
3235
}
3336

34-
public function processOutput(Output $output): ResponseInterface
37+
public function processOutput(Output $output): void
3538
{
36-
$response = $output->response;
3739
$messages = clone $output->messages;
3840

39-
while ($response instanceof ToolCallResponse) {
40-
$toolCalls = $response->getContent();
41+
while ($output->response instanceof ToolCallResponse) {
42+
$toolCalls = $output->response->getContent();
4143
$messages[] = Message::ofAssistant(toolCalls: $toolCalls);
4244

4345
foreach ($toolCalls as $toolCall) {
4446
$result = $this->toolBox->execute($toolCall);
4547
$messages[] = Message::ofToolCall($toolCall, $result);
4648
}
4749

48-
$response = $output->llm->call($messages, $output->options);
50+
$output->response = $this->chain->call($messages, $output->options);
4951
}
50-
51-
return $response;
5252
}
5353
}

tests/ToolBox/ChainProcessorTest.php renamed to tests/StructuredOutput/ChainProcessorTest.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
declare(strict_types=1);
44

5-
namespace PhpLlm\LlmChain\Tests\ToolBox;
5+
namespace PhpLlm\LlmChain\Tests\StructuredOutput;
66

77
use PhpLlm\LlmChain\Chain\Input;
88
use PhpLlm\LlmChain\Chain\Output;
99
use PhpLlm\LlmChain\Exception\MissingModelSupport;
1010
use PhpLlm\LlmChain\LanguageModel;
1111
use PhpLlm\LlmChain\Message\MessageBag;
1212
use PhpLlm\LlmChain\Response\Choice;
13+
use PhpLlm\LlmChain\Response\StructuredResponse;
1314
use PhpLlm\LlmChain\Response\TextResponse;
1415
use PhpLlm\LlmChain\StructuredOutput\ChainProcessor;
1516
use PhpLlm\LlmChain\Tests\Double\ConfigurableResponseFormatFactory;
@@ -96,12 +97,13 @@ public function processOutputWithResponseFormat(): void
9697

9798
$response = new TextResponse('{"some": "data"}');
9899

99-
$output = new Output($llm, $response, new MessageBag(), $options);
100+
$output = new Output($llm, $response, new MessageBag(), $input->getOptions());
100101

101-
$result = $chainProcessor->processOutput($output)->getContent();
102+
$chainProcessor->processOutput($output);
102103

103-
self::assertInstanceOf(SomeStructure::class, $result);
104-
self::assertSame('data', $result->some);
104+
self::assertInstanceOf(StructuredResponse::class, $output->response);
105+
self::assertInstanceOf(SomeStructure::class, $output->response->getContent());
106+
self::assertSame('data', $output->response->getContent()->some);
105107
}
106108

107109
#[Test]
@@ -116,8 +118,8 @@ public function processOutputWithoutResponseFormat(): void
116118

117119
$output = new Output($llm, $response, new MessageBag(), []);
118120

119-
$result = $chainProcessor->processOutput($output);
121+
$chainProcessor->processOutput($output);
120122

121-
self::assertNull($result);
123+
self::assertSame($response, $output->response);
122124
}
123125
}

0 commit comments

Comments
 (0)