Skip to content

Commit 26fdc3a

Browse files
authored
Merge pull request #35 from DUT-Team-21TCLC-DT3/feat/ket_hop_pipeline
feat: Implement API for AI Pipeline
2 parents 85df981 + de33e12 commit 26fdc3a

File tree

11 files changed

+1280
-50
lines changed

11 files changed

+1280
-50
lines changed

ai_service/Dockerfile.local

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Dockerfile.local - Dùng cho môi trường dev/local
2+
FROM python:3.11-slim
3+
4+
WORKDIR /srv/app
5+
6+
# Cài đặt các dependencies hệ thống nếu cần (ví dụ gcc để build một số thư viện python)
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
build-essential \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# Copy requirements.txt
12+
COPY requirements.txt ./
13+
14+
# Cài đặt thư viện
15+
RUN pip install --upgrade pip && \
16+
pip install --no-cache-dir -r requirements.txt
17+
18+
# Copy mã nguồn
19+
COPY . .
20+
21+
# --- KHỞI CHẠY ---
22+
ENV PYTHONUNBUFFERED=1
23+
24+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

ai_service/PIPELINE_MIGRATION.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Tài liệu Chuyển đổi: Từ Notebook sang Production Pipeline
2+
3+
Tài liệu này mô tả quá trình và cấu trúc mã nguồn được chuyển đổi từ file thực nghiệm `05_ai_agent_pipeline.ipynb` sang hệ thống backend `ai_service` hoàn chỉnh.
4+
5+
## 1. Tổng quan
6+
7+
Mục tiêu là chuyển đổi logic của AI Agent từ môi trường notebook (chạy tuần tự, khó tái sử dụng) sang kiến trúc **Microservices-ready** bên trong FastAPI, đảm bảo các yếu tố:
8+
9+
- **Modularity**: Tách nhỏ các chức năng thành từng service riêng biệt.
10+
- **Performance**: Sử dụng Singleton Pattern cho các model nặng (Embedding, Neo4j Driver).
11+
- **Scalability**: Hỗ trợ Asynchronous (Async/Await) và Streaming Response.
12+
- **Maintainability**: Dễ dàng debug và mở rộng từng module.
13+
14+
## 2. Ánh xạ Cấu trúc (Mapping)
15+
16+
Dưới đây là bảng ánh xạ từ các cell trong Notebook sang các file source code:
17+
18+
| Chức năng trong Notebook | File Source Code tương ứng (`app/services/`) | Nhiệm vụ chính |
19+
| ------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------- |
20+
| **Query Rewriting** | `query_processor.py` | Phân tích câu hỏi, kiểm tra hợp lệ, tách từ khóa search. |
21+
| **Vector Search** | `vector_search.py` | Tạo embedding cho query và tìm kiếm vector trong Neo4j. Tự động tạo index nếu chưa có. |
22+
| **Reranking** | `reranker.py` | Dùng Gemini để chấm điểm và chọn lọc lại các node kết quả từ Vector Search. |
23+
| **Graph Traversal** | `graph_search.py` | Sinh câu lệnh Cypher động, duyệt đồ thị để lấy ngữ cảnh (Điều, Khoản, Điểm) liên quan. |
24+
| **Web Search** | `web_search.py` | Fallback tìm kiếm Google/Tavily khi dữ liệu nội bộ không đủ. |
25+
| **Answer Generation** | `answer_composer.py` | Tổng hợp tất cả thông tin và sinh câu trả lời cuối cùng (có hỗ trợ Streaming). |
26+
| **Orchestration** | `streaming_service.py` | "Nhạc trưởng" điều phối luồng chạy tuần tự qua các bước trên. |
27+
28+
## 3. Luồng xử lý dữ liệu (Data Flow)
29+
30+
Khi người dùng gọi API `POST /api/v1/ask`:
31+
32+
1. **Request** đi vào `app/main.py`.
33+
2. **StreamingService** (`generate_streaming_response`) được kích hoạt.
34+
3. **Bước 1 - Query Processor**:
35+
- Input: "Người lao động được hưởng BHYT thế nào?"
36+
- Output: Intent "CONSULTATION", Keywords ["chế độ BHYT", "mức hưởng"].
37+
4. **Bước 2 - Vector Search**:
38+
- Input: Keywords.
39+
- Action: Embed keywords -> Query Neo4j Vector Index.
40+
- Output: Top 5-10 Nodes có nội dung tương đồng.
41+
5. **Bước 3 - Reranker**:
42+
- Input: Top Nodes + Câu hỏi gốc.
43+
- Action: Dùng Gemini chấm điểm sự liên quan.
44+
- Output: Top 3 Nodes tốt nhất (Focus Nodes).
45+
6. **Bước 4 - Graph Search**:
46+
- Input: Focus Nodes.
47+
- Action: Từ mỗi Node, duyệt đồ thị (Parent/Children/Relationships) để lấy ngữ cảnh luật đầy đủ.
48+
- Output: Đoạn văn bản luật chính xác nhất.
49+
7. **Bước 5 - Web Search (Optional)**:
50+
- Nếu Graph không tìm thấy thông tin -> Gọi Tavily API tìm kiếm online.
51+
8. **Bước 6 - Answer Composer**:
52+
- Input: Context từ Graph + Context từ Web + Câu hỏi.
53+
- Action: Gemini sinh câu trả lời streaming.
54+
- Output: Từng token text được gửi về client.
55+
56+
## 4. Các cải tiến kỹ thuật
57+
58+
- **Singleton Pattern**: `VectorSearchService``GraphSearchService` chỉ khởi tạo kết nối Database và load Model **một lần duy nhất** khi ứng dụng khởi chạy. Không load lại mỗi request như notebook.
59+
- **Error Handling**: Mỗi service đều có try/catch riêng biệt, đảm bảo một bước lỗi (ví dụ Web Search) không làm sập cả luồng.
60+
- **Environment Variables**: Tất cả cấu hình (API Key, URI) được quản lý qua file `.env``config.py` thay vì hardcode.
61+
- **Streaming**: API trả về dữ liệu dạng `Server-Sent Events (SSE)` hoặc `NDJSON` chunk, giúp UX mượt mà hơn (thấy chữ chạy ra ngay lập tức).
62+
63+
## 5. Hướng dẫn chạy
64+
65+
Để chạy hệ thống với code mới nhất:
66+
67+
```bash
68+
# 1. Đảm bảo file .env đã có đủ key (GEMINI_API_KEY, NEO4J_URI,...)
69+
70+
# 2. Chạy Docker (Mount volume để hot reload khi sửa code)
71+
docker run -p 3000:8000 --env-file .env -v "%cd%:/srv/app" ai_service uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
72+
```
73+
74+
_(Lưu ý: Trên PowerShell thay `%cd%` bằng `${PWD}`)_

