(feat) Add tools, memory, structured output, and multi-agent handoff support - #46
Conversation
|
|
||
| /** | ||
| * Register tool objects (classes carrying #[AsTool] on themselves) the | ||
| * model may call during execute()/stream(). Each | ||
| * | ||
| * @param array<int, object> $tools | ||
| * @return static | ||
| */ | ||
| public function withTools(array $tools): static; | ||
|
|
||
| /** | ||
| * Register memory providers whose content is injected into the system | ||
| * prompt before every call, giving the model persistent/contextual | ||
| * information beyond the current message history. | ||
| * | ||
| * @param iterable<MemoryProviderInterface> $memoryProviders | ||
| * @return static | ||
| */ | ||
| public function withMemory(iterable $memoryProviders): static; | ||
|
|
||
| /** | ||
| * Request the model's response be constrained to, and deserialized into, the given shape | ||
| * | ||
| * @param string|object|null $responseFormat | ||
| * @return static | ||
| */ | ||
| public function asStructured(string|object|null $responseFormat): static; | ||
|
|
||
| /** | ||
| * The agent's name, used to identify it in MultiAgent handoff routing | ||
| * | ||
| * @return string | ||
| */ | ||
| public function getName(): string; | ||
|
|
||
| /** | ||
| * Set the agent's name (see getName()). | ||
| * | ||
| * @param string $name | ||
| * @return static | ||
| */ | ||
| public function named(string $name): static; | ||
|
|
||
| /** | ||
| * Expose the underlying Symfony AI agent | ||
| * | ||
| * @return SymfonyAgentInterface | ||
| */ | ||
| public function toAgent(): SymfonyAgentInterface; |
There was a problem hiding this comment.
It could break some custom agents created by users. We should write the breaking change in the docs (or just tell them to add the missing functions)
| public function toAgent(): SymfonyAgentInterface | ||
| { | ||
| if (null !== $this->agent) { | ||
| return $this->agent; | ||
| } | ||
|
|
||
| $inputProcessors = []; | ||
| $outputProcessors = []; | ||
|
|
||
| if ([] !== $this->memoryProviders) { | ||
| $inputProcessors[] = new MemoryInputProcessor($this->memoryProviders); | ||
| } | ||
|
|
||
| if ([] !== $this->tools) { | ||
| $toolProcessor = new AgentProcessor(new Toolbox($this->tools)); | ||
| $inputProcessors[] = $toolProcessor; | ||
| $outputProcessors[] = $toolProcessor; | ||
| } | ||
|
|
||
| return $this->agent = new SymfonyAgent( | ||
| $this->getPlatform(), | ||
| $this->model, | ||
| $inputProcessors, | ||
| $outputProcessors, | ||
| $this->getName(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
toAgent() does not preserve the format set by asStructured(). Please fix this or add a test covering asStructured()->toAgent()->call()
they should behave the same :
$agent->asStructured(MyDto::class)->execute([]);
$agent->asStructured(MyDto::class)->toAgent()->call($messages);execute() preserves MyDto::class, while toAgent()->call() ignores it.
| public function named(string $name): static | ||
| { | ||
| $this->name = $name; | ||
| $this->agent = null; | ||
|
|
||
| return $this; | ||
| } |
There was a problem hiding this comment.
add this to match with MultiAgent.php and to be sure name is not empty
if ('' === $name) {
throw new RuntimeException('$name must not be empty.');
}|
@rrr63 I think all are ok now |
rrr63
left a comment
There was a problem hiding this comment.
all seems ok to me, thank you @techmahedy
Yes, locally tested manually, all seems ok |
Summary
This PR extracts the duplicated platform-building/message-hydration/execute/stream logic out of the five provider classes (Claude, Gemini, OpenAI, OpenRouter, SelfHost) into a new shared AbstractAgent base class, and uses that consolidation to wire in Symfony AI's Agent pipeline — unlocking tool calling, persistent memory, structured output, and multi-agent handoff, none of which the old per-provider implementation could support
Changes