feat(assistant): add approval inbox and e2e hardening
This commit is contained in:
@@ -5,10 +5,11 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { AssistantApprovalStatus, type PrismaClient } from "@capakraken/db";
|
||||
import { PermissionKey, resolvePermissions, type PermissionOverrides, type SystemRole } from "@capakraken/shared";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||
import { createAiClient, isAiConfigured, loggedAiCall, parseAiError } from "../ai-client.js";
|
||||
import { ADVANCED_ASSISTANT_TOOLS, TOOL_DEFINITIONS, executeTool, type ToolContext, type ToolAction } from "./assistant-tools.js";
|
||||
import { ADVANCED_ASSISTANT_TOOLS, MUTATION_TOOLS, TOOL_DEFINITIONS, executeTool, type ToolContext, type ToolAction } from "./assistant-tools.js";
|
||||
import { buildAssistantInsight, type AssistantInsight } from "./assistant-insights.js";
|
||||
import { checkPromptInjection } from "../lib/prompt-guard.js";
|
||||
import { checkAiOutput } from "../lib/content-filter.js";
|
||||
@@ -16,6 +17,33 @@ import { createAuditEntry } from "../lib/audit.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
|
||||
const MAX_TOOL_ITERATIONS = 8;
|
||||
const PENDING_APPROVAL_TTL_MS = 15 * 60 * 1000;
|
||||
export const ASSISTANT_CONFIRMATION_PREFIX = "CONFIRMATION_REQUIRED:";
|
||||
|
||||
type ChatMessage = { role: "user" | "assistant"; content: string };
|
||||
|
||||
type AssistantApprovalStore = Pick<PrismaClient, "assistantApproval">;
|
||||
|
||||
export interface PendingAssistantApproval {
|
||||
id: string;
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
toolName: string;
|
||||
toolArguments: string;
|
||||
summary: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface AssistantApprovalPayload {
|
||||
id: string;
|
||||
status: "pending" | "approved" | "cancelled";
|
||||
conversationId: string;
|
||||
toolName: string;
|
||||
summary: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `Du bist der CapaKraken-Assistent — ein hilfreicher AI-Assistent für Ressourcenplanung und Projektmanagement in einer 3D-Produktionsumgebung.
|
||||
|
||||
@@ -38,6 +66,7 @@ Wichtige Regeln:
|
||||
- Antworte in der Sprache des Users (Deutsch oder Englisch)
|
||||
- Geldbeträge: intern in Cent, konvertiere zu EUR für den User
|
||||
- KRITISCH — Human-in-the-Loop (EGAI 4.1.3.1 / IAAI 3.6.26): Bevor du eine Aktion ausführst, die Daten erstellt, ändert oder löscht (create, update, delete, approve, reject, cancel, deactivate, fill, set, generate, remove, send), MUSST du dem User IMMER zuerst eine Zusammenfassung zeigen, was du tun wirst, und EXPLIZIT auf seine Bestätigung warten. Führe NIEMALS eine schreibende Aktion aus ohne vorherige Bestätigung des Users. Wenn der User "ja", "ok", "mach das", "bestätigt" o.ä. antwortet, dann erst ausführen.
|
||||
- Wenn du eine Bestätigung brauchst, antworte zuerst mit einer Zeile, die GENAU mit "${ASSISTANT_CONFIRMATION_PREFIX}" beginnt, gefolgt von einer kurzen Maßnahmen-Zusammenfassung und der Bitte um Bestätigung. Erst nach einer bestätigenden User-Antwort darfst du ein Mutation-Tool aufrufen.
|
||||
- Sei KURZ und DIREKT. Keine langen Erklärungen wenn nicht nötig. Antworte knapp und präzise.
|
||||
- Rufe Tools PARALLEL auf wenn möglich (z.B. search_resources + list_allocations gleichzeitig)
|
||||
- Fasse Ergebnisse kompakt zusammen — keine unnötigen Wiederholungen der Tool-Ergebnisse
|
||||
@@ -129,7 +158,360 @@ function mergeInsights(existing: AssistantInsight[], next: AssistantInsight): As
|
||||
return [...existing, next].slice(-6);
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function mapPendingApproval(record: {
|
||||
id: string;
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
toolName: string;
|
||||
toolArguments: string;
|
||||
summary: string;
|
||||
createdAt: Date;
|
||||
expiresAt: Date;
|
||||
}): PendingAssistantApproval {
|
||||
return {
|
||||
id: record.id,
|
||||
userId: record.userId,
|
||||
conversationId: record.conversationId,
|
||||
toolName: record.toolName,
|
||||
toolArguments: record.toolArguments,
|
||||
summary: record.summary,
|
||||
createdAt: record.createdAt.getTime(),
|
||||
expiresAt: record.expiresAt.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
function toApprovalPayload(
|
||||
approval: PendingAssistantApproval,
|
||||
status: AssistantApprovalPayload["status"],
|
||||
): AssistantApprovalPayload {
|
||||
return {
|
||||
id: approval.id,
|
||||
status,
|
||||
conversationId: approval.conversationId,
|
||||
toolName: approval.toolName,
|
||||
summary: approval.summary,
|
||||
createdAt: new Date(approval.createdAt).toISOString(),
|
||||
expiresAt: new Date(approval.expiresAt).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listPendingAssistantApprovals(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
): Promise<PendingAssistantApproval[]> {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
|
||||
const approvals = await db.assistantApproval.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { gt: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return approvals.map(mapPendingApproval);
|
||||
}
|
||||
|
||||
export async function clearPendingAssistantApproval(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
): Promise<void> {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.CANCELLED,
|
||||
cancelledAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function peekPendingAssistantApproval(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
): Promise<PendingAssistantApproval | null> {
|
||||
await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { lte: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.EXPIRED,
|
||||
},
|
||||
});
|
||||
|
||||
const pending = await db.assistantApproval.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
if (!pending) return null;
|
||||
return mapPendingApproval(pending);
|
||||
}
|
||||
|
||||
export async function consumePendingAssistantApproval(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
): Promise<PendingAssistantApproval | null> {
|
||||
const pending = await peekPendingAssistantApproval(db, userId, conversationId);
|
||||
if (!pending) return null;
|
||||
const approvedAt = new Date();
|
||||
const updateResult = await db.assistantApproval.updateMany({
|
||||
where: {
|
||||
id: pending.id,
|
||||
userId,
|
||||
conversationId,
|
||||
status: AssistantApprovalStatus.PENDING,
|
||||
expiresAt: { gt: approvedAt },
|
||||
},
|
||||
data: {
|
||||
status: AssistantApprovalStatus.APPROVED,
|
||||
approvedAt,
|
||||
},
|
||||
});
|
||||
if (updateResult.count === 0) return null;
|
||||
|
||||
const approved = await db.assistantApproval.findFirst({
|
||||
where: {
|
||||
id: pending.id,
|
||||
userId,
|
||||
conversationId,
|
||||
},
|
||||
});
|
||||
if (!approved) return null;
|
||||
return mapPendingApproval(approved);
|
||||
}
|
||||
|
||||
export async function createPendingAssistantApproval(
|
||||
db: AssistantApprovalStore,
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
toolName: string,
|
||||
toolArguments: string,
|
||||
options?: { summary?: string; ttlMs?: number },
|
||||
): Promise<PendingAssistantApproval> {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + (options?.ttlMs ?? PENDING_APPROVAL_TTL_MS));
|
||||
const summary = options?.summary ?? buildApprovalSummary(toolName, toolArguments);
|
||||
await clearPendingAssistantApproval(db, userId, conversationId);
|
||||
const pendingApproval = await db.assistantApproval.create({
|
||||
data: {
|
||||
userId,
|
||||
conversationId,
|
||||
toolName,
|
||||
toolArguments,
|
||||
summary,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
return mapPendingApproval(pendingApproval);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function readToolError(result: Awaited<ReturnType<typeof executeTool>>): string | null {
|
||||
if (result.data && typeof result.data === "object" && result.data !== null && "error" in (result.data as Record<string, unknown>)) {
|
||||
const error = (result.data as Record<string, unknown>).error;
|
||||
return typeof error === "string" ? error : null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(result.content) as unknown;
|
||||
if (parsed && typeof parsed === "object" && "error" in (parsed as Record<string, unknown>)) {
|
||||
const error = (parsed as Record<string, unknown>).error;
|
||||
return typeof error === "string" ? error : null;
|
||||
}
|
||||
} catch {
|
||||
// tool content may be plain text
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readToolSuccessMessage(result: Awaited<ReturnType<typeof executeTool>>): string | null {
|
||||
if (result.data && typeof result.data === "object" && result.data !== null) {
|
||||
const data = result.data as Record<string, unknown>;
|
||||
if (typeof data.message === "string" && data.message.trim().length > 0) return data.message;
|
||||
if (typeof data.description === "string" && data.description.trim().length > 0) return data.description;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(result.content) as unknown;
|
||||
if (parsed && typeof parsed === "object") {
|
||||
const content = parsed as Record<string, unknown>;
|
||||
if (typeof content.message === "string" && content.message.trim().length > 0) return content.message;
|
||||
if (typeof content.description === "string" && content.description.trim().length > 0) return content.description;
|
||||
}
|
||||
} catch {
|
||||
// tool content may be plain text
|
||||
}
|
||||
|
||||
return typeof result.content === "string" && result.content.trim().length > 0
|
||||
? result.content
|
||||
: null;
|
||||
}
|
||||
|
||||
export const assistantRouter = createTRPCRouter({
|
||||
listPendingApprovals: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const approvals = await listPendingAssistantApprovals(ctx.db, ctx.dbUser!.id);
|
||||
return approvals.map((approval) => toApprovalPayload(approval, "pending"));
|
||||
}),
|
||||
chat: protectedProcedure
|
||||
.input(z.object({
|
||||
messages: z.array(z.object({
|
||||
@@ -137,6 +519,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
content: z.string(),
|
||||
})).min(1).max(200),
|
||||
pageContext: z.string().optional(),
|
||||
conversationId: z.string().max(120).optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// 1. Load AI settings
|
||||
@@ -217,9 +600,94 @@ export const assistantRouter = createTRPCRouter({
|
||||
const availableTools = getAvailableAssistantTools(permissions);
|
||||
|
||||
// 5. Function calling loop
|
||||
const toolCtx: ToolContext = { db: ctx.db, userId: ctx.dbUser!.id, userRole, permissions };
|
||||
const toolCtx: ToolContext = {
|
||||
db: ctx.db,
|
||||
userId: ctx.dbUser!.id,
|
||||
userRole,
|
||||
permissions,
|
||||
session: ctx.session,
|
||||
dbUser: ctx.dbUser,
|
||||
roleDefaults: ctx.roleDefaults,
|
||||
};
|
||||
const 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 },
|
||||
});
|
||||
|
||||
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 } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -258,6 +726,40 @@ export const assistantRouter = createTRPCRouter({
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const toolCall of msg.tool_calls as Array<{ id: string; function: { name: string; arguments: string } }>) {
|
||||
if (MUTATION_TOOLS.has(toolCall.function.name)) {
|
||||
const approval = await createPendingAssistantApproval(
|
||||
ctx.db,
|
||||
userId,
|
||||
conversationId,
|
||||
toolCall.function.name,
|
||||
toolCall.function.arguments,
|
||||
);
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "AiToolExecution",
|
||||
entityId: toolCall.id,
|
||||
entityName: toolCall.function.name,
|
||||
action: "CREATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
source: "ai",
|
||||
summary: `AI tool blocked pending confirmation: ${toolCall.function.name}`,
|
||||
after: {
|
||||
approvalId: approval.id,
|
||||
params: parseToolArguments(toolCall.function.arguments),
|
||||
executed: false,
|
||||
},
|
||||
});
|
||||
|
||||
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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await executeTool(
|
||||
toolCall.function.name,
|
||||
toolCall.function.arguments,
|
||||
@@ -296,7 +798,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
userId: ctx.dbUser?.id,
|
||||
source: "ai",
|
||||
summary: `AI executed tool: ${toolCall.function.name}`,
|
||||
after: { params: parsedArgs },
|
||||
after: { params: parsedArgs, executed: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user