Changeset 0.28.0.7 (#179)
This commit is contained in:
@@ -5,16 +5,6 @@ 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 |
|
||||
@@ -29,67 +19,78 @@ All three return the same Persona object shape:
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Code Reviewer",
|
||||
"handle": "code-reviewer",
|
||||
"description": "Reviews code for bugs and style",
|
||||
"system_prompt": "You are a senior code reviewer...",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"icon": "🔍",
|
||||
"avatar": "data:image/png;base64,...",
|
||||
"base_model_id": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid|null",
|
||||
"system_prompt": "You are a senior code reviewer...",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 4096,
|
||||
"thinking_budget": null,
|
||||
"top_p": null,
|
||||
"scope": "personal|team|global",
|
||||
"owner_id": "uuid|null",
|
||||
"is_default": false,
|
||||
"created_by": "uuid",
|
||||
"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"
|
||||
},
|
||||
"is_shared": false,
|
||||
"memory_enabled": false,
|
||||
"memory_extraction_prompt": null,
|
||||
"grants": [],
|
||||
"kb_ids": [],
|
||||
"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.
|
||||
`grants` and `kb_ids` are loaded from junction tables, not stored on the
|
||||
persona row. All nullable fields (`provider_config_id`, `owner_id`,
|
||||
`temperature`, `max_tokens`, `thinking_budget`, `top_p`,
|
||||
`memory_extraction_prompt`) are omitted from JSON when null.
|
||||
|
||||
`handle` is auto-generated from `name` on creation if not provided
|
||||
(e.g. "Code Reviewer" → "code-reviewer"). Used for @mention routing.
|
||||
|
||||
### Personal Personas
|
||||
|
||||
Gated by `allow_user_personas` policy.
|
||||
|
||||
```
|
||||
GET /personas → { "personas": [...] }
|
||||
POST /personas ← { "name", "description", "system_prompt", "model", ... }
|
||||
GET /personas → { "data": [...] }
|
||||
POST /personas ← { "name", "base_model_id", ... }
|
||||
PUT /personas/:id ← partial update
|
||||
DELETE /personas/:id
|
||||
```
|
||||
|
||||
**Auth:** JWT required. `POST` requires `persona:create` permission.
|
||||
`PUT`/`DELETE` require `persona:manage` permission and ownership check
|
||||
(`scope == "personal"` and `owner_id == caller`).
|
||||
|
||||
### Team Personas
|
||||
|
||||
```
|
||||
GET /teams/:teamId/personas → { "personas": [...] }
|
||||
GET /teams/:teamId/personas → { "data": [...] }
|
||||
POST /teams/:teamId/personas ← same shape
|
||||
PUT /teams/:teamId/personas/:id
|
||||
PUT /teams/:teamId/personas/:id ← partial update
|
||||
DELETE /teams/:teamId/personas/:id
|
||||
```
|
||||
|
||||
**Auth:** JWT required. Team membership for `GET`, team admin for
|
||||
`POST`/`PUT`/`DELETE`. All mutating endpoints verify the persona belongs
|
||||
to the team in the URL path (`scope == "team"` and `owner_id == teamId`).
|
||||
|
||||
### Admin (Global) Personas
|
||||
|
||||
```
|
||||
GET /admin/personas → { "personas": [...] }
|
||||
GET /admin/personas → { "data": [...] }
|
||||
POST /admin/personas ← same shape
|
||||
PUT /admin/personas/:id
|
||||
DELETE /admin/personas/:id
|
||||
```
|
||||
|
||||
**Auth:** Admin JWT required (middleware enforced).
|
||||
|
||||
### Persona KB Bindings
|
||||
|
||||
A Persona can have Knowledge Bases bound to it. When a channel uses
|
||||
@@ -127,23 +128,41 @@ PUT /admin/personas/:id/knowledge-bases
|
||||
context). When `false`, it's available via the `kb_search` tool but
|
||||
not auto-injected.
|
||||
|
||||
Team-scoped KB binding endpoints verify the persona belongs to the
|
||||
team in the URL path before reading or writing bindings.
|
||||
|
||||
### Persona Avatars
|
||||
|
||||
Avatars are stored as `data:image/png;base64,...` data URIs in the
|
||||
`avatar` column. Upload accepts a JSON body (not multipart):
|
||||
|
||||
```json
|
||||
{ "image": "data:image/png;base64,..." }
|
||||
```
|
||||
POST /personas/:id/avatar ← multipart/form-data (field: "avatar")
|
||||
|
||||
The image is decoded, resized to 128×128 PNG, and stored.
|
||||
|
||||
```
|
||||
POST /personas/:id/avatar ← { "image": "base64..." }
|
||||
DELETE /personas/:id/avatar
|
||||
POST /admin/personas/:id/avatar
|
||||
POST /admin/personas/:id/avatar ← same
|
||||
DELETE /admin/personas/:id/avatar
|
||||
POST /teams/:teamId/personas/:id/avatar ← same (verify team ownership)
|
||||
DELETE /teams/:teamId/personas/:id/avatar
|
||||
```
|
||||
|
||||
**Auth:** Personal avatar routes verify the caller owns the persona
|
||||
(`scope == "personal"` and `owner_id == caller`). Admin routes are
|
||||
protected by admin middleware.
|
||||
|
||||
### Persona Tool Grants
|
||||
|
||||
Control which tools a persona can use during completions. The completion
|
||||
handler applies tool grants as a second-pass allowlist.
|
||||
|
||||
```
|
||||
GET /personas/:id/tool-grants → { "grants": ["web_search", "kb_search", ...] }
|
||||
PUT /personas/:id/tool-grants ← { "tool_ids": ["web_search", "calculator"] }
|
||||
GET /personas/:id/tool-grants → { "data": ["web_search", "kb_search", ...] }
|
||||
PUT /personas/:id/tool-grants ← { "tool_names": ["web_search", "calculator"] }
|
||||
```
|
||||
|
||||
Admin equivalents:
|
||||
@@ -153,6 +172,13 @@ GET /admin/personas/:id/tool-grants
|
||||
PUT /admin/personas/:id/tool-grants
|
||||
```
|
||||
|
||||
Team equivalents (verify persona belongs to team):
|
||||
|
||||
```
|
||||
GET /teams/:teamId/personas/:id/tool-grants
|
||||
PUT /teams/:teamId/personas/:id/tool-grants
|
||||
```
|
||||
|
||||
When a workflow version is published, persona tool grants at that moment
|
||||
are frozen into the version snapshot.
|
||||
|
||||
@@ -163,7 +189,7 @@ can be stamped onto new conversations.
|
||||
|
||||
```
|
||||
GET /persona-groups → { "data": [...] }
|
||||
POST /persona-groups ← { "name", "description", "scope", "team_id" }
|
||||
POST /persona-groups ← { "name", "description" }
|
||||
GET /persona-groups/:id → group with members
|
||||
PUT /persona-groups/:id ← partial update
|
||||
DELETE /persona-groups/:id
|
||||
@@ -172,12 +198,13 @@ DELETE /persona-groups/:id
|
||||
**Members:**
|
||||
|
||||
```
|
||||
POST /persona-groups/:id/members ← { "persona_id", "is_leader": false, "sort_order": 0 }
|
||||
POST /persona-groups/:id/members ← { "persona_id", "is_leader": false }
|
||||
DELETE /persona-groups/:id/members/:memberId
|
||||
```
|
||||
|
||||
`is_leader`: the leader persona responds when no @mention is present in
|
||||
a group channel. Only one leader per group.
|
||||
a group channel. Only one leader per group. Setting a new leader
|
||||
automatically clears the existing one.
|
||||
|
||||
**Persona Group Object:**
|
||||
|
||||
@@ -187,18 +214,30 @@ a group channel. Only one leader per group.
|
||||
"name": "Support Team",
|
||||
"description": "...",
|
||||
"owner_id": "uuid",
|
||||
"scope": "personal|team|global",
|
||||
"team_id": "uuid|null",
|
||||
"scope": "personal",
|
||||
"team_id": null,
|
||||
"members": [
|
||||
{ "id": "uuid", "persona_id": "uuid", "is_leader": true, "sort_order": 0 }
|
||||
{
|
||||
"id": "uuid",
|
||||
"group_id": "uuid",
|
||||
"persona_id": "uuid",
|
||||
"is_leader": true,
|
||||
"sort_order": 0,
|
||||
"persona_name": "...",
|
||||
"persona_handle": "...",
|
||||
"persona_avatar": "..."
|
||||
}
|
||||
],
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Persona groups are currently personal-scope only. The `scope`
|
||||
and `team_id` fields exist in the schema but `POST` hardcodes
|
||||
`scope = 'personal'`. Team-scoped persona groups are a future item.
|
||||
|
||||
### Resource Grants
|
||||
|
||||
See [teams.md](teams.md) §15.3 for the grant system. Personas use grants
|
||||
to control who can see and use them beyond their base scope.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user