Reusable, workspace-shared agent roles for the built-in AI chat. A role is a named persona (system-prompt instructions) + optional model override; a chat is bound to a role at creation and applies it every turn. Backend: - migration 20260620T120000: ai_agent_roles table + ai_chats.role_id (FK ON DELETE SET NULL); hand-merged types into db.d.ts/entity.types.ts (db.d.ts is hand-curated here, full codegen would clobber it). - core/ai-chat/roles: CRUD module. list = any workspace member; create/ update/delete = admin (Manage Settings ability, like ai-settings/mcp). All repo queries scoped by workspace_id; soft-delete (deleted_at). - buildSystemPrompt gains roleInstructions: role REPLACES the persona base (admin prompt / DEFAULT_PROMPT) but SAFETY_FRAMEWORK + context are always still appended. - stream(): role resolved from ai_chats.role_id for existing chats (never the request body -> no per-turn role swap); body.roleId only on creation. Disabled (enabled=false) and soft-deleted roles fall back to universal. - getChatModel(workspaceId, override): role model_config can swap model id / driver; a driver without configured creds throws 503 with a clear message naming the driver+role, resolved BEFORE response hijack. Client: - new-chat role picker (enabled roles only, default Universal assistant), roleId sent only on the first message; role badge (emoji+name) in the chat header and conversation list; admin Agent-roles management section in Settings -> AI (add/edit/delete, MCP-form pattern). Tests: ai-chat.prompt.spec (role layering + safety always present, incl. jailbreak); ai.service.spec (override on unconfigured driver -> 503). Implements docs/ai-agent-roles-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import api from "@/lib/api-client";
|
|
import { IPagination } from "@/lib/types.ts";
|
|
import {
|
|
IAiChat,
|
|
IAiChatListParams,
|
|
IAiChatMessageRow,
|
|
IAiChatMessagesParams,
|
|
IAiRole,
|
|
IAiRoleCreate,
|
|
IAiRoleUpdate,
|
|
} from "@/features/ai-chat/types/ai-chat.types.ts";
|
|
|
|
/**
|
|
* Per-user AI chat CRUD. The server uses POST for reads (its convention) and
|
|
* wraps every (non-stream) response in `{ data }` via the global transform
|
|
* interceptor, which the axios client unwraps to the body — so we read `.data`
|
|
* (mirroring `comment-service`). The `/ai-chat/stream` endpoint is consumed by
|
|
* the AI SDK `useChat` transport directly, not here.
|
|
*/
|
|
|
|
/** List the current user's chats (most recent first, paginated). */
|
|
export async function getAiChats(
|
|
params: IAiChatListParams,
|
|
): Promise<IPagination<IAiChat>> {
|
|
const req = await api.post<IPagination<IAiChat>>("/ai-chat/chats", params);
|
|
return req.data;
|
|
}
|
|
|
|
/** Fetch a chat's messages (oldest first, paginated). */
|
|
export async function getAiChatMessages(
|
|
params: IAiChatMessagesParams,
|
|
): Promise<IPagination<IAiChatMessageRow>> {
|
|
const req = await api.post<IPagination<IAiChatMessageRow>>(
|
|
"/ai-chat/messages",
|
|
params,
|
|
);
|
|
return req.data;
|
|
}
|
|
|
|
/** Rename a chat. */
|
|
export async function renameAiChat(data: {
|
|
chatId: string;
|
|
title: string;
|
|
}): Promise<void> {
|
|
await api.post("/ai-chat/rename", data);
|
|
}
|
|
|
|
/** Soft-delete a chat. */
|
|
export async function deleteAiChat(chatId: string): Promise<void> {
|
|
await api.post("/ai-chat/delete", { chatId });
|
|
}
|
|
|
|
/**
|
|
* Agent roles API (`/ai-chat/roles`). `list` is available to any workspace
|
|
* member (for the chat-creation picker); create/update/delete are admin-only
|
|
* (the server enforces this). Same `{ data }` unwrap convention as above.
|
|
*/
|
|
|
|
/** List the workspace's agent roles. */
|
|
export async function getAiRoles(): Promise<IAiRole[]> {
|
|
const req = await api.post<IAiRole[]>("/ai-chat/roles");
|
|
return req.data;
|
|
}
|
|
|
|
/** Create a role (admin). */
|
|
export async function createAiRole(data: IAiRoleCreate): Promise<IAiRole> {
|
|
const req = await api.post<IAiRole>("/ai-chat/roles/create", data);
|
|
return req.data;
|
|
}
|
|
|
|
/** Update a role (admin). */
|
|
export async function updateAiRole(data: IAiRoleUpdate): Promise<IAiRole> {
|
|
const req = await api.post<IAiRole>("/ai-chat/roles/update", data);
|
|
return req.data;
|
|
}
|
|
|
|
/** Soft-delete a role (admin). */
|
|
export async function deleteAiRole(id: string): Promise<{ success: true }> {
|
|
const req = await api.post<{ success: true }>("/ai-chat/roles/delete", { id });
|
|
return req.data;
|
|
}
|