Skip to content

Commit c1d32f4

Browse files
feat(profiler): trace and display AgentInterface calls
1 parent 9eb72bc commit c1d32f4

File tree

4 files changed

+271
-117
lines changed

4 files changed

+271
-117
lines changed

src/ai-bundle/config/services.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\AI\Agent\AgentInterface;
1415
use Symfony\AI\Agent\StructuredOutput\AgentProcessor as StructureOutputProcessor;
1516
use Symfony\AI\Agent\StructuredOutput\ResponseFormatFactory;
1617
use Symfony\AI\Agent\StructuredOutput\ResponseFormatFactoryInterface;
@@ -23,6 +24,7 @@
2324
use Symfony\AI\Agent\Toolbox\ToolResultConverter;
2425
use Symfony\AI\AiBundle\Command\ChatCommand;
2526
use Symfony\AI\AiBundle\Profiler\DataCollector;
27+
use Symfony\AI\AiBundle\Profiler\TraceableAgent;
2628
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
2729
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
2830
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
@@ -127,6 +129,13 @@
127129
])
128130
->tag('kernel.event_listener')
129131

132+
->set('ai.traceable_agent', TraceableAgent::class)
133+
->decorate(AgentInterface::class, priority: 5)
134+
->args([
135+
service('.inner'),
136+
service('ai.data_collector'),
137+
])
138+
130139
// profiler
131140
->set('ai.data_collector', DataCollector::class)
132141
->args([

src/ai-bundle/src/Profiler/DataCollector.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ final class DataCollector extends AbstractDataCollector implements LateDataColle
3737
*/
3838
private readonly array $toolboxes;
3939

40+
/**
41+
* @var list<array{method: string, duration: float, input: mixed, result: mixed, error: ?\Throwable}>
42+
*/
43+
private array $collectedChatCalls = [];
44+
4045
/**
4146
* @param TraceablePlatform[] $platforms
4247
* @param TraceableToolbox[] $toolboxes
@@ -52,7 +57,6 @@ public function __construct(
5257

5358
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
5459
{
55-
$this->lateCollect();
5660
}
5761

5862
public function lateCollect(): void
@@ -61,6 +65,18 @@ public function lateCollect(): void
6165
'tools' => $this->defaultToolBox->getTools(),
6266
'platform_calls' => array_merge(...array_map($this->awaitCallResults(...), $this->platforms)),
6367
'tool_calls' => array_merge(...array_map(fn (TraceableToolbox $toolbox) => $toolbox->calls, $this->toolboxes)),
68+
'chat_calls' => $this->cloneVar($this->collectedChatCalls),
69+
];
70+
}
71+
72+
public function collectChatCall(string $method, float $duration, mixed $input, mixed $result, ?\Throwable $error): void
73+
{
74+
$this->collectedChatCalls[] = [
75+
'method' => $method,
76+
'duration' => $duration,
77+
'input' => $input,
78+
'result' => $result,
79+
'error' => $error,
6480
];
6581
}
6682

@@ -93,6 +109,27 @@ public function getToolCalls(): array
93109
return $this->data['tool_calls'] ?? [];
94110
}
95111

112+
/**
113+
* @return list<array{method: string, duration: float, input: mixed, result: mixed, error: ?\Throwable}>
114+
*/
115+
public function getChatCalls(): array
116+
{
117+
if (!isset($this->data['chat_calls'])) {
118+
return [];
119+
}
120+
121+
/** @var list<array{method: string, duration: float, input: mixed, result: mixed, error: ?\Throwable}> $chatCalls */
122+
$chatCalls = $this->data['chat_calls']->getValue(true);
123+
124+
return $chatCalls;
125+
}
126+
127+
public function reset(): void
128+
{
129+
$this->data = [];
130+
$this->collectedChatCalls = [];
131+
}
132+
96133
/**
97134
* @return array{
98135
* model: Model,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\AiBundle\Profiler;
13+
14+
use Symfony\AI\Agent\AgentInterface;
15+
use Symfony\AI\Platform\Message\MessageBag;
16+
use Symfony\AI\Platform\Result\ResultInterface;
17+
use Symfony\Contracts\Service\ResetInterface;
18+
19+
final class TraceableAgent implements AgentInterface, ResetInterface
20+
{
21+
public function __construct(
22+
private readonly AgentInterface $decorated,
23+
private readonly DataCollector $collector,
24+
) {
25+
}
26+
27+
public function call(MessageBag $messages, array $options = []): ResultInterface
28+
{
29+
$startTime = microtime(true);
30+
$error = null;
31+
$response = null;
32+
33+
try {
34+
return $response = $this->decorated->call($messages, $options);
35+
} catch (\Throwable $e) {
36+
$error = $e;
37+
throw $e;
38+
} finally {
39+
$this->collector->collectChatCall(
40+
'call',
41+
microtime(true) - $startTime,
42+
$messages,
43+
$response,
44+
$error
45+
);
46+
}
47+
}
48+
49+
public function reset(): void
50+
{
51+
if ($this->decorated instanceof ResetInterface) {
52+
$this->decorated->reset();
53+
}
54+
}
55+
56+
public function getName(): string
57+
{
58+
return 'TraceableAgent';
59+
}
60+
}

0 commit comments

Comments
 (0)