# AI assistant model

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

The in-app assistant is `App\Ai\Agents\AppAssistant`: a conversational, tool-using agent (laravel/ai) whose **tool list is filtered by the user’s effective permissions**. Chat history, attachments, imports, and sync sources sit around that agent.

> **Related**
> Product guides: [AI chat](https://docs.qiick.io/externa/ai-chat), [AI imports & sync](https://docs.qiick.io/externa/ai-imports-and-sync). Config: [AI configuration](https://docs.qiick.io/externa/ai-configuration). HTTP: [AI API & webhooks](https://docs.qiick.io/externa/ai-api).

## AppAssistant

| Item               | Detail                                              |
| ------------------ | --------------------------------------------------- |
| Class              | `App\Ai\Agents\AppAssistant`                        |
| Path               | `app/Ai/Agents/AppAssistant.php`                    |
| Contracts          | `Agent`, `Conversational`, `HasSkills`, `HasTools`  |
| Provider attribute | `#[Provider('local')]`                              |
| Traits             | `Promptable`, `RemembersConversations`              |
| Steps              | `maxSteps()` = `count(FieldTypeEnum::cases()) + 16` |
| Timeout            | `300` seconds                                       |
| Entry              | `AiChatController` → `new AppAssistant($user)`      |
| Route gate         | `permission:can-use-ai` on `routes/ai.php`          |

From externa-core **1.4.0**, `AppAssistant::tools()` returns `AiToolRegistryService::chatToolsFor($user)`: the shared registry that also backs the [MCP server](https://docs.qiick.io/externa/mcp-server) ([#202](https://github.com/qiick-io/externa-core/issues/202)). A tool is listed only when the user holds the required permission(s) via `EffectivePermissionResolverService` **and** it passes the config gates (`AI_TOOLS_*`, write gate). Each tool then re-checks action-level permissions through `ChecksAiPermissions::requirePermission()`.

> With `AI_WRITE_TOOLS=false` (default), chat keeps the read actions of the `Manage*` tools (`list`, `get`, `search`, …) and refuses their write and destructive actions. Pure write tools (`ImportCollectionCsv`, `ImportRemoteJson`) and `RollbackLastAiTurn` are not offered. Enable them per tool class and approve each write in the chat — [AI tool approvals & audit](https://docs.qiick.io/externa/ai-tool-approvals).

## Language policy

- The assistant **replies in the same language as the user’s latest message**. If it cannot confidently use that language, it uses **English**. It must **never assume Italian**.
- Tool errors, fallback turn summaries, and AI HTTP messages are **English machine strings**; the model translates them for the user.
- Prompt chips / AI UI chrome in `resources/js` ship English literals (admin UI i18n elsewhere is unchanged).

## Field-type knowledge

| Piece       | Detail                                                                                                                                                                                  |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cookbook    | `App\Ai\Support\FieldTypeCookbook` — curated purpose / required keys / `settings_json` examples / nested rules (aligned with [Field types](https://docs.qiick.io/externa/field-types) + `BlocksFieldSchema`) |
| Tool        | `DescribeFieldTypes` — input `types` = list, `all`, or `complex`                                                                                                                        |
| Gate        | Registered with collection tools; needs show/create/edit collections                                                                                                                    |
| Prompt rule | Before `create_field` / `update_field` on complex types, **MUST** call `DescribeFieldTypes`; treat `ManageCollections` `get` as source of truth for existing schema                     |
| Validation  | `create_field` / `update_field` run `CollectionFieldSettingsPipeline` (same normalize + rules as HTTP Store/Update field)                                                               |
| MCP         | `describe_collection` on the [MCP server](https://docs.qiick.io/externa/mcp-server) exposes the same schema to external agents; the cookbook stays chat-only                                                 |

## Permission-gated tools

### Registration (tool available in the agent)

Permission needed for the tool to be listed (before the write gate):

| Tool class                     | Registered when the user has                                                        |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| `ManageCollections`            | Any of: show / create / edit / delete / restore / force-delete **collections**      |
| `ManageCollectionItems`        | Same collection permission set                                                      |
| `DescribeFieldTypes`           | Same collection permission set (action also accepts show **or** create **or** edit) |
| `QueryCollectionItems`         | `can-show-collections`                                                              |
| `ExportCollection`             | `can-show-collections`                                                              |
| `SearchSimilarCollectionItems` | `can-show-collections`                                                              |
| `ImportCollectionCsv`          | `can-create-collections`                                                            |
| `ImportRemoteJson`             | `can-create-collections`                                                            |
| `ExtractPdfText`               | `can-create-collections`                                                            |
| `GetImportJobStatus`           | `can-create-collections`                                                            |
| `ManageAiSyncSources`          | `can-create-collections`                                                            |
| `RollbackLastAiTurn`           | `can-delete-collections`                                                            |
| `ManageFiles`                  | Any of: show / create / edit / delete / restore / force-delete **files**            |
| `ManageUsers`                  | Any of: show / create / edit / delete / restore / force-delete **users**            |
| `ManageRoles`                  | Any of: show / create / edit / delete **roles**                                     |
| `ManageGroups`                 | Any of: show / create / edit / delete / restore / force-delete **groups**           |
| `QueryActivityLogs`            | `can-show-activity-logs`                                                            |

Paths: `app/Ai/Tools/{ToolName}.php`.

### Action-level gates (high level)

**ManageCollections**

| Actions                                                                      | Permission                     |
| ---------------------------------------------------------------------------- | ------------------------------ |
| `list`, `get`, `list_field_packs`                                            | `can-show-collections`         |
| `create`, `duplicate`                                                        | `can-create-collections`       |
| `update`, `create_field`, `update_field`, `delete_field`, `apply_field_pack` | `can-edit-collections`         |
| `delete`                                                                     | `can-delete-collections`       |
| `restore`                                                                    | `can-restore-collections`      |
| `force_delete`                                                               | `can-force-delete-collections` |

**Collection packs:** for SEO entity / Articles / Pages / Products / Categories, the assistant **MUST** call `apply_collection_pack` (`pack`, e.g. `seo` or `articles`). Dependencies auto-create. UI: Collections **Create from pack…**. Presets: “Apply SEO collection”, “Scaffold Articles”, “Scaffold Products”.

**Field packs:** for denormalized SEO (`seo_inline`) or publishing/contact/social on an existing collection, **MUST** call `apply_field_pack` (`collection_id` + `pack`). Use `seo_inline` only when the user wants inline `seo_*` fields. UI: Fields page **Add field pack…**. Registry: [Field types — Field packs](https://docs.qiick.io/externa/field-types#field-packs).

**ManageCollectionItems**

| Actions                                | Permission                     |
| -------------------------------------- | ------------------------------ |
| `list`, `get`, `list_relation_options` | `can-show-collections`         |
| `create`                               | `can-create-collections`       |
| `update`, `bulk_update`, `publish`     | `can-edit-collections`         |
| `delete`, `bulk_delete`                | `can-delete-collections`       |
| `restore`                              | `can-restore-collections`      |
| `force_delete`                         | `can-force-delete-collections` |

**Single-permission tools**

| Tool                           | Permission               |
| ------------------------------ | ------------------------ |
| `QueryCollectionItems`         | `can-show-collections`   |
| `ExportCollection`             | `can-show-collections`   |
| `SearchSimilarCollectionItems` | `can-show-collections`   |
| `ImportCollectionCsv`          | `can-create-collections` |
| `ImportRemoteJson`             | `can-create-collections` |
| `ExtractPdfText`               | `can-create-collections` |
| `GetImportJobStatus`           | `can-create-collections` |
| `RollbackLastAiTurn`           | `can-delete-collections` |
| `QueryActivityLogs`            | `can-show-activity-logs` |

**ManageAiSyncSources**

| Actions                                 | Permission               |
| --------------------------------------- | ------------------------ |
| `list`, `create`                        | `can-create-collections` |
| `update`, `enable`, `disable`, `delete` | `can-edit-collections`   |

**ManageFiles**

| Actions                                       | Permission               |
| --------------------------------------------- | ------------------------ |
| `list`, `search`                              | `can-show-files`         |
| `create` / `create_folder`, `save_attachment` | `can-create-files`       |
| `rename`, `move`, `move_many`, `tag`          | `can-edit-files`         |
| `delete`                                      | `can-delete-files`       |
| `restore`                                     | `can-restore-files`      |
| `force_delete`                                | `can-force-delete-files` |

**ManageUsers** / **ManageGroups** — show / create / edit / delete / restore / force-delete mapped to the matching `can-*-users` or `can-*-groups` permissions.

**ManageRoles** — `list`/`get` → `can-show-roles`; `create` / `duplicate` → `can-create-roles` (+ show for duplicate); `update` / `delete` → edit/delete roles; `list_permissions` → `can-show-permissions` **or** create/edit roles. **Duplicate** clones Spatie permissions and collection/file matrices into a new named role (no user/group memberships). Cannot duplicate `super-admin` or locked system roles (same as the UI).

> **Same gates as HTTP**
> Collections HTTP routes use Spatie `permission:can-*-collections` (and FormRequest checks on bulk). The assistant re-checks the same permission names per tool action, plus `CollectionPermissionEnforcer` for item ACL / `item_filter`. See [Effective permissions](https://docs.qiick.io/externa/permissions) · [Collections API](https://docs.qiick.io/externa/collections-api).

## Conversations

Configured under `config/ai.php` → `ai.conversations`:

| Key              | Value                                                                  |
| ---------------- | ---------------------------------------------------------------------- |
| Tables           | `agent_conversations`, `agent_conversation_messages`                   |
| `generate_title` | `false` (title derived from the first prompt without an extra LM call) |

Controllers: `AiConversationController`, `AiPageController`. Routes cover list/create/update/destroy, pin, truncate, bulk-destroy, and deep-link `GET /ai/{conversation}`.

Since 1.4.0 each assistant message keeps what the stream showed, so a reload loses nothing ([core #572](https://github.com/qiick-io/externa-core/pull/572)):

| Column on `agent_conversation_messages` | Content                                                                                               |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `steps`                                 | The steps in order: reasoning, text, tool calls (arguments, result, error, denial) and provider tools |
| `usage`                                 | Input, output, reasoning and cache tokens                                                             |
| `meta`                                  | Provider, model, the cited sources (`citations`) and the error, if any                                |
| `status`                                | How the turn ended                                                                                    |

`AiConversationMessagePresenterService` turns a stored message into the payload the Assistant page and the conversation API send, with each tool call's final state. What the user sees: [AI chat — What an answer shows](https://docs.qiick.io/externa/ai-chat#what-an-answer-shows).

## Attachments

| Piece        | Detail                                      |
| ------------ | ------------------------------------------- |
| Model        | `App\Models\AiChatAttachment`               |
| Disk         | `local` (`AiChatAttachment::DISK`)          |
| Max size     | 5 MiB                                       |
| TTL          | 24 hours                                    |
| Extensions   | `csv`, `txt`, `xlsx`, `pdf`                 |
| Upload route | `POST /ai/attachments` (throttle 30/min)    |
| Chat usage   | Up to 5 `attachment_ids` on `POST /ai/chat` |

Items, files, and saved prompts can also be attached as conversation context (1.4.0) — [AI chat — Context attachments](https://docs.qiick.io/externa/ai-chat#context-attachments).
| Cleanup | `ai:cleanup-attachments` (daily schedule) |

## Daily prompt limit

| Config                  | Env                     | Default        |
| ----------------------- | ----------------------- | -------------- |
| `ai.daily_prompt_limit` | `AI_DAILY_PROMPT_LIMIT` | `0` (disabled) |

When the limit is greater than `0`, `AiChatController` counts today’s Spatie activity rows (`log_name = ai`, `event = ai_prompt`, causer = user). At or above the limit the chat endpoint returns **429** with message `Daily AI prompt limit reached.`

## Embeddings and similarity search

| Config                  | Env                     | Default |
| ----------------------- | ----------------------- | ------- |
| `ai.embeddings.enabled` | `AI_EMBEDDINGS_ENABLED` | `false` |

`SearchSimilarCollectionItems` (`can-show-collections`) respects the flag:

| Mode              | When                                                                                                      | Response `mode`   |
| ----------------- | --------------------------------------------------------------------------------------------------------- | ----------------- |
| **Embeddings**    | Flag `true`, query embedding succeeds, and stored `CollectionItemEmbedding` rows exist for the collection | `"embeddings"`    |
| **Semantic-lite** | Flag `false`, provider failure, or no stored vectors yet                                                  | `"semantic-lite"` |

**Embeddings path:** generate a query vector via Laravel AI `Embeddings`, score stored vectors (cosine over JSON float arrays; pgvector later), return scored items. Item saves queue `GenerateCollectionItemEmbeddingJob`. If the collection has **zero** stored embeddings, the tool dispatches generation for up to **50** recent items and falls back this request.

**Semantic-lite path:** load up to **500** recent items (permission-filtered), case-insensitive `str_contains` over lowercased JSON of assembled field values.

Tool description text also switches when the flag is on. Config details: [AI configuration — Embeddings](https://docs.qiick.io/externa/ai-configuration#embeddings).

## Agent Skills

From externa-core **1.4.0** ([#181](https://github.com/qiick-io/externa-core/issues/181)), `AppAssistant` implements laravel/ai `HasSkills`. Playbooks live under `resources/skills/*/SKILL.md` and the model loads one on demand (`LoadSkill`) instead of carrying every rule in the system prompt.

| Skill                   | Used when                                                                                                |
| ----------------------- | -------------------------------------------------------------------------------------------------------- |
| `collection-operations` | Creating, updating, querying, packing, exporting, or deleting collections, fields, or items              |
| `collection-import`     | CSV/XLSX attachments, remote JSON/API URLs, PDF extraction, queued import status, scheduled sync sources |
| `approvals`             | A write or destructive action needs confirmation, or an approval was rejected or abandoned               |

Skills only add instructions. They do not grant tools or permissions; the registry and write gate still decide what can run. The playbook texts are marked as drafts for maintainer review in core.

## MCP server

`ai.mcp.enabled` (`AI_MCP_ENABLED`, default `false`) turns on a real MCP endpoint at `/mcp` (externa-core 1.4.0) that reuses this registry for external AI clients, with owned API keys and `mcp:read` / `mcp:write` scopes. See [MCP server](https://docs.qiick.io/externa/mcp-server).

## Surrounding jobs and schedule

| Piece                    | Role                                                               |
| ------------------------ | ------------------------------------------------------------------ |
| `ImportCollectionJob`    | CSV / Excel / remote JSON imports; status in cache                 |
| `ai:run-sync-sources`    | Every minute — due `AiSyncSource` rows enqueue imports             |
| `ai:cleanup-attachments` | Daily — purge expired chat attachments                             |
| Webhook                  | `POST /ai/webhooks/collection-import` (HMAC-signed, CSRF-excepted) |

## Related pages

- [Effective permissions](https://docs.qiick.io/externa/permissions)
- [Architecture](https://docs.qiick.io/externa/architecture)
- [Collections data model](https://docs.qiick.io/externa/collections-data-model)
- [AI configuration](https://docs.qiick.io/externa/ai-configuration)
- [AI tool approvals & audit](https://docs.qiick.io/externa/ai-tool-approvals)
- [MCP server](https://docs.qiick.io/externa/mcp-server)
