evOo-local/packages/adapters/src/prose-handoff-recovery.ts

113 lines
3.2 KiB
TypeScript

import type { MessageBlock } from "@rakazo/contracts";
import { type ProseToolCall, sanitizeBotReplyText } from "@rakazo/core";
import type { ExecutorDeps } from "./executor.js";
import { handoffToGroupBot } from "./group-handoff.js";
export const INBOUND_HANDOFF_WORKSHOP_FALLBACK =
"Ich übernehme das direkt in meiner Spur — gleich skizziere ich erste Richtungen und stelle dir gezielte Fragen.";
export const INBOUND_HANDOFF_FAILURE_FALLBACK =
"Die Übergabe ist gerade fehlgeschlagen — ich bleibe dabei und mache hier mit dir weiter.";
type HandoffRun = {
id: string;
spaceId: string;
threadId: string;
botId: string;
userId: string;
};
export function sanitizeTextBlocks(blocks: readonly MessageBlock[]): MessageBlock[] {
const next: MessageBlock[] = [];
for (const block of blocks) {
if (block.kind !== "text") {
next.push(block);
continue;
}
const sanitized = sanitizeBotReplyText(block.text);
if (sanitized.text) next.push({ kind: "text", text: sanitized.text });
}
return next;
}
export function proseHandoffArgs(call: ProseToolCall): {
bot_id?: string;
confirm_name?: string;
message: string;
} {
const params = call.parameters;
return {
bot_id: params.bot_id != null ? String(params.bot_id) : undefined,
confirm_name: params.confirm_name != null ? String(params.confirm_name) : undefined,
message: String(params.message ?? ""),
};
}
export async function tryRecoverProseHandoff(
deps: Pick<ExecutorDeps, "prisma" | "events" | "jobs">,
run: HandoffRun,
groupId: string | null | undefined,
text: string,
handedOff: boolean,
): Promise<{
handedOff: boolean;
sanitizedText: string;
blockedUpward: boolean;
handoffFailed: boolean;
proseToolCalls: ProseToolCall[];
}> {
const sanitized = sanitizeBotReplyText(text);
if (handedOff || !groupId || sanitized.proseToolCalls.length === 0) {
return {
handedOff,
sanitizedText: sanitized.text,
blockedUpward: false,
handoffFailed: false,
proseToolCalls: sanitized.proseToolCalls,
};
}
let nextHandedOff: boolean = handedOff;
let blockedUpward = false;
let handoffFailed = false;
for (const call of sanitized.proseToolCalls) {
if (call.name !== "handoff_to_bot") continue;
const args = proseHandoffArgs(call);
if (!args.message.trim()) continue;
const result = await handoffToGroupBot(deps, run, groupId, args);
if ("ok" in result && result.ok) {
nextHandedOff = true;
continue;
}
if ("error" in result) {
if (
result.error.includes("sender") ||
result.error.includes("you own this handed-off stage")
) {
blockedUpward = true;
} else {
handoffFailed = true;
}
}
}
return {
handedOff: nextHandedOff,
sanitizedText: sanitized.text,
blockedUpward,
handoffFailed,
proseToolCalls: sanitized.proseToolCalls,
};
}
export function workshopFallbackAfterBlockedHandoff(
sanitizedText: string,
blockedUpward: boolean,
handoffFailed: boolean,
): string | undefined {
if (sanitizedText.trim()) return undefined;
if (blockedUpward) return INBOUND_HANDOFF_WORKSHOP_FALLBACK;
if (handoffFailed) return INBOUND_HANDOFF_FAILURE_FALLBACK;
return undefined;
}