ai_service/app/new_pipelines/query_preprocessor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def __init__(self):
151151

152152
# Khởi tạo model với System Instruction riêng biệt
153153
# 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'
154+
gemini_model = 'gemini-2.5-pro'
155155
self.model = genai.GenerativeModel(
156156
gemini_model,
157157
generation_config=generation_config,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import logging
2+
import google.generativeai as genai
3+
from typing import Dict, Any, List
4+
from ..dependencies import get_settings
5+
6+
log = logging.getLogger(__name__)
7+
8+
class AnswerComposerService:
9+
def __init__(self):
10+
settings = get_settings()
11+
if not settings.gemini_api_key:
12+
raise ValueError("GEMINI_API_KEY not found")
13+
genai.configure(api_key=settings.gemini_api_key)
14+
self.model_name = 'gemini-2.5-flash' # Or settings.GEMINI_COMPOSER_MODEL
15+
16+
def compose(
17+
self,
18+
question: str,
19+
graph_result: Dict[str, Any],
20+
web_context: str = ""
21+
) -> str:
22+
"""
23+
Tổng hợp câu trả lời cuối cùng.
24+
"""
25+
final_choice = graph_result.get("final_choice")
26+
27+
# Xây dựng context từ Graph
28+
graph_context_str = ""
29+
if final_choice:
30+
graph_context_str = (
31+
f"THÔNG TIN TỪ CƠ SỞ DỮ LIỆU LUẬT (Tin cậy cao):\n"
32+
f"- Nguồn: {final_choice['source_type']} (Node ID: {final_choice['source_node_id']})\n"
33+
f"- Nội dung:\n{final_choice['chosen_snippet']}\n"
34+
)
35+
else:
36+
graph_context_str = "Không tìm thấy thông tin trong cơ sở dữ liệu luật nội bộ."
37+
38+
# Xây dựng context từ Web (nếu có)
39+
web_context_str = ""
40+
if web_context:
41+
web_context_str = f"\nTHÔNG TIN BỔ SUNG TỪ WEB (Tham khảo):\n{web_context}\n"
42+
43+
prompt = f"""
44+
Bạn là Trợ lý Luật sư AI chuyên nghiệp (Legal Assistant).
45+
46+
NHIỆM VỤ:
47+
Trả lời câu hỏi của người dùng dựa trên các nguồn thông tin được cung cấp dưới đây.
48+
49+
CÂU HỎI: "{question}"
50+
51+
NGUỒN THÔNG TIN:
52+
{graph_context_str}
53+
{web_context_str}
54+
55+
YÊU CẦU TRẢ LỜI:
56+
1. **Chính xác & Cẩn trọng**: Chỉ trả lời dựa trên thông tin được cung cấp. Nếu thông tin mâu thuẫn, ưu tiên "THÔNG TIN TỪ CƠ SỞ DỮ LIỆU LUẬT".
57+
2. **Trích dẫn rõ ràng**:
58+
- Luôn trích dẫn Điều/Khoản/Luật nếu có trong nội dung (ví dụ: "Theo Khoản 1 Điều 60 Luật BHXH...").
59+
- Nếu dùng thông tin từ Web, hãy nói rõ "Theo thông tin tham khảo từ nguồn web...".
60+
3. **Cấu trúc mạch lạc**:
61+
- Mở đầu: Trả lời trực tiếp vào vấn đề.
62+
- Thân bài: Giải thích chi tiết, nêu căn cứ pháp lý.
63+
- Kết luận: Tóm tắt lại hoặc đưa ra lời khuyên (nếu phù hợp).
64+
4. **Không bịa đặt**: Nếu không có đủ thông tin để trả lời, hãy thành thật xin lỗi và đề xuất người dùng cung cấp thêm chi tiết hoặc tra cứu nguồn khác.
65+
66+
HÃY VIẾT CÂU TRẢ LỜI HOÀN CHỈNH:
67+
"""
68+
model = genai.GenerativeModel(
69+
self.model_name,
70+
generation_config={"temperature": 0.2} # Hơi sáng tạo một chút để viết văn mượt mà
71+
)
72+
73+
try:
74+
response = model.generate_content(prompt)
75+
return response.text.strip()
76+
except Exception as e:
77+
log.error(f"Lỗi compose answer: {e}")
78+
return "Xin lỗi, hệ thống gặp sự cố khi tổng hợp câu trả lời."
79+
80+
def compose_stream(
81+
self,
82+
question: str,
83+
graph_result: Dict[str, Any],
84+
web_context: str = ""
85+
):
86+
"""
87+
Tổng hợp câu trả lời cuối cùng (Streaming).
88+
"""
89+
final_choice = graph_result.get("final_choice")
90+
91+
# Xây dựng context từ Graph
92+
graph_context_str = ""
93+
if final_choice:
94+
graph_context_str = (
95+
f"THÔNG TIN TỪ CƠ SỞ DỮ LIỆU LUẬT (Tin cậy cao):\n"
96+
f"- Nguồn: {final_choice['source_type']} (Node ID: {final_choice['source_node_id']})\n"
97+
f"- Nội dung:\n{final_choice['chosen_snippet']}\n"
98+
)
99+
else:
100+
graph_context_str = "Không tìm thấy thông tin trong cơ sở dữ liệu luật nội bộ."
101+
102+
# Xây dựng context từ Web (nếu có)
103+
web_context_str = ""
104+
if web_context:
105+
web_context_str = f"\nTHÔNG TIN TỪ WEB (Tham khảo):\n{web_context}\n"
106+
107+
prompt = f"""
108+
Bạn là Trợ lý Luật sư AI chuyên nghiệp (Legal Assistant).
109+
110+
NHIỆM VỤ:
111+
Trả lời câu hỏi của người dùng dựa trên các nguồn thông tin được cung cấp dưới đây.
112+
113+
CÂU HỎI: "{question}"
114+
115+
NGUỒN THÔNG TIN:
116+
{graph_context_str}
117+
{web_context_str}
118+
119+
YÊU CẦU TRẢ LỜI:
120+
1. **Chính xác & Cẩn trọng**: Chỉ trả lời dựa trên thông tin được cung cấp. Nếu thông tin mâu thuẫn, ưu tiên "THÔNG TIN TỪ CƠ SỞ DỮ LIỆU LUẬT".
121+
2. **Trích dẫn rõ ràng**:
122+
- Luôn trích dẫn Điều/Khoản/Luật nếu có trong nội dung (ví dụ: "Theo Khoản 1 Điều 60 Luật BHXH...").
123+
- Nếu dùng thông tin từ Web, hãy nói rõ "Theo thông tin tham khảo từ nguồn web...".
124+
3. **Cấu trúc mạch lạc**:
125+
- Mở đầu: Trả lời trực tiếp vào vấn đề.
126+
- Thân bài: Giải thích chi tiết, nêu căn cứ pháp lý.
127+
- Kết luận: Tóm tắt lại hoặc đưa ra lời khuyên (nếu phù hợp).
128+
4. **Không bịa đặt**: Nếu không có đủ thông tin để trả lời, hãy thành thật xin lỗi và đề xuất người dùng cung cấp thêm chi tiết hoặc tra cứu nguồn khác.
129+
130+
HÃY VIẾT CÂU TRẢ LỜI HOÀN CHỈNH:
131+
"""
132+
model = genai.GenerativeModel(
133+
self.model_name,
134+
generation_config={"temperature": 0.2}
135+
)
136+
137+
try:
138+
response = model.generate_content(prompt, stream=True)
139+
for chunk in response:
140+
if chunk.text:
141+
yield chunk.text
142+
except Exception as e:
143+
log.error(f"Lỗi compose answer stream: {e}")
144+
yield "Xin lỗi, hệ thống gặp sự cố khi tổng hợp câu trả lời."

0 commit comments

Comments
 (0)