mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
[Agent] Add ToolCallRequested event for human-in-the-loop tool confirmation
This commit is contained in:
committed by
Oskar Stark
parent
d9fb5ef744
commit
c476ed32f4
@@ -389,10 +389,15 @@ Tool Call Lifecycle Events
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you need to react more granularly to the lifecycle of individual tool calls, you can listen to the
|
||||
:class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallRequested`,
|
||||
:class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallArgumentsResolved`,
|
||||
:class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallSucceeded` and
|
||||
:class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallFailed` events. These are dispatched at different stages::
|
||||
|
||||
$eventDispatcher->addListener(ToolCallRequested::class, function (ToolCallRequested $event): void {
|
||||
// Intercept a tool call before execution, e.g. to deny it or set a custom result
|
||||
});
|
||||
|
||||
$eventDispatcher->addListener(ToolCallArgumentsResolved::class, function (ToolCallArgumentsResolved $event): void {
|
||||
// Let the client know, that the tool $event->toolCall->name was executed
|
||||
});
|
||||
@@ -405,6 +410,11 @@ If you need to react more granularly to the lifecycle of individual tool calls,
|
||||
// Let the client know, that the tool $event->toolCall->name failed with the exception: $event->exception
|
||||
});
|
||||
|
||||
See the :doc:`/cookbook/human-in-the-loop` cookbook article for a complete guide on building a human-in-the-loop
|
||||
confirmation system using the ``ToolCallRequested`` event.
|
||||
|
||||
* `Human-in-the-Loop Confirmation`_
|
||||
|
||||
Excluding Tool Messages from MessageBag
|
||||
---------------------------------------
|
||||
|
||||
@@ -755,3 +765,4 @@ Code Examples
|
||||
.. _`RAG with Pinecone`: https://github.com/symfony/ai/blob/main/examples/rag/pinecone.php
|
||||
.. _`Chat with static memory`: https://github.com/symfony/ai/blob/main/examples/memory/static.php
|
||||
.. _`Chat with embedding search memory`: https://github.com/symfony/ai/blob/main/examples/memory/mariadb.php
|
||||
.. _`Human-in-the-Loop Confirmation`: https://github.com/symfony/ai/blob/main/examples/toolbox/confirmation.php
|
||||
|
||||
240
docs/cookbook/human-in-the-loop.rst
Normal file
240
docs/cookbook/human-in-the-loop.rst
Normal file
@@ -0,0 +1,240 @@
|
||||
Human-in-the-Loop Tool Confirmation
|
||||
===================================
|
||||
|
||||
When AI agents execute tools, some actions — like deleting files, sending emails, or modifying
|
||||
data — should require human approval. This guide shows how to build a confirmation system using
|
||||
the :class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallRequested` event.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
* Symfony AI Platform component
|
||||
* Symfony AI Agent component
|
||||
* Symfony EventDispatcher component
|
||||
|
||||
How It Works
|
||||
------------
|
||||
|
||||
The :class:`Symfony\\AI\\Agent\\Toolbox\\Toolbox` dispatches a
|
||||
:class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallRequested` event before each tool execution.
|
||||
An event listener can inspect the tool call and either:
|
||||
|
||||
* **Allow it** — do nothing, the tool executes normally
|
||||
* **Deny it** — call ``$event->deny($reason)`` to block execution and return the reason to the LLM
|
||||
* **Replace it** — call ``$event->setResult($result)`` to skip execution and return a custom result
|
||||
|
||||
Basic Example: Confirm Every Tool Call
|
||||
--------------------------------------
|
||||
|
||||
The simplest approach asks for confirmation on every tool call::
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addListener(ToolCallRequested::class, function (ToolCallRequested $event): void {
|
||||
$toolCall = $event->getToolCall();
|
||||
|
||||
echo \sprintf(
|
||||
"Tool '%s' wants to execute with args: %s\nAllow? [y/N] ",
|
||||
$toolCall->getName(),
|
||||
json_encode($toolCall->getArguments())
|
||||
);
|
||||
|
||||
$input = strtolower(trim(fgets(\STDIN)));
|
||||
|
||||
if ('y' !== $input) {
|
||||
$event->deny('User denied tool execution.');
|
||||
}
|
||||
});
|
||||
|
||||
Pass this dispatcher to the :class:`Symfony\\AI\\Agent\\Toolbox\\Toolbox`::
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Toolbox;
|
||||
|
||||
$toolbox = new Toolbox($tools, eventDispatcher: $dispatcher);
|
||||
|
||||
Adding a Policy Layer
|
||||
---------------------
|
||||
|
||||
In practice, you don't want to confirm every single call. A policy decides which tools need
|
||||
confirmation and which can run automatically. Here is an outline for a policy-based approach.
|
||||
|
||||
Step 1: Define a Policy
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A policy inspects the tool call and returns a decision::
|
||||
|
||||
enum PolicyDecision
|
||||
{
|
||||
case Allow;
|
||||
case Deny;
|
||||
case AskUser;
|
||||
}
|
||||
|
||||
interface PolicyInterface
|
||||
{
|
||||
public function decide(ToolCall $toolCall): PolicyDecision;
|
||||
}
|
||||
|
||||
A simple policy could auto-allow read operations based on tool name patterns::
|
||||
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
|
||||
class ReadAllowPolicy implements PolicyInterface
|
||||
{
|
||||
public function decide(ToolCall $toolCall): PolicyDecision
|
||||
{
|
||||
$name = strtolower($toolCall->getName());
|
||||
|
||||
foreach (['read', 'get', 'list', 'search', 'find', 'show'] as $pattern) {
|
||||
if (str_contains($name, $pattern)) {
|
||||
return PolicyDecision::Allow;
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision::AskUser;
|
||||
}
|
||||
}
|
||||
|
||||
Step 2: Build a Confirmation Handler
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The confirmation handler is responsible for prompting the user and returning a decision.
|
||||
Its implementation depends on your application context — CLI, web, async, etc.::
|
||||
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
|
||||
class CliConfirmationHandler
|
||||
{
|
||||
public function confirm(ToolCall $toolCall): bool
|
||||
{
|
||||
echo \sprintf(
|
||||
"Allow tool '%s' with args %s? [y/N] ",
|
||||
$toolCall->getName(),
|
||||
json_encode($toolCall->getArguments())
|
||||
);
|
||||
|
||||
return 'y' === strtolower(trim(fgets(\STDIN)));
|
||||
}
|
||||
}
|
||||
|
||||
For web applications, you might store pending confirmations in a database and wait for
|
||||
a user response through an HTTP endpoint or WebSocket.
|
||||
|
||||
Step 3: Wire It Together
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Combine the policy and handler in an event listener::
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
|
||||
$policy = new ReadAllowPolicy();
|
||||
$handler = new CliConfirmationHandler();
|
||||
|
||||
$dispatcher->addListener(ToolCallRequested::class, function (ToolCallRequested $event) use ($policy, $handler): void {
|
||||
$decision = $policy->decide($event->getToolCall());
|
||||
|
||||
if (PolicyDecision::Allow === $decision) {
|
||||
return; // Auto-approved, proceed with execution
|
||||
}
|
||||
|
||||
if (PolicyDecision::Deny === $decision) {
|
||||
$event->deny('Tool blocked by policy.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// PolicyDecision::AskUser
|
||||
if (!$handler->confirm($event->getToolCall())) {
|
||||
$event->deny('User denied tool execution.');
|
||||
}
|
||||
});
|
||||
|
||||
Remembering User Decisions
|
||||
--------------------------
|
||||
|
||||
To avoid asking the user repeatedly for the same tool, you can cache decisions::
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
|
||||
$decisions = [];
|
||||
|
||||
$dispatcher->addListener(ToolCallRequested::class, function (ToolCallRequested $event) use (&$decisions): void {
|
||||
$toolName = $event->getToolCall()->getName();
|
||||
|
||||
if (isset($decisions[$toolName])) {
|
||||
if (!$decisions[$toolName]) {
|
||||
$event->deny('Tool previously denied by user.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo \sprintf(
|
||||
"Allow tool '%s'? [y/N/always/never] ",
|
||||
$toolName
|
||||
);
|
||||
|
||||
$input = strtolower(trim(fgets(\STDIN)));
|
||||
|
||||
$allowed = \in_array($input, ['y', 'always'], true);
|
||||
|
||||
if (\in_array($input, ['always', 'never'], true)) {
|
||||
$decisions[$toolName] = $allowed;
|
||||
}
|
||||
|
||||
if (!$allowed) {
|
||||
$event->deny('User denied tool execution.');
|
||||
}
|
||||
});
|
||||
|
||||
Using Tool Metadata
|
||||
-------------------
|
||||
|
||||
The event also provides access to the tool's metadata via ``$event->getMetadata()``, which
|
||||
includes the tool's description and parameter schema. This can be useful for displaying
|
||||
more context to the user before they decide::
|
||||
|
||||
$dispatcher->addListener(ToolCallRequested::class, function (ToolCallRequested $event): void {
|
||||
$metadata = $event->getMetadata();
|
||||
|
||||
echo \sprintf(
|
||||
"Tool: %s\nDescription: %s\nArguments: %s\n",
|
||||
$metadata->getName(),
|
||||
$metadata->getDescription(),
|
||||
json_encode($event->getToolCall()->getArguments())
|
||||
);
|
||||
|
||||
// ... ask for confirmation
|
||||
});
|
||||
|
||||
Integration with Symfony Framework
|
||||
----------------------------------
|
||||
|
||||
In a Symfony application, register the listener as a service::
|
||||
|
||||
# config/services.yaml
|
||||
services:
|
||||
App\EventListener\ToolConfirmationListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: Symfony\AI\Agent\Toolbox\Event\ToolCallRequested }
|
||||
|
||||
And implement the listener::
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
|
||||
class ToolConfirmationListener
|
||||
{
|
||||
public function __invoke(ToolCallRequested $event): void
|
||||
{
|
||||
// Your confirmation logic here
|
||||
}
|
||||
}
|
||||
|
||||
Code Examples
|
||||
-------------
|
||||
|
||||
* `Human-in-the-Loop Confirmation Example <https://github.com/symfony/ai/blob/main/examples/toolbox/confirmation.php>`_
|
||||
@@ -12,6 +12,7 @@ Getting Started Guides
|
||||
|
||||
chatbot-with-memory
|
||||
dynamic-tools
|
||||
human-in-the-loop
|
||||
rag-implementation
|
||||
structured-output-object-instances
|
||||
|
||||
@@ -24,6 +25,7 @@ Tools
|
||||
-----
|
||||
|
||||
* :doc:`dynamic-tools` - Build a dynamic Toolbox for flexible tool management at runtime
|
||||
* :doc:`human-in-the-loop` - Implement human-in-the-loop confirmation for tool execution
|
||||
|
||||
Retrieval Augmented Generation
|
||||
------------------------------
|
||||
|
||||
48
examples/toolbox/confirmation.php
Normal file
48
examples/toolbox/confirmation.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\AI\Agent\Agent;
|
||||
use Symfony\AI\Agent\Bridge\Filesystem\Filesystem;
|
||||
use Symfony\AI\Agent\Toolbox\AgentProcessor;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
use Symfony\AI\Agent\Toolbox\Toolbox;
|
||||
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
|
||||
use Symfony\AI\Platform\Message\Message;
|
||||
use Symfony\AI\Platform\Message\MessageBag;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
|
||||
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$eventDispatcher->addListener(ToolCallRequested::class, static function (ToolCallRequested $event): void {
|
||||
output()->write(sprintf('Allow tool "%s"? [y/N] ', $event->getToolCall()->getName()));
|
||||
|
||||
if ('y' !== strtolower(trim(fgets(\STDIN)))) {
|
||||
$event->deny('User denied tool execution.');
|
||||
}
|
||||
});
|
||||
|
||||
$toolbox = new Toolbox([new Filesystem(new SymfonyFilesystem(), __DIR__)], logger: logger(), eventDispatcher: $eventDispatcher);
|
||||
$processor = new AgentProcessor($toolbox, eventDispatcher: $eventDispatcher);
|
||||
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
|
||||
|
||||
$messages = new MessageBag(Message::ofUser('First, list the files in this folder. Then delete the file confirmation.php'));
|
||||
|
||||
$result = $agent->call($messages, ['stream' => true]);
|
||||
|
||||
foreach ($result->getContent() as $chunk) {
|
||||
echo $chunk;
|
||||
}
|
||||
|
||||
echo \PHP_EOL;
|
||||
@@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.7
|
||||
---
|
||||
|
||||
* Add `ToolCallRequested` event dispatched before tool execution
|
||||
|
||||
0.4
|
||||
---
|
||||
|
||||
|
||||
85
src/agent/src/Toolbox/Event/ToolCallRequested.php
Normal file
85
src/agent/src/Toolbox/Event/ToolCallRequested.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Agent\Toolbox\Event;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
use Symfony\AI\Agent\Toolbox\ToolResult;
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
use Symfony\AI\Platform\Tool\Tool;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class ToolCallRequested implements StoppableEventInterface
|
||||
{
|
||||
private bool $denied = false;
|
||||
private ?string $denialReason = null;
|
||||
private ?ToolResult $result = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ToolCall $toolCall,
|
||||
private readonly Tool $metadata,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getToolCall(): ToolCall
|
||||
{
|
||||
return $this->toolCall;
|
||||
}
|
||||
|
||||
public function getMetadata(): Tool
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the tool execution with an optional reason.
|
||||
*/
|
||||
public function deny(?string $reason = null): void
|
||||
{
|
||||
$this->denied = true;
|
||||
$this->denialReason = $reason;
|
||||
}
|
||||
|
||||
public function isDenied(): bool
|
||||
{
|
||||
return $this->denied;
|
||||
}
|
||||
|
||||
public function getDenialReason(): ?string
|
||||
{
|
||||
return $this->denialReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom result to skip the actual tool execution.
|
||||
*/
|
||||
public function setResult(ToolResult $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
public function hasResult(): bool
|
||||
{
|
||||
return null !== $this->result;
|
||||
}
|
||||
|
||||
public function getResult(): ?ToolResult
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return $this->denied || null !== $this->result;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallArgumentsResolved;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallFailed;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallSucceeded;
|
||||
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionException;
|
||||
use Symfony\AI\Agent\Toolbox\Exception\ToolExecutionExceptionInterface;
|
||||
@@ -69,6 +70,20 @@ final class Toolbox implements ToolboxInterface
|
||||
public function execute(ToolCall $toolCall): ToolResult
|
||||
{
|
||||
$metadata = $this->getMetadata($toolCall);
|
||||
|
||||
$event = new ToolCallRequested($toolCall, $metadata);
|
||||
$this->eventDispatcher?->dispatch($event);
|
||||
|
||||
if ($event->isDenied()) {
|
||||
$this->logger->debug(\sprintf('Tool "%s" denied: %s', $toolCall->getName(), $event->getDenialReason()));
|
||||
|
||||
return new ToolResult($toolCall, $event->getDenialReason() ?? 'Tool execution denied.');
|
||||
}
|
||||
|
||||
if ($event->hasResult()) {
|
||||
return $event->getResult();
|
||||
}
|
||||
|
||||
$tool = $this->getExecutable($metadata);
|
||||
|
||||
try {
|
||||
|
||||
107
src/agent/tests/Toolbox/Event/ToolCallRequestedTest.php
Normal file
107
src/agent/tests/Toolbox/Event/ToolCallRequestedTest.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Agent\Tests\Toolbox\Event;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
use Symfony\AI\Agent\Toolbox\ToolResult;
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
use Symfony\AI\Platform\Tool\ExecutionReference;
|
||||
use Symfony\AI\Platform\Tool\Tool;
|
||||
|
||||
final class ToolCallRequestedTest extends TestCase
|
||||
{
|
||||
private ToolCall $toolCall;
|
||||
private Tool $metadata;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->toolCall = new ToolCall('call_123', 'my_tool', ['arg' => 'value']);
|
||||
$this->metadata = new Tool(new ExecutionReference(self::class, '__invoke'), 'my_tool', 'A test tool');
|
||||
}
|
||||
|
||||
public function testGetToolCall()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$this->assertSame($this->toolCall, $event->getToolCall());
|
||||
}
|
||||
|
||||
public function testGetMetadata()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$this->assertSame($this->metadata, $event->getMetadata());
|
||||
}
|
||||
|
||||
public function testInitialStateIsNotDenied()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$this->assertFalse($event->isDenied());
|
||||
$this->assertNull($event->getDenialReason());
|
||||
}
|
||||
|
||||
public function testDeny()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$event->deny('Not allowed');
|
||||
|
||||
$this->assertTrue($event->isDenied());
|
||||
$this->assertSame('Not allowed', $event->getDenialReason());
|
||||
}
|
||||
|
||||
public function testInitialStateHasNoResult()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$this->assertFalse($event->hasResult());
|
||||
$this->assertNull($event->getResult());
|
||||
}
|
||||
|
||||
public function testSetResult()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
$result = new ToolResult($this->toolCall, 'custom result');
|
||||
|
||||
$event->setResult($result);
|
||||
|
||||
$this->assertTrue($event->hasResult());
|
||||
$this->assertSame($result, $event->getResult());
|
||||
}
|
||||
|
||||
public function testPropagationNotStoppedInitially()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$this->assertFalse($event->isPropagationStopped());
|
||||
}
|
||||
|
||||
public function testPropagationStoppedWhenDenied()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$event->deny('Denied');
|
||||
|
||||
$this->assertTrue($event->isPropagationStopped());
|
||||
}
|
||||
|
||||
public function testPropagationStoppedWhenResultSet()
|
||||
{
|
||||
$event = new ToolCallRequested($this->toolCall, $this->metadata);
|
||||
|
||||
$event->setResult(new ToolResult($this->toolCall, 'result'));
|
||||
|
||||
$this->assertTrue($event->isPropagationStopped());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Symfony\AI\Agent\Tests\Fixtures\Tool\ToolException;
|
||||
use Symfony\AI\Agent\Tests\Fixtures\Tool\ToolNoParams;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallArgumentsResolved;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallFailed;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallRequested;
|
||||
use Symfony\AI\Agent\Toolbox\Event\ToolCallSucceeded;
|
||||
use Symfony\AI\Agent\Toolbox\Toolbox;
|
||||
use Symfony\AI\Platform\Result\ToolCall;
|
||||
@@ -68,6 +69,7 @@ final class ToolboxEventDispatcherTest extends TestCase
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
$this->assertSame([
|
||||
ToolCallRequested::class,
|
||||
ToolCallArgumentsResolved::class,
|
||||
ToolCallFailed::class,
|
||||
], $this->dispatchedEvents);
|
||||
@@ -80,6 +82,7 @@ final class ToolboxEventDispatcherTest extends TestCase
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
$this->assertSame([
|
||||
ToolCallRequested::class,
|
||||
ToolCallArgumentsResolved::class,
|
||||
ToolCallFailed::class,
|
||||
], $this->dispatchedEvents);
|
||||
@@ -92,6 +95,7 @@ final class ToolboxEventDispatcherTest extends TestCase
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
$this->assertSame([
|
||||
ToolCallRequested::class,
|
||||
ToolCallArgumentsResolved::class,
|
||||
ToolCallSucceeded::class,
|
||||
], $this->dispatchedEvents);
|
||||
|
||||
Reference in New Issue
Block a user