Compare commits

..

1 Commits

Author SHA1 Message Date
BOHA
949bf6c86f chore(release): v2.4.14 - Odin caller identity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:16:06 +02:00
4 changed files with 52 additions and 5 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "app-ts",
"version": "2.4.13",
"version": "2.4.14",
"description": "",
"main": "dist/server.js",
"scripts": {

View File

@@ -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({

View File

@@ -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,
});