Files
CapaKraken/packages/api/src/router/assistant-confirmation.ts
T

142 lines
4.1 KiB
TypeScript

import { MUTATION_TOOLS } from "./assistant-tools.js";
import type { PendingAssistantApproval } from "./assistant-approvals.js";
export const ASSISTANT_CONFIRMATION_PREFIX = "CONFIRMATION_REQUIRED:";
export type ChatMessage = { role: "user" | "assistant"; content: string };
export function parseToolArguments(args: string): Record<string, unknown> {
try {
const parsed = JSON.parse(args) as unknown;
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: {};
} catch {
return {};
}
}
export function formatApprovalValue(value: unknown): string {
if (typeof value === "string") {
return value.length > 48 ? `${value.slice(0, 45)}...` : value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (Array.isArray(value)) {
if (value.length === 0) return "[]";
return `[${value.slice(0, 3).map((item) => formatApprovalValue(item)).join(", ")}${value.length > 3 ? ", ..." : ""}]`;
}
if (value && typeof value === "object") {
return "{...}";
}
return "null";
}
export function buildApprovalSummary(toolName: string, toolArguments: string): string {
const params = parseToolArguments(toolArguments);
const details = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== "")
.slice(0, 4)
.map(([key, value]) => `${key}=${formatApprovalValue(value)}`)
.join(", ");
const action = toolName.replace(/_/g, " ");
return details ? `${action} (${details})` : action;
}
export function isAffirmativeConfirmationReply(content: string): boolean {
const normalized = content.trim().toLowerCase();
if (!normalized) return false;
const exactMatches = new Set([
"ja",
"yes",
"y",
"ok",
"okay",
"okey",
"mach das",
"bitte machen",
"bitte ausführen",
"bitte ausfuehren",
"ausführen",
"ausfuehren",
"bestätigt",
"bestaetigt",
"bestätigen",
"bestaetigen",
"confirm",
"confirmed",
"do it",
"go ahead",
"proceed",
]);
if (exactMatches.has(normalized)) return true;
const affirmativePatterns = [
/^(ja|yes|ok|okay)\b/,
/\b(mach|make|do|führ|fuehr|execute|run)\b.*\b(das|it|bitte|jetzt)\b/,
/\b(bit(?:te)?|please)\b.*\b(ausführen|ausfuehren|execute|run|machen|do)\b/,
/\b(bestätig|bestaetig|confirm)\w*\b/,
/\b(go ahead|proceed)\b/,
];
return affirmativePatterns.some((pattern) => pattern.test(normalized));
}
export function isCancellationReply(content: string): boolean {
const normalized = content.trim().toLowerCase();
if (!normalized) return false;
const exactMatches = new Set([
"nein",
"no",
"abbrechen",
"cancel",
"stopp",
"stop",
"doch nicht",
"nicht ausführen",
"nicht ausfuehren",
]);
if (exactMatches.has(normalized)) return true;
return [
/\b(nein|no|cancel|abbrechen|stop|stopp)\b/,
/\b(doch nicht|nicht ausführen|nicht ausfuehren)\b/,
].some((pattern) => pattern.test(normalized));
}
export function hasPendingAssistantConfirmation(messages: ChatMessage[]): boolean {
if (messages.length < 2) return false;
const lastMessage = messages[messages.length - 1];
if (!lastMessage || lastMessage.role !== "user") return false;
for (let index = messages.length - 2; index >= 0; index -= 1) {
const message = messages[index];
if (!message) continue;
if (message.role === "assistant") {
return message.content.trimStart().startsWith(ASSISTANT_CONFIRMATION_PREFIX);
}
}
return false;
}
export function canExecuteMutationTool(
messages: ChatMessage[],
toolName: string,
pendingApproval?: PendingAssistantApproval | null,
): boolean {
if (!MUTATION_TOOLS.has(toolName)) return true;
const lastMessage = messages[messages.length - 1];
if (!lastMessage || lastMessage.role !== "user") return false;
if (!isAffirmativeConfirmationReply(lastMessage.content)) return false;
if (pendingApproval) {
return pendingApproval.toolName === toolName && pendingApproval.expiresAt > Date.now();
}
return hasPendingAssistantConfirmation(messages);
}