feat: web prompt editor + agent personalities #58
@@ -0,0 +1,59 @@
|
||||
-- Add agent-direct prompts and per-agent personalities (with personality<->prompt join).
|
||||
|
||||
-- ── Prompt: optional agentId, mirrors projectId pattern ──
|
||||
ALTER TABLE "Prompt" ADD COLUMN "agentId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "Prompt_name_agentId_key" ON "Prompt"("name", "agentId");
|
||||
CREATE INDEX "Prompt_agentId_idx" ON "Prompt"("agentId");
|
||||
|
||||
ALTER TABLE "Prompt" ADD CONSTRAINT "Prompt_agentId_fkey"
|
||||
FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ── Agent: defaultPersonalityId (optional FK; SET NULL on delete to keep agent alive) ──
|
||||
ALTER TABLE "Agent" ADD COLUMN "defaultPersonalityId" TEXT;
|
||||
|
||||
CREATE INDEX "Agent_defaultPersonalityId_idx" ON "Agent"("defaultPersonalityId");
|
||||
|
||||
-- ── Personality table ──
|
||||
CREATE TABLE "Personality" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL DEFAULT '',
|
||||
"agentId" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 5,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Personality_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "Personality_name_agentId_key" ON "Personality"("name", "agentId");
|
||||
CREATE INDEX "Personality_agentId_idx" ON "Personality"("agentId");
|
||||
|
||||
ALTER TABLE "Personality" ADD CONSTRAINT "Personality_agentId_fkey"
|
||||
FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Now that Personality exists, the Agent.defaultPersonalityId FK can point at it.
|
||||
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_defaultPersonalityId_fkey"
|
||||
FOREIGN KEY ("defaultPersonalityId") REFERENCES "Personality"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- ── PersonalityPrompt join table ──
|
||||
CREATE TABLE "PersonalityPrompt" (
|
||||
"id" TEXT NOT NULL,
|
||||
"personalityId" TEXT NOT NULL,
|
||||
"promptId" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 5,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PersonalityPrompt_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "PersonalityPrompt_personalityId_promptId_key" ON "PersonalityPrompt"("personalityId", "promptId");
|
||||
CREATE INDEX "PersonalityPrompt_personalityId_idx" ON "PersonalityPrompt"("personalityId");
|
||||
CREATE INDEX "PersonalityPrompt_promptId_idx" ON "PersonalityPrompt"("promptId");
|
||||
|
||||
ALTER TABLE "PersonalityPrompt" ADD CONSTRAINT "PersonalityPrompt_personalityId_fkey"
|
||||
FOREIGN KEY ("personalityId") REFERENCES "Personality"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "PersonalityPrompt" ADD CONSTRAINT "PersonalityPrompt_promptId_fkey"
|
||||
FOREIGN KEY ("promptId") REFERENCES "Prompt"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -21,13 +21,13 @@ model User {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
auditLogs AuditLog[]
|
||||
ownedProjects Project[]
|
||||
groupMemberships GroupMember[]
|
||||
mcpTokens McpToken[]
|
||||
ownedAgents Agent[]
|
||||
chatThreads ChatThread[]
|
||||
sessions Session[]
|
||||
auditLogs AuditLog[]
|
||||
ownedProjects Project[]
|
||||
groupMemberships GroupMember[]
|
||||
mcpTokens McpToken[]
|
||||
ownedAgents Agent[]
|
||||
chatThreads ChatThread[]
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
@@ -56,23 +56,23 @@ model Session {
|
||||
// ── MCP Servers ──
|
||||
|
||||
model McpServer {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("")
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("")
|
||||
packageName String?
|
||||
runtime String?
|
||||
dockerImage String?
|
||||
transport Transport @default(STDIO)
|
||||
transport Transport @default(STDIO)
|
||||
repositoryUrl String?
|
||||
externalUrl String?
|
||||
command Json?
|
||||
containerPort Int?
|
||||
replicas Int @default(1)
|
||||
env Json @default("[]")
|
||||
replicas Int @default(1)
|
||||
env Json @default("[]")
|
||||
healthCheck Json?
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
templateName String?
|
||||
templateVersion String?
|
||||
@@ -125,15 +125,15 @@ model McpTemplate {
|
||||
model SecretBackend {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
type String // plaintext | openbao | (future: vault, aws-sm, ...)
|
||||
config Json @default("{}") // type-specific: url, mount, namespace, tokenSecretRef
|
||||
type String // plaintext | openbao | (future: vault, aws-sm, ...)
|
||||
config Json @default("{}") // type-specific: url, mount, namespace, tokenSecretRef
|
||||
// Runtime metadata for auto-rotating backend credentials (openbao token
|
||||
// auth). Fields: generatedAt, nextRenewalAt, validUntil, lastRotationAt,
|
||||
// lastRotationError, rotatable (true only for wizard-provisioned tokens).
|
||||
// Empty object for backends that don't use rotation (plaintext, kubernetes
|
||||
// auth, or static tokens). Managed entirely by the rotator service.
|
||||
tokenMeta Json @default("{}")
|
||||
isDefault Boolean @default(false) // exactly one row has isDefault=true
|
||||
isDefault Boolean @default(false) // exactly one row has isDefault=true
|
||||
description String @default("")
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -156,8 +156,8 @@ model Secret {
|
||||
// on next mcpd startup. New rows written by SecretService always carry a
|
||||
// valid FK immediately.
|
||||
backendId String @default("")
|
||||
data Json @default("{}") // populated by plaintext backend only
|
||||
externalRef String @default("") // populated by non-plaintext backends (e.g. "mount/path#v3")
|
||||
data Json @default("{}") // populated by plaintext backend only
|
||||
externalRef String @default("") // populated by non-plaintext backends (e.g. "mount/path#v3")
|
||||
// Sorted list of the secret's data keys WITHOUT their values. Populated on
|
||||
// every create/update/migrate so list views and describe-without-reveal can
|
||||
// show "this secret has GRAFANA_URL + GRAFANA_TOKEN" without fetching the
|
||||
@@ -186,14 +186,14 @@ model Secret {
|
||||
model Llm {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
type String // anthropic | openai | deepseek | vllm | ollama | gemini-cli
|
||||
model String // e.g. claude-3-5-sonnet-20241022
|
||||
url String @default("") // endpoint (empty for provider default)
|
||||
tier String @default("fast") // fast | heavy
|
||||
type String // anthropic | openai | deepseek | vllm | ollama | gemini-cli
|
||||
model String // e.g. claude-3-5-sonnet-20241022
|
||||
url String @default("") // endpoint (empty for provider default)
|
||||
tier String @default("fast") // fast | heavy
|
||||
description String @default("")
|
||||
apiKeySecretId String? // FK to Secret
|
||||
apiKeySecretKey String? // key inside the Secret's data
|
||||
extraConfig Json @default("{}") // per-type extras
|
||||
apiKeySecretId String? // FK to Secret
|
||||
apiKeySecretKey String? // key inside the Secret's data
|
||||
extraConfig Json @default("{}") // per-type extras
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -252,19 +252,19 @@ model RbacDefinition {
|
||||
// ── Projects ──
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("")
|
||||
prompt String @default("")
|
||||
proxyModel String @default("")
|
||||
gated Boolean @default(true)
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("")
|
||||
prompt String @default("")
|
||||
proxyModel String @default("")
|
||||
gated Boolean @default(true)
|
||||
llmProvider String?
|
||||
llmModel String?
|
||||
serverOverrides Json?
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
servers ProjectServer[]
|
||||
@@ -322,11 +322,11 @@ model McpToken {
|
||||
// ── MCP Instances (running containers) ──
|
||||
|
||||
model McpInstance {
|
||||
id String @id @default(cuid())
|
||||
serverId String
|
||||
containerId String?
|
||||
status InstanceStatus @default(STOPPED)
|
||||
port Int?
|
||||
id String @id @default(cuid())
|
||||
serverId String
|
||||
containerId String?
|
||||
status InstanceStatus @default(STOPPED)
|
||||
port Int?
|
||||
metadata Json @default("{}")
|
||||
healthStatus String?
|
||||
lastHealthCheck DateTime?
|
||||
@@ -356,6 +356,7 @@ model Prompt {
|
||||
name String
|
||||
content String @db.Text
|
||||
projectId String?
|
||||
agentId String?
|
||||
priority Int @default(5)
|
||||
summary String? @db.Text
|
||||
chapters Json?
|
||||
@@ -364,10 +365,14 @@ model Prompt {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
agent Agent? @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
personalities PersonalityPrompt[]
|
||||
|
||||
@@unique([name, projectId])
|
||||
@@unique([name, agentId])
|
||||
@@index([projectId])
|
||||
@@index([agentId])
|
||||
}
|
||||
|
||||
// ── Prompt Requests (pending proposals from LLM sessions) ──
|
||||
@@ -392,21 +397,21 @@ model PromptRequest {
|
||||
// ── Audit Events (pipeline/gate/tool trace from mcplocal) ──
|
||||
|
||||
model AuditEvent {
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime
|
||||
sessionId String
|
||||
projectName String
|
||||
eventKind String
|
||||
source String
|
||||
verified Boolean @default(false)
|
||||
serverName String?
|
||||
correlationId String?
|
||||
parentEventId String?
|
||||
userName String?
|
||||
tokenName String?
|
||||
tokenSha String?
|
||||
payload Json
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime
|
||||
sessionId String
|
||||
projectName String
|
||||
eventKind String
|
||||
source String
|
||||
verified Boolean @default(false)
|
||||
serverName String?
|
||||
correlationId String?
|
||||
parentEventId String?
|
||||
userName String?
|
||||
tokenName String?
|
||||
tokenSha String?
|
||||
payload Json
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([sessionId])
|
||||
@@index([projectName])
|
||||
@@ -423,7 +428,7 @@ model BackupPending {
|
||||
id String @id @default(cuid())
|
||||
resourceKind String
|
||||
resourceName String
|
||||
action String // 'create' | 'update' | 'delete'
|
||||
action String // 'create' | 'update' | 'delete'
|
||||
userName String
|
||||
yamlContent String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
@@ -439,29 +444,80 @@ model BackupPending {
|
||||
// Per-call LiteLLM-style overrides stack on top of `defaultParams`.
|
||||
|
||||
model Agent {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("") // shown in MCP tools/list
|
||||
systemPrompt String @default("") @db.Text // agent persona
|
||||
llmId String
|
||||
projectId String?
|
||||
proxyModelName String? // optional informational override
|
||||
defaultParams Json @default("{}") // LiteLLM-style: temperature, top_p, top_k, max_tokens, stop, ...
|
||||
extras Json @default("{}") // future LoRA / tool-allowlist
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("") // shown in MCP tools/list
|
||||
systemPrompt String @default("") @db.Text // agent persona
|
||||
llmId String
|
||||
projectId String?
|
||||
defaultPersonalityId String? // applied at chat time when no --personality flag
|
||||
proxyModelName String? // optional informational override
|
||||
defaultParams Json @default("{}") // LiteLLM-style: temperature, top_p, top_k, max_tokens, stop, ...
|
||||
extras Json @default("{}") // future LoRA / tool-allowlist
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
llm Llm @relation(fields: [llmId], references: [id], onDelete: Restrict)
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
threads ChatThread[]
|
||||
llm Llm @relation(fields: [llmId], references: [id], onDelete: Restrict)
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
threads ChatThread[]
|
||||
prompts Prompt[]
|
||||
personalities Personality[] @relation("AgentPersonalities")
|
||||
defaultPersonality Personality? @relation("AgentDefaultPersonality", fields: [defaultPersonalityId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([name])
|
||||
@@index([llmId])
|
||||
@@index([projectId])
|
||||
@@index([ownerId])
|
||||
@@index([defaultPersonalityId])
|
||||
}
|
||||
|
||||
// ── Personalities (named overlay bundles of prompts on top of an Agent) ──
|
||||
//
|
||||
// VLAN-on-ethernet semantics: an Agent works without a Personality (today's
|
||||
// flow). Selecting a Personality adds its bound Prompts to the system block
|
||||
// after the Agent's own systemPrompt, agent-direct prompts, and project
|
||||
// prompts — additive, never replacing.
|
||||
|
||||
model Personality {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String @default("")
|
||||
agentId String
|
||||
priority Int @default(5)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
agent Agent @relation("AgentPersonalities", fields: [agentId], references: [id], onDelete: Cascade)
|
||||
prompts PersonalityPrompt[]
|
||||
defaultForAgent Agent[] @relation("AgentDefaultPersonality")
|
||||
|
||||
@@unique([name, agentId])
|
||||
@@index([agentId])
|
||||
}
|
||||
|
||||
// ── Personality ↔ Prompt join table ──
|
||||
//
|
||||
// A Prompt can be bound to many Personalities; a Personality can bind many
|
||||
// Prompts. The `priority` here overrides the Prompt's own priority *within
|
||||
// this Personality's overlay slice* (so the same prompt can be high-priority
|
||||
// in one personality and low in another).
|
||||
|
||||
model PersonalityPrompt {
|
||||
id String @id @default(cuid())
|
||||
personalityId String
|
||||
promptId String
|
||||
priority Int @default(5)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
personality Personality @relation(fields: [personalityId], references: [id], onDelete: Cascade)
|
||||
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([personalityId, promptId])
|
||||
@@index([personalityId])
|
||||
@@index([promptId])
|
||||
}
|
||||
|
||||
// ── Chat Threads (persisted conversation per Agent) ──
|
||||
@@ -493,11 +549,11 @@ model ChatMessage {
|
||||
id String @id @default(cuid())
|
||||
threadId String
|
||||
turnIndex Int
|
||||
role String // 'system' | 'user' | 'assistant' | 'tool'
|
||||
role String // 'system' | 'user' | 'assistant' | 'tool'
|
||||
content String @db.Text
|
||||
toolCalls Json? // assistant turn: [{id,name,arguments}]
|
||||
toolCallId String? // tool turn: which call this answers
|
||||
status String @default("complete") // 'pending' | 'complete' | 'error'
|
||||
toolCalls Json? // assistant turn: [{id,name,arguments}]
|
||||
toolCallId String? // tool turn: which call this answers
|
||||
status String @default("complete") // 'pending' | 'complete' | 'error'
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
thread ChatThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
||||
@@ -509,13 +565,13 @@ model ChatMessage {
|
||||
// ── Audit Logs ──
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
action String
|
||||
resource String
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
action String
|
||||
resource String
|
||||
resourceId String?
|
||||
details Json @default("{}")
|
||||
createdAt DateTime @default(now())
|
||||
details Json @default("{}")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
@@ -201,4 +201,146 @@ describe('agent / chat-thread / chat-message schema', () => {
|
||||
});
|
||||
expect(ordered.map((t) => t.id)).toEqual([t2.id, t1.id]);
|
||||
});
|
||||
|
||||
// ── Agent-direct prompts (Prompt.agentId) ──
|
||||
|
||||
it('attaches a prompt directly to an agent (no project)', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-agent-prompt');
|
||||
const agent = await makeAgent({ name: 'directprompt', llmId: llm.id, ownerId: user.id });
|
||||
|
||||
const prompt = await prisma.prompt.create({
|
||||
data: { name: 'agent-only', content: 'hi from agent', agentId: agent.id },
|
||||
});
|
||||
expect(prompt.agentId).toBe(agent.id);
|
||||
expect(prompt.projectId).toBeNull();
|
||||
});
|
||||
|
||||
it('cascade-deletes agent-direct prompts when the agent is removed', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-agent-prompt-cascade');
|
||||
const agent = await makeAgent({ name: 'goingaway', llmId: llm.id, ownerId: user.id });
|
||||
await prisma.prompt.create({
|
||||
data: { name: 'p1', content: 'c', agentId: agent.id },
|
||||
});
|
||||
|
||||
await prisma.agent.delete({ where: { id: agent.id } });
|
||||
|
||||
expect(await prisma.prompt.count({ where: { agentId: agent.id } })).toBe(0);
|
||||
});
|
||||
|
||||
it('enforces (name, agentId) uniqueness independently of (name, projectId)', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-uniq-prompt-agent');
|
||||
const agent = await makeAgent({ name: 'has-prompts', llmId: llm.id, ownerId: user.id });
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: { name: 'shared', content: 'a', agentId: agent.id },
|
||||
});
|
||||
await expect(
|
||||
prisma.prompt.create({
|
||||
data: { name: 'shared', content: 'b', agentId: agent.id },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
// Same name on a different agent is fine.
|
||||
const agent2 = await makeAgent({ name: 'other-agent', llmId: llm.id, ownerId: user.id });
|
||||
const ok = await prisma.prompt.create({
|
||||
data: { name: 'shared', content: 'c', agentId: agent2.id },
|
||||
});
|
||||
expect(ok.id).toBeDefined();
|
||||
});
|
||||
|
||||
// ── Personalities ──
|
||||
|
||||
it('creates a personality bound to an agent', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-pers-1');
|
||||
const agent = await makeAgent({ name: 'with-pers', llmId: llm.id, ownerId: user.id });
|
||||
|
||||
const p = await prisma.personality.create({
|
||||
data: { name: 'grumpy', description: 'curmudgeonly', agentId: agent.id },
|
||||
});
|
||||
expect(p.id).toBeDefined();
|
||||
expect(p.priority).toBe(5);
|
||||
});
|
||||
|
||||
it('enforces (name, agentId) uniqueness on personalities', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-pers-uniq');
|
||||
const agent = await makeAgent({ name: 'pers-uniq', llmId: llm.id, ownerId: user.id });
|
||||
|
||||
await prisma.personality.create({ data: { name: 'mode', agentId: agent.id } });
|
||||
await expect(
|
||||
prisma.personality.create({ data: { name: 'mode', agentId: agent.id } }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('cascade-deletes personalities + bindings when the agent is removed', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-pers-cascade');
|
||||
const agent = await makeAgent({ name: 'kaboom', llmId: llm.id, ownerId: user.id });
|
||||
|
||||
const personality = await prisma.personality.create({
|
||||
data: { name: 'snarky', agentId: agent.id },
|
||||
});
|
||||
const prompt = await prisma.prompt.create({
|
||||
data: { name: 'tone', content: 'be snarky', agentId: agent.id },
|
||||
});
|
||||
await prisma.personalityPrompt.create({
|
||||
data: { personalityId: personality.id, promptId: prompt.id },
|
||||
});
|
||||
|
||||
await prisma.agent.delete({ where: { id: agent.id } });
|
||||
|
||||
expect(await prisma.personality.count({ where: { id: personality.id } })).toBe(0);
|
||||
expect(await prisma.personalityPrompt.count({ where: { personalityId: personality.id } })).toBe(0);
|
||||
// Prompts directly attached to the agent also go via Prompt.agentId cascade.
|
||||
expect(await prisma.prompt.count({ where: { id: prompt.id } })).toBe(0);
|
||||
});
|
||||
|
||||
it('SetNull on Agent.defaultPersonalityId when the personality is deleted', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-default-pers');
|
||||
const agent = await makeAgent({ name: 'has-default', llmId: llm.id, ownerId: user.id });
|
||||
const personality = await prisma.personality.create({
|
||||
data: { name: 'def', agentId: agent.id },
|
||||
});
|
||||
await prisma.agent.update({
|
||||
where: { id: agent.id },
|
||||
data: { defaultPersonalityId: personality.id },
|
||||
});
|
||||
|
||||
await prisma.personality.delete({ where: { id: personality.id } });
|
||||
|
||||
const reloaded = await prisma.agent.findUnique({ where: { id: agent.id } });
|
||||
expect(reloaded?.defaultPersonalityId).toBeNull();
|
||||
});
|
||||
|
||||
it('binds the same prompt to multiple personalities of an agent', async () => {
|
||||
const user = await makeUser();
|
||||
const llm = await makeLlm('llm-shared-prompt');
|
||||
const agent = await makeAgent({ name: 'shared', llmId: llm.id, ownerId: user.id });
|
||||
const p1 = await prisma.personality.create({ data: { name: 'a', agentId: agent.id } });
|
||||
const p2 = await prisma.personality.create({ data: { name: 'b', agentId: agent.id } });
|
||||
const prompt = await prisma.prompt.create({
|
||||
data: { name: 'shared-text', content: 'reusable', agentId: agent.id },
|
||||
});
|
||||
|
||||
await prisma.personalityPrompt.create({
|
||||
data: { personalityId: p1.id, promptId: prompt.id, priority: 9 },
|
||||
});
|
||||
await prisma.personalityPrompt.create({
|
||||
data: { personalityId: p2.id, promptId: prompt.id, priority: 1 },
|
||||
});
|
||||
|
||||
expect(await prisma.personalityPrompt.count({ where: { promptId: prompt.id } })).toBe(2);
|
||||
|
||||
// Same (personalityId, promptId) cannot collide.
|
||||
await expect(
|
||||
prisma.personalityPrompt.create({
|
||||
data: { personalityId: p1.id, promptId: prompt.id },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,10 @@ export async function clearAllTables(client: PrismaClient): Promise<void> {
|
||||
await client.auditLog.deleteMany();
|
||||
await client.chatMessage.deleteMany();
|
||||
await client.chatThread.deleteMany();
|
||||
await client.personalityPrompt.deleteMany();
|
||||
// Break Agent.defaultPersonalityId before personalities can be removed.
|
||||
await client.agent.updateMany({ data: { defaultPersonalityId: null } });
|
||||
await client.personality.deleteMany();
|
||||
await client.agent.deleteMany();
|
||||
await client.llm.deleteMany();
|
||||
await client.mcpInstance.deleteMany();
|
||||
|
||||
Reference in New Issue
Block a user