AI internals
Two separate stacks: the core's LLM layer behind the panel helpers, and the ai-chatbot module on laravel/ai. User sides: Editor AI, AI chatbot.
Core LLM layer
core/src/LLM/:
| Class | Role |
|---|---|
Core | the manager behind the LLM facade: generateText(), streamText(), generateObject(); resolves the driver from config/llm.php (default, providers) and prepends custom_instructions to the system message |
DriverContract, Drivers\OpenAiDriver | the only driver; openai-php/client with OPENAI_MODEL, key, organisation and project; streaming through the chat completions stream, objects through function calling |
PromptContract, Prompts\Text* | one class per editor command: TextRephrase, TextShorten, TextExtend, TextSummarize, TextContinue, TextFixGrammar, TextBulletpoints, TextCustomPrompt |
TextResponse, ObjectResponse | normalised responses with model and usage |
Consumers:
Http\Controllers\Backend\EditorAiController(POST evolve/api/editor-ai/{command}, roles fromevolve.roles.editor-ai): validatesinput(3 to 5000 characters), picks the prompt class for the command and streams the result withX-Accel-Buffering: no. The rich text toolbar incore/views/components/form/input.blade.phpcallswindow.editorAi(command, text, prompt, onChunk).Http\Livewire\BlockContentGenerator(mounted in the block editor): builds one long prompt with the other pages, the current page's blocks as YAML, an optional reference page and image, and callsLLM::generateObject()with thecreate_landing_pageschema.BLOCK_TYPESmaps generated types tocms.heading,cms.simpleText1Col,cms.simpleTextMedia;CONTAINER_TYPESwraps cards and tiles incms.cardsandcms.tiles(2.12).Http\Livewire\PageAnalyzer(page screen):LLM::generateText()with the page's LLM context (Page::getLlmContext()), temperature 0.25, cached a day under a key that includes the content hash.
Media links in the LLM context (2.18). Block fields reach the model as a YAML dump, so a linked file used to arrive as a bare /media/{hash}/. Services\Media\MediaReferenceAnnotator appends a trailing comment to every line that links one: url: /media/qK31yvw308/ # Pflegeanleitung DE: Tipps zur Reinigung (PDF, 1.2 MB). It recognises /media/{hash} with or without trailing slash, /download, /preview/{slug}, relative or absolute, also inside rich text HTML; raster /img/ URLs are left alone. The label is the media name in the requested locale (then the fallback locale, then any translation, last {hash}.{ext}), the description in the requested locale capped at 160 characters, the upper-cased extension and a decimal size. Block::getLlmContext() annotates; Block::dumpLlmContext() returns the raw dump so Page::getLlmContext() annotates the whole page in one pass (one query for the files, one for their translations). The comment is not part of the value, so YAML parsers ignore it and the URL stays what the router serves. Every consumer benefits: the chatbot's PageContextSource, FaqContextSource and PostContextSource, the page analyzer and the block context endpoint. Faq::getLlmContext() and Post::getLlmContext() annotate too. They run rich text through HtmlText::convert(), which keeps links as Markdown [text](href) (a link whose text is its URL stays a bare URL, an anchor without href keeps only its text), so a linked file reaches the annotator like in a block.
Chatbot module
ai-chatbot/src/:
| Class | Role |
|---|---|
AiChatbotServiceProvider | requiredPackages() names laravel/ai (Laravel\Ai\Contracts\Agent), suggested only since 2.21; registers the module, schedules model:prune for ChatbotQuestion daily, publishes blueprint and block view |
Agents\ChatbotAgent | laravel/ai agent (Promptable, Conversational, HasTools, #[MaxTokens(2048)], #[Temperature(0.7)]); instructions() = block prompt or config prompt or built-in default, plus the page hint and the site context; messages() replays the history; tools() resolves ai-chatbot.tools from the container; buildContext() caches the joined output of the context sources per locale |
Contracts\ContextSource, Context\PageContextSource, FaqContextSource, PostContextSource | render published content as text; available() skips a source whose module is off |
Http\Controllers\AiChatbotController | POST api/chatbot: rate limit per IP in cache (CHATBOT_RATE_LIMIT), validation (question 2 to 500 characters, block, messages limited by history_limit with roles user and assistant only, page.url and page.title, conversation UUID), resolves the system prompt from the published block server-side, logs the question, streams the agent's answer and writes answer, tool calls, model and tokens in the stream's then() |
Models\ChatbotQuestion | the log row: conversation ID, question, answer, page URL, tools, model, token counts, no IP (2.14); prunable() after retention_days |
Http\Livewire\ConversationManager, ConversationThread, Http\Controllers\Backend\ConversationExportController | the panel list, thread view and CSV export under evolve/ai-chatbot/conversation |
The model is whatever laravel/ai resolves for its default provider (config/ai.php, OpenAI by default); ChatbotAgent sets none. A project changes model or steps through a subclass named in ai-chatbot.agent.
Frontend
ai-chatbot/views/blocks/cms/aiChatbot.blade.php (block) and ai-chatbot::widget share ai-chatbot::partials.script: they send question, block, messages, page, conversation and locale, buffer SSE lines split across chunks, escape the model output before Markdown rendering and allow only http(s), relative and mailto links. Tests: ai-chatbot/tests/Feature/*.