Skip to content

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 response

Global Entry

The assistant panel is unconditionally included in templates/base.html.twig:

twig
<body>
    {% block body %}{% endblock %}
    {% include 'admin/ai_chat.html.twig' %}
</body>
  • Open/Close: Floating button toggle-ai-chat / ai-chat-close at 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__:

js
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:

twig
<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:

php
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:

yaml
App\Service\AI\Context\ViewEditorAiContext:
    tags:
        - { name: 'app.ai_context' }

Adding a New Page Context

  1. Create a class implementing AiContextProviderInterface
  2. Add tags: [{ name: 'app.ai_context' }] in services.yaml
  3. 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:

php
$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:
    1. Append the ToolCall array to the MessageBag
    2. Execute ToolCalls → ToolResults written back to the MessageBag
    3. Recursively call until a TextResult is returned

Tool System

Tool methods are marked with the #[AsTool] attribute from symfony/ai-agent:

php
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

ToolPurpose
view.getInfoGet view info (entity, fields, layout)
view.updateSectionConfigUpdate layout config
view.updateFieldConfigUpdate field config
view.listEntityFieldsList 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

json
{
    "context": "view_editor",
    "message": "Change this to a three-column layout"
}

Success response:

json
{
    "code": 200,
    "message": "success",
    "data": { "reply": "Changed to a three-column layout." }
}
FileResponsibility
AiChatController.phpReceives {context, message}, delegates to the provider
AiContextRegistry.phpCollects app.ai_context tagged services, indexed by name
AiAssistant.phpGeneric Agent factory
LlmRouterPlatform.phpPlatformInterface bridge (MessageBag ↔ array)
ChatResultConverter.phpResponse data → TextResult/ToolCallResult
ViewEditorToolProvider.phpView editor tool methods
ChromeDevToolsToolProvider.phpCDP browser debugging tools
ViewFileToolProvider.phpView file read/write tools

Open Source under MIT | Copyright © 2026 Doggy