Phase 7: Hoàn thiện Modular RAG Backend với FastAPI và Đa LLM Provider

This commit is contained in:
2026-05-08 07:30:30 +00:00
commit 26d1298cf6
51 changed files with 5360 additions and 0 deletions

26
chat/llm_factory.py Normal file
View File

@@ -0,0 +1,26 @@
import logging
from core.config import settings
from .llm_providers.base_llm import BaseLLMProvider
from .llm_providers.gemini_llm import GeminiLLM
from .llm_providers.groq_llm import GroqLLM
from .llm_providers.local_llm import LocalLLM
logger = logging.getLogger("LLMFactory")
class LLMFactory:
"""
Mẫu thiết kế Factory (Nhà máy) để tự động nạp Provider tuỳ theo cấu hình trong .env
"""
@staticmethod
def get_provider() -> BaseLLMProvider:
provider_name = settings.llm_provider.lower().strip()
if provider_name == "gemini":
return GeminiLLM()
elif provider_name == "groq":
return GroqLLM()
elif provider_name == "local":
return LocalLLM()
else:
logger.warning(f"Provider '{provider_name}' không được hỗ trợ. Khởi động mặc định: GeminiLLM")
return GeminiLLM()

View File

@@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
from typing import List, Dict
class BaseLLMProvider(ABC):
"""
Interface gốc cho tất cả các mô hình Ngôn ngữ (Chat LLMs).
Đảm bảo tính dễ mở rộng (Cắm rút API khác nhau) mà không làm vỡ kiến trúc RAG.
"""
@abstractmethod
def generate_response(self, prompt: str, context: str, history: List[Dict[str, str]] = None) -> str:
"""
Nhận vào ngữ cảnh (từ VectorDB) và câu hỏi của user (prompt),
Trả về câu trả lời cuối cùng dưới dạng Text.
"""
pass

View File

@@ -0,0 +1,44 @@
import logging
from typing import List, Dict
from core.config import settings
from .base_llm import BaseLLMProvider
logger = logging.getLogger("GeminiLLM")
class GeminiLLM(BaseLLMProvider):
def __init__(self):
try:
import google.generativeai as genai
except ImportError:
logger.error("LỖI: Chưa cài đặt thư viện Gemini. Chạy lệnh: pip install google-generativeai")
raise
if not settings.gemini_api_key:
raise ValueError("GEMINI_API_KEY chưa được cấu hình trong .env!")
genai.configure(api_key=settings.gemini_api_key)
# Sử dụng model flash mới nhất, cực nhanh và có ngữ cảnh lớn
self.model = genai.GenerativeModel('gemini-1.5-flash')
logger.info("Đã khởi tạo Gemini 1.5 Flash Provider.")
def generate_response(self, prompt: str, context: str, history: List[Dict[str, str]] = None) -> str:
full_prompt = f"""Bạn là một trợ lý ảo thông minh chuyên giải đáp thông tin dựa trên cơ sở dữ liệu nội bộ.
Hãy trả lời câu hỏi của người dùng một cách chính xác, lịch sự và DỰA HOÀN TOÀN vào ngữ cảnh được cung cấp.
=== NGỮ CẢNH (CONTEXT) ===
{context}
=== LỊCH SỬ CHAT ===
{history if history else 'Không có'}
=== CÂU HỎI CỦA NGƯỜI DÙNG ===
{prompt}
=== TRẢ LỜI ===
"""
try:
response = self.model.generate_content(full_prompt)
return response.text
except Exception as e:
logger.error(f"Lỗi khi gọi Gemini API: {e}")
return f"Xin lỗi, tôi đang gặp sự cố khi xử lý câu trả lời: {e}"

View File

