mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
88 lines
4.4 KiB
TypeScript
88 lines
4.4 KiB
TypeScript
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)
|
||
}
|