diff --git a/package-lock.json b/package-lock.json index 4e81f3f..23c1b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "app-ts", - "version": "2.4.13", + "version": "2.4.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "app-ts", - "version": "2.4.13", + "version": "2.4.14", "license": "ISC", "dependencies": { "@anthropic-ai/sdk": "^0.102.0", diff --git a/package.json b/package.json index 664f3d7..2b68f9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app-ts", - "version": "2.4.13", + "version": "2.4.14", "description": "", "main": "dist/server.js", "scripts": { diff --git a/src/__tests__/ai-tools.test.ts b/src/__tests__/ai-tools.test.ts index 4b9ca3d..e89a29f 100644 --- a/src/__tests__/ai-tools.test.ts +++ b/src/__tests__/ai-tools.test.ts @@ -534,6 +534,27 @@ describe("agenticChat loop (SDK mocked)", () => { expect(usageRows.length).toBe(2); }); + it("injects the caller's identity into the system prompt", async () => { + createMock.mockReset(); + createMock.mockResolvedValueOnce({ + stop_reason: "end_turn", + content: [{ type: "text", text: "Ahoj." }], + usage: { input_tokens: 10, output_tokens: 5 }, + }); + await agenticChat([{ role: "user", content: "Kdo jsem?" }], { + userId: fixUserId, + roleName: "viewer", + permissions: ["attendance.record"], + }); + const system: string = createMock.mock.calls[0][0].system; + expect(system).toContain(`${FIX.first} ${FIX.last}`); + expect(system).toContain(`user_id ${fixUserId}`); + // Cleanup the usage row this extra fixture-user turn recorded. + await prisma.ai_usage.deleteMany({ + where: { user_id: fixUserId, kind: "agent" }, + }); + }); + it("a user without permissions gets NO tools passed to the model", async () => { createMock.mockReset(); createMock.mockResolvedValueOnce({ diff --git a/src/services/ai.service.ts b/src/services/ai.service.ts index d122e52..e1b40e3 100644 --- a/src/services/ai.service.ts +++ b/src/services/ai.service.ts @@ -257,12 +257,25 @@ export interface ToolTraceEntry { // Phase 2a (read-only agent). The date is interpolated so "tento měsíc" // questions resolve correctly — it changes once a day, which is fine because // this prompt is small and we don't use prompt caching here. -function agentSystemPrompt(tools: Anthropic.Tool[]): string { +interface CallerIdentity { + name: string; + username: string; + userId: number; +} + +function agentSystemPrompt( + tools: Anthropic.Tool[], + caller: CallerIdentity | null, +): string { const today = new Date(); const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; const hasFindEmployee = tools.some((t) => t.name === "find_employee"); return ( "Jsi Odin, asistent v interním systému české firmy (docházka, fakturace, nabídky, objednávky, projekty, sklad). " + + (caller + ? `Přihlášený uživatel: ${caller.name} (user_id ${caller.userId}, username ${caller.username}). ` + + "Otázky v první osobě („moje docházka“, „kolik jsem najel“, „můj plán práce“) se týkají tohoto uživatele — použij jeho user_id a na identitu se nikdy neptej. " + : "") + "Odpovídej VŽDY česky, stručně a věcně; částky formátuj s měnou. " + "Odpovědi piš jako PROSTÝ TEXT — žádný Markdown (žádné tabulky, **tučné**, nadpisy); výčty piš jako řádky s pomlčkou. " + (tools.length > 0 @@ -293,6 +306,19 @@ export async function agenticChat( ctx: AiAuthCtx, ): Promise<{ reply: string; toolTrace: ToolTraceEntry[] }> { const tools = toolDefinitionsFor(ctx); + // The model must know who it is talking to — first-person questions + // ("moje docházka") are unanswerable otherwise. PK lookup, negligible cost. + const me = await prisma.users.findUnique({ + where: { id: ctx.userId }, + select: { first_name: true, last_name: true, username: true }, + }); + const caller = me + ? { + name: `${me.first_name} ${me.last_name}`.trim() || me.username, + username: me.username, + userId: ctx.userId, + } + : null; const convo: Anthropic.MessageParam[] = messages.map((m) => ({ role: m.role, content: m.content, @@ -305,7 +331,7 @@ export async function agenticChat( res = await client().messages.create({ model: AI_MODEL, max_tokens: 2048, - system: agentSystemPrompt(tools), + system: agentSystemPrompt(tools, caller), tools: tools.length > 0 ? tools : undefined, messages: convo, });