import { NextResponse } from "next/server"; import type { Citation, SendMessageResponse } from "@duoc-thu/shared-types"; export const runtime = "nodejs"; const AI_SERVICE_URL = process.env.AI_SERVICE_URL ?? "http://localhost:8079"; const DISCLAIMER = "Nội dung trích từ Dược thư Quốc gia Việt Nam, chỉ mang tính tra cứu chuyên môn và không thay thế chỉ định của bác sĩ hoặc dược sĩ."; interface RagCitation { chunk_id: string; printed_page_start: number; printed_page_end: number; physical_page: number; attachment?: string | null; } interface RagResponse { trace_id: string; decision: string; reason: string; answer: string | null; resolved_drug_id: string | null; citations: RagCitation[]; } /** * What the user reads when the system declines. * * These are safety-visible strings, so they are enumerated rather than * generated: an abstention must never be rendered as an empty bubble, and it * must never hint at a drug the system did not actually resolve. Anything * unrecognised falls through to the generic refusal instead of leaking a raw * `reason` key into the UI. */ const REFUSALS: Record = { drug_not_resolved: "Chưa xác định được thuốc trong câu hỏi này, nên hệ thống không đưa ra nội dung chuyên môn. Vui lòng nêu rõ tên hoạt chất cần tra cứu.", drug_resolution_ambiguous: "Câu hỏi có thể ứng với nhiều thuốc khác nhau. Vui lòng nêu rõ tên hoạt chất cần tra cứu.", recommendation_out_of_scope: "Đây là câu hỏi xin tư vấn hoặc quyết định điều trị. Hệ thống chỉ tra cứu Dược thư và không đưa ra khuyến cáo điều trị — vui lòng hỏi bác sĩ hoặc dược sĩ.", out_of_scope_non_human: "Dược thư Quốc gia Việt Nam áp dụng cho người. Hệ thống không tra cứu cho đối tượng khác.", subject_scope_unknown: "Chưa rõ câu hỏi áp dụng cho đối tượng nào, nên hệ thống không trả lời.", query_embedding_unavailable: "Chưa tra được mục tương ứng cho câu hỏi này. Vui lòng nêu rõ thuộc tính cần tra (liều dùng, chống chỉ định, tương tác thuốc…).", insufficient_retrieval_score: "Không tìm thấy nội dung đủ liên quan trong Dược thư cho câu hỏi này.", }; const GENERIC_REFUSAL = "Hệ thống không tìm thấy căn cứ trong Dược thư để trả lời câu hỏi này."; function toCitations(raw: RagCitation[], resolvedDrugId: string | null): Citation[] { return raw.map((item) => { // Chunk ids are `__
__`. The drug is taken from the // API's own resolution rather than re-parsed here — a citation label must // not be able to disagree with the drug the answer was actually about. const parts = item.chunk_id.split("__"); return { drugName: resolvedDrugId ?? parts[0] ?? item.chunk_id, sectionType: parts.length > 1 ? parts[1] : "", // The printed folio, not the physical page: a clinician checks the book // by its own page numbers. sourcePageRange: [item.printed_page_start, item.printed_page_end], }; }); } export async function POST(request: Request) { let content: string; let conversationId: string | null = null; try { const body = await request.json(); content = typeof body?.content === "string" ? body.content.trim() : ""; conversationId = typeof body?.conversationId === "string" ? body.conversationId : null; } catch { return NextResponse.json({ error: "invalid_body" }, { status: 400 }); } if (!content) { return NextResponse.json({ error: "empty_query" }, { status: 400 }); } let rag: RagResponse; try { const upstream = await fetch(`${AI_SERVICE_URL}/v1/rag/query`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: content, subject_scope: "human", intent: "fact_lookup", conversation_id: conversationId, }), cache: "no-store", }); if (!upstream.ok) { return NextResponse.json( { error: "upstream_error", status: upstream.status }, { status: 502 } ); } rag = (await upstream.json()) as RagResponse; } catch { return NextResponse.json({ error: "upstream_unreachable" }, { status: 502 }); } const refused = rag.decision === "abstain" || rag.answer === null; const message: SendMessageResponse["message"] = { id: rag.trace_id, role: "assistant", content: refused ? REFUSALS[rag.reason] ?? GENERIC_REFUSAL : (rag.answer as string), citations: refused ? [] : toCitations(rag.citations, rag.resolved_drug_id), disclaimer: DISCLAIMER, createdAt: new Date().toISOString(), }; return NextResponse.json({ message } satisfies SendMessageResponse); }