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 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
+ }