Skip to content

Commit a73aa24

Browse files
committed
Introducing fundation for human-in-the-loop mechanism for tool calling
1 parent 9842d72 commit a73aa24

24 files changed

+1417
-3
lines changed

docs/components/agent.rst

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,107 @@ If you need to react more granularly to the lifecycle of individual tool calls,
405405
// Let the client know, that the tool $event->toolCall->name failed with the exception: $event->exception
406406
});
407407

408+
Human-in-the-Loop Confirmation
409+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
410+
411+
The Agent component provides a confirmation system for implementing human-in-the-loop patterns when executing tools.
412+
This allows you to require user approval before potentially dangerous operations are performed.
413+
414+
The confirmation system uses the :class:`Symfony\\AI\\Agent\\Toolbox\\Event\\ToolCallRequested` event, which is
415+
dispatched before each tool execution. A :class:`Symfony\\AI\\Agent\\Toolbox\\Confirmation\\ConfirmationSubscriber`
416+
listens to this event and applies a policy-based confirmation workflow::
417+
418+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationSubscriber;
419+
use Symfony\AI\Agent\Toolbox\Confirmation\DefaultPolicy;
420+
use Symfony\AI\Agent\Toolbox\Toolbox;
421+
use Symfony\Component\EventDispatcher\EventDispatcher;
422+
423+
// Create a policy that auto-allows read operations
424+
$policy = new DefaultPolicy();
425+
426+
// Create a confirmation handler (implement ConfirmationHandlerInterface)
427+
$handler = new MyConfirmationHandler();
428+
429+
// Wire everything together
430+
$dispatcher = new EventDispatcher();
431+
$dispatcher->addSubscriber(new ConfirmationSubscriber($policy, $handler));
432+
433+
$toolbox = new Toolbox($tools, eventDispatcher: $dispatcher);
434+
435+
Policy Configuration
436+
....................
437+
438+
The :class:`Symfony\\AI\\Agent\\Toolbox\\Confirmation\\DefaultPolicy` provides smart defaults:
439+
440+
- **Auto-allows** read-only operations (tools with names containing: read, get, list, search, find, show, describe)
441+
- **Asks the user** for all other operations
442+
- **Remembers** user decisions for subsequent calls
443+
444+
You can customize the policy behavior::
445+
446+
$policy = new DefaultPolicy();
447+
448+
// Explicitly allow specific tools without confirmation
449+
$policy->allow('safe_tool');
450+
451+
// Explicitly deny tools (blocked without asking)
452+
$policy->deny('dangerous_tool');
453+
454+
// Customize read patterns
455+
$policy->setReadPatterns(['fetch', 'query', 'lookup']);
456+
457+
For scenarios where no confirmation is needed, use the :class:`Symfony\\AI\\Agent\\Toolbox\\Confirmation\\AlwaysAllowPolicy`::
458+
459+
use Symfony\AI\Agent\Toolbox\Confirmation\AlwaysAllowPolicy;
460+
461+
$policy = new AlwaysAllowPolicy(); // Bypasses all confirmation - use with caution
462+
463+
Implementing a Confirmation Handler
464+
...................................
465+
466+
To ask the user for confirmation, implement the :class:`Symfony\\AI\\Agent\\Toolbox\\Confirmation\\ConfirmationHandlerInterface`.
467+
Here's an example CLI handler::
468+
469+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationHandlerInterface;
470+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationResult;
471+
use Symfony\AI\Platform\Result\ToolCall;
472+
473+
class CliConfirmationHandler implements ConfirmationHandlerInterface
474+
{
475+
public function requestConfirmation(ToolCall $toolCall): ConfirmationResult
476+
{
477+
echo sprintf(
478+
"Allow tool '%s' with args %s? [y/N/always/never] ",
479+
$toolCall->getName(),
480+
json_encode($toolCall->getArguments())
481+
);
482+
483+
$input = strtolower(trim(fgets(\STDIN)));
484+
485+
return match ($input) {
486+
'y', 'yes' => ConfirmationResult::confirmed(),
487+
'always' => ConfirmationResult::always(),
488+
'never' => ConfirmationResult::never(),
489+
default => ConfirmationResult::denied(),
490+
};
491+
}
492+
}
493+
494+
The :class:`Symfony\\AI\\Agent\\Toolbox\\Confirmation\\ConfirmationResult` supports remembering decisions:
495+
496+
- ``ConfirmationResult::confirmed()`` - Allow this execution only
497+
- ``ConfirmationResult::always()`` - Allow this and future executions of the same tool
498+
- ``ConfirmationResult::denied()`` - Deny this execution only
499+
- ``ConfirmationResult::never()`` - Deny this and future executions of the same tool
500+
501+
When a tool is denied, the toolbox returns the denial reason as the tool result, allowing the LLM to handle
502+
the situation gracefully.
503+
504+
Code Examples
505+
.............
506+
507+
* `Human-in-the-Loop Confirmation`_
508+
408509
Keeping Tool Messages
409510
~~~~~~~~~~~~~~~~~~~~~
410511

