import { NextResponse } from "next/server"; import type { ChatMessage } from "@duoc-thu/shared-types"; import { buildAssistantMessage, type RagAnswerRaw } from "../_lib/ragResponse"; export const runtime = "nodejs"; // Same resolution rule as `/api/chat` and `/api/history` — RAG never goes // through api-gateway (it only proxies `/auth/*`), so this always talks to // ai-service directly. const AI_SERVICE_URL = process.env.AI_SERVICE_URL ?? "http://localhost:8000"; interface TranscriptMessageRaw { role: "user" | "assistant"; trace_id: string; content: string | null; decision?: string | null; reason?: string | null; resolved_drug_id?: string | null; citations?: RagAnswerRaw["citations"]; generated?: boolean; quick_replies?: string[]; blocks?: RagAnswerRaw["blocks"]; answer_mode?: RagAnswerRaw["answer_mode"]; answer_plan?: RagAnswerRaw["answer_plan"]; candidate_assessments?: RagAnswerRaw["candidate_assessments"]; disclaimer?: string | null; created_at: string; } interface TranscriptResponseRaw { messages: TranscriptMessageRaw[]; } function toChatMessage(row: TranscriptMessageRaw): ChatMessage { if (row.role === "user") { return { id: `user-${row.trace_id}`, role: "user", content: row.content ?? "", createdAt: row.created_at, }; } // Reuses the exact live-turn mapping (`/api/chat`'s `buildAssistantMessage`) // so a replayed answer renders identically to the one the user originally // saw — same abstain/refusal fallback, same citation grouping. const raw: RagAnswerRaw = { decision: row.decision ?? "answerable", reason: row.reason ?? "", answer: row.content, resolved_drug_id: row.resolved_drug_id ?? null, citations: row.citations ?? [], generated: row.generated, quick_replies: row.quick_replies, blocks: row.blocks, answer_mode: row.answer_mode, answer_plan: row.answer_plan, candidate_assessments: row.candidate_assessments, disclaimer: row.disclaimer, }; return buildAssistantMessage(raw, row.trace_id, row.created_at); } export async function GET(request: Request) { const { searchParams } = new URL(request.url); const conversationId = searchParams.get("conversation_id")?.trim() || ""; if (!conversationId) { return NextResponse.json({ messages: [] }); } try { const base = AI_SERVICE_URL.replace(/\/v1\/rag(?:\/query)?\/?$/, ""); const targetUrl = `${base}/v1/rag/transcript?conversation_id=${encodeURIComponent(conversationId)}`; const upstream = await fetch(targetUrl, { method: "GET", headers: { "X-Client-Version": "1.0.0" }, cache: "no-store", signal: AbortSignal.timeout(8_000), }); if (!upstream.ok) { return NextResponse.json({ messages: [] }); } const data = (await upstream.json()) as TranscriptResponseRaw; const messages = (data.messages ?? []).map(toChatMessage); return NextResponse.json({ messages }); } catch { return NextResponse.json({ messages: [] }); } }