Skip to content

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

ClassRole
Corethe 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\OpenAiDriverthe 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, ObjectResponsenormalised responses with model and usage

Consumers:

  • Http\Controllers\Backend\EditorAiController (POST evolve/api/editor-ai/{command}, roles from evolve.roles.editor-ai): validates input (3 to 5000 characters), picks the prompt class for the command and streams the result with X-Accel-Buffering: no. The rich text toolbar in core/views/components/form/input.blade.php calls window.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 calls LLM::generateObject() with the create_landing_page schema. BLOCK_TYPES maps generated types to cms.heading, cms.simpleText1Col, cms.simpleTextMedia; CONTAINER_TYPES wraps cards and tiles in cms.cards and cms.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/:

ClassRole
AiChatbotServiceProviderrequiredPackages() 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\ChatbotAgentlaravel/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, PostContextSourcerender published content as text; available() skips a source whose module is off
Http\Controllers\AiChatbotControllerPOST 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\ChatbotQuestionthe 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\ConversationExportControllerthe 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/*.