Skip to content

Commit 5b9da48

Browse files
authored
Merge pull request #20 from llm-agents-php/feature/output-formatter
Adds an ability to format LLM response into a given format
2 parents 6e2d40f + cf2973f commit 5b9da48

35 files changed

+557
-114
lines changed

src/Agent/AgentExecutor.php

-98
This file was deleted.

src/Agent/AgentRegistry.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function register(AgentInterface $agent): void
2727

2828
public function get(string $key): AgentInterface
2929
{
30-
if (! $this->has($key)) {
30+
if (!$this->has($key)) {
3131
throw new AgentNotFoundException(\sprintf('Agent with key \'%s\' is not registered.', $key));
3232
}
3333

src/AgentExecutor/ExecutionInput.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use LLM\Agents\LLM\ContextInterface;
88
use LLM\Agents\LLM\OptionsInterface;
9-
use LLM\Agents\LLM\Prompt\Chat\PromptInterface;
9+
use LLM\Agents\LLM\Prompt\PromptInterface;
1010
use LLM\Agents\LLM\PromptContextInterface;
1111

1212
/**

src/AgentExecutor/Interceptor/GeneratePromptInterceptor.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface;
1111
use LLM\Agents\AgentExecutor\InterceptorHandler;
1212
use LLM\Agents\LLM\AgentPromptGeneratorInterface;
13-
use LLM\Agents\LLM\Prompt\Chat\PromptInterface;
13+
use LLM\Agents\LLM\Prompt\PromptInterface;
1414

1515
/**
1616
* This interceptor is responsible for generating the prompt for the agent.
17+
* If the input does not have a prompt, it will generate one using the prompt generator.
18+
* After the execution, it will remove temporary messages (which implement LLM\Agents\LLM\Prompt\Chat\TempMessageInterface) from the prompt.
1719
*/
1820
final readonly class GeneratePromptInterceptor implements ExecutorInterceptorInterface
1921
{
@@ -36,6 +38,17 @@ public function execute(
3638
);
3739
}
3840

39-
return $next($input);
41+
$execution = $next($input);
42+
43+
// Remove temporary messages from the prompt.
44+
$prompt = $execution->prompt;
45+
if ($prompt instanceof \LLM\Agents\LLM\Prompt\Chat\PromptInterface) {
46+
$prompt = $prompt->withoutTempMessages();
47+
}
48+
49+
return new Execution(
50+
result: $execution->result,
51+
prompt: $prompt,
52+
);
4053
}
4154
}

src/AgentExecutor/Interceptor/InjectOptionsInterceptor.php

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface;
1111
use LLM\Agents\AgentExecutor\InterceptorHandler;
1212

