93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
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: [] });
|
|
}
|
|
}
|