---
url: /internals/ai.md
---
# 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](/panel/editor-ai), [AI chatbot](/modules/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 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/`:

| 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/*`.
