Skip to content

Commit a7a7d7a

Browse files
Merge pull request #30 from DUT-Team-21TCLC-DT3/feat/law-apply-flow-ai-agents-when-ask-question
[LAW] Apply flow ai agents when ask question
2 parents 5f4382e + 9c1ed6d commit a7a7d7a

28 files changed

+9515
-153
lines changed

ai_service/.env.example

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
1-
# HTTP & gRPC
2-
START_GRPC=false
3-
HTTP_HOST=0.0.0.0
4-
HTTP_PORT=8000
5-
GRPC_BIND=0.0.0.0:50051
6-
71
# Neo4j
82
NEO4J_URI=bolt://localhost:7687
93
NEO4J_USER=neo4j
104
NEO4J_PASSWORD=your_password
115

12-
# OpenAI
13-
OPENAI_API_KEY=sk-...
14-
156
# Models
16-
CYPHER_MODEL=gpt-4o-mini
17-
QA_MODEL=gpt-4o-mini
7+
CYPHER_MODEL=gemini-2.5-pro
8+
QA_MODEL=gemini-2.5-pro
189
CYPHER_TEMPERATURE=0.0
19-
QA_TEMPERATURE=0.0
10+
QA_TEMPERATURE=0.0
11+
GEMINI_API_KEY=your_gemini_api_key

ai_service/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ RUN pip install --upgrade pip
55
RUN pip install --no-cache-dir -r requirements.txt
66
COPY . .
77
ENV PYTHONUNBUFFERED=1
8-
EXPOSE 8000 50051
8+
EXPOSE 8000
99
CMD ["python", "-m", "app.main"]

ai_service/__init__.py

Whitespace-only changes.

ai_service/app/adapters/chain_builders.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Optional
2+
from typing import Optional, List
33
from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain
44
from langchain_core.prompts import PromptTemplate
55
from .langchain_graph import GraphProvider
@@ -77,6 +77,41 @@ def custom_chain(self, prompt: PromptTemplate):
7777
# Fallback: build default chain, will still work
7878
log.warning("Custom cypher_prompt not supported by this langchain version; falling back to default prompt")
7979
return self.default_chain()
80+
81+
def get_all_prompts(self) -> List[PromptTemplate]:
82+
"""
83+
Return list of prompts to try in order of preference.
84+
Used for retry strategy.
85+
"""
86+
from ..pipelines.prompts import (
87+
CUSTOM_CYPHER_PROMPT,
88+
SIMPLE_SEARCH_PROMPT,
89+
KEYWORD_PATTERN_PROMPT,
90+
LAYER1_NODES, LAYER1_RELATIONSHIPS,
91+
LAYER2_NODES, LAYER2_RELATIONSHIPS,
92+
LAYER3_NODES, LAYER3_RELATIONSHIPS,
93+
LAYER4_NODES, LAYER4_RELATIONSHIPS,
94+
)
95+
96+
# Partially fill the detailed prompt with schema layers
97+
detailed_prompt = PromptTemplate.from_template(
98+
CUSTOM_CYPHER_PROMPT.template
99+
).partial(
100+
layer1_nodes=LAYER1_NODES,
101+
layer1_rels=LAYER1_RELATIONSHIPS,
102+
layer2_nodes=LAYER2_NODES,
103+
layer2_rels=LAYER2_RELATIONSHIPS,
104+
layer3_nodes=LAYER3_NODES,
105+
layer3_rels=LAYER3_RELATIONSHIPS,
106+
layer4_nodes=LAYER4_NODES,
107+
layer4_rels=LAYER4_RELATIONSHIPS,
108+
)
109+
110+
return [
111+
detailed_prompt, # Strategy 1: Most detailed
112+
SIMPLE_SEARCH_PROMPT, # Strategy 2: Simplified
113+
KEYWORD_PATTERN_PROMPT, # Strategy 3: Pattern-based
114+
]
80115

81116

82117
class FallbackChain:

ai_service/app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ class Settings(BaseSettings):
3535
"protected_namespaces": (),
3636
"env_file": [".env", "../.env"], # Check both current dir and parent dir
3737
"case_sensitive": True,
38+
"extra": "ignore", # Ignore extra fields from .env (e.g., old gRPC settings)
3839
}

ai_service/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ async def lifespan(app: FastAPI):
5454
port=port,
5555
log_level="info",
5656
access_log=True
57-
)
57+
)

