Spaces:
Paused
Paused
Nhat Anh commited on
Commit ·
25e08f6
1
Parent(s): 07a9546
Phase 2: full 5-node pipeline working end-to-end - cross-reference and synthesis verified
Browse files- src/agents/cross_reference.py +129 -0
- src/agents/graph.py +84 -0
- src/agents/synthesizer.py +120 -0
src/agents/cross_reference.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/cross_reference.py
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 5 |
+
from src.agents.state import AgentState
|
| 6 |
+
from src.utils.logger import logger
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
llm = ChatGoogleGenerativeAI(
|
| 12 |
+
model="gemini-2.5-flash",
|
| 13 |
+
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
| 14 |
+
temperature=0
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
CROSS_REFERENCE_PROMPT = """Bạn là chuyên gia pháp lý so sánh luật Việt Nam với các hiệp định thương mại quốc tế.
|
| 18 |
+
|
| 19 |
+
Nhiệm vụ: Phân tích và đối chiếu các quy định pháp luật Việt Nam với các tiêu chuẩn quốc tế (EVFTA, CPTPP) dựa trên các đoạn văn bản được cung cấp.
|
| 20 |
+
|
| 21 |
+
## Câu hỏi gốc:
|
| 22 |
+
{original_query}
|
| 23 |
+
|
| 24 |
+
## Quy định pháp luật Việt Nam (nội địa):
|
| 25 |
+
{domestic_context}
|
| 26 |
+
|
| 27 |
+
## Tiêu chuẩn quốc tế (EVFTA/CPTPP):
|
| 28 |
+
{international_context}
|
| 29 |
+
|
| 30 |
+
## Yêu cầu phân tích:
|
| 31 |
+
1. Tóm tắt ngắn gọn nội dung chính từ pháp luật Việt Nam liên quan đến câu hỏi
|
| 32 |
+
2. Tóm tắt ngắn gọn các yêu cầu từ hiệp định quốc tế liên quan
|
| 33 |
+
3. Đánh giá mức độ tương thích:
|
| 34 |
+
- "aligned": pháp luật Việt Nam đáp ứng đầy đủ yêu cầu quốc tế
|
| 35 |
+
- "conflict": có mâu thuẫn trực tiếp giữa luật Việt Nam và yêu cầu quốc tế
|
| 36 |
+
- "gap": pháp luật Việt Nam chưa đáp ứng đầy đủ hoặc còn thiếu sót
|
| 37 |
+
- "no_international": không có tiêu chuẩn quốc tế liên quan trong dữ liệu
|
| 38 |
+
4. Giải thích cụ thể lý do đánh giá
|
| 39 |
+
5. Liệt kê các văn bản đã hết hiệu lực được tìm thấy (nếu có)
|
| 40 |
+
|
| 41 |
+
Trả lời ONLY bằng JSON hợp lệ, không có text nào khác:
|
| 42 |
+
{{
|
| 43 |
+
"domestic_summary": "...",
|
| 44 |
+
"international_summary": "...",
|
| 45 |
+
"alignment": "aligned|conflict|gap|no_international",
|
| 46 |
+
"explanation": "...",
|
| 47 |
+
"expired_docs": ["tên văn bản 1", "tên văn bản 2"]
|
| 48 |
+
}}"""
|
| 49 |
+
|
| 50 |
+
def format_chunks_for_context(chunks: list[dict], max_chars_per_chunk: int = 400) -> str:
|
| 51 |
+
if not chunks:
|
| 52 |
+
return "Không có dữ liệu."
|
| 53 |
+
|
| 54 |
+
lines = []
|
| 55 |
+
for i, chunk in enumerate(chunks):
|
| 56 |
+
title = chunk.get("title", "")[:60]
|
| 57 |
+
article = chunk.get("article_number", "N/A")
|
| 58 |
+
text = chunk.get("text", "")[:max_chars_per_chunk]
|
| 59 |
+
status = chunk.get("tinh_trang_hieu_luc", "")
|
| 60 |
+
agreement = chunk.get("agreement", "")
|
| 61 |
+
source_label = f"[{agreement}]" if agreement else "[Việt Nam]"
|
| 62 |
+
expired_note = " ⚠️ HẾT HIỆU LỰC" if "hết hiệu lực" in status.lower() else ""
|
| 63 |
+
|
| 64 |
+
lines.append(
|
| 65 |
+
f"[{i+1}] {source_label} {title} — {article}{expired_note}\n{text}"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
return "\n\n".join(lines)
|
| 69 |
+
|
| 70 |
+
def cross_reference_node(state: AgentState) -> dict:
|
| 71 |
+
original_query = state["original_query"]
|
| 72 |
+
domestic_chunks = state.get("domestic_chunks", [])
|
| 73 |
+
international_chunks = state.get("international_chunks", [])
|
| 74 |
+
|
| 75 |
+
# Skip if no international chunks — no cross-reference needed
|
| 76 |
+
if not international_chunks:
|
| 77 |
+
logger.info("Cross-Reference: no international chunks, skipping")
|
| 78 |
+
return {
|
| 79 |
+
"cross_reference": {
|
| 80 |
+
"domestic_summary": "",
|
| 81 |
+
"international_summary": "",
|
| 82 |
+
"alignment": "no_international",
|
| 83 |
+
"explanation": "Câu hỏi chỉ liên quan đến pháp luật nội địa Việt Nam.",
|
| 84 |
+
"expired_docs": []
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
logger.info(f"Cross-Reference: analyzing {len(domestic_chunks)} domestic + {len(international_chunks)} international chunks")
|
| 89 |
+
|
| 90 |
+
domestic_context = format_chunks_for_context(domestic_chunks)
|
| 91 |
+
international_context = format_chunks_for_context(international_chunks)
|
| 92 |
+
|
| 93 |
+
prompt = CROSS_REFERENCE_PROMPT.format(
|
| 94 |
+
original_query=original_query,
|
| 95 |
+
domestic_context=domestic_context,
|
| 96 |
+
international_context=international_context
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
response = llm.invoke(prompt)
|
| 101 |
+
raw = response.content.strip()
|
| 102 |
+
|
| 103 |
+
# Strip markdown fences if present
|
| 104 |
+
if "```" in raw:
|
| 105 |
+
raw = raw.split("```")[1]
|
| 106 |
+
if raw.startswith("json"):
|
| 107 |
+
raw = raw[4:]
|
| 108 |
+
raw = raw.strip()
|
| 109 |
+
|
| 110 |
+
result = json.loads(raw)
|
| 111 |
+
|
| 112 |
+
logger.success(f"Cross-Reference: alignment = {result['alignment']}")
|
| 113 |
+
logger.info(f" Explanation: {result['explanation'][:120]}...")
|
| 114 |
+
if result.get("expired_docs"):
|
| 115 |
+
logger.warning(f" Expired docs flagged: {result['expired_docs']}")
|
| 116 |
+
|
| 117 |
+
return {"cross_reference": result}
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.error(f"Cross-Reference failed: {e}")
|
| 121 |
+
return {
|
| 122 |
+
"cross_reference": {
|
| 123 |
+
"domestic_summary": "Lỗi phân tích",
|
| 124 |
+
"international_summary": "Lỗi phân tích",
|
| 125 |
+
"alignment": "gap",
|
| 126 |
+
"explanation": f"Lỗi hệ thống: {str(e)}",
|
| 127 |
+
"expired_docs": []
|
| 128 |
+
}
|
| 129 |
+
}
|
src/agents/graph.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/graph.py
|
| 2 |
+
import os
|
| 3 |
+
from langgraph.graph import StateGraph, START, END
|
| 4 |
+
from src.agents.state import AgentState
|
| 5 |
+
from src.agents.query_decomposer import query_decomposer_node
|
| 6 |
+
from src.agents.domestic_retriever import domestic_retriever_node
|
| 7 |
+
from src.agents.international_retriever import international_retriever_node
|
| 8 |
+
from src.agents.cross_reference import cross_reference_node
|
| 9 |
+
from src.agents.synthesizer import synthesizer_node
|
| 10 |
+
from src.utils.logger import logger
|
| 11 |
+
|
| 12 |
+
def build_graph() -> StateGraph:
|
| 13 |
+
graph = StateGraph(AgentState)
|
| 14 |
+
|
| 15 |
+
# Add nodes
|
| 16 |
+
graph.add_node("query_decomposer", query_decomposer_node)
|
| 17 |
+
graph.add_node("domestic_retriever", domestic_retriever_node)
|
| 18 |
+
graph.add_node("international_retriever", international_retriever_node)
|
| 19 |
+
graph.add_node("cross_reference", cross_reference_node)
|
| 20 |
+
graph.add_node("synthesizer", synthesizer_node)
|
| 21 |
+
|
| 22 |
+
# Entry point
|
| 23 |
+
graph.add_edge(START, "query_decomposer")
|
| 24 |
+
|
| 25 |
+
# After decomposition, run both retrievers
|
| 26 |
+
graph.add_edge("query_decomposer", "domestic_retriever")
|
| 27 |
+
graph.add_edge("query_decomposer", "international_retriever")
|
| 28 |
+
|
| 29 |
+
# After both retrievers complete, cross-reference
|
| 30 |
+
graph.add_edge("domestic_retriever", "cross_reference")
|
| 31 |
+
graph.add_edge("international_retriever", "cross_reference")
|
| 32 |
+
|
| 33 |
+
# After cross-reference, synthesize
|
| 34 |
+
graph.add_edge("cross_reference", "synthesizer")
|
| 35 |
+
|
| 36 |
+
# End
|
| 37 |
+
graph.add_edge("synthesizer", END)
|
| 38 |
+
|
| 39 |
+
return graph.compile()
|
| 40 |
+
|
| 41 |
+
# Module-level compiled graph
|
| 42 |
+
vilexagent = build_graph()
|
| 43 |
+
|
| 44 |
+
def run_query(query: str) -> dict:
|
| 45 |
+
logger.info(f"\n{'='*60}")
|
| 46 |
+
logger.info(f"Query: {query}")
|
| 47 |
+
|
| 48 |
+
initial_state: AgentState = {
|
| 49 |
+
"original_query": query,
|
| 50 |
+
"sub_questions": [],
|
| 51 |
+
"requires_international": False,
|
| 52 |
+
"domestic_chunks": [],
|
| 53 |
+
"international_chunks": [],
|
| 54 |
+
"cross_reference": None,
|
| 55 |
+
"final_answer": "",
|
| 56 |
+
"citations": [],
|
| 57 |
+
"has_expired_docs": False,
|
| 58 |
+
"error": None
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
result = vilexagent.invoke(initial_state)
|
| 62 |
+
|
| 63 |
+
logger.info(f"\n--- FINAL ANSWER ---")
|
| 64 |
+
logger.info(result["final_answer"])
|
| 65 |
+
logger.info(f"\n--- CITATIONS ---")
|
| 66 |
+
for c in result["citations"]:
|
| 67 |
+
logger.info(f" {c}")
|
| 68 |
+
logger.info(f"\nAlignment: {result['cross_reference']['alignment'] if result['cross_reference'] else 'N/A'}")
|
| 69 |
+
logger.info(f"Expired docs: {result['has_expired_docs']}")
|
| 70 |
+
|
| 71 |
+
return result
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
# Test with all three query types
|
| 75 |
+
queries = [
|
| 76 |
+
# Type A — domestic only
|
| 77 |
+
"Hợp đồng lao động phải có những nội dung gì theo quy định hiện hành?",
|
| 78 |
+
# Type C — cross-corpus
|
| 79 |
+
"Việt Nam có đáp ứng các tiêu chuẩn lao động của CPTPP về tự do hiệp hội không?",
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
for query in queries:
|
| 83 |
+
run_query(query)
|
| 84 |
+
print("\n" + "="*60 + "\n")
|
src/agents/synthesizer.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/synthesizer.py
|
| 2 |
+
import os
|
| 3 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 4 |
+
from src.agents.state import AgentState
|
| 5 |
+
from src.utils.logger import logger
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
llm = ChatGoogleGenerativeAI(
|
| 11 |
+
model="gemini-2.5-flash",
|
| 12 |
+
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
| 13 |
+
temperature=0
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
SYNTHESIS_PROMPT = """Bạn là trợ lý pháp lý chuyên về luật Việt Nam và các hiệp định thương mại quốc tế.
|
| 17 |
+
|
| 18 |
+
Dựa trên kết quả phân tích dưới đây, hãy trả lời câu hỏi của người dùng một cách rõ ràng, chính xác và có trích dẫn nguồn.
|
| 19 |
+
|
| 20 |
+
## Câu hỏi:
|
| 21 |
+
{original_query}
|
| 22 |
+
|
| 23 |
+
## Tóm tắt từ pháp luật Việt Nam:
|
| 24 |
+
{domestic_summary}
|
| 25 |
+
|
| 26 |
+
## Tóm tắt từ tiêu chuẩn quốc tế:
|
| 27 |
+
{international_summary}
|
| 28 |
+
|
| 29 |
+
## Đánh giá tương thích:
|
| 30 |
+
Mức độ: {alignment}
|
| 31 |
+
Giải thích: {explanation}
|
| 32 |
+
|
| 33 |
+
## Các văn bản pháp luật Việt Nam liên quan:
|
| 34 |
+
{domestic_citations}
|
| 35 |
+
|
| 36 |
+
## Các điều khoản quốc tế liên quan:
|
| 37 |
+
{international_citations}
|
| 38 |
+
|
| 39 |
+
{expired_warning}
|
| 40 |
+
|
| 41 |
+
## Yêu cầu trả lời:
|
| 42 |
+
1. Trả lời trực tiếp câu hỏi trong 1-2 câu đầu tiên
|
| 43 |
+
2. Giải thích chi tiết dựa trên các văn bản pháp luật
|
| 44 |
+
3. Nếu có sự khác biệt giữa luật Việt Nam và tiêu chuẩn quốc tế, nêu rõ khoảng cách đó
|
| 45 |
+
4. Trích dẫn cụ thể điều khoản, số hiệu văn bản
|
| 46 |
+
5. Nếu có văn bản hết hiệu lực, cảnh báo người dùng và chỉ dùng văn bản còn hiệu lực làm căn cứ chính
|
| 47 |
+
|
| 48 |
+
Trả lời bằng tiếng Việt, rõ ràng và chuyên nghiệp."""
|
| 49 |
+
|
| 50 |
+
def format_citations(chunks: list[dict]) -> str:
|
| 51 |
+
if not chunks:
|
| 52 |
+
return "Không có dữ liệu."
|
| 53 |
+
lines = []
|
| 54 |
+
for c in chunks[:5]:
|
| 55 |
+
title = c.get("title", "")[:70]
|
| 56 |
+
article = c.get("article_number", "N/A")
|
| 57 |
+
so_ky_hieu = c.get("so_ky_hieu", "")
|
| 58 |
+
status = c.get("tinh_trang_hieu_luc", "")
|
| 59 |
+
agreement = c.get("agreement", "")
|
| 60 |
+
expired = " ⚠️ HẾT HIỆU LỰC" if "hết hiệu lực" in status.lower() else ""
|
| 61 |
+
ref = f"[{agreement}] " if agreement else ""
|
| 62 |
+
lines.append(f"- {ref}{title} ({so_ky_hieu}) — {article}{expired}")
|
| 63 |
+
return "\n".join(lines)
|
| 64 |
+
|
| 65 |
+
def synthesizer_node(state: AgentState) -> dict:
|
| 66 |
+
original_query = state["original_query"]
|
| 67 |
+
domestic_chunks = state.get("domestic_chunks", [])
|
| 68 |
+
international_chunks = state.get("international_chunks", [])
|
| 69 |
+
cross_reference = state.get("cross_reference", {})
|
| 70 |
+
has_expired = state.get("has_expired_docs", False)
|
| 71 |
+
|
| 72 |
+
logger.info("Synthesizer: generating final answer...")
|
| 73 |
+
|
| 74 |
+
expired_warning = ""
|
| 75 |
+
if has_expired:
|
| 76 |
+
expired_warning = """## ⚠️ CẢNH BÁO:
|
| 77 |
+
Một số văn bản pháp luật được tìm thấy đã HẾT HIỆU LỰC.
|
| 78 |
+
Hãy cảnh báo rõ ràng người dùng và chỉ sử dụng văn bản CÒN HIỆU LỰC làm căn cứ pháp lý chính."""
|
| 79 |
+
|
| 80 |
+
prompt = SYNTHESIS_PROMPT.format(
|
| 81 |
+
original_query=original_query,
|
| 82 |
+
domestic_summary=cross_reference.get("domestic_summary", "Không có dữ liệu nội địa."),
|
| 83 |
+
international_summary=cross_reference.get("international_summary", "Không áp dụng."),
|
| 84 |
+
alignment=cross_reference.get("alignment", "unknown"),
|
| 85 |
+
explanation=cross_reference.get("explanation", ""),
|
| 86 |
+
domestic_citations=format_citations(domestic_chunks),
|
| 87 |
+
international_citations=format_citations(international_chunks),
|
| 88 |
+
expired_warning=expired_warning
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
response = llm.invoke(prompt)
|
| 93 |
+
final_answer = response.content.strip()
|
| 94 |
+
|
| 95 |
+
# Build citation list
|
| 96 |
+
citations = []
|
| 97 |
+
for c in domestic_chunks[:5]:
|
| 98 |
+
so_ky_hieu = c.get("so_ky_hieu", "")
|
| 99 |
+
article = c.get("article_number", "")
|
| 100 |
+
if so_ky_hieu:
|
| 101 |
+
citations.append(f"{so_ky_hieu} — {article}")
|
| 102 |
+
|
| 103 |
+
for c in international_chunks[:5]:
|
| 104 |
+
title = c.get("title", "")[:40]
|
| 105 |
+
article = c.get("article_number", "")
|
| 106 |
+
citations.append(f"{title} — {article}")
|
| 107 |
+
|
| 108 |
+
logger.success(f"Synthesizer: answer generated ({len(final_answer)} chars)")
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
"final_answer": final_answer,
|
| 112 |
+
"citations": citations
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Synthesizer failed: {e}")
|
| 117 |
+
return {
|
| 118 |
+
"final_answer": f"Lỗi hệ thống khi tổng hợp câu trả lời: {str(e)}",
|
| 119 |
+
"citations": []
|
| 120 |
+
}
|