# AI chat

Source: https://docs.qiick.io/externa/ai-chat

AI chat is the product surface for `AppAssistant`: a dedicated `/ai` page, a global FAB drawer, streaming replies, conversation pin/truncate/bulk delete, and short-lived file attachments. The whole `/ai/*` authenticated group requires `can-use-ai`.

<!-- screenshot omitted -->

> **Related**
> Agent & tool model: [AI assistant model](https://docs.qiick.io/externa/ai-assistant). Imports, sync, webhook: [AI imports & sync](https://docs.qiick.io/externa/ai-imports-and-sync). Env/provider keys: [AI configuration](https://docs.qiick.io/externa/ai-configuration). HTTP: [AI API & webhooks](https://docs.qiick.io/externa/ai-api).

> Human-to-human threads (item + DM hub at `/chat`) are documented in [Chat (hub & items)](https://docs.qiick.io/externa/chat). They use Echo, not SSE.

## Access

| Item            | Detail                                                                   |
| --------------- | ------------------------------------------------------------------------ |
| Permission      | `can-use-ai` (`PermissionEnum::CanUseAi`)                                |
| Routes          | `routes/ai.php` — middleware `auth`, `verified`, `permission:can-use-ai` |
| Page controller | `App\Http\Controllers\Ai\AiPageController`                               |
| Frontend page   | `resources/js/pages/ai/index.tsx`                                        |
| FAB             | `resources/js/components/ai/ai-fab.tsx` in `app-sidebar-layout`          |

### AI page

| Route                    | Name        | Behavior                                                     |
| ------------------------ | ----------- | ------------------------------------------------------------ |
| `GET /ai`                | `ai.index`  | Conversation list (25, pinned first) + optional selection    |
| `GET /ai/{conversation}` | `ai.show`   | Same page with selected conversation (UUID)                  |
| `GET /ai/status`         | `ai.status` | `AiStatusController` — cached local provider `/models` probe |

Props include normalized messages (content, tools, attachments). Empty assistant tool turns get `AiToolTurnSummary`.

### FAB

- Exported as `AiFab`, mounted in the app sidebar layout.
- Shown only when the user has `can-use-ai`.
- Hidden on `/ai` routes.
- Drawer: status, presets, attachments, SSE chat via `streamAiChat` → `POST /ai/chat`.

## Ask AI

List pages (users, groups, files, collections, items, activity logs) expose **Ask AI** via `AskAiButton` (`resources/js/components/ai/ask-ai-button.tsx`):

| Behavior   | Detail                                                                                                |
| ---------- | ----------------------------------------------------------------------------------------------------- |
| Permission | Shown only when `can-use-ai`                                                                          |
| Modes      | `icon` (row) or `labeled` (bulk toolbar)                                                              |
| Action     | Opens the FAB composer with a **seeded prompt** (`openAiWithPrompt` in `resources/js/lib/ai-open.ts`) |
| Send       | **Never auto-sends** — the user reviews/edits and submits                                             |

Bulk selection can seed multi-entity prompts (see `tests/Browser/AiContextBulkBrowserTest.php`).

## Conversations

Controller: `App\Http\Controllers\Ai\AiConversationController`.

Models use laravel/ai conversation tables (`agent_conversations` / `agent_conversation_messages` via `config/ai.php`).

| Action       | Route                                            | Behavior                                                                  |
| ------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
| Index        | `GET /ai/conversations`                          | List owned conversations                                                  |
| Store        | `POST /ai/conversations`                         | Create                                                                    |
| Show         | `GET /ai/conversations/{conversation}`           | Detail                                                                    |
| Destroy      | `DELETE /ai/conversations/{conversation}`        | Delete one                                                                |
| Pin          | `POST /ai/conversations/{conversation}/pin`      | Toggle `pinned_at` null ↔ `now()`                                         |
| Truncate     | `POST /ai/conversations/{conversation}/truncate` | Body `from_message_id` (UUID); deletes that message and all later         |
| Bulk destroy | `POST /ai/conversations/bulk-destroy`            | Body `ids` (1–100 UUIDs); must be owned; deletes messages + conversations |

## Chat streaming

| Item       | Detail                                                 |
| ---------- | ------------------------------------------------------ |
| Controller | `App\Http\Controllers\Ai\AiChatController` (invokable) |
| Route      | `POST /ai/chat` (`ai.chat`)                            |
| Throttle   | `throttle:30,1` (**30 requests / minute**)             |
| Agent      | `App\Ai\Agents\AppAssistant`                           |
| Response   | SSE `text/event-stream`, ends with `[DONE]`            |

Validated body:

| Field                | Rules                                    |
| -------------------- | ---------------------------------------- |
| `message`            | Required, max 20000                      |
| `conversation_id`    | Optional UUID                            |
| `attachment_ids`     | Optional, max 5 UUIDs                    |
| `context_item_ids`   | Optional, max 10 item ids (1.4.0)        |
| `context_file_ids`   | Optional, max 10 file ids (1.4.0)        |
| `context_prompt_ids` | Optional, max 5 saved prompt ids (1.4.0) |

Optional daily cap: `config('ai.daily_prompt_limit')` ← `AI_DAILY_PROMPT_LIMIT` (`0` = off), counted from Spatie activity `log_name=ai`, `event=ai_prompt`.

Events may include `{ type: 'conversation', conversation_id }` and `{ type: 'error', … }`. Logging goes through `App\Ai\Support\AiActivityLogger`.

Since 1.4.0 the stream also carries the model's work, not only its text: `reasoning_start` / `reasoning_delta` / `reasoning_end`, `tool_call` and `tool_result`, `citation` (and provider tools such as `web_search_call`), plus the token usage at `stream_end`.

## What an answer shows

From 1.4.0 ([core #565](https://github.com/qiick-io/externa-core/pull/565), [#566](https://github.com/qiick-io/externa-core/pull/566), [#570](https://github.com/qiick-io/externa-core/pull/570), [#572](https://github.com/qiick-io/externa-core/pull/572), [#615](https://github.com/qiick-io/externa-core/pull/615), [#619](https://github.com/qiick-io/externa-core/pull/619), [#621](https://github.com/qiick-io/externa-core/pull/621)) every Assistant answer shows how it was produced, in the order the model produced it:

| Part        | What you see                                                                                                                                                                                                                                                                         |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Reasoning   | **Thinking…** while the model reasons, open so you can follow it; when it finishes it closes to **Thought for N seconds** ("Thought for a few seconds" after a reload). Open or close it yourself and it stays that way.                                                             |
| Tool calls  | Each call as one line with a sentence ("List collections", "Searched the web") and the record it works on, never its JSON. The icon shows the state: running, done, error or denied; a failed call opens to its error. Several calls in a row are grouped under **Done in N steps**. |
| Text        | The answer itself, in Markdown, with tables and highlighted code blocks.                                                                                                                                                                                                             |
| Sources     | **Used N sources**: the pages the model cited (for example from a provider web search), as links.                                                                                                                                                                                    |
| Token usage | The tokens used by the turn against the provider's context window (`*_CONTEXT_WINDOW`), with input, output, reasoning and cache tokens on hover.                                                                                                                                     |

Reasoning appears only when the provider returns it: set the reasoning effort per provider — [AI configuration — Assistant provider, reasoning and context window](https://docs.qiick.io/externa/ai-configuration#assistant-provider-reasoning-and-context-window-1-4-0). A write tool that needs approval pauses the answer on an approval card instead of a line: the call's sentence, **Reject** and **Approve** (red only when the call deletes data). The answer offers no follow-up suggestions until you decide; once you do, the call becomes a line and the model continues — [AI tool approvals & audit](https://docs.qiick.io/externa/ai-tool-approvals).

All of it is **persisted**: reload the page or open the conversation later and the reasoning, every tool call with its state or error, the sources and the usage are shown again, the same way they streamed. See [AI assistant model — Conversations](https://docs.qiick.io/externa/ai-assistant#conversations) for the stored fields.

## Attachments

| Item       | Detail                                               |
| ---------- | ---------------------------------------------------- |
| Controller | `App\Http\Controllers\Ai\AiChatAttachmentController` |
| Route      | `POST /ai/attachments` (`ai.attachments.store`)      |
| Throttle   | `throttle:30,1`                                      |
| Model      | `App\Models\AiChatAttachment`                        |

Allowed extensions: **`csv`**, **`txt`**, **`xlsx`**, **`pdf`** (MIME allowlist matches those families).

| Limit    | Value                                 |
| -------- | ------------------------------------- |
| Max size | `MAX_BYTES = 5 * 1024 * 1024` (5 MiB) |
| TTL      | `TTL_HOURS = 24`                      |
| Disk     | `local`                               |

Cleanup: scheduled `ai:cleanup-attachments` daily (`routes/console.php`).

## Context attachments

From externa-core **1.4.0** ([#195](https://github.com/qiick-io/externa-core/issues/195)), the composer's **Attach** button opens an **Attach context** picker for collection items, files, and saved prompts. The selection is sent with the next message as `context_item_ids`, `context_file_ids`, and `context_prompt_ids`.

| Piece         | Detail                                                                                                            |
| ------------- | ----------------------------------------------------------------------------------------------------------------- |
| Picker search | `GET /ai/context/collections`, `/ai/context/items`, `/ai/context/files` (`throttle:60,1`)                         |
| Limits        | 10 items, 10 files, 5 saved prompts per message (more → **422**)                                                  |
| Items         | Need `can-show-collections`, collection read and the role's item filter; field values are stripped by field rules |
| Files         | Need `can-show-files`; only metadata (id, name, MIME, size, type) is sent, never file contents                    |
| Saved prompts | Their body is added as a saved-prompt block                                                                       |

Access is **re-checked when the message is sent**, not only when the picker lists results, so access revoked in between cannot leak into the model prompt. Any id that is missing or not readable fails the whole send with **422** (`One or more context items are not accessible.`).

The resolved context is appended to the prompt as `[AI_CONTEXT_ITEMS]` / `[AI_CONTEXT_FILES]` / `[AI_SAVED_PROMPT]` blocks, and the model is told to treat them as operator-selected context.

Deleting a conversation from the `/ai` row menu asks for confirmation (1.4.0, [core #522](https://github.com/qiick-io/externa-core/pull/522)).

## Saved prompts

Project-level reusable prompts (table `ai_prompts`), shared by everyone with `can-use-ai`.

| Method   | Path               | Permission          | Notes                                                                 |
| -------- | ------------------ | ------------------- | --------------------------------------------------------------------- |
| `GET`    | `/ai/prompts`      | `can-use-ai`        | Optional `search` (name / description), max 100; returns `can_manage` |
| `POST`   | `/ai/prompts`      | `ai.prompts.manage` | `name` (≤120), `description` (≤255), `body` (≤20000)                  |
| `PUT`    | `/ai/prompts/{id}` | `ai.prompts.manage` | Same fields                                                           |
| `DELETE` | `/ai/prompts/{id}` | `ai.prompts.manage` |                                                                       |

`ai.prompts.manage` is new in 1.4.0. `externa:upgrade` (via `permissions:sync`) grants it to the built-in `admin` role — [Upgrade](https://docs.qiick.io/externa/upgrade#1-3-x-1-4-0).

## Write actions and approvals

From 1.4.0, write and destructive tool actions are **denied by default** in chat, while read actions keep working. When an operator enables a tool for writes, each write or destructive action asks for your approval in the conversation before it runs. Details: [AI tool approvals & audit](https://docs.qiick.io/externa/ai-tool-approvals).

## Presets

Presets are **static frontend prompt templates**, not backend entities.

| Item       | Detail                                                                                                                                                                      |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Definition | `resources/js/lib/ai-action-presets.ts`                                                                                                                                     |
| Types      | `AiActionPreset`, categories `comuni` \| `dati` \| `file` \| `admin` \| `avanzate`                                                                                          |
| List       | `AI_ACTION_PRESETS` (~25 templates: CSV/Excel/URL import, dry-run, upsert, export, PDF→schema, files, roles/users/groups, audit, async import, scheduled sync, rollback, …) |
| Filter     | `filterAiActionPresets()` — show when `anyOf` is empty or the user has any listed permission                                                                                |
| UI         | `resources/js/components/ai/ai-action-presets-drawer.tsx`                                                                                                                   |

Related suggested-action chips (tool results): `resources/js/lib/ai-suggested-actions.ts`.

## Client helpers

| Helper            | Path                           |
| ----------------- | ------------------------------ |
| Stream / chat API | `resources/js/lib/ai-chat.ts`  |
| Components        | `resources/js/components/ai/*` |

## Source map

| Concern       | Location                                                                                             |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| Page          | `app/Http/Controllers/Ai/AiPageController.php`                                                       |
| Chat          | `app/Http/Controllers/Ai/AiChatController.php`                                                       |
| Conversations | `app/Http/Controllers/Ai/AiConversationController.php`                                               |
| Attachments   | `app/Http/Controllers/Ai/AiChatAttachmentController.php`                                             |
| Context       | `app/Services/Ai/AiConversationContextService.php`, `AiContextSearchController.php`                  |
| Saved prompts | `app/Http/Controllers/Ai/AiPromptController.php`, `app/Models/AiPrompt.php`                          |
| Routes        | `routes/ai.php`                                                                                      |
| UI            | `resources/js/pages/ai/index.tsx`, `components/ai/ai-fab.tsx`, `ask-ai-button.tsx`, `lib/ai-open.ts` |
