Spaces:
Paused
Paused
Nhat Anh commited on
Commit ·
693d806
1
Parent(s): df859f4
Phase 2: state schema and query decomposer node verified
Browse files- src/agents/__init__.py +0 -0
- src/agents/query_decomposer.py +86 -0
- src/agents/state.py +26 -0
- src/agents/test_decomposer.py +28 -0
src/agents/__init__.py
ADDED
|
File without changes
|
src/agents/query_decomposer.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/query_decomposer.py
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from langgraph.graph import START
|
| 5 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 6 |
+
from src.agents.state import AgentState
|
| 7 |
+
from src.utils.logger import logger
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
llm = ChatGoogleGenerativeAI(
|
| 13 |
+
model="gemini-2.5-flash",
|
| 14 |
+
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
| 15 |
+
temperature=0
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
DECOMPOSER_PROMPT = """Bạn là một chuyên gia phân tích câu hỏi pháp lý Việt Nam.
|
| 19 |
+
|
| 20 |
+
Nhiệm vụ: phân tích câu hỏi và chia thành các câu hỏi con để tra cứu.
|
| 21 |
+
|
| 22 |
+
Với mỗi câu hỏi con, xác định:
|
| 23 |
+
- "question": nội dung câu hỏi con
|
| 24 |
+
- "source": nguồn cần tra cứu
|
| 25 |
+
+ "domestic": chỉ luật Việt Nam
|
| 26 |
+
+ "international": chỉ hiệp định quốc tế (EVFTA, CPTPP)
|
| 27 |
+
+ "both": cả hai
|
| 28 |
+
- "domain": lĩnh vực
|
| 29 |
+
+ "labor": lao động, việc làm, hợp đồng lao động, tiền lương, công đoàn
|
| 30 |
+
+ "food_safety": an toàn thực phẩm, vệ sinh thực phẩm, kiểm dịch
|
| 31 |
+
|
| 32 |
+
Trả lời ONLY bằng JSON hợp lệ, không có text nào khác:
|
| 33 |
+
{{
|
| 34 |
+
"sub_questions": [
|
| 35 |
+
{{
|
| 36 |
+
"question": "...",
|
| 37 |
+
"source": "domestic|international|both",
|
| 38 |
+
"domain": "labor|food_safety"
|
| 39 |
+
}}
|
| 40 |
+
],
|
| 41 |
+
"requires_international": true|false
|
| 42 |
+
}}
|
| 43 |
+
|
| 44 |
+
Câu hỏi: {query}"""
|
| 45 |
+
|
| 46 |
+
def query_decomposer_node(state: AgentState) -> dict:
|
| 47 |
+
query = state["original_query"]
|
| 48 |
+
logger.info(f"Query Decomposer: '{query[:80]}'")
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
response = llm.invoke(DECOMPOSER_PROMPT.format(query=query))
|
| 52 |
+
raw = response.content.strip()
|
| 53 |
+
|
| 54 |
+
# Strip markdown fences if present
|
| 55 |
+
if "```" in raw:
|
| 56 |
+
raw = raw.split("```")[1]
|
| 57 |
+
if raw.startswith("json"):
|
| 58 |
+
raw = raw[4:]
|
| 59 |
+
raw = raw.strip()
|
| 60 |
+
|
| 61 |
+
parsed = json.loads(raw)
|
| 62 |
+
|
| 63 |
+
sub_questions = parsed["sub_questions"]
|
| 64 |
+
requires_international = parsed["requires_international"]
|
| 65 |
+
|
| 66 |
+
logger.success(f"Decomposed into {len(sub_questions)} sub-question(s)")
|
| 67 |
+
for sq in sub_questions:
|
| 68 |
+
logger.info(f" → [{sq['source']}|{sq['domain']}] {sq['question']}")
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"sub_questions": sub_questions,
|
| 72 |
+
"requires_international": requires_international,
|
| 73 |
+
"error": None
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error(f"Query Decomposer failed: {e}")
|
| 78 |
+
return {
|
| 79 |
+
"sub_questions": [{
|
| 80 |
+
"question": query,
|
| 81 |
+
"source": "domestic",
|
| 82 |
+
"domain": "labor"
|
| 83 |
+
}],
|
| 84 |
+
"requires_international": False,
|
| 85 |
+
"error": str(e)
|
| 86 |
+
}
|
src/agents/state.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/state.py
|
| 2 |
+
from typing import TypedDict, Optional, Annotated
|
| 3 |
+
import operator
|
| 4 |
+
|
| 5 |
+
class AgentState(TypedDict):
|
| 6 |
+
# Input
|
| 7 |
+
original_query: str
|
| 8 |
+
|
| 9 |
+
# Query decomposition
|
| 10 |
+
sub_questions: list[dict] # each: {question, source, domain}
|
| 11 |
+
requires_international: bool
|
| 12 |
+
|
| 13 |
+
# Retrieval results — store only references, not full text
|
| 14 |
+
domestic_chunks: Annotated[list[dict], operator.add]
|
| 15 |
+
international_chunks: Annotated[list[dict], operator.add]
|
| 16 |
+
|
| 17 |
+
# Cross-reference output
|
| 18 |
+
cross_reference: Optional[dict]
|
| 19 |
+
|
| 20 |
+
# Final output
|
| 21 |
+
final_answer: str
|
| 22 |
+
citations: list[str]
|
| 23 |
+
has_expired_docs: bool
|
| 24 |
+
|
| 25 |
+
# Control
|
| 26 |
+
error: Optional[str]
|
src/agents/test_decomposer.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/agents/test_decomposer.py
|
| 2 |
+
from src.agents.state import AgentState
|
| 3 |
+
from src.agents.query_decomposer import query_decomposer_node
|
| 4 |
+
from src.utils.logger import logger
|
| 5 |
+
|
| 6 |
+
test_queries = [
|
| 7 |
+
"Hợp đồng lao động phải có những nội dung gì theo quy định hiện hành?",
|
| 8 |
+
"Điều kiện an toàn thực phẩm trong sản xuất thực phẩm xuất khẩu sang EU theo EVFTA là gì?",
|
| 9 |
+
"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?",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
for query in test_queries:
|
| 13 |
+
logger.info(f"\n{'='*60}")
|
| 14 |
+
initial_state: AgentState = {
|
| 15 |
+
"original_query": query,
|
| 16 |
+
"sub_questions": [],
|
| 17 |
+
"requires_international": False,
|
| 18 |
+
"domestic_chunks": [],
|
| 19 |
+
"international_chunks": [],
|
| 20 |
+
"cross_reference": None,
|
| 21 |
+
"final_answer": "",
|
| 22 |
+
"citations": [],
|
| 23 |
+
"has_expired_docs": False,
|
| 24 |
+
"error": None
|
| 25 |
+
}
|
| 26 |
+
result = query_decomposer_node(initial_state)
|
| 27 |
+
logger.info(f"requires_international: {result['requires_international']}")
|
| 28 |
+
logger.info(f"sub_questions: {result['sub_questions']}")
|