Skip to content

Commit 3513643

Browse files
authored
feat: implement universal chain (#5)
1 parent 0d6863b commit 3513643

25 files changed

+388
-183
lines changed

examples/chat-claude-anthropic.php

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

33
use PhpLlm\LlmChain\Anthropic\Model\Claude;
44
use PhpLlm\LlmChain\Anthropic\Runtime\Anthropic;
5-
use PhpLlm\LlmChain\Chat;
5+
use PhpLlm\LlmChain\Chain;
66
use PhpLlm\LlmChain\Message\Message;
77
use PhpLlm\LlmChain\Message\MessageBag;
88
use Symfony\Component\HttpClient\HttpClient;
@@ -12,11 +12,11 @@
1212
$runtime = new Anthropic(HttpClient::create(), getenv('ANTHROPIC_API_KEY'));
1313
$llm = new Claude($runtime);
1414

15-
$chat = new Chat($llm);
15+
$chain = new Chain($llm);
1616
$messages = new MessageBag(
1717
Message::forSystem('You are a pirate and you write funny.'),
1818
Message::ofUser('What is the Symfony framework?'),
1919
);
20-
$response = $chat->call($messages);
20+
$response = $chain->call($messages);
2121

2222
echo $response.PHP_EOL;

examples/chat-gpt-azure.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
use PhpLlm\LlmChain\Chat;
3+
use PhpLlm\LlmChain\Chain;
44
use PhpLlm\LlmChain\Message\Message;
55
use PhpLlm\LlmChain\Message\MessageBag;
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
@@ -18,11 +18,11 @@
1818
);
1919
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
2020

21-
$chat = new Chat($llm);
21+
$chain = new Chain($llm);
2222
$messages = new MessageBag(
2323
Message::forSystem('You are a pirate and you write funny.'),
2424
Message::ofUser('What is the Symfony framework?'),
2525
);
26-
$response = $chat->call($messages);
26+
$response = $chain->call($messages);
2727

2828
echo $response.PHP_EOL;

examples/chat-gpt-openai.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
use PhpLlm\LlmChain\Chat;
3+
use PhpLlm\LlmChain\Chain;
44
use PhpLlm\LlmChain\Message\Message;
55
use PhpLlm\LlmChain\Message\MessageBag;
66
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
@@ -13,11 +13,11 @@
1313
$runtime = new OpenAI(HttpClient::create(), getenv('OPENAI_API_KEY'));
1414
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
1515

16-
$chat = new Chat($llm);
16+
$chain = new Chain($llm);
1717
$messages = new MessageBag(
1818
Message::forSystem('You are a pirate and you write funny.'),
1919
Message::ofUser('What is the Symfony framework?'),
2020
);
21-
$response = $chat->call($messages);
21+
$response = $chain->call($messages);
2222

2323
echo $response.PHP_EOL;
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<?php
22

3+
use PhpLlm\LlmChain\Chain;
34
use PhpLlm\LlmChain\Message\Message;
45
use PhpLlm\LlmChain\Message\MessageBag;
56
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
67
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
78
use PhpLlm\LlmChain\OpenAI\Runtime\OpenAI;
9+
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
810
use PhpLlm\LlmChain\StructuredOutput\SchemaFactory;
9-
use PhpLlm\LlmChain\StructuredOutputChain;
1011
use PhpLlm\LlmChain\Tests\StructuredOutput\Data\MathReasoning;
1112
use Symfony\Component\HttpClient\HttpClient;
1213
use Symfony\Component\Serializer\Encoder\JsonEncoder;
@@ -17,13 +18,14 @@
1718

1819
$runtime = new OpenAI(HttpClient::create(), getenv('OPENAI_API_KEY'));
1920
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
21+
$responseFormatFactory = new ResponseFormatFactory(SchemaFactory::create());
22+
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
2023

21-
$chain = new StructuredOutputChain($llm, SchemaFactory::create(), new Serializer([new ObjectNormalizer()], [new JsonEncoder()]));
22-
24+
$chain = new Chain($llm, responseFormatFactory: $responseFormatFactory, serializer: $serializer);
2325
$messages = new MessageBag(
2426
Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'),
2527
Message::ofUser('how can I solve 8x + 7 = -23'),
2628
);
27-
$response = $chain->call($messages, MathReasoning::class);
29+
$response = $chain->call($messages, ['output_structure' => MathReasoning::class]);
2830

2931
dump($response);

examples/toolchain-clock.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use PhpLlm\LlmChain\Chain;
34
use PhpLlm\LlmChain\Message\Message;
45
use PhpLlm\LlmChain\Message\MessageBag;
56
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
@@ -9,8 +10,6 @@
910
use PhpLlm\LlmChain\ToolBox\Registry;
1011
use PhpLlm\LlmChain\ToolBox\Tool\Clock;
1112
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
12-
use PhpLlm\LlmChain\ToolChain;
13-
use Psr\Log\NullLogger;
1413
use Symfony\Component\Clock\Clock as SymfonyClock;
1514
use Symfony\Component\HttpClient\HttpClient;
1615

@@ -20,8 +19,8 @@
2019
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
2120

2221
$clock = new Clock(new SymfonyClock());
23-
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), new NullLogger(), [$clock]);
24-
$chain = new ToolChain($llm, $registry);
22+
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), [$clock]);
23+
$chain = new Chain($llm, $registry);
2524

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

