refactor(api): extract assistant approval flow helpers

This commit is contained in:
2026-03-31 12:16:20 +02:00
parent 0760887a20
commit 002114fcb1
2 changed files with 204 additions and 104 deletions
@@ -0,0 +1,162 @@
import { createAuditEntry } from "../lib/audit.js";
import {
clearPendingAssistantApproval,
consumePendingAssistantApproval,
toApprovalPayload,
type PendingAssistantApproval,
} from "./assistant-approvals.js";
import {
canExecuteMutationTool,
isCancellationReply,
parseToolArguments,
type ChatMessage,
} from "./assistant-confirmation.js";
import { buildAssistantInsight, type AssistantInsight } from "./assistant-insights.js";
import { readToolError, readToolSuccessMessage } from "./assistant-tool-results.js";
import { executeTool, type ToolAction, type ToolContext } from "./assistant-tools.js";
export type AssistantChatResponse = {
content: string;
role: "assistant";
approval?: ReturnType<typeof toApprovalPayload>;
insights?: AssistantInsight[];
actions?: ToolAction[];
};
export function mergeInsights(existing: AssistantInsight[], next: AssistantInsight): AssistantInsight[] {
const duplicateIndex = existing.findIndex((item) => item.kind === next.kind && item.title === next.title && item.subtitle === next.subtitle);
if (duplicateIndex >= 0) {
const copy = [...existing];
copy[duplicateIndex] = next;
return copy;
}
return [...existing, next].slice(-6);
}
export function buildAssistantChatResponse(input: {
content: string;
approval?: ReturnType<typeof toApprovalPayload> | undefined;
insights: AssistantInsight[];
actions: ToolAction[];
}): AssistantChatResponse {
return {
content: input.content,
role: "assistant",
...(input.approval !== undefined ? { approval: input.approval } : {}),
...(input.insights.length > 0 ? { insights: input.insights } : {}),
...(input.actions.length > 0 ? { actions: input.actions } : {}),
};
}
export async function handlePendingAssistantApproval(input: {
db: ToolContext["db"];
dbUserId?: string | undefined;
toolCtx: ToolContext;
conversationId: string;
pendingApproval: PendingAssistantApproval | null;
lastUserMessage?: ChatMessage | undefined;
messages: ChatMessage[];
collectedActions: ToolAction[];
collectedInsights: AssistantInsight[];
}): Promise<{
response: AssistantChatResponse;
collectedActions: ToolAction[];
collectedInsights: AssistantInsight[];
} | null> {
const { pendingApproval, lastUserMessage } = input;
if (!pendingApproval || lastUserMessage?.role !== "user") {
return null;
}
if (isCancellationReply(lastUserMessage.content)) {
await clearPendingAssistantApproval(input.db, input.toolCtx.userId, input.conversationId);
void createAuditEntry({
db: input.db,
entityType: "AiToolExecution",
entityId: pendingApproval.id,
entityName: pendingApproval.toolName,
action: "DELETE",
source: "ai",
summary: `AI approval cancelled: ${pendingApproval.toolName}`,
after: {
approvalId: pendingApproval.id,
params: parseToolArguments(pendingApproval.toolArguments),
executed: false,
},
...(input.dbUserId !== undefined ? { userId: input.dbUserId } : {}),
});
return {
response: buildAssistantChatResponse({
content: `Aktion verworfen: ${pendingApproval.summary}`,
approval: toApprovalPayload(pendingApproval, "cancelled"),
insights: input.collectedInsights,
actions: input.collectedActions,
}),
collectedActions: input.collectedActions,
collectedInsights: input.collectedInsights,
};
}
if (!canExecuteMutationTool(input.messages, pendingApproval.toolName, pendingApproval)) {
return null;
}
const approvedAction =
await consumePendingAssistantApproval(input.db, input.toolCtx.userId, input.conversationId)
?? pendingApproval;
const result = await executeTool(
approvedAction.toolName,
approvedAction.toolArguments,
input.toolCtx,
);
let collectedInsights = input.collectedInsights;
const insight = buildAssistantInsight(approvedAction.toolName, result.data);
if (insight) {
collectedInsights = mergeInsights(collectedInsights, insight);
}
const collectedActions = result.action
? [...input.collectedActions, result.action]
: input.collectedActions;
const errorMessage = readToolError(result);
const successMessage = readToolSuccessMessage(result);
const finalContent = errorMessage
? `Die bestätigte Aktion konnte nicht ausgeführt werden: ${errorMessage}`
: successMessage
? `Ausgeführt: ${successMessage}`
: `Ausgeführt: ${approvedAction.summary}`;
void createAuditEntry({
db: input.db,
entityType: "AiToolExecution",
entityId: approvedAction.id,
entityName: approvedAction.toolName,
action: "CREATE",
source: "ai",
summary: errorMessage
? `AI confirmed tool failed: ${approvedAction.toolName}`
: `AI executed previously approved tool: ${approvedAction.toolName}`,
after: {
approvalId: approvedAction.id,
params: parseToolArguments(approvedAction.toolArguments),
executed: !errorMessage,
},
...(input.dbUserId !== undefined ? { userId: input.dbUserId } : {}),
});
return {
response: buildAssistantChatResponse({
content: finalContent,
approval: toApprovalPayload(approvedAction, "approved"),
insights: collectedInsights,
actions: collectedActions,
}),
collectedActions,
collectedInsights,
};
}