@@ -755,3 +856,4 @@ Code Examples
755856
.. _`RAG with Pinecone`: https://github.com/symfony/ai/blob/main/examples/rag/pinecone.php
756857
.. _`Chat with static memory`: https://github.com/symfony/ai/blob/main/examples/memory/static.php
757858
.. _`Chat with embedding search memory`: https://github.com/symfony/ai/blob/main/examples/memory/mariadb.php
859+
.. _`Human-in-the-Loop Confirmation`: https://github.com/symfony/ai/blob/main/examples/toolbox/confirmation.php

examples/composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,13 @@
4444
"symfony/ai-cloudflare-store": "^0.3",
4545
"symfony/ai-decart-platform": "^0.3",
4646
"symfony/ai-deep-seek-platform": "^0.3",
47-
"symfony/ai-doctrine-message-store": "^0.3",
4847
"symfony/ai-docker-model-runner-platform": "^0.3",
48+
"symfony/ai-doctrine-message-store": "^0.3",
4949
"symfony/ai-elasticsearch-store": "^0.3",
5050
"symfony/ai-eleven-labs-platform": "^0.3",
5151
"symfony/ai-failover-platform": "^0.3",
5252
"symfony/ai-gemini-platform": "^0.3",
5353
"symfony/ai-generic-platform": "^0.3",
54-
"symfony/ai-session-message-store": "^0.3",
5554
"symfony/ai-hugging-face-platform": "^0.3",
5655
"symfony/ai-lm-studio-platform": "^0.3",
5756
"symfony/ai-manticore-search-store": "^0.3",
@@ -79,6 +78,7 @@
7978
"symfony/ai-scaleway-platform": "^0.3",
8079
"symfony/ai-scraper-tool": "^0.3",
8180
"symfony/ai-serp-api-tool": "^0.3",
81+
"symfony/ai-session-message-store": "^0.3",
8282
"symfony/ai-similarity-search-tool": "^0.3",
8383
"symfony/ai-supabase-store": "^0.3",
8484
"symfony/ai-surreal-db-message-store": "^0.3",
@@ -94,10 +94,11 @@
9494
"symfony/console": "^7.4|^8.0",
9595
"symfony/dependency-injection": "^7.4|^8.0",
9696
"symfony/dotenv": "^7.4|^8.0",
97+
"symfony/filesystem": "8.1.x-dev",
9798
"symfony/finder": "^7.4|^8.0",
9899
"symfony/http-foundation": "^7.4|^8.0",
99-
"symfony/rate-limiter": "^7.4|^8.0",
100100
"symfony/process": "^7.4|^8.0",
101+
"symfony/rate-limiter": "^7.4|^8.0",
101102
"symfony/var-dumper": "^7.4|^8.0"
102103
},
103104
"require-dev": {

examples/toolbox/confirmation.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Agent\Bridge\Filesystem\Filesystem;
14+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
15+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationHandlerInterface;
16+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationResult;
17+
use Symfony\AI\Agent\Toolbox\Confirmation\ConfirmationSubscriber;
18+
use Symfony\AI\Agent\Toolbox\Confirmation\DefaultPolicy;
19+
use Symfony\AI\Agent\Toolbox\Toolbox;
20+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
21+
use Symfony\AI\Platform\Message\Message;
22+
use Symfony\AI\Platform\Message\MessageBag;
23+
use Symfony\AI\Platform\Result\ToolCall;
24+
use Symfony\Component\Console\Helper\QuestionHelper;
25+
use Symfony\Component\Console\Input\ArgvInput;
26+
use Symfony\Component\Console\Question\ChoiceQuestion;
27+
use Symfony\Component\EventDispatcher\EventDispatcher;
28+
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
29+
30+
require_once dirname(__DIR__).'/bootstrap.php';
31+
32+
$eventDispatcher = new EventDispatcher();
33+
$eventDispatcher->addSubscriber(new ConfirmationSubscriber(new DefaultPolicy(), new class implements ConfirmationHandlerInterface {
34+
public function requestConfirmation(ToolCall $toolCall): ConfirmationResult
35+
{
36+
$args = json_encode($toolCall->getArguments());
37+
output()->writeln(sprintf('🔐 Tool "%s" wants to execute with args: %s', $toolCall->getName(), $args));
38+
$question = new ChoiceQuestion('Do you want to allow this?', ['y', 'N', 'always', 'never'], 1);
39+
40+
return match ((new QuestionHelper())->ask(new ArgvInput(), output(), $question)) {
41+
'y' => ConfirmationResult::confirmed(),
42+
'always' => ConfirmationResult::always(),
43+
'never' => ConfirmationResult::never(),
44+
default => ConfirmationResult::denied(),
45+
};
46+
}
47+
}));
48+
49+
$toolbox = new Toolbox(
50+
[new Filesystem(new SymfonyFilesystem(), __DIR__)],
51+
logger: logger(),
52+
eventDispatcher: $eventDispatcher,
53+
);
54+
55+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
56+
$processor = new AgentProcessor($toolbox, eventDispatcher: $eventDispatcher);
57+
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
58+
59+
output()->writeln('Human-in-the-Loop Tool Confirmation Demo');
60+
output()->writeln('=========================================');
61+
output()->writeln('The DefaultPolicy auto-allows read operations (filesystem_list) but asks for write operations (filesystem_delete).');
62+
63+
$messages = new MessageBag(Message::ofUser(
64+
'First, list the files in this folder. Then delete the file confirmation.php',
65+
));
66+
67+
$result = $agent->call($messages, ['stream' => true]);
68+
69+
foreach ($result->getContent() as $chunk) {
70+
echo $chunk;
71+
}
72+
73+
echo \PHP_EOL;

src/agent/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
---
66

77
* [BC BREAK] Rename `Symfony\AI\Agent\Toolbox\Tool\Agent` to `Symfony\AI\Agent\Toolbox\Tool\Subagent`
8+
* Add human-in-the-loop tool confirmation system
9+
* Add `ToolCallRequested` event dispatched before tool execution
810

911
0.3
1012
---
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Toolbox\Confirmation;
13+
14+
use Symfony\AI\Platform\Result\ToolCall;
15+
16+
/**
17+
* Policy that always allows tool execution without confirmation.
18+
* Use with caution - this bypasses all safety checks.
19+
*
20+
* @author Christopher Hertel <mail@christopher-hertel.de>
21+
*/
22+
final class AlwaysAllowPolicy implements PolicyInterface
23+
{
24+
public function decide(ToolCall $toolCall): PolicyDecision
25+
{
26+
return PolicyDecision::Allow;
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Toolbox\Confirmation;
13+
14+
use Symfony\AI\Platform\Result\ToolCall;
15+
16+
/**
17+
* Handles user confirmation requests for tool execution.
18+
*
19+
* Implementations can provide CLI prompts, web UI dialogs, or other
20+
* mechanisms to ask the user for permission.
21+
*
22+
* @author Christopher Hertel <mail@christopher-hertel.de>
23+
*/
24+
interface ConfirmationHandlerInterface
25+
{
26+
public function requestConfirmation(ToolCall $toolCall): ConfirmationResult;
27+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Toolbox\Confirmation;
13+
14+
/**
15+
* @author Christopher Hertel <mail@christopher-hertel.de>
16+
*/
17+
final class ConfirmationResult
18+
{
19+
private function __construct(
20+
private readonly bool $confirmed,
21+
private readonly bool $remember,
22+
) {
23+
}
24+
25+
public function isConfirmed(): bool
26+
{
27+
return $this->confirmed;
28+
}
29+
30+
public function shouldRemember(): bool
31+
{
32+
return $this->remember;
33+
}
34+
35+
public static function confirmed(): self
36+
{
37+
return new self(true, false);
38+
}
39+
40+
public static function denied(): self
41+
{
42+
return new self(false, false);
43+
}
44+
45+
public static function always(): self
46+
{
47+
return new self(true, true);
48+
}
49+
50+
public static function never(): self
51+
{
52+
return new self(false, true);
53+
}
54+
}

0 commit comments

Comments
 (0)