Remove corpus counts from chat chrome

This commit is contained in:
2026-08-10 17:26:58 +07:00
parent 46469468bb
commit 97cb6d16f4
31 changed files with 2192 additions and 424 deletions
+59 -9
View File
@@ -1,12 +1,9 @@
import { NextResponse } from "next/server";
import type { Citation, SendMessageResponse } from "@duoc-thu/shared-types";
import type { AnswerBlock, AnswerPlan, Citation, SendMessageResponse } from "@duoc-thu/shared-types";
export const runtime = "nodejs";
const API_GATEWAY_URL = process.env.API_GATEWAY_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ĩ.";
const API_GATEWAY_URL = process.env.API_GATEWAY_URL ?? process.env.AI_SERVICE_URL ?? "http://localhost:8000";
interface RagCitation {
chunk_id: string;
@@ -29,6 +26,40 @@ interface RagResponse {
citations: RagCitation[];
generated?: boolean;
quick_replies?: string[];
blocks?: Array<{
title: string;
kind: string;
claims: Array<{ text: string; source_ids: string[] }>;
}>;
answer_mode?: "concise" | "normal" | "detailed";
answer_plan?: {
verbosity: "concise" | "normal" | "detailed";
layout: string;
reasoning_mode: string;
show_heading: boolean;
needs_warning: boolean;
} | null;
}
function toAnswerBlocks(raw: NonNullable<RagResponse["blocks"]>): AnswerBlock[] {
return raw.map((block) => ({
title: block.title,
kind: block.kind,
claims: block.claims.map((claim) => ({
text: claim.text,
sourceIds: claim.source_ids,
})),
}));
}
function toAnswerPlan(raw: NonNullable<RagResponse["answer_plan"]>): AnswerPlan {
return {
verbosity: raw.verbosity,
layout: raw.layout,
reasoningMode: raw.reasoning_mode,
showHeading: raw.show_heading,
needsWarning: raw.needs_warning,
};
}
const REFUSALS: Record<string, string> = {
@@ -87,6 +118,8 @@ const REFUSALS: Record<string, string> = {
"Hệ thống phát hiện một phần câu trả lời không có trích dẫn nguồn rõ ràng nên đã huỷ để tránh sai sót. Vui lòng thử lại.",
unsupported_claim:
"Dược thư có nội dung liên quan đến câu hỏi này, nhưng bước đối chiếu lại chưa xác nhận được câu trả lời khớp hoàn toàn với nguồn. Vui lòng thử lại.",
incomplete_answer:
"Câu trả lời vừa tạo đã bị huỷ vì bước đối chiếu phát hiện còn bỏ sót dữ kiện liên quan trong nguồn. Vui lòng thử lại để hệ thống tạo câu trả lời đầy đủ hơn.",
// Kept as the fallback `answer.py` itself falls back to when, for some
// reason, none of the specific codes above was set.
generation_unavailable:
@@ -171,6 +204,12 @@ export async function POST(request: Request) {
if (!content) {
return NextResponse.json({ error: "empty_query" }, { status: 400 });
}
if (content.length > 4000) {
return NextResponse.json({ error: "query_too_long" }, { status: 400 });
}
if (conversationId && conversationId.length > 128) {
return NextResponse.json({ error: "conversation_id_too_long" }, { status: 400 });
}
const correlationId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
@@ -194,6 +233,10 @@ export async function POST(request: Request) {
conversation_id: conversationId,
}),
cache: "no-store",
// Propagate a browser disconnect/Stop action to the upstream fetch.
// The synchronous Bedrock call already in flight may finish, but this
// prevents the BFF itself from keeping an orphaned HTTP request open.
signal: request.signal,
});
if (!upstream.ok) {
rag = {
@@ -212,7 +255,7 @@ export async function POST(request: Request) {
trace_id: `fallback-${Date.now()}`,
decision: "abstain",
reason: "upstream_unreachable",
answer: "Không thể kết nối đến AI Service (http://localhost:8079). Vui lòng đảm bảo AI Service đã được bật.",
answer: `Không thể kết nối đến AI Service (${API_GATEWAY_URL}). Vui lòng đảm bảo AI Service đã được bật.`,
resolved_drug_id: null,
citations: [],
};
@@ -227,23 +270,30 @@ export async function POST(request: Request) {
// answer-less case (retrieval abstained with no message to show).
const noAnswer = rag.answer === null;
const isAbstain = rag.decision === "abstain";
const isGroundedAnswer =
rag.decision === "answerable" && !noAnswer && rag.citations.length > 0;
const message: SendMessageResponse["message"] = {
id: rag.trace_id || `msg-${Date.now()}`,
role: "assistant",
content: noAnswer ? (REFUSALS[rag.reason] ?? GENERIC_REFUSAL) : (rag.answer ?? GENERIC_REFUSAL),
citations: isAbstain || noAnswer ? [] : toCitations(rag.citations),
disclaimer: DISCLAIMER,
traceId: rag.trace_id,
decision: rag.decision,
reason: rag.reason,
grounded: !isAbstain && !noAnswer,
generated: !isAbstain && !noAnswer ? Boolean(rag.generated) : false,
grounded: isGroundedAnswer,
generated: isGroundedAnswer ? Boolean(rag.generated) : false,
resolvedDrugId: rag.resolved_drug_id ?? undefined,
createdAt: new Date().toISOString(),
quickReplies:
rag.decision === "clarify" && rag.quick_replies && rag.quick_replies.length > 0
? rag.quick_replies
: undefined,
blocks:
isGroundedAnswer && rag.blocks && rag.blocks.length > 0
? toAnswerBlocks(rag.blocks)
: undefined,
answerMode: rag.answer_mode,
answerPlan: rag.answer_plan ? toAnswerPlan(rag.answer_plan) : undefined,
};
return NextResponse.json(
+1 -1
View File
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
export const runtime = "nodejs";
const API_GATEWAY_URL =
process.env.API_GATEWAY_URL ?? process.env.AI_SERVICE_URL ?? "http://localhost:8079";
process.env.API_GATEWAY_URL ?? process.env.AI_SERVICE_URL ?? "http://localhost:8000";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);