examples/toolchain-serpapi.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use PhpLlm\LlmChain\Chain;
34
use PhpLlm\LlmChain\Message\Message;
45
use PhpLlm\LlmChain\Message\MessageBag;
56
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
@@ -9,8 +10,6 @@
910
use PhpLlm\LlmChain\ToolBox\Registry;
1011
use PhpLlm\LlmChain\ToolBox\Tool\SerpApi;
1112
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
12-
use PhpLlm\LlmChain\ToolChain;
13-
use Psr\Log\NullLogger;
1413
use Symfony\Component\HttpClient\HttpClient;
1514

1615
require_once dirname(__DIR__).'/vendor/autoload.php';
@@ -20,8 +19,8 @@
2019
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
2120

2221
$serpApi = new SerpApi($httpClient, getenv('SERP_API_KEY'));
23-
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), new NullLogger(), [$serpApi]);
24-
$chain = new ToolChain($llm, $registry);
22+
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), [$serpApi]);
23+
$chain = new Chain($llm, $registry);
2524

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

examples/toolchain-wikipedia.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use PhpLlm\LlmChain\Chain;
34
use PhpLlm\LlmChain\Message\Message;
45
use PhpLlm\LlmChain\Message\MessageBag;
56
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
@@ -9,8 +10,6 @@
910
use PhpLlm\LlmChain\ToolBox\Registry;
1011
use PhpLlm\LlmChain\ToolBox\Tool\Wikipedia;
1112
use PhpLlm\LlmChain\ToolBox\ToolAnalyzer;
12-
use PhpLlm\LlmChain\ToolChain;
13-
use Psr\Log\NullLogger;
1413
use Symfony\Component\HttpClient\HttpClient;
1514

1615
require_once dirname(__DIR__).'/vendor/autoload.php';
@@ -20,8 +19,8 @@
2019
$llm = new Gpt($runtime, Version::GPT_4o_MINI);
2120

2221
$wikipedia = new Wikipedia($httpClient);
23-
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), new NullLogger(), [$wikipedia]);
24-
$chain = new ToolChain($llm, $registry);
22+
$registry = new Registry(new ToolAnalyzer(new ParameterAnalyzer()), [$wikipedia]);
23+
$chain = new Chain($llm, $registry);
2524

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

src/Anthropic/Model/Claude.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use PhpLlm\LlmChain\Anthropic\Model\Claude\Version;
99
use PhpLlm\LlmChain\LanguageModel;
1010
use PhpLlm\LlmChain\Message\MessageBag;
11+
use PhpLlm\LlmChain\Response\Choice;
12+
use PhpLlm\LlmChain\Response\Response;
1113

1214
final class Claude implements LanguageModel
1315
{
@@ -19,16 +21,28 @@ public function __construct(
1921
) {
2022
}
2123

22-
public function call(MessageBag $messages, array $options = []): array
24+
public function call(MessageBag $messages, array $options = []): Response
2325
{
2426
$system = $messages->getSystemMessage();
2527

26-
return $this->runtime->request([
28+
$response = $this->runtime->request([
2729
'model' => $this->version->value,
2830
'temperature' => $this->temperature,
2931
'max_tokens' => $this->maxTokens,
3032
'system' => $system->content,
3133
'messages' => $messages->withoutSystemMessage(),
3234
]);
35+
36+
return new Response(new Choice($response['content'][0]['text']));
37+
}
38+
39+
public function hasToolSupport(): bool
40+
{
41+
return false; // it does, but implementation here is still open.
42+
}
43+
44+
public function hasStructuredOutputSupport(): bool
45+
{
46+
return false;
3347
}
3448
}

src/Chain.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain;
6+
7+
use PhpLlm\LlmChain\Message\Message;
8+
use PhpLlm\LlmChain\Message\MessageBag;
9+
use PhpLlm\LlmChain\StructuredOutput\ResponseFormatFactory;
10+
use PhpLlm\LlmChain\ToolBox\RegistryInterface;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
13+
final readonly class Chain
14+
{
15+
public function __construct(
16+
private LanguageModel $llm,
17+
private ?RegistryInterface $toolRegistry = null,
18+
private ?ResponseFormatFactory $responseFormatFactory = null,
19+
private ?SerializerInterface $serializer = null,
20+
) {
21+
}
22+
23+
/**
24+
* @param array<string, mixed> $options
25+
*/
26+
public function call(MessageBag $messages, array $options = []): string|object
27+
{
28+
$llmOptions = $options;
29+
30+
if (!array_key_exists('tools', $llmOptions) && null !== $this->toolRegistry && $this->llm->hasToolSupport()) {
31+
$llmOptions['tools'] = $this->toolRegistry->getMap();
32+
}
33+
34+
if (array_key_exists('output_structure', $llmOptions) && null !== $this->responseFormatFactory && $this->llm->hasStructuredOutputSupport()) {
35+
$llmOptions['response_format'] = $this->responseFormatFactory->create($llmOptions['output_structure']);
36+
unset($llmOptions['output_structure']);
37+
}
38+
39+
$response = $this->llm->call($messages, $llmOptions);
40+
41+
while ($response->hasToolCalls()) {
42+
$clonedMessages = clone $messages;
43+
$clonedMessages[] = Message::ofAssistant(toolCalls: $response->getToolCalls());
44+
45+
foreach ($response->getToolCalls() as $toolCall) {
46+
$result = $this->toolRegistry->execute($toolCall);
47+
$clonedMessages[] = Message::ofToolCall($toolCall, $result);
48+
}
49+
50+
$response = $this->llm->call($clonedMessages, $llmOptions);
51+
}
52+
53+
if (!array_key_exists('output_structure', $options) || null === $this->serializer) {
54+
return $response->getContent();
55+
}
56+
57+
return $this->serializer->deserialize($response->getContent(), $options['output_structure'], 'json');
58+
}
59+
}

src/Chat.php

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)