since3-portal/app/api/ai/chat/route.ts
2026-10-05 08:23:02 +02:00

88 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Anthropic from "@anthropic-ai/sdk"
import { BOOKING_TOOL, chatSystem, nowNote } from "@/modules/templates/ai/prompts"
import { BookingSchema, ChatRequestSchema, type AiEvent } from "@/modules/templates/ai/schemas"
import { AI_MODEL, FALLBACK, aiErrorMessage, getAnthropic, ndjsonResponse, resolveAiAccess } from "@/modules/templates/ai/server"
export const maxDuration = 60
// Chat-Assistent der Webseite und KI-Telefon: streamt Text, erfasst Anfragen über das Tool anfrage_erfassen
export async function POST(request: Request) {
const parsed = ChatRequestSchema.safeParse(await request.json().catch(() => null))
if (!parsed.success) return Response.json({ error: "Ungültige Anfrage." }, { status: 400 })
const { access, mode, messages } = parsed.data
const first = messages.findIndex((message) => message.role === "user")
if (first < 0 || messages.at(-1)?.role !== "user") return Response.json({ error: "Ungültige Anfrage." }, { status: 400 })
const ctx = await resolveAiAccess(access, mode)
if ("error" in ctx) return Response.json({ error: ctx.error }, { status: ctx.status })
const system: Anthropic.Beta.BetaTextBlockParam[] = [
{ type: "text", text: chatSystem(ctx, mode), cache_control: { type: "ephemeral" } },
{ type: "text", text: nowNote() },
]
const encoder = new TextEncoder()
const body = new ReadableStream<Uint8Array>({
async start(controller) {
const send = (event: AiEvent) => controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`))
try {
const anthropic = getAnthropic()
let conversation: Anthropic.Beta.BetaMessageParam[] = messages.slice(first)
let retried = false
// Höchstens drei Runden: Antwort, Anfrage erfassen, Bestätigung
for (let round = 0; round < 3; round++) {
const stream = anthropic.beta.messages.stream({
...FALLBACK,
model: AI_MODEL,
max_tokens: 4096,
output_config: { effort: "low" },
system,
tools: [BOOKING_TOOL],
messages: conversation,
})
stream.on("text", (text) => send({ type: "text", text }))
let message: Anthropic.Beta.BetaMessage
try {
message = await stream.finalMessage()
} catch (error) {
// Nur unlesbare Tool-Eingaben einmal wiederholen, API-Fehler gehen nach außen
if (error instanceof Anthropic.APIError || retried) throw error
retried = true
continue
}
if (message.stop_reason === "refusal") {
send({ type: "error", message: "Dabei kann ich leider nicht helfen." })
break
}
const calls = message.content.filter((block): block is Anthropic.Beta.BetaToolUseBlock => block.type === "tool_use")
if (message.stop_reason !== "tool_use" || calls.length === 0) break
const results: Anthropic.Beta.BetaToolResultBlockParam[] = calls.map((call) => {
const booking = BookingSchema.safeParse(call.input)
if (booking.success) send({ type: "booking", data: booking.data })
return {
type: "tool_result",
tool_use_id: call.id,
is_error: !booking.success,
content: booking.success
? "Anfrage gespeichert und an das Team weitergeleitet (Demo, es wird nichts versendet)."
: "Angaben unvollständig – bitte die fehlenden Daten erfragen.",
}
})
conversation = [...conversation, { role: "assistant", content: message.content }, { role: "user", content: results }]
}
send({ type: "done" })
} catch (error) {
console.error("AI chat failed", error)
send({ type: "error", message: aiErrorMessage(error) })
}
controller.close()
},
})
return ndjsonResponse(body)
}