13+
/**
14+
* This interceptor is responsible for injecting the agent's configuration options into the execution options.
15+
*/
1316
final readonly class InjectOptionsInterceptor implements ExecutorInterceptorInterface
1417
{
1518
public function __construct(

src/AgentExecutor/Interceptor/InjectResponseIntoPromptInterceptor.php

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use LLM\Agents\LLM\Response\ChatResponse;
1212
use LLM\Agents\LLM\Response\ToolCalledResponse;
1313

14+
/**
15+
* This interceptor is responsible for injecting the LLM response into the prompt history.
16+
*/
1417
final class InjectResponseIntoPromptInterceptor implements ExecutorInterceptorInterface
1518
{
1619
public function execute(

src/AgentExecutor/Interceptor/InjectToolsInterceptor.php

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use LLM\Agents\Tool\ToolInterface;
1717
use LLM\Agents\Tool\ToolRepositoryInterface;
1818

19+
/**
20+
* This interceptor is responsible for injecting the tools into the prompt if the agent has linked tools.
21+
*/
1922
final readonly class InjectToolsInterceptor implements ExecutorInterceptorInterface
2023
{
2124
public function __construct(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LLM\Agents\AgentExecutor\Interceptor;
6+
7+
use LLM\Agents\Agent\Execution;
8+
use LLM\Agents\AgentExecutor\ExecutionInput;
9+
use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface;
10+
use LLM\Agents\AgentExecutor\InterceptorHandler;
11+
use LLM\Agents\LLM\Exception\FormatterException;
12+
use LLM\Agents\LLM\Output\EnumFormatter;
13+
use LLM\Agents\LLM\Output\FormatterInterface;
14+
use LLM\Agents\LLM\Output\JsonSchemaFormatter;
15+
use LLM\Agents\LLM\Prompt\PromptInterface;
16+
use LLM\Agents\LLM\Response\ChatResponse;
17+
18+
/**
19+
* This interceptor is responsible for formatting the output of the agent based on the provided output formatter.
20+
*/
21+
final readonly class OutputFormatterInterceptor implements ExecutorInterceptorInterface
22+
{
23+
public function __construct(
24+
private JsonSchemaFormatter $jsonSchemaFormatter,
25+
) {}
26+
27+
public function execute(ExecutionInput $input, InterceptorHandler $next): Execution
28+
{
29+
$outputFormatter = $input->options->get('output_formatter');
30+
31+
if ($outputFormatter === null) {
32+
return $next($input);
33+
}
34+
35+
if (!$input->prompt instanceof PromptInterface) {
36+
throw new FormatterException('Prompt must implement PromptInterface');
37+
}
38+
39+
if (!$outputFormatter instanceof FormatterInterface) {
40+
$outputFormatter = $this->createFormatter($outputFormatter);
41+
}
42+
43+
$input = $input->withPrompt(
44+
$input->prompt->withValues(
45+
['output_format_instruction' => $outputFormatter->getInstruction()],
46+
),
47+
);
48+
49+
return $this->formatResponse($next($input), $outputFormatter);
50+
}
51+
52+
private function formatResponse(Execution $execution, FormatterInterface $outputFormatter): Execution
53+
{
54+
$result = $execution->result;
55+
56+
if (!$result instanceof ChatResponse) {
57+
return $execution;
58+
}
59+
60+
return new Execution(
61+
result: new ChatResponse(
62+
content: $outputFormatter->format($result->content),
63+
),
64+
prompt: $execution->prompt,
65+
);
66+
}
67+
68+
/**
69+
* @param non-empty-string|class-string $schema
70+
*/
71+
private function createFormatter(string $schema): FormatterInterface
72+
{
73+
// If the schema is an existing class, check if it is an enum.
74+
if (\class_exists($schema)) {
75+
$refl = new \ReflectionClass($schema);
76+
if ($refl->isEnum()) {
77+
return new EnumFormatter($refl->getName());
78+
}
79+
}
80+
81+
return $this->jsonSchemaFormatter->withJsonSchema($schema);
82+
}
83+
}

src/AgentExecutor/Interceptor/TokenLimitRetryInterceptor.php

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
use LLM\Agents\AgentExecutor\InterceptorHandler;
1111
use LLM\Agents\LLM\Exception\LimitExceededException;
1212

13+
/**
14+
* This interceptor is responsible for retrying the execution if the token limit is exceeded.
15+
* It will increment the limit by a specified step and retry the execution.
16+
*/
1317
final readonly class TokenLimitRetryInterceptor implements ExecutorInterceptorInterface
1418
{
1519
public function __construct(

src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
use LLM\Agents\LLM\Response\ToolCalledResponse;
1515
use LLM\Agents\Tool\ToolExecutor;
1616

17+
/**
18+
* This interceptor is responsible for calling the tools if LLM asks for it. After calling the tools, it adds the
19+
* tools responses to the prompt history and return the result of tools execution to the LLM.
20+
*
21+
* If the option 'return_tool_result' is set to true, the interceptor will return the tools result instead of adding
22+
* it to the prompt.
23+
*/
1724
final readonly class ToolExecutorInterceptor implements ExecutorInterceptorInterface
1825
{
1926
public function __construct(

src/AgentExecutor/InterceptorHandler.php

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
use LLM\Agents\Agent\Execution;
88

9+
/**
10+
* @internal
11+
*/
912
final readonly class InterceptorHandler
1013
{
1114
public function __construct(
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LLM\Agents\LLM\Exception;
6+
7+
final class FormatterException extends LLMException
8+
{
9+
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LLM\Agents\LLM\Exception;
6+
7+
final class InvalidArgumentException extends LLMException
8+
{
9+
10+
}

src/LLM/Output/EnumFormatter.php

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LLM\Agents\LLM\Output;
6+
7+
use LLM\Agents\LLM\Exception\FormatterException;
8+
use LLM\Agents\LLM\Exception\InvalidArgumentException;
9+
10+
/**
11+
* This formatter is used to validate that the response is one of the provided options.
12+
*/
13+
final readonly class EnumFormatter implements FormatterInterface
14+
{
15+
private \ReflectionEnum $enum;
16+
private FormatterInterface $formatter;
17+
18+
/**
19+
* @param class-string $enumClass
20+
*/
21+
public function __construct(
22+
string $enumClass,
23+
) {
24+
// validate class is type of enum of strings and fetch options
25+
try {
26+
$this->enum = new \ReflectionEnum($enumClass);
27+
} catch (\ReflectionException $e) {
28+
throw new InvalidArgumentException("Class {$enumClass} is not a valid enum");
29+
}
30+
31+
if (!$this->enum->getBackingType() instanceof \ReflectionNamedType) {
32+
throw new InvalidArgumentException("Enum {$enumClass} is not a valid enum of strings");
33+
}
34+
35+
if ($this->enum->getBackingType()->getName() !== 'string') {
36+
throw new InvalidArgumentException("Enum {$enumClass} is not a valid enum of strings");
37+
}
38+
39+
// fetching options
40+
$this->formatter = new SelectFormatter(
41+
...\array_map(static fn($option) => $option->value, $this->enum->getConstants()),
42+
);
43+
}
44+
45+
public function format(string|\Stringable $output): mixed
46+
{
47+
$value = $this->formatter->format($output);
48+
49+
// new enum value
50+
foreach ($this->enum->getConstants() as $option) {
51+
if ($option->value === $value) {
52+
return $option;
53+
}
54+
}
55+
56+
throw new FormatterException("Invalid enum value {$value}");
57+
}
58+
59+
public function getInstruction(): ?string
60+
{
61+
return $this->formatter->getInstruction();
62+
}
63+
}

0 commit comments

Comments
 (0)