ai_service/app/main_new.py

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import logging
2+
import json
3+
import os
4+
import sys
5+
import google.generativeai as genai
6+
from typing import Dict, Any, List
7+
from dotenv import load_dotenv
8+
from app.dependencies import get_settings
9+
10+
# Cấu hình logging
11+
logging.basicConfig(
12+
level=logging.INFO,
13+
format='%(asctime)s - %(levelname)s - %(message)s'
14+
)
15+
log = logging.getLogger(__name__)
16+
17+
# --- CẤU HÌNH SYSTEM INSTRUCTION ---
18+
# Đây là phần quan trọng nhất: Tách biệt luật lệ ra khỏi dữ liệu đầu vào
19+
SYSTEM_INSTRUCTION = """
20+
Bạn là chuyên gia AI về phân tích câu hỏi pháp luật Việt Nam (Luật Lao động, bảo hiểm xã hội).
21+
22+
NHIỆM VỤ:
23+
Phân tích câu hỏi người dùng (có thể người dùng sử dụng một số từ địa phương và viết tắt, ví dụ: "BHXH" thay vì "Bảo hiểm xã hội",... do đó cần chuyển đổi các từ viết tắt thành đầy đủ)
24+
và trả về JSON có cấu trúc để phục vụ tìm kiếm dữ liệu (RAG).
25+
26+
BƯỚC 1: KIỂM TRA ĐỘ LIÊN QUAN (RELEVANCE CHECK)
27+
- Phạm vi nội bộ: Luật Lao động, BHXH, BHYT, BHTN, Tiền lương.
28+
- Phạm vi mở rộng: Giao thông, Dân sự, Hình sự... -> Category: "WEBSITE_SEARCH", vẫn coi là RELEVANT
29+
- Không liên quan: Thời tiết, bóng đá... -> is_relevant: false.
30+
31+
BƯỚC 2: XỬ LÝ ĐẦU RA (OUTPUT HANDLING)
32+
33+
TRƯỜNG HỢP 1: IRRELEVANT (KHÔNG LIÊN QUAN)
34+
- Trả về JSON: {"is_relevant": false, "intent": "Mô tả lý do không liên quan", "category": "IRRELEVANT", ...các trường khác để trống...}
35+
36+
TRƯỜNG HỢP 2: RELEVANT (LIÊN QUAN)
37+
- Trả về JSON: {"is_relevant": true, ...}
38+
- Thực hiện phân tích chi tiết theo 3 QUY TẮC QUY TẮC BẤT DI BẤT DỊCH (CRITICAL RULES) sau:
39+
40+
QUY TẮC 1. FACTS (Sự kiện & Metadata):
41+
- CHỈ trích xuất các thông tin (con số, thời gian, mức lương) ĐƯỢC NÊU RÕ RÀNG trong câu hỏi, KHÔNG SUY DIỄN.
42+
- Đặc biệt với category "GRAPH_LOOKUP", bắt buộc trích xuất các key sau nếu có:
43+
+ "legal_article": Số hiệu điều luật (VD: "Điều 60", "Điều 3").
44+
+ "clause": Khoản (VD: "Khoản 1").
45+
+ "law_name": Tên luật/bộ luật/văn bản (VD: "Bộ luật Lao động", "Luật Bảo hiểm xã hội", "Nghị định 100/2020/NĐ-CP").
46+
+ "chapter": Chương.
47+
- Nếu câu hỏi chung chung (ví dụ: "Điều kiện hưởng là gì?"), trường "extracted_facts" PHẢI LÀ object rỗng {}.
48+
49+
QUY TẮC 2. SEARCH QUERIES:
50+
- Tạo 4-6 câu truy vấn tìm kiếm tối ưu.
51+
- PHẢI giải nghĩa từ viết tắt thành tiếng Việt đầy đủ.
52+
- Dùng từ ngữ pháp lý chính xác (ví dụ: dùng "trợ cấp thất nghiệp" thay vì "tiền thất nghiệp").
53+
- Các query phải bao quát được ý định của người dùng.
54+
55+
QUY TẮC 3. PHÂN LOẠI CATEGORY (QUAN TRỌNG)
56+
1. "GRAPH_LOOKUP" (Ưu tiên dùng Graph DB/Cypher):
57+
- Khi câu hỏi yêu cầu chính xác một đối tượng cụ thể: "Điều 60 quy định gì?", "Khoản 2 Điều 3 nói gì?".
58+
- Khi câu hỏi về cấu trúc văn bản: "Chương 5 có bao nhiêu điều?", "Luật này ban hành năm nào?".
59+
- Khi câu hỏi đếm số lượng hoặc liệt kê: "Có bao nhiêu trường hợp...", "Liệt kê các loại...".
60+
=> Dấu hiệu nhận biết: Có chứa số hiệu điều luật, tên chương, hoặc từ khóa "bao nhiêu", "liệt kê", "danh sách".
61+
62+
2. "CONSULTATION" (Dùng Vector Search + LLM):
63+
- Khi câu hỏi mô tả tình huống, cần tư vấn: "Tôi nghỉ việc thì được gì?", "Cách tính lương hưu?".
64+
- Câu hỏi "như thế nào", "ra sao", "điều kiện gì" mà KHÔNG chỉ đích danh điều luật cụ thể.
65+
- Các câu hỏi cần tổng hợp thông tin từ nhiều nơi.
66+
67+
3. "WEBSITE_SEARCH": Các luật ngoài phạm vi nội bộ (Giao thông, Đất đai...).
68+
4. "SOCIAL_CHAT": Chào hỏi xã giao.
69+
70+
CẤU TRÚC JSON TRẢ VỀ:
71+
{
72+
"is_relevant": true/false,
73+
"intent": "Mô tả ý định thực sự của user (VD: Hỏi về điều kiện hưởng thai sản)",
74+
"search_queries": ["query 1", "query 2"],
75+
"extracted_facts": {
76+
"key": "value",
77+
"legal_article": "Điều X (nếu có)",
78+
"law_name": "Tên luật (nếu có)"
79+
},
80+
"category": "GRAPH_LOOKUP" | "CONSULTATION" | "WEBSITE_SEARCH" | "SOCIAL_CHAT"
81+
}
82+
83+
--- VÍ DỤ MẪU (CHỈ THAM KHẢO CẤU TRÚC, KHÔNG COPY DỮ LIỆU) ---
84+
Ví dụ 1 (Graph Lookup - Hỏi cấu trúc):
85+
Input: "Luật BHXH có bao nhiêu chương?"
86+
Output:
87+
{
88+
"is_relevant": true,
89+
"intent": "Hỏi về số lượng chương trong Luật Bảo hiểm xã hội",
90+
"search_queries": ["cấu trúc luật bảo hiểm xã hội", "số lượng chương luật bảo hiểm xã hội"],
91+
"extracted_facts": {
92+
"law_name": "Luật Bảo hiểm xã hội"
93+
},
94+
"category": "GRAPH_LOOKUP"
95+
}
96+
97+
Ví dụ 2 (Câu hỏi có dữ liệu cụ thể, câu hỏi dạng tình huống):
98+
Input: "Chị A lương 10 triệu, đóng bảo hiểm 2 năm thì được nhận bao nhiêu?"
99+
Output:
100+
{
101+
"is_relevant": true,
102+
"intent": "Hỏi về mức hưởng bảo hiểm dựa trên mức lương và thời gian đóng cụ thể",
103+
"search_queries": ["cách tính mức hưởng bảo hiểm xã hội một lần", "công thức tính trợ cấp bảo hiểm xã hội", "mức hưởng bảo hiểm xã hội theo mức lương đóng"],
104+
"extracted_facts": {
105+
"name": "Chị A",
106+
"salary": "10 triệu",
107+
"insurance_duration": "2 năm"
108+
},
109+
"category": "CONSULTATION"
110+
}
111+
112+
Ví dụ 3 (Graph Lookup - Tra cứu đích danh):
113+
Input: "Điều 60 Bộ luật Lao động quy định gì?"
114+
Output:
115+
{
116+
"is_relevant": true,
117+
"intent": "Tra cứu nội dung quy định tại Điều 60 Bộ luật Lao động",
118+
"search_queries": ["nội dung Điều 60 Bộ luật Lao động", "quy định tại Điều 60 Bộ luật Lao động năm 2019"],
119+
"extracted_facts": {
120+
"legal_article": "Điều 60",
121+
"law_name": "Bộ luật Lao động"
122+
},
123+
"category": "GRAPH_LOOKUP"
124+
}
125+
126+
Ví dụ 4 (Web Search - Ngoài phạm vi):
127+
Input: "Vượt đèn đỏ phạt bao nhiêu?"
128+
Output:
129+
{
130+
"is_relevant": true,
131+
"intent": "Hỏi về mức phạt vi phạm giao thông (vượt đèn đỏ)",
132+
"search_queries": ["mức phạt lỗi vượt đèn đỏ xe máy 2024", "mức phạt vượt đèn đỏ ô tô"],
133+
"extracted_facts": {},
134+
"category": "WEBSITE_SEARCH"
135+
}
136+
"""
137+
138+
class QueryPreprocessor:
139+
def __init__(self):
140+
# Load API Key
141+
settings = get_settings()
142+
print(f'config: {settings.model_dump_json()}')
143+
genai.configure(api_key=settings.gemini_api_key)
144+
145+
# Cấu hình Model
146+
# Temperature = 0.0: Quan trọng để loại bỏ sự sáng tạo/ảo giác
147+
generation_config = {
148+
"response_mime_type": "application/json",
149+
"temperature": 0.0,
150+
}
151+
152+
# Khởi tạo model với System Instruction riêng biệt
153+
# Khuyên dùng gemini-1.5-flash (nhanh, rẻ, tuân thủ tốt) hoặc gemini-1.5-pro
154+
gemini_model = 'gemini-2.5-flash'
155+
self.model = genai.GenerativeModel(
156+
gemini_model,
157+
generation_config=generation_config,
158+
system_instruction=SYSTEM_INSTRUCTION
159+
)
160+
161+
log.info(f"QueryRewriter initialized successfully ({gemini_model})")
162+
163+
def _clean_json_string(self, text: str) -> str:
164+
"""Làm sạch chuỗi JSON nếu model trả về markdown"""
165+
text = text.strip()
166+
if text.startswith("```"):
167+
lines = text.split('\n')
168+
# Bỏ dòng đầu (```json) và dòng cuối (```)
169+
if lines[0].startswith("```"): lines = lines[1:]
170+
if lines and lines[-1].strip() == "```": lines = lines[:-1]
171+
text = '\n'.join(lines).strip()
172+
return text
173+
174+
def rewrite(self, question: str) -> Dict[str, Any]:
175+
"""
176+
Xử lý câu hỏi và trả về cấu trúc phân tích
177+
"""
178+
log.info(f"Processing: '{question}'")
179+
180+
try:
181+
# Prompt gửi đi bây giờ rất đơn giản, chỉ chứa câu hỏi
182+
# Điều này giúp model tập trung hoàn toàn vào input hiện tại
183+
user_prompt = f"Phân tích câu hỏi sau: \"{question}\""
184+
185+
response = self.model.generate_content(user_prompt)
186+
json_text = self._clean_json_string(response.text)
187+
188+
try:
189+
structured_data = json.loads(json_text)
190+
except json.JSONDecodeError:
191+
log.error(f"JSON Decode Error. Raw text: {json_text}")
192+
# Fallback cơ bản nếu lỗi JSON
193+
structured_data = {
194+
"is_relevant": True,
195+
"intent": "Lỗi phân tích cú pháp",
196+
"search_queries": [question],
197+
"extracted_facts": {},
198+
"category": "CONSULTATION"
199+
}
200+
201+
return {
202+
"original": question,
203+
"structured": structured_data,
204+
"status": "success"
205+
}
206+
207+
except Exception as e:
208+
log.error(f"Error in rewrite: {e}")
209+
return {
210+
"original": question,
211+
"structured": {
212+
"is_relevant": True,
213+
"intent": "System Error",
214+
"search_queries": [question],
215+
"extracted_facts": {},
216+
"category": "CONSULTATION"
217+
},
218+
"status": "error"
219+
}
220+
221+
# --- PHẦN CHẠY THỬ NGHIỆM ---
222+
if __name__ == "__main__":
223+
preprocessor = QueryPreprocessor()
224+
225+
queries = [
226+
"Thời tiết hôm nay như thế nào?",
227+
"tôi muốn hỏi về luật giao thông đường bộ?",
228+
"Điều 60 Bộ luật Lao động quy định gì?",
229+
"Có bao nhiêu trường hợp được hưởng trợ cấp thất nghiệp?"
230+
]
231+
232+
for query in queries:
233+
result = preprocessor.rewrite(query)
234+
print(f'result: {result["structured"]}')

0 commit comments

Comments
 (0)