@@ -0,0 +1,54 @@
import logging
from typing import List, Dict
from core.config import settings
from .base_llm import BaseLLMProvider
logger = logging.getLogger("GroqLLM")
class GroqLLM(BaseLLMProvider):
def __init__(self):
try:
from openai import OpenAI
except ImportError:
logger.error("LỖI: Thư viện openai chưa được cài đặt.")
raise
if not settings.groq_api_key:
raise ValueError("GROQ_API_KEY chưa được cấu hình trong .env!")
# Groq hỗ trợ thư viện OpenAI SDK, chỉ cần đổi Base URL
self.client = OpenAI(
api_key=settings.groq_api_key,
base_url="https://api.groq.com/openai/v1"
)
self.model_name = settings.groq_model
logger.info(f"Đã khởi tạo Groq Provider với model: {self.model_name}")
def generate_response(self, prompt: str, context: str, history: List[Dict[str, str]] = None) -> str:
system_prompt = f"""Bạn là một trợ lý ảo thông minh chuyên giải đáp thông tin dựa trên cơ sở dữ liệu nội bộ.
Hãy trả lời câu hỏi của người dùng một cách chính xác, lịch sự và DỰA HOÀN TOÀN vào ngữ cảnh được cung cấp.
=== NGỮ CẢNH (CONTEXT) ===
{context}
"""
messages = [{"role": "system", "content": system_prompt}]
# Thêm lịch sử chat (nếu có)
if history:
for msg in history:
messages.append({"role": msg.get("role", "user"), "content": msg.get("content", "")})
# Thêm câu hỏi hiện tại
messages.append({"role": "user", "content": prompt})
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=messages,
temperature=0.2, # Giữ temperature thấp để AI không bịa chuyện
max_tokens=1024
)
return response.choices[0].message.content
except Exception as e:
logger.error(f"Lỗi khi gọi Groq API: {e}")
return f"Xin lỗi, tôi đang gặp sự cố khi xử lý câu trả lời qua Groq: {e}"

View File

@@ -0,0 +1,54 @@
import logging
from typing import List, Dict
from core.config import settings
from .base_llm import BaseLLMProvider
logger = logging.getLogger("LocalLLM")
class LocalLLM(BaseLLMProvider):
def __init__(self):
try:
from openai import OpenAI
except ImportError:
logger.error("LỖI: Thư viện openai chưa được cài đặt.")
raise
if not settings.local_llm_endpoint:
raise ValueError("LOCAL_LLM_ENDPOINT chưa được cấu hình trong .env!")
# llama.cpp server hỗ trợ API y hệt OpenAI
# Ta cần cắt đuôi /chat/completions để ra base_url
base_url = settings.local_llm_endpoint.replace("/chat/completions", "")
self.client = OpenAI(
api_key="sk-no-key-required", # Llama.cpp local không cần key
base_url=base_url
)
logger.info(f"Đã khởi tạo Local Llama.cpp Provider kết nối tới: {base_url}")
def generate_response(self, prompt: str, context: str, history: List[Dict[str, str]] = None) -> str:
system_prompt = f"""Bạn là trợ lý RAG nội bộ.
Chỉ trả lời dựa trên NGỮ CẢNH dưới đây. Nếu không biết thì nói không biết.
=== NGỮ CẢNH ===
{context}
"""
messages = [{"role": "system", "content": system_prompt}]
if history:
for msg in history:
messages.append({"role": msg.get("role", "user"), "content": msg.get("content", "")})
messages.append({"role": "user", "content": prompt})
try:
response = self.client.chat.completions.create(
model="local-model", # llama.cpp thường phớt lờ tên model
messages=messages,
temperature=0.1,
max_tokens=1024
)
return response.choices[0].message.content
except Exception as e:
logger.error(f"Lỗi khi gọi Local LLM: {e}")
return f"Xin lỗi, máy chủ AI nội bộ đang bận hoặc mất kết nối: {e}"

49
chat/rag_engine.py Normal file
View File

@@ -0,0 +1,49 @@
import logging
from typing import List, Dict
from search.retriever import SearchRetriever
from .llm_factory import LLMFactory
logger = logging.getLogger("RAGEngine")
class RAGEngine:
def __init__(self):
self.retriever = SearchRetriever()
self.llm = LLMFactory.get_provider()
logger.info(f"RAG Engine đã sẵn sàng với LLM Provider: {type(self.llm).__name__}")
def chat(self, user_query: str, history: List[Dict[str, str]] = None) -> Dict:
"""
Quy trình RAG hoàn chỉnh: Search -> Augment -> Generate
"""
# 1. RETRIEVAL: Tìm kiếm ngữ cảnh liên quan
relevant_chunks = self.retriever.retrieve(user_query, top_k=5)
if not relevant_chunks:
context_text = "Không tìm thấy thông tin liên quan trong cơ sở dữ liệu nội bộ."
else:
# Gộp text từ các chunks lại thành 1 khối context
context_text = "\n---\n".join([
f"[Nguồn: {c.file_name}, Trang: {c.page_from}]\nNội dung: {c.text}"
for c in relevant_chunks
])
# 2. GENERATION: Gửi sang LLM để trả lời
logger.info("Đang yêu cầu LLM tổng hợp câu trả lời...")
answer = self.llm.generate_response(
prompt=user_query,
context=context_text,
history=history
)
# 3. Trả về kết quả kèm theo nguồn trích dẫn (Citations)
return {
"answer": answer,
"context_used": context_text,
"sources": [
{
"file_name": c.file_name,
"page": c.page_from,
"url": c.source_url
} for c in relevant_chunks
]
}