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

Commit 245e53f

Browse files
authored
fix: implement failing test and cleanup (#308)
1 parent 2fc8250 commit 245e53f

File tree

4 files changed

+76
-29
lines changed

4 files changed

+76
-29
lines changed

examples/openai/structured-output-clock.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@
44
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
55
use PhpLlm\LlmChain\Chain;
66
use PhpLlm\LlmChain\Chain\StructuredOutput\ChainProcessor as StructuredOutputProcessor;
7-
use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactory;
87
use PhpLlm\LlmChain\Chain\Toolbox\ChainProcessor as ToolProcessor;
98
use PhpLlm\LlmChain\Chain\Toolbox\Tool\Clock;
109
use PhpLlm\LlmChain\Chain\Toolbox\Toolbox;
1110
use PhpLlm\LlmChain\Model\Message\Message;
1211
use PhpLlm\LlmChain\Model\Message\MessageBag;
1312
use Symfony\Component\Clock\Clock as SymfonyClock;
1413
use Symfony\Component\Dotenv\Dotenv;
15-
use Symfony\Component\Serializer\Encoder\JsonEncoder;
16-
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
17-
use Symfony\Component\Serializer\Serializer;
1814

1915
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
2016
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
@@ -30,8 +26,7 @@
3026
$clock = new Clock(new SymfonyClock());
3127
$toolbox = Toolbox::create($clock);
3228
$toolProcessor = new ToolProcessor($toolbox);
33-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
34-
$structuredOutputProcessor = new StructuredOutputProcessor(new ResponseFormatFactory(), $serializer);
29+
$structuredOutputProcessor = new StructuredOutputProcessor();
3530
$chain = new Chain($platform, $llm, [$toolProcessor, $structuredOutputProcessor], [$toolProcessor, $structuredOutputProcessor]);
3631

3732
$messages = new MessageBag(Message::ofUser('What date and time is it?'));

examples/openai/structured-output-math.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@
44
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
55
use PhpLlm\LlmChain\Chain;
66
use PhpLlm\LlmChain\Chain\StructuredOutput\ChainProcessor;
7-
use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactory;
87
use PhpLlm\LlmChain\Model\Message\Message;
98
use PhpLlm\LlmChain\Model\Message\MessageBag;
109
use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\MathReasoning;
1110
use Symfony\Component\Dotenv\Dotenv;
12-
use Symfony\Component\Serializer\Encoder\JsonEncoder;
13-
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
14-
use Symfony\Component\Serializer\Serializer;
1511

1612
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
1713
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
@@ -23,9 +19,8 @@
2319

2420
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
2521
$llm = new GPT(GPT::GPT_4O_MINI);
26-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
2722

28-
$processor = new ChainProcessor(new ResponseFormatFactory(), $serializer);
23+
$processor = new ChainProcessor();
2924
$chain = new Chain($platform, $llm, [$processor], [$processor]);
3025
$messages = new MessageBag(
3126
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),

src/Chain/StructuredOutput/ChainProcessor.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,27 @@
1111
use PhpLlm\LlmChain\Exception\InvalidArgumentException;
1212
use PhpLlm\LlmChain\Exception\MissingModelSupport;
1313
use PhpLlm\LlmChain\Model\Response\StructuredResponse;
14+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
15+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
16+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
17+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
18+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
19+
use Symfony\Component\Serializer\Serializer;
1420
use Symfony\Component\Serializer\SerializerInterface;
1521

1622
final class ChainProcessor implements InputProcessor, OutputProcessor
1723
{
1824
private string $outputStructure;
1925

2026
public function __construct(
21-
private readonly ResponseFormatFactoryInterface $responseFormatFactory,
22-
private readonly SerializerInterface $serializer,
27+
private readonly ResponseFormatFactoryInterface $responseFormatFactory = new ResponseFormatFactory(),
28+
private ?SerializerInterface $serializer = null,
2329
) {
30+
if (null === $this->serializer) {
31+
$propertyInfo = new PropertyInfoExtractor([], [new PhpDocExtractor()]);
32+
$normalizers = [new ObjectNormalizer(propertyTypeExtractor: $propertyInfo), new ArrayDenormalizer()];
33+
$this->serializer = $serializer ?? new Serializer($normalizers, [new JsonEncoder()]);
34+
}
2435
}
2536

2637
public function processInput(Input $input): void

tests/Chain/StructuredOutput/ChainProcessorTest.php

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
use PhpLlm\LlmChain\Model\Response\TextResponse;
1616
use PhpLlm\LlmChain\Tests\Double\ConfigurableResponseFormatFactory;
1717
use PhpLlm\LlmChain\Tests\Fixture\SomeStructure;
18+
use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\MathReasoning;
19+
use PhpLlm\LlmChain\Tests\Fixture\StructuredOutput\Step;
1820
use PHPUnit\Framework\Attributes\CoversClass;
1921
use PHPUnit\Framework\Attributes\Test;
2022
use PHPUnit\Framework\Attributes\UsesClass;
2123
use PHPUnit\Framework\TestCase;
22-
use Symfony\Component\Serializer\Encoder\JsonEncoder;
23-
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
24-
use Symfony\Component\Serializer\Serializer;
2524
use Symfony\Component\Serializer\SerializerInterface;
2625

2726
#[CoversClass(ChainProcessor::class)]
@@ -37,9 +36,7 @@ final class ChainProcessorTest extends TestCase
3736
#[Test]
3837
public function processInputWithOutputStructure(): void
3938
{
40-
$responseFormatFactory = new ConfigurableResponseFormatFactory(['some' => 'format']);
41-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
42-
$chainProcessor = new ChainProcessor($responseFormatFactory, $serializer);
39+
$chainProcessor = new ChainProcessor(new ConfigurableResponseFormatFactory(['some' => 'format']));
4340

4441
$llm = self::createMock(LanguageModel::class);
4542
$llm->method('supportsStructuredOutput')->willReturn(true);
@@ -54,9 +51,7 @@ public function processInputWithOutputStructure(): void
5451
#[Test]
5552
public function processInputWithoutOutputStructure(): void
5653
{
57-
$responseFormatFactory = new ConfigurableResponseFormatFactory();
58-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
59-
$chainProcessor = new ChainProcessor($responseFormatFactory, $serializer);
54+
$chainProcessor = new ChainProcessor(new ConfigurableResponseFormatFactory());
6055

6156
$llm = self::createMock(LanguageModel::class);
6257
$input = new Input($llm, new MessageBag(), []);
@@ -71,9 +66,7 @@ public function processInputThrowsExceptionWhenLlmDoesNotSupportStructuredOutput
7166
{
7267
self::expectException(MissingModelSupport::class);
7368

74-
$responseFormatFactory = new ConfigurableResponseFormatFactory();
75-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
76-
$chainProcessor = new ChainProcessor($responseFormatFactory, $serializer);
69+
$chainProcessor = new ChainProcessor(new ConfigurableResponseFormatFactory());
7770

7871
$llm = self::createMock(LanguageModel::class);
7972
$llm->method('supportsStructuredOutput')->willReturn(false);
@@ -86,9 +79,7 @@ public function processInputThrowsExceptionWhenLlmDoesNotSupportStructuredOutput
8679
#[Test]
8780
public function processOutputWithResponseFormat(): void
8881
{
89-
$responseFormatFactory = new ConfigurableResponseFormatFactory(['some' => 'format']);
90-
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
91-
$chainProcessor = new ChainProcessor($responseFormatFactory, $serializer);
82+
$chainProcessor = new ChainProcessor(new ConfigurableResponseFormatFactory(['some' => 'format']));
9283

9384
$llm = self::createMock(LanguageModel::class);
9485
$llm->method('supportsStructuredOutput')->willReturn(true);
@@ -108,6 +99,61 @@ public function processOutputWithResponseFormat(): void
10899
self::assertSame('data', $output->response->getContent()->some);
109100
}
110101

102+
#[Test]
103+
public function processOutputWithComplexResponseFormat(): void
104+
{
105+
$chainProcessor = new ChainProcessor(new ConfigurableResponseFormatFactory(['some' => 'format']));
106+
107+
$llm = self::createMock(LanguageModel::class);
108+
$llm->method('supportsStructuredOutput')->willReturn(true);
109+
110+
$options = ['output_structure' => MathReasoning::class];
111+
$input = new Input($llm, new MessageBag(), $options);
112+
$chainProcessor->processInput($input);
113+
114+
$response = new TextResponse(<<<JSON
115+
{
116+
"steps": [
117+
{
118+
"explanation": "We want to isolate the term with x. First, let's subtract 7 from both sides of the equation.",
119+
"output": "8x + 7 - 7 = -23 - 7"
120+
},
121+
{
122+
"explanation": "This simplifies to 8x = -30.",
123+
"output": "8x = -30"
124+
},
125+
{
126+
"explanation": "Next, to solve for x, we need to divide both sides of the equation by 8.",
127+
"output": "x = -30 / 8"
128+
},
129+
{
130+
"explanation": "Now we simplify -30 / 8 to its simplest form.",
131+
"output": "x = -15 / 4"
132+
},
133+
{
134+
"explanation": "Dividing both the numerator and the denominator by their greatest common divisor, we finalize our solution.",
135+
"output": "x = -3.75"
136+
}
137+
],
138+
"finalAnswer": "x = -3.75"
139+
}
140+
JSON);
141+
142+
$output = new Output($llm, $response, new MessageBag(), $input->getOptions());
143+
144+
$chainProcessor->processOutput($output);
145+
146+
self::assertInstanceOf(StructuredResponse::class, $output->response);
147+
self::assertInstanceOf(MathReasoning::class, $structure = $output->response->getContent());
148+
self::assertCount(5, $structure->steps);
149+
self::assertInstanceOf(Step::class, $structure->steps[0]);
150+
self::assertInstanceOf(Step::class, $structure->steps[1]);
151+
self::assertInstanceOf(Step::class, $structure->steps[2]);
152+
self::assertInstanceOf(Step::class, $structure->steps[3]);
153+
self::assertInstanceOf(Step::class, $structure->steps[4]);
154+
self::assertSame('x = -3.75', $structure->finalAnswer);
155+
}
156+
111157
#[Test]
112158
public function processOutputWithoutResponseFormat(): void
113159
{

0 commit comments

Comments
 (0)