refactor(api): modularize assistant router workflow
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user