From a27c238904b75ad7348f50dd4a7b1c51c3bccfb8 Mon Sep 17 00:00:00 2001 From: Pavel Buchnev Date: Mon, 9 Sep 2024 23:38:05 +0400 Subject: [PATCH 1/2] Working with tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. In some cases we need to call specific tool or ask LLM that it must use one of the provided tools, but doesn’t force a particular tool. 2. Adds a new interceptor for tools execution if LLM asks about it. --- .../Interceptor/ToolExecutorInterceptor.php | 71 +++++++++++++++++++ .../Prompt/Chat/ToolsCallResultResponse.php | 29 ++++++++ src/Tool/ToolChoice.php | 53 ++++++++++++++ src/Tool/ToolChoiceType.php | 13 ++++ 4 files changed, 166 insertions(+) create mode 100644 src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php create mode 100644 src/LLM/Prompt/Chat/ToolsCallResultResponse.php create mode 100644 src/Tool/ToolChoice.php create mode 100644 src/Tool/ToolChoiceType.php diff --git a/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php b/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php new file mode 100644 index 0000000..39036cf --- /dev/null +++ b/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php @@ -0,0 +1,71 @@ +options->get('return_tool_result', false); + + while (true) { + $result = $execution->result; + $prompt = $execution->prompt; + + if ($result instanceof ToolCalledResponse) { + // First, call all tools. + $toolsResponse = []; + foreach ($result->tools as $tool) { + $toolsResponse[] = $this->callTool($tool); + } + + // In some cases we want to return the tool result instead of adding it to the prompt. + // We don't need an answer from the LLM, all we wanted is to ask LLM to execute desired tools. + if ($shouldReturnToolResult) { + return new Execution( + result: new ToolsCallResultResponse(results: $toolsResponse), + prompt: $prompt, + ); + } + + // Then add the tools responses to the prompt. + foreach ($toolsResponse as $toolResponse) { + $input = $input->withPrompt($prompt->withAddedMessage($toolResponse)); + } + + $execution = $next($input); + continue; + } + + return $execution; + } + } + + private function callTool(ToolCall $tool): ToolCallResultMessage + { + $functionResult = $this->toolExecutor->execute($tool->name, $tool->arguments); + + return new ToolCallResultMessage( + id: $tool->id, + content: [$functionResult], + ); + } +} diff --git a/src/LLM/Prompt/Chat/ToolsCallResultResponse.php b/src/LLM/Prompt/Chat/ToolsCallResultResponse.php new file mode 100644 index 0000000..81b7148 --- /dev/null +++ b/src/LLM/Prompt/Chat/ToolsCallResultResponse.php @@ -0,0 +1,29 @@ + $results + */ + public function __construct( + public array $results, + ) { + parent::__construct(''); + } + + public function jsonSerialize(): array + { + return [ + 'results' => \array_map( + static fn(ToolCallResultMessage $result): array => $result->toArray(), + $this->results, + ), + ]; + } +} diff --git a/src/Tool/ToolChoice.php b/src/Tool/ToolChoice.php new file mode 100644 index 0000000..659b0f7 --- /dev/null +++ b/src/Tool/ToolChoice.php @@ -0,0 +1,53 @@ +type === ToolChoiceType::Auto; + } + + public function isAny(): bool + { + return $this->type === ToolChoiceType::Any; + } + + public function isSpecific(): bool + { + return $this->type === ToolChoiceType::Specific; + } + + public function isNone(): bool + { + return $this->type === ToolChoiceType::None; + } +} diff --git a/src/Tool/ToolChoiceType.php b/src/Tool/ToolChoiceType.php new file mode 100644 index 0000000..754520a --- /dev/null +++ b/src/Tool/ToolChoiceType.php @@ -0,0 +1,13 @@ + Date: Mon, 9 Sep 2024 23:50:26 +0400 Subject: [PATCH 2/2] Adds phpdoc --- .../Interceptor/GeneratePromptInterceptor.php | 3 +++ .../Interceptor/InjectModelInterceptor.php | 3 +++ .../Interceptor/ToolExecutorInterceptor.php | 4 ++++ src/Tool/ToolChoice.php | 18 ++++++++++++++++++ src/Tool/ToolChoiceType.php | 7 +++++++ 5 files changed, 35 insertions(+) diff --git a/src/AgentExecutor/Interceptor/GeneratePromptInterceptor.php b/src/AgentExecutor/Interceptor/GeneratePromptInterceptor.php index 2c3daca..8b152b8 100644 --- a/src/AgentExecutor/Interceptor/GeneratePromptInterceptor.php +++ b/src/AgentExecutor/Interceptor/GeneratePromptInterceptor.php @@ -12,6 +12,9 @@ use LLM\Agents\LLM\AgentPromptGeneratorInterface; use LLM\Agents\LLM\Prompt\Chat\PromptInterface; +/** + * This interceptor is responsible for generating the prompt for the agent. + */ final readonly class GeneratePromptInterceptor implements ExecutorInterceptorInterface { public function __construct( diff --git a/src/AgentExecutor/Interceptor/InjectModelInterceptor.php b/src/AgentExecutor/Interceptor/InjectModelInterceptor.php index 23a97be..c181e05 100644 --- a/src/AgentExecutor/Interceptor/InjectModelInterceptor.php +++ b/src/AgentExecutor/Interceptor/InjectModelInterceptor.php @@ -10,6 +10,9 @@ use LLM\Agents\AgentExecutor\ExecutorInterceptorInterface; use LLM\Agents\AgentExecutor\InterceptorHandler; +/** + * This interceptor is responsible for injecting the model name into the execution options. + */ final readonly class InjectModelInterceptor implements ExecutorInterceptorInterface { public function __construct( diff --git a/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php b/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php index 39036cf..e473eeb 100644 --- a/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php +++ b/src/AgentExecutor/Interceptor/ToolExecutorInterceptor.php @@ -24,12 +24,14 @@ public function execute(ExecutionInput $input, InterceptorHandler $next): Execut { $execution = $next($input); + // Check if we should return the tool result instead of adding it to the prompt. $shouldReturnToolResult = $input->options->get('return_tool_result', false); while (true) { $result = $execution->result; $prompt = $execution->prompt; + // If the result is a ToolCalledResponse, we need to call the tools. if ($result instanceof ToolCalledResponse) { // First, call all tools. $toolsResponse = []; @@ -51,7 +53,9 @@ public function execute(ExecutionInput $input, InterceptorHandler $next): Execut $input = $input->withPrompt($prompt->withAddedMessage($toolResponse)); } + // Continue to the next execution. $execution = $next($input); + continue; } diff --git a/src/Tool/ToolChoice.php b/src/Tool/ToolChoice.php index 659b0f7..87e3d9a 100644 --- a/src/Tool/ToolChoice.php +++ b/src/Tool/ToolChoice.php @@ -4,6 +4,12 @@ namespace LLM\Agents\Tool; +/** + * In some cases, you may want LLM to use a specific tool to answer the user’s question, even if LLM thinks it can + * provide an answer without using a tool. + * + * This class allows you to instruct LLM to have a specific behavior when it comes to using tools. + */ readonly class ToolChoice { private function __construct( @@ -11,21 +17,33 @@ private function __construct( public ?string $toolName = null, ) {} + /** + * Let LLM decide which function to call or not to call at all + */ public static function auto(): self { return new self(ToolChoiceType::Auto); } + /** + * Force LLM to always call one or more functions + */ public static function any(): self { return new self(ToolChoiceType::Any); } + /** + * Force LLM to call a specific function with the given name + */ public static function specific(string $toolName): self { return new self(ToolChoiceType::Specific, $toolName); } + /** + * Force LLM not to call any function + */ public static function none(): self { return new self(ToolChoiceType::None); diff --git a/src/Tool/ToolChoiceType.php b/src/Tool/ToolChoiceType.php index 754520a..80aae7a 100644 --- a/src/Tool/ToolChoiceType.php +++ b/src/Tool/ToolChoiceType.php @@ -6,8 +6,15 @@ enum ToolChoiceType { + // Let LLM decide which function to call or not to call at all case Auto; + + // Force LLM to always call one or more functions case Any; + + // Force LLM to call a specific function with the given name case Specific; + + // Force LLM not to call any function case None; }