refactor(api): extract assistant approval flow helpers
This commit is contained in:
@@ -12,8 +12,6 @@ import { MUTATION_TOOLS, TOOL_DEFINITIONS, executeTool, type ToolContext, type T
|
||||
import {
|
||||
AssistantApprovalStorageUnavailableError,
|
||||
createPendingAssistantApproval,
|
||||
clearPendingAssistantApproval,
|
||||
consumePendingAssistantApproval,
|
||||
listPendingAssistantApprovals,
|
||||
peekPendingAssistantApproval,
|
||||
toApprovalPayload,
|
||||
@@ -21,14 +19,16 @@ import {
|
||||
} from "./assistant-approvals.js";
|
||||
import {
|
||||
ASSISTANT_CONFIRMATION_PREFIX,
|
||||
canExecuteMutationTool,
|
||||
isCancellationReply,
|
||||
parseToolArguments,
|
||||
type ChatMessage,
|
||||
} from "./assistant-confirmation.js";
|
||||
import {
|
||||
buildAssistantChatResponse,
|
||||
handlePendingAssistantApproval,
|
||||
mergeInsights,
|
||||
} from "./assistant-chat-response.js";
|
||||
import { getAvailableAssistantTools } from "./assistant-tool-policy.js";
|
||||
import { selectAssistantToolsForRequest } from "./assistant-tool-selection.js";
|
||||
import { readToolError, readToolSuccessMessage } from "./assistant-tool-results.js";
|
||||
import { buildAssistantInsight, type AssistantInsight } from "./assistant-insights.js";
|
||||
import { checkPromptInjection } from "../lib/prompt-guard.js";
|
||||
import { checkAiOutput } from "../lib/content-filter.js";
|
||||
@@ -105,16 +105,6 @@ Datenmodell:
|
||||
- Feiertage: können je nach Land, Bundesland und Stadt unterschiedlich sein; nutze Feiertags-Tools statt zu raten
|
||||
`;
|
||||
|
||||
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 const assistantRouter = createTRPCRouter({
|
||||
listPendingApprovals: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
@@ -222,84 +212,28 @@ export const assistantRouter = createTRPCRouter({
|
||||
dbUser: ctx.dbUser,
|
||||
roleDefaults: ctx.roleDefaults,
|
||||
};
|
||||
const collectedActions: ToolAction[] = [];
|
||||
let collectedActions: ToolAction[] = [];
|
||||
let collectedInsights: AssistantInsight[] = [];
|
||||
const userId = ctx.dbUser!.id;
|
||||
const conversationId = input.conversationId?.trim().slice(0, 120) || "default";
|
||||
const pendingApproval = await peekPendingAssistantApproval(ctx.db, userId, conversationId);
|
||||
|
||||
if (pendingApproval && lastUserMsg?.role === "user") {
|
||||
if (isCancellationReply(lastUserMsg.content)) {
|
||||
await clearPendingAssistantApproval(ctx.db, userId, conversationId);
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "AiToolExecution",
|
||||
entityId: pendingApproval.id,
|
||||
entityName: pendingApproval.toolName,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
source: "ai",
|
||||
summary: `AI approval cancelled: ${pendingApproval.toolName}`,
|
||||
after: { approvalId: pendingApproval.id, params: parseToolArguments(pendingApproval.toolArguments), executed: false },
|
||||
});
|
||||
const pendingApprovalResult = await handlePendingAssistantApproval({
|
||||
db: ctx.db,
|
||||
dbUserId: ctx.dbUser?.id,
|
||||
toolCtx,
|
||||
conversationId,
|
||||
pendingApproval,
|
||||
lastUserMessage: lastUserMsg,
|
||||
messages: input.messages,
|
||||
collectedActions,
|
||||
collectedInsights,
|
||||
});
|
||||
|
||||
return {
|
||||
content: `Aktion verworfen: ${pendingApproval.summary}`,
|
||||
role: "assistant" as const,
|
||||
approval: toApprovalPayload(pendingApproval, "cancelled"),
|
||||
};
|
||||
}
|
||||
|
||||
if (canExecuteMutationTool(input.messages, pendingApproval.toolName, pendingApproval)) {
|
||||
const approvedAction = await consumePendingAssistantApproval(ctx.db, userId, conversationId) ?? pendingApproval;
|
||||
const result = await executeTool(
|
||||
approvedAction.toolName,
|
||||
approvedAction.toolArguments,
|
||||
toolCtx,
|
||||
);
|
||||
|
||||
const insight = buildAssistantInsight(approvedAction.toolName, result.data);
|
||||
if (insight) {
|
||||
collectedInsights = mergeInsights(collectedInsights, insight);
|
||||
}
|
||||
if (result.action) {
|
||||
collectedActions.push(result.action);
|
||||
}
|
||||
|
||||
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: ctx.db,
|
||||
entityType: "AiToolExecution",
|
||||
entityId: approvedAction.id,
|
||||
entityName: approvedAction.toolName,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: finalContent,
|
||||
role: "assistant" as const,
|
||||
approval: toApprovalPayload(approvedAction, "approved"),
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
};
|
||||
}
|
||||
if (pendingApprovalResult) {
|
||||
collectedActions = pendingApprovalResult.collectedActions;
|
||||
collectedInsights = pendingApprovalResult.collectedInsights;
|
||||
return pendingApprovalResult.response;
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
|
||||
@@ -354,10 +288,11 @@ export const assistantRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
content: "Schreibende Assistant-Aktionen sind gerade nicht verfuegbar, weil der Bestaetigungsspeicher in der Datenbank fehlt. Bitte die CapaKraken-DB-Migration anwenden und dann erneut versuchen.",
|
||||
role: "assistant" as const,
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
...buildAssistantChatResponse({
|
||||
content: "Schreibende Assistant-Aktionen sind gerade nicht verfuegbar, weil der Bestaetigungsspeicher in der Datenbank fehlt. Bitte die CapaKraken-DB-Migration anwenden und dann erneut versuchen.",
|
||||
insights: collectedInsights,
|
||||
actions: collectedActions,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -378,11 +313,12 @@ export const assistantRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
content: `${ASSISTANT_CONFIRMATION_PREFIX} ${approval.summary}. Bitte bestätigen.`,
|
||||
role: "assistant" as const,
|
||||
approval: toApprovalPayload(approval, "pending"),
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
...buildAssistantChatResponse({
|
||||
content: `${ASSISTANT_CONFIRMATION_PREFIX} ${approval.summary}. Bitte bestätigen.`,
|
||||
approval: toApprovalPayload(approval, "pending"),
|
||||
insights: collectedInsights,
|
||||
actions: collectedActions,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -453,19 +389,21 @@ export const assistantRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
return {
|
||||
content: finalContent,
|
||||
role: "assistant" as const,
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
...buildAssistantChatResponse({
|
||||
content: finalContent,
|
||||
insights: collectedInsights,
|
||||
actions: collectedActions,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Exceeded max iterations
|
||||
return {
|
||||
content: "I had to stop after too many tool calls. Please try a simpler question.",
|
||||
role: "assistant" as const,
|
||||
...(collectedInsights.length > 0 ? { insights: collectedInsights } : {}),
|
||||
...(collectedActions.length > 0 ? { actions: collectedActions } : {}),
|
||||
...buildAssistantChatResponse({
|
||||
content: "I had to stop after too many tool calls. Please try a simpler question.",
|
||||
insights: collectedInsights,
|
||||
actions: collectedActions,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user