AI Assistant
Doggy provides a global AI assistant available on all pages via a floating button and slide-out panel. Through a page-context registration mechanism, it deeply integrates with pages and supports the ReAct reasoning loop and tool calling.
Overview
The AI assistant is built on the symfony/ai-agent + symfony/ai-platform stack, bridging to the platform's own LlmRouter gateway system:
Client POST /api/admin/ai/chat { context, message }
→ AiChatController
→ AiContextRegistry::get(context)
→ AiContextProviderInterface (e.g. ViewEditorAiContext)
→ AiAssistant::chat(roleCode, systemPrompt, toolProviders, message)
→ Agent::call(MessageBag, options)
→ AgentProcessor::processInput (inject Tools)
→ LlmRouterPlatform::invoke → LlmRouter → LlmGatewayFactory
→ AgentProcessor::processOutput (ReAct loop)
← string responseGlobal Entry
The assistant panel is unconditionally included in templates/base.html.twig:
<body>
{% block body %}{% endblock %}
{% include 'admin/ai_chat.html.twig' %}
</body>- Open/Close: Floating button
toggle-ai-chat/ai-chat-closeat bottom-right - Input: Enter to send, Shift+Enter for newline
- Loading state: Input disabled during requests, "Thinking..." shown
- Styles:
public/sunui/admin/ai_chat.css
Page Context Registration
Each page registers context via window.__AI_CONTEXT__:
window.__AI_CONTEXT__ = {
name: 'view_editor', // Backend AiContextProviderInterface::getName()
buildPrompt: function(msg) { // Optional: build message sent to backend
return '[Current view ID: xxx]\n\n' + msg;
},
};View editor registration example:
<script>
window.__VIEW_ID__ = {{ id|json_encode|raw }};
window.__AI_CONTEXT__ = {
name: 'view_editor',
buildPrompt: function(msg) {
var vid = window.__VIEW_ID__;
return vid ? '[Current view ID: ' + vid + ']\n\n' + msg : msg;
},
};
</script>Context Provider System
AiContextProviderInterface defines the context provider:
namespace App\Service\AI\Runtime;
interface AiContextProviderInterface
{
public function getName(): string; // Context identifier
public function getRoleCode(): string; // LlmRouter role code
public function getSystemPrompt(): string; // Agent system prompt
public function getToolProviders(): array; // Array of Tool objects
}Registered via the app.ai_context tag:
App\Service\AI\Context\ViewEditorAiContext:
tags:
- { name: 'app.ai_context' }Adding a New Page Context
- Create a class implementing
AiContextProviderInterface - Add
tags: [{ name: 'app.ai_context' }]inservices.yaml - Set
window.__AI_CONTEXT__ = { name: 'your_name' }in the page template
Agent Runtime
AiAssistant is a generic Agent factory. Each chat() creates a fresh Agent instance to avoid state pollution:
$platform = new LlmRouterPlatform($this->router, $this->converter);
$toolbox = new Toolbox($toolProviders);
$agent = new Agent(
platform: $platform,
model: $roleCode,
inputProcessors: [
new SystemPromptInputProcessor($systemPrompt),
new AgentProcessor($toolbox),
],
outputProcessors: [
new AgentProcessor($toolbox),
],
name: 'ai-assistant',
);ReAct Loop
AgentProcessor acts as both input and output processor to implement the tool-calling loop:
- processInput: Injects Toolbox tool metadata
- processOutput: When the result is a
ToolCallResult:- Append the ToolCall array to the MessageBag
- Execute ToolCalls → ToolResults written back to the MessageBag
- Recursively call until a
TextResultis returned
Tool System
Tool methods are marked with the #[AsTool] attribute from symfony/ai-agent:
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
class ViewEditorToolProvider
{
#[AsTool(name: 'view.getInfo', description: 'Get complete view information')]
public function getViewInfo(string $viewId): array { ... }
#[AsTool(name: 'view.updateSectionConfig', description: 'Update view layout configuration')]
public function updateSectionConfig(string $viewId, string $contentWidth, ...): array { ... }
}Current View Editor Tools
| Tool | Purpose |
|---|---|
view.getInfo | Get view info (entity, fields, layout) |
view.updateSectionConfig | Update layout config |
view.updateFieldConfig | Update field config |
view.listEntityFields | List entity fields |
Design Principles
- Each tool is a single-responsibility method
- Parameter names match the
#[AsTool]JSON Schema (auto-mapped) - Return
array(converted to tool result text)
API Endpoint
POST /api/admin/ai/chat
{
"context": "view_editor",
"message": "Change this to a three-column layout"
}Success response:
{
"code": 200,
"message": "success",
"data": { "reply": "Changed to a three-column layout." }
}Related Files
| File | Responsibility |
|---|---|
AiChatController.php | Receives {context, message}, delegates to the provider |
AiContextRegistry.php | Collects app.ai_context tagged services, indexed by name |
AiAssistant.php | Generic Agent factory |
LlmRouterPlatform.php | PlatformInterface bridge (MessageBag ↔ array) |
ChatResultConverter.php | Response data → TextResult/ToolCallResult |
ViewEditorToolProvider.php | View editor tool methods |
ChromeDevToolsToolProvider.php | CDP browser debugging tools |
ViewFileToolProvider.php | View file read/write tools |