refactor(api): extract assistant approval flow helpers
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user