Changeset 0.22.8 (#150)
This commit is contained in:
512
docs/ICD-API.md
512
docs/ICD-API.md
@@ -1,7 +1,7 @@
|
||||
# ICD-API — Chat Switchboard Backend API Contract
|
||||
|
||||
**Version:** 0.22.7
|
||||
**Updated:** 2026-03-03
|
||||
**Version:** 0.23.0
|
||||
**Updated:** 2026-03-04
|
||||
**Audience:** Frontend developers, integrators, API consumers
|
||||
|
||||
> **Organization principle:** Each section is one domain — the complete story.
|
||||
@@ -31,6 +31,7 @@
|
||||
15. [Teams & Access Control](#15-teams--access-control)
|
||||
16. [Platform Administration](#16-platform-administration)
|
||||
17. [Export & Utility](#17-export--utility)
|
||||
17b. [Files & Storage](#17b-files--storage)
|
||||
18. [WebSocket Protocol](#18-websocket-protocol)
|
||||
19. [Appendix: Enums](#19-appendix-enums)
|
||||
|
||||
@@ -118,14 +119,6 @@ both Postgres and SQLite backends.
|
||||
ISO 8601 with timezone: `"2025-06-15T14:30:00Z"`. Stored as
|
||||
`TIMESTAMPTZ` (Postgres) or `TEXT` (SQLite).
|
||||
|
||||
### Naming Note
|
||||
|
||||
The domain concept is **Persona** (identity + system prompt + model +
|
||||
KB bindings + grant scoping). API routes currently use `/personas` for
|
||||
historical reasons. A pre-1.0 rename to `/personas` is planned. This
|
||||
document uses "Persona" for the concept and shows the current route
|
||||
paths as-is.
|
||||
|
||||
---
|
||||
|
||||
## 2. Auth
|
||||
@@ -209,8 +202,9 @@ Revokes the refresh token. Returns `{ "message": "logged out" }`.
|
||||
|
||||
The **channel** is the universal conversation container. Messages,
|
||||
tool activity, attachments, and KB bindings all hang off a channel.
|
||||
Today channels are single-user direct chats; the architecture
|
||||
anticipates multi-participant channels for v0.23.0.
|
||||
Channels support multiple participants — users, personas, and
|
||||
sessions — making them the foundation for collaborative and
|
||||
multi-model workflows.
|
||||
|
||||
### 3.1 Channel CRUD
|
||||
|
||||
@@ -225,9 +219,8 @@ Returns pagination envelope. Each channel:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"title": "My Chat",
|
||||
"type": "direct",
|
||||
"type": "direct|group|workflow",
|
||||
"description": "",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid|null",
|
||||
@@ -235,11 +228,20 @@ Returns pagination envelope. Each channel:
|
||||
"project_id": "uuid|null",
|
||||
"folder": "string|null",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"participant_count": 1,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Channel types:**
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `direct` | Single-user chat (default, backward compatible) |
|
||||
| `group` | Multi-user/multi-model collaborative channel |
|
||||
| `workflow` | Staged channel driven by workflow definitions |
|
||||
|
||||
**Create channel:**
|
||||
|
||||
```
|
||||
@@ -259,9 +261,10 @@ POST /channels
|
||||
}
|
||||
```
|
||||
|
||||
On create, if `model` and `provider_config_id` are provided, the
|
||||
channel model roster (`channel_models`) is auto-populated with an
|
||||
initial entry.
|
||||
On create, the authenticated user is automatically added as a
|
||||
participant with `role: "owner"`. If `model` and `provider_config_id`
|
||||
are provided, the channel model roster (`channel_models`) is
|
||||
auto-populated with an initial entry.
|
||||
|
||||
**Get channel:**
|
||||
|
||||
@@ -315,6 +318,8 @@ following the cursor at each fork. This is what the UI renders.
|
||||
"content": "...",
|
||||
"model": "claude-sonnet-4-20250514|null",
|
||||
"provider_config_id": "uuid|null",
|
||||
"participant_id": "uuid|null",
|
||||
"participant_type": "user|persona|session|null",
|
||||
"thinking": "...|null",
|
||||
"tool_calls": [...],
|
||||
"tool_results": [...],
|
||||
@@ -328,6 +333,11 @@ following the cursor at each fork. This is what the UI renders.
|
||||
}
|
||||
```
|
||||
|
||||
`participant_id` and `participant_type` identify who authored the
|
||||
message. For `user` messages, this is the authenticated user. For
|
||||
`assistant` messages, this is the persona (if set) or null for raw
|
||||
model responses. Existing messages without participants return null.
|
||||
|
||||
**List all messages** — includes all branches, flat:
|
||||
|
||||
```
|
||||
@@ -432,15 +442,23 @@ POST /chat/completions
|
||||
}
|
||||
```
|
||||
|
||||
`channel_id` is required. `chat_id` is accepted as a deprecated alias
|
||||
(maps to `channel_id`; frontend no longer sends it — removal planned).
|
||||
`channel_id` is required.
|
||||
|
||||
`extra_body` is a freeform JSON object merged into the provider request
|
||||
(provider-specific parameters like `temperature`, `reasoning`, etc.).
|
||||
|
||||
**Persona resolution:** If `persona_id` is set, the handler loads the
|
||||
persona's system prompt, model, provider_config_id, and KB bindings.
|
||||
Per-message `model`/`provider_config_id` override the persona's defaults.
|
||||
**Participant scoping:** The requesting user must be a participant in
|
||||
the channel. Messages are attributed to the authenticated user (or
|
||||
session). In `group` channels, all participants see all messages in
|
||||
real time via WebSocket.
|
||||
|
||||
**Persona resolution:** The primary completion path. If `persona_id`
|
||||
is set (explicitly or via `@mention` resolution from message content),
|
||||
the handler loads the persona's system prompt, model, provider config,
|
||||
and KB bindings. Per-message `model`/`provider_config_id` override the
|
||||
persona's defaults. In multi-persona channels, `@mention` in the
|
||||
message content is parsed to resolve the target persona from the
|
||||
channel's participant list.
|
||||
|
||||
**Routing resolution:** After config resolution, the routing policy
|
||||
evaluator (`evaluateRouting()`) may redirect to a different provider
|
||||
@@ -502,10 +520,16 @@ GET /tools
|
||||
Returns `{ "tools": [...] }` where each tool has `name`, `description`,
|
||||
`parameters` (JSON Schema), and `category`.
|
||||
|
||||
### 3.4 Multi-Model Roster
|
||||
### 3.4 Multi-Model Roster (Raw Access)
|
||||
|
||||
Channels can have multiple AI models. The `channel_models` table tracks
|
||||
the roster. `@mention` in message content routes to specific models.
|
||||
The primary way to add AI to a channel is by adding a **persona as a
|
||||
participant** (§3.7). Adding a persona automatically populates the
|
||||
channel model roster with the persona's underlying model.
|
||||
|
||||
For the rare case where a user needs raw model access without a
|
||||
persona wrapper, the `channel_models` table provides direct model
|
||||
roster management. This is the 1% escape hatch — most users should
|
||||
never need it.
|
||||
|
||||
```
|
||||
GET /channels/:id/models → { "models": [...] }
|
||||
@@ -524,10 +548,20 @@ Each roster entry:
|
||||
"provider_config_id": "uuid",
|
||||
"display_name": "Claude",
|
||||
"is_default": true,
|
||||
"persona_id": "uuid|null",
|
||||
"created_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
`persona_id`: set when this roster entry was auto-created by adding a
|
||||
persona participant. Null for manually-added raw models.
|
||||
|
||||
**`@mention` resolution:** When a user types `@` in a channel, the
|
||||
autocomplete shows persona participants first (by display name), then
|
||||
raw model roster entries. The target of a `@mention` is always
|
||||
resolved to a specific `provider_config_id` + `model` pair — never
|
||||
an ambiguous bare model name.
|
||||
|
||||
### 3.5 Channel Utilities
|
||||
|
||||
**Summarize channel** (compaction):
|
||||
@@ -548,65 +582,236 @@ POST /channels/:id/generate-title
|
||||
Uses the utility model to auto-generate a title from the first
|
||||
exchange. Returns `{ "title": "Generated Title" }`.
|
||||
|
||||
### 3.6 Attachments
|
||||
### 3.6 Files
|
||||
|
||||
File attachments are channel-scoped. Upload stores the file in blob
|
||||
storage; a background extraction pipeline processes documents (PDF,
|
||||
DOCX, XLSX, etc.) into searchable text.
|
||||
All binary content associated with channels — user uploads, tool-
|
||||
generated artifacts, system files — lives in the unified `files`
|
||||
table backed by the ObjectStore (see §X). The old `attachments`
|
||||
table and routes are removed.
|
||||
|
||||
**Upload:**
|
||||
**Upload (user):**
|
||||
|
||||
```
|
||||
POST /channels/:id/attachments
|
||||
POST /channels/:id/files
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
Field: `file`. Returns the attachment object.
|
||||
Field: `file`. Sets `origin: "user_upload"`. Returns the file object.
|
||||
|
||||
**Create (tool output):**
|
||||
|
||||
```
|
||||
POST /files
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid",
|
||||
"filename": "generated_image.png",
|
||||
"content_type": "image/png",
|
||||
"display_hint": "inline",
|
||||
"metadata": { "tool_name": "image_generation" }
|
||||
}
|
||||
```
|
||||
|
||||
Body: multipart `file` field or base64 `data` field.
|
||||
Sets `origin: "tool_output"`. Returns file object.
|
||||
|
||||
**List by channel:**
|
||||
|
||||
```
|
||||
GET /channels/:id/attachments
|
||||
GET /channels/:id/files?origin=user_upload
|
||||
```
|
||||
|
||||
Returns `{ "attachments": [...] }`.
|
||||
Returns `{ "files": [...] }`. Filterable by `origin`, `content_type`.
|
||||
|
||||
**List by message:**
|
||||
|
||||
```
|
||||
GET /messages/:id/files → { "files": [...] }
|
||||
```
|
||||
|
||||
How the frontend discovers generated artifacts to render inline.
|
||||
|
||||
**List by user (file manager):**
|
||||
|
||||
```
|
||||
GET /files?page=1&per_page=50 → { "files": [...], "total": N }
|
||||
```
|
||||
|
||||
All files owned by the authenticated user, paginated.
|
||||
|
||||
**Get metadata:**
|
||||
|
||||
```
|
||||
GET /attachments/:id
|
||||
GET /files/:id
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid|null",
|
||||
"user_id": "uuid",
|
||||
"origin": "user_upload|tool_output|system",
|
||||
"filename": "report.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"size": 1048576,
|
||||
"storage_key": "attachments/chan-id/att-id_report.pdf",
|
||||
"extracted_text_key": "...|null",
|
||||
"size_bytes": 1048576,
|
||||
"display_hint": "inline|download|thumbnail",
|
||||
"extracted_text": "...|null",
|
||||
"metadata": {},
|
||||
"created_at": "..."
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Download:**
|
||||
|
||||
```
|
||||
GET /attachments/:id/download
|
||||
GET /files/:id/download
|
||||
```
|
||||
|
||||
Returns the file with appropriate `Content-Type` and
|
||||
`Content-Disposition` headers.
|
||||
|
||||
**Thumbnail:**
|
||||
|
||||
```
|
||||
GET /files/:id/thumbnail
|
||||
```
|
||||
|
||||
**Delete:**
|
||||
|
||||
```
|
||||
DELETE /attachments/:id
|
||||
DELETE /files/:id
|
||||
```
|
||||
|
||||
Deletes metadata + blob. Channel deletion cascades via FK +
|
||||
`DeletePrefix`.
|
||||
|
||||
### 3.7 Channel Participants
|
||||
|
||||
The participant system is how users and AI interact in a channel.
|
||||
**Personas are the primary way to add AI** — adding a persona as a
|
||||
participant brings its full identity (name, avatar, system prompt,
|
||||
model, provider config, KB bindings) into the channel. This is the
|
||||
99% path. Raw model access without a persona is available via the
|
||||
model roster (§3.4) for power users.
|
||||
|
||||
Every channel has at least one participant (the owner). `direct`
|
||||
channels have exactly one user participant and optionally one or more
|
||||
persona participants. `group` and `workflow` channels support multiple
|
||||
participants of mixed types.
|
||||
|
||||
**List participants:**
|
||||
|
||||
```
|
||||
GET /channels/:id/participants
|
||||
```
|
||||
|
||||
Returns `{ "participants": [...] }`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"participant_type": "user|persona|session",
|
||||
"participant_id": "uuid",
|
||||
"role": "owner|member|observer",
|
||||
"display_name": "Jane Doe",
|
||||
"avatar_url": "...|null",
|
||||
"joined_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Participant types:**
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `user` | Authenticated user. `participant_id` = `users.id` |
|
||||
| `persona` | AI persona added to channel. `participant_id` = `personas.id`. Brings model, system prompt, KB bindings |
|
||||
| `session` | Anonymous/session participant (workflow intake). `participant_id` = session token |
|
||||
|
||||
**Participant roles:**
|
||||
|
||||
| Role | Capabilities |
|
||||
|------|-------------|
|
||||
| `owner` | Full control: add/remove participants, delete channel, all member capabilities |
|
||||
| `member` | Send messages, trigger completions, view history |
|
||||
| `observer` | Read-only: view messages, no send |
|
||||
|
||||
**Add participant:**
|
||||
|
||||
```
|
||||
POST /channels/:id/participants
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"participant_type": "user|persona",
|
||||
"participant_id": "uuid",
|
||||
"role": "member"
|
||||
}
|
||||
```
|
||||
|
||||
Requires `owner` role in the channel. Adding a `persona` participant:
|
||||
- Makes the persona available as an `@mention` target
|
||||
- Adds the persona's model to the channel model roster automatically
|
||||
- Scopes the persona's KB bindings into the channel's KB resolution chain
|
||||
- The persona's avatar and display name appear in the participant list
|
||||
|
||||
**Update participant role:**
|
||||
|
||||
```
|
||||
PATCH /channels/:id/participants/:participantId
|
||||
```
|
||||
|
||||
```json
|
||||
{ "role": "observer" }
|
||||
```
|
||||
|
||||
**Remove participant:**
|
||||
|
||||
```
|
||||
DELETE /channels/:id/participants/:participantId
|
||||
```
|
||||
|
||||
Cannot remove the last owner. Removing a `persona` participant also
|
||||
removes its auto-created model roster entry.
|
||||
|
||||
**Completion routing:** When `@PersonaName` appears in message
|
||||
content, the completion handler resolves to that persona's model,
|
||||
provider config, and system prompt. When no `@mention` is present in
|
||||
a multi-persona channel, the channel's default persona (first added)
|
||||
handles the response. The `persona_id` field in the completion
|
||||
request (§3.3) can also be set explicitly to override `@mention`
|
||||
resolution.
|
||||
|
||||
**Backward compatibility:** Existing `direct` channels that predate
|
||||
the participant system are auto-migrated on first access — a single
|
||||
`user` participant with `role: "owner"` is created from the channel's
|
||||
legacy `user_id` column.
|
||||
|
||||
### 3.8 Presence
|
||||
|
||||
Real-time presence for channel participants. Delivered via WebSocket
|
||||
to all subscribers of the channel room.
|
||||
|
||||
**Who's here:**
|
||||
|
||||
```
|
||||
GET /channels/:id/presence
|
||||
```
|
||||
|
||||
Returns `{ "participants": [{ "participant_id", "display_name", "status", "last_seen" }] }`.
|
||||
|
||||
`status`: `online`, `idle`, `offline`.
|
||||
|
||||
**Typing indicators** are sent via WebSocket (see §18.4). The typing
|
||||
event payload includes `participant_id` and `participant_type` so the
|
||||
UI can distinguish between human and AI typing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Personas
|
||||
@@ -616,6 +821,16 @@ prompt + model + provider config + KB bindings + grant scoping. Users
|
||||
interact with Personas, not raw provider configs. Admins curate which
|
||||
Personas are available; team admins create team-scoped Personas.
|
||||
|
||||
**Model inheritance:** A Persona inherits all properties of its
|
||||
underlying model. This includes context window size, max output
|
||||
tokens, supported capabilities (thinking, tools, vision, streaming),
|
||||
input/output pricing, and provider status. When the frontend displays
|
||||
a Persona, it resolves the underlying model's capabilities and shows
|
||||
the same pills/badges (e.g. thinking, tools, 200k context) that the
|
||||
raw model would show. The Persona adds identity (name, avatar, system
|
||||
prompt, KB bindings) on top of the model's capabilities — it never
|
||||
masks or reduces them.
|
||||
|
||||
Three scopes, three route namespaces, one domain:
|
||||
|
||||
| Scope | Routes | Who manages |
|
||||
@@ -637,31 +852,46 @@ All three return the same Persona object shape:
|
||||
"scope": "personal|team|global",
|
||||
"owner_id": "uuid|null",
|
||||
"is_default": false,
|
||||
"is_active": true,
|
||||
"avatar_url": "/api/v1/personas/uuid/avatar?v=123",
|
||||
"inherited": {
|
||||
"context_window": 200000,
|
||||
"max_output_tokens": 8192,
|
||||
"supports_vision": true,
|
||||
"supports_tools": true,
|
||||
"supports_thinking": true,
|
||||
"supports_streaming": true,
|
||||
"input_price_per_m": 3.00,
|
||||
"output_price_per_m": 15.00,
|
||||
"provider_status": "healthy|degraded|down|null"
|
||||
},
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
The `inherited` block is populated by the backend from the resolved
|
||||
model capabilities at response time — it is not stored on the persona.
|
||||
If the underlying model's capabilities change (e.g. provider upgrades
|
||||
context window), the persona inherits the new values automatically.
|
||||
The frontend uses `inherited` to render capability pills and context
|
||||
size badges identical to the raw model.
|
||||
|
||||
### 4.1 Personal Personas
|
||||
|
||||
Gated by `allow_user_personas` policy.
|
||||
|
||||
```
|
||||
GET /personas → { "personas": [...], "personas": [...] }
|
||||
GET /personas → { "personas": [...] }
|
||||
POST /personas ← { "name", "description", "system_prompt", "model", ... }
|
||||
PUT /personas/:id ← partial update
|
||||
DELETE /personas/:id
|
||||
```
|
||||
|
||||
**Note:** List response currently sends both `"personas"` and `"personas"`
|
||||
keys (identical arrays) for backward compatibility. The `"personas"` key
|
||||
is what the frontend reads. Dual keys will be collapsed pre-1.0.
|
||||
|
||||
### 4.2 Team Personas
|
||||
|
||||
```
|
||||
GET /teams/:teamId/personas → { "personas": [...], "personas": [...] }
|
||||
GET /teams/:teamId/personas → { "personas": [...] }
|
||||
POST /teams/:teamId/personas ← same shape
|
||||
PUT /teams/:teamId/personas/:id
|
||||
DELETE /teams/:teamId/personas/:id
|
||||
@@ -670,7 +900,7 @@ DELETE /teams/:teamId/personas/:id
|
||||
### 4.3 Admin (Global) Personas
|
||||
|
||||
```
|
||||
GET /admin/personas → { "personas": [...], "personas": [...] }
|
||||
GET /admin/personas → { "personas": [...] }
|
||||
POST /admin/personas ← same shape
|
||||
PUT /admin/personas/:id
|
||||
DELETE /admin/personas/:id
|
||||
@@ -1512,9 +1742,6 @@ The primary endpoint for populating model selectors:
|
||||
GET /models/enabled
|
||||
```
|
||||
|
||||
> **Note:** `GET /models` exists as an alias. The alias is unused by
|
||||
> the frontend and will be removed pre-1.0.
|
||||
|
||||
Returns `{ "models": [UserModel objects] }`:
|
||||
|
||||
```json
|
||||
@@ -1536,23 +1763,59 @@ Returns `{ "models": [UserModel objects] }`:
|
||||
"output_price_per_m": 15.00,
|
||||
"provider_status": "healthy|degraded|down|null",
|
||||
"scope": "global|team|personal",
|
||||
"source": "catalog|heuristic"
|
||||
"source": "catalog|heuristic",
|
||||
"is_persona": false,
|
||||
"persona_id": "uuid|null",
|
||||
"persona_scope": "global|team|personal|null",
|
||||
"persona_avatar": "url|null",
|
||||
"persona_team_name": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
The capabilities on each model are resolved through the three-tier
|
||||
chain: catalog DB → heuristic inference → admin overrides (see §10.5).
|
||||
|
||||
When `is_persona` is `true`, the capability fields (`context_window`,
|
||||
`max_output_tokens`, `supports_*`, pricing, `provider_status`) are
|
||||
inherited from the persona's underlying model. The frontend renders
|
||||
the same capability pills and badges for a persona as for its raw
|
||||
model — the persona adds identity, not capability restrictions.
|
||||
|
||||
### 11.2 User Model Preferences
|
||||
|
||||
Users can hide models they don't want to see.
|
||||
Users can hide models they don't want to see and set per-model
|
||||
defaults. Preferences are keyed on the **composite identity**
|
||||
`provider_config_id:model_id` — the same model from different
|
||||
providers can have independent visibility.
|
||||
|
||||
```
|
||||
GET /models/preferences → { "preferences": { "model-id": { "hidden": true } } }
|
||||
PUT /models/preferences ← { "model_id": "...", "hidden": true }
|
||||
POST /models/preferences/bulk ← { "preferences": { "id1": { "hidden": true }, "id2": { ... } } }
|
||||
GET /models/preferences → { "preferences": [PreferenceEntry objects] }
|
||||
PUT /models/preferences ← { "model_id": "...", "provider_config_id": "uuid", "hidden": true }
|
||||
POST /models/preferences/bulk ← { "entries": [{ "model_id": "...", "provider_config_id": "uuid", "hidden": true }] }
|
||||
```
|
||||
|
||||
PreferenceEntry:
|
||||
|
||||
```json
|
||||
{
|
||||
"model_id": "grok-4.1-fast",
|
||||
"provider_config_id": "uuid",
|
||||
"hidden": true,
|
||||
"preferred_temperature": 0.7,
|
||||
"preferred_max_tokens": 4096,
|
||||
"sort_order": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Identity rule:** `provider_config_id` is required on write. The same
|
||||
bare `model_id` from two different provider configs (e.g. global Venice
|
||||
vs personal BYOK Venice) are independent preference entries. The
|
||||
frontend composite ID format is `{provider_config_id}:{model_id}`.
|
||||
|
||||
**Persona preferences:** Personas are not in this table. A persona's
|
||||
visibility is controlled by the grant system (§15.3) and the
|
||||
`is_active` flag, not by model preferences.
|
||||
|
||||
---
|
||||
|
||||
## 12. Notifications
|
||||
@@ -2022,6 +2285,121 @@ Docker images).
|
||||
|
||||
---
|
||||
|
||||
## 17b. Files & Storage
|
||||
|
||||
All binary content in Chat Switchboard flows through a single
|
||||
**ObjectStore** abstraction backed by PVC (filesystem) or S3. All
|
||||
file metadata lives in one `files` table. The old `attachments`
|
||||
table is dropped.
|
||||
|
||||
### 17b.1 Storage Backend
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| `Put(key, reader, size, contentType)` | Store a blob |
|
||||
| `Get(key)` → reader, size, contentType | Retrieve a blob |
|
||||
| `Delete(key)` | Remove a blob |
|
||||
| `DeletePrefix(prefix)` | Bulk remove (channel deletion) |
|
||||
| `Exists(key)` | Check without reading |
|
||||
| `Healthy()` | Backend health check |
|
||||
| `Stats()` | Aggregate metrics |
|
||||
|
||||
Backends: `pvc` (`STORAGE_PATH=/data/storage`) or `s3`
|
||||
(`S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`).
|
||||
|
||||
### 17b.2 Key Namespacing
|
||||
|
||||
| Prefix | Pattern | What |
|
||||
|--------|---------|------|
|
||||
| `attachments/` | `attachments/{channel_id}/{file_id}_{filename}` | User uploads + AI outputs (legacy prefix retained for existing blobs) |
|
||||
| `kb/` | `kb/{kb_id}/{doc_id}_{filename}` | Knowledge base documents |
|
||||
| `exports/` | `exports/{user_id}/{export_id}_{filename}` | Chat/data exports |
|
||||
| `workspace/` | `workspace/{workspace_id}/{path}` | Workspace files |
|
||||
|
||||
### 17b.3 File Object
|
||||
|
||||
```sql
|
||||
CREATE TABLE files (
|
||||
id UUID PRIMARY KEY,
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE CASCADE,
|
||||
message_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
||||
origin TEXT NOT NULL DEFAULT 'user_upload'
|
||||
CHECK (origin IN ('user_upload','tool_output','system')),
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
storage_key TEXT NOT NULL,
|
||||
display_hint TEXT NOT NULL DEFAULT 'download'
|
||||
CHECK (display_hint IN ('inline','download','thumbnail')),
|
||||
extracted_text TEXT,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Origin:**
|
||||
|
||||
| Value | Who creates | Examples |
|
||||
|-------|-------------|---------|
|
||||
| `user_upload` | User via upload UI | PDF, image, doc attached to message |
|
||||
| `tool_output` | Tool execution during completion | DALL-E image, video gen, code artifact |
|
||||
| `system` | Platform | Export, avatar stored as file |
|
||||
|
||||
**Display hint:**
|
||||
|
||||
| Value | Frontend behavior |
|
||||
|-------|-------------------|
|
||||
| `inline` | Render in message stream (images, video, audio, HTML) |
|
||||
| `download` | Filename + size + download button |
|
||||
| `thumbnail` | Thumbnail preview, click to expand |
|
||||
|
||||
### 17b.4 Tool Output Flow
|
||||
|
||||
1. Tool executes (image gen, video render, etc.)
|
||||
2. Tool writes blob via `ObjectStore.Put`
|
||||
3. Tool creates file record: `POST /files` with `origin: "tool_output"`, `message_id` = current assistant message
|
||||
4. Tool result includes `file_id` reference
|
||||
5. WebSocket emits `file.created` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "file.created",
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid",
|
||||
"file": { ...file object... }
|
||||
}
|
||||
```
|
||||
|
||||
6. Frontend renders inline based on `display_hint` + `content_type`
|
||||
|
||||
The event fires during streaming so the user sees the artifact
|
||||
before the completion finishes.
|
||||
|
||||
### 17b.5 Content Type → Display Hint Defaults
|
||||
|
||||
| Content Type | Default Hint | Rendering |
|
||||
|-------------|-------------|-----------|
|
||||
| `image/*` | `inline` | `<img>` in message, lightbox on click |
|
||||
| `video/*` | `inline` | `<video>` player |
|
||||
| `audio/*` | `inline` | `<audio>` player |
|
||||
| `text/html` | `inline` | Sandboxed iframe |
|
||||
| `application/pdf` | `thumbnail` | Thumb + PDF viewer on click |
|
||||
| `text/*`, `application/json` | `inline` | Syntax-highlighted code block |
|
||||
| Everything else | `download` | Filename + download button |
|
||||
|
||||
Tools can override the default hint explicitly.
|
||||
|
||||
### 17b.6 File Manager (Future Surface)
|
||||
|
||||
A user-scoped file browser backed by `GET /files?page=1&per_page=50`.
|
||||
Displays all files owned by the user across all channels, grouped by
|
||||
channel or date. Supports download and delete. Deleting a file that
|
||||
is referenced by a message replaces the inline render with a
|
||||
"file deleted" placeholder.
|
||||
|
||||
## 18. WebSocket Protocol
|
||||
|
||||
Real-time bidirectional event bus. Single connection per client.
|
||||
@@ -2069,8 +2447,12 @@ Clients subscribe to rooms for scoped event delivery:
|
||||
| `message.updated` | → client | Message content changed |
|
||||
| `message.deleted` | → client | Message removed |
|
||||
| `cursor.updated` | → client | Branch cursor moved |
|
||||
| `typing.start` | ↔ both | User/AI started typing |
|
||||
| `typing.start` | ↔ both | Participant started typing (payload includes `participant_id`, `participant_type`) |
|
||||
| `typing.stop` | ↔ both | Typing ended |
|
||||
| `participant.joined` | → client | Participant added to channel |
|
||||
| `participant.left` | → client | Participant removed from channel |
|
||||
| `participant.updated` | → client | Participant role changed |
|
||||
| `presence.update` | → client | Participant online/idle/offline status change |
|
||||
| `model.changed` | → client | Channel model roster changed |
|
||||
| `persona.updated` | → client | Persona metadata changed |
|
||||
| `kb.updated` | → client | Knowledge base changed |
|
||||
@@ -2088,6 +2470,8 @@ Clients subscribe to rooms for scoped event delivery:
|
||||
| `extension.updated` | → client | Extension config changed |
|
||||
| `settings.updated` | → client | Global settings changed |
|
||||
| `user.updated` | → client | User profile changed |
|
||||
| `file.created` | → client | File uploaded or generated (§17b.4) |
|
||||
| `file.deleted` | → client | File removed |
|
||||
|
||||
Direction: `→ client` = server-to-client only, `← client` = client-to-server
|
||||
only, `↔ both` = bidirectional.
|
||||
@@ -2098,7 +2482,19 @@ only, `↔ both` = bidirectional.
|
||||
|
||||
### Channel Types
|
||||
|
||||
`direct` (current default), `group` (v0.23.0), `workflow` (v0.25.0)
|
||||
`direct` (single-user), `group` (multi-user/multi-model), `workflow` (staged)
|
||||
|
||||
### Participant Types
|
||||
|
||||
`user`, `persona`, `session`
|
||||
|
||||
### Participant Roles
|
||||
|
||||
`owner`, `member`, `observer`
|
||||
|
||||
### Presence Statuses
|
||||
|
||||
`online`, `idle`, `offline`
|
||||
|
||||
### Message Roles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user