diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..d62b104e2f77692a87914f51a9b4f3e2a200548d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +data/raw/*.xlsx filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..536c8c32b09f5daf60e9e8e96585a34fec3b37f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,209 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +data/processed/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..28259fb7aba54f79c1c4a5a53ad451abfaef8fd1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Sử dụng một ảnh base Python 3.13 chính thức, phiên bản slim +FROM python:3.13-slim + +# Thiết lập các biến môi trường để tối ưu hóa Python +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Thiết lập thư mục làm việc bên trong container +WORKDIR /app + +# Sao chép file requirements.txt vào trước +COPY ./requirements.txt /app/requirements.txt + +# Cài đặt các thư viện +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +RUN pip install --no-cache-dir -r /app/requirements.txt + +# Sao chép toàn bộ code của ứng dụng vào container +COPY . /app/ + +# --- THÊM BƯỚC NÀY --- +# Chạy script để chuyển đổi file Excel thành CSDL SQLite +# Bước này sẽ tạo ra thư mục data/processed/phongthuy.sqlite bên trong container +RUN python scripts/preprocess_data.py + +# Expose cổng mà uvicorn sẽ chạy +EXPOSE 7860 + +# Lệnh để khởi động ứng dụng FastAPI +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2167a58e48256d205429b490533862b44bdb5fc2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 anh-khoa-nguyen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 07843ce69820472bc917869dfc09236940af04e6..d7465589905ecf0641a2f248cdc680ec5d1b3334 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ --- -title: Landify Chatbot V2 -emoji: 👁 -colorFrom: pink -colorTo: green +title: Landify Fengshui Chatbot V2 +emoji: 🌖 +colorFrom: red +colorTo: indigo sdk: docker pinned: false license: mit diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..a633bd2e244b3049f85f63607f5c0ab883143799 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,62 @@ +# app/core/config.py + +import os +from pydantic_settings import BaseSettings +from pathlib import Path +from typing import Optional + +# --- Xác định đường dẫn gốc của project --- +PROJECT_ROOT = Path(__file__).parent.parent.parent + +class Settings(BaseSettings): + """ + Lớp quản lý cấu hình cho toàn bộ ứng dụng. + Sử dụng Pydantic để validate và đọc các biến môi trường từ file .env. + """ + # --- Cấu hình chung cho project --- + PROJECT_NAME: str = "Phong Thuy Chatbot v2" + DEBUG: bool = True + + # --- Cấu hình đường dẫn CSDL --- + # Ưu tiên đọc DATABASE_URL từ file .env trước. + # Nếu không có, mới tự động tạo đường dẫn tới file SQLite. + # Điều này giúp code linh hoạt hơn. + DATABASE_URL: Optional[str] = None + + # --- Cấu hình API Keys --- + # Khai báo tất cả các API key có trong file .env của bạn + # Pydantic sẽ tự động tìm và nạp các biến này. + GROQ_API_KEY: str + OPENAI_API_KEY: str + HUGGINGFACEHUB_API_TOKEN: str + + # Cấu hình để Pydantic biết đọc từ file .env + class Config: + env_file = os.path.join(PROJECT_ROOT, ".env") + env_file_encoding = 'utf-8' + +# Tạo một instance của Settings +settings = Settings() + +# --- Xử lý logic cho DATABASE_URL --- +# Nếu người dùng không cung cấp DATABASE_URL trong .env, +# chúng ta sẽ tự động gán đường dẫn mặc định tới file SQLite. +if settings.DATABASE_URL is None: + default_db_path = os.path.join(PROJECT_ROOT, 'data', 'processed', 'phongthuy.sqlite') + # Kiểm tra xem file SQLite có thực sự tồn tại không + if not os.path.exists(default_db_path): + raise FileNotFoundError( + f"DATABASE_URL không được cung cấp trong .env và file SQLite mặc định không tồn tại tại: {default_db_path}. " + "Vui lòng chạy script 'scripts/preprocess_data.py' trước." + ) + settings.DATABASE_URL = f"sqlite:///{default_db_path}" + + +# In ra để kiểm tra khi khởi chạy (chỉ cho mục đích debug) +if __name__ == "__main__": + print("--- Cấu hình ứng dụng ---") + print(f"Tên Project: {settings.PROJECT_NAME}") + print(f"Đường dẫn CSDL đang sử dụng: {settings.DATABASE_URL}") + print(f"GROQ API Key đã được load: {'Có' if settings.GROQ_API_KEY else 'Không'}") + print(f"OpenAI API Key đã được load: {'Có' if settings.OPENAI_API_KEY else 'Không'}") + print(f"HuggingFace API Token đã được load: {'Có' if settings.HUGGINGFACEHUB_API_TOKEN else 'Không'}") \ No newline at end of file diff --git a/app/core/logging_config.py b/app/core/logging_config.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/database/connection.py b/app/database/connection.py new file mode 100644 index 0000000000000000000000000000000000000000..0c7083813928db68ccd391fecf4b0af8a1f8b316 --- /dev/null +++ b/app/database/connection.py @@ -0,0 +1,92 @@ +# app/database/connection.py + +import sqlite3 +import pandas as pd +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from contextlib import contextmanager +import logging + +# Import đối tượng settings từ module config +from app.core.config import settings + +# --- Thiết lập SQLAlchemy --- +# create_engine là điểm khởi đầu cho bất kỳ ứng dụng SQLAlchemy nào. +# Nó thiết lập một "nhà máy" kết nối đến CSDL của chúng ta. +# connect_args={"check_same_thread": False} là một yêu cầu đặc biệt cho SQLite +# khi sử dụng trong các ứng dụng đa luồng như FastAPI. +try: + engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False} + ) + # SessionLocal là một "nhà máy" tạo ra các phiên làm việc (session) với CSDL. + # Mỗi instance của SessionLocal sẽ là một session riêng biệt. + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + logging.info("Kết nối SQLAlchemy đến CSDL đã được thiết lập thành công.") +except Exception as e: + logging.error(f"Lỗi khi thiết lập kết nối SQLAlchemy: {e}") + engine = None + SessionLocal = None + + +@contextmanager +def get_db(): + """ + Cung cấp một session CSDL và đảm bảo nó được đóng đúng cách. + Đây là một Dependency Injection pattern thường dùng trong FastAPI. + """ + if SessionLocal is None: + raise ConnectionError("Không thể tạo session do lỗi kết nối CSDL ban đầu.") + + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# --- Hàm tiện ích để truy vấn trực tiếp bằng Pandas (rất hữu ích cho tools) --- +def query_to_dataframe(query: str, params: dict = None) -> pd.DataFrame: + """ + Thực thi một câu lệnh SQL và trả về kết quả dưới dạng Pandas DataFrame. + An toàn hơn khi sử dụng params để tránh SQL Injection. + """ + if engine is None: + raise ConnectionError("Không thể thực thi query do lỗi kết nối CSDL ban đầu.") + + try: + with engine.connect() as connection: + df = pd.read_sql(query, connection, params=params) + return df + except Exception as e: + logging.error(f"Lỗi khi thực thi query: {query} với params: {params}. Lỗi: {e}") + # Trả về DataFrame rỗng nếu có lỗi + return pd.DataFrame() + + +# --- Hàm kiểm tra kết nối --- +def test_connection(): + """Kiểm tra xem có thể kết nối và đọc dữ liệu từ một bảng không.""" + try: + logging.info("Đang kiểm tra kết nối CSDL...") + # Thử đọc 1 dòng từ bảng 'menh' (giả sử bảng này tồn tại) + df = query_to_dataframe("SELECT * FROM menh LIMIT 1") + if not df.empty: + logging.info("Kiểm tra kết nối CSDL thành công! Có thể đọc dữ liệu.") + return True + else: + logging.warning("Kết nối CSDL thành công nhưng bảng 'menh' có vẻ rỗng hoặc không tồn tại.") + return False + except Exception as e: + logging.error(f"Kiểm tra kết nối CSDL thất bại: {e}") + return False + + +if __name__ == "__main__": + test_connection() + # Ví dụ cách sử dụng + # sample_query = "SELECT * FROM menh WHERE hanh_ngu_hanh = :menh" + # result_df = query_to_dataframe(sample_query, params={"menh": "Kim"}) + # print("\nKết quả truy vấn mẫu:") + # print(result_df) \ No newline at end of file diff --git a/app/database/models.py b/app/database/models.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..db771504cac463afbe970d009d8b6e9dbfb4fee3 --- /dev/null +++ b/app/main.py @@ -0,0 +1,167 @@ +# app/main.py + +import logging +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Any +import uuid + +# Import các module đã tạo +from app.core.config import settings +from app.database.connection import test_connection +from app.services.intent_analyzer import analyze_intent, IntentResult, ExtractedEntities +from app.orchestrator.workflow_manager import run_workflow, preprocess_entities +from app.services.response_synthesizer import synthesize_response +from app.services.context_manager import ToolCallRecord, ChatContext +from fastapi.responses import RedirectResponse + +# --- Cấu hình Logging --- +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +description_md = """ +### Microservice Chatbot Tư vấn Phong Thủy 🔮 + +API này ứng dụng các Mô hình Ngôn ngữ Lớn (LLM) và Cơ sở Tri thức có cấu trúc để thực hiện các chức năng sau: + +1. **Phân tích ý định** người dùng và **trích xuất thực thể** từ câu hỏi tiếng Việt. +2. **Quản lý ngữ cảnh** hội thoại, hỏi lại thông tin còn thiếu. +3. **Truy vấn cơ sở dữ liệu phong thủy** (Bát Trạch, Ngũ Hành, Loan Đầu, Phi Tinh) bằng các "công cụ" chuyên biệt. +4. **Tổng hợp câu trả lời** tự nhiên, thân thiện và cá nhân hóa dựa trên dữ liệu đã tra cứu. + +_API được xây dựng với FastAPI._ +""" + +# --- Khởi tạo ứng dụng FastAPI --- +app = FastAPI( + title=settings.PROJECT_NAME, + debug=settings.DEBUG, + description=description_md, +) + +# Key: session_id (str), Value: ChatContext object +CONTEXT_STORE: Dict[str, ChatContext] = {} + +# --- Định nghĩa model cho request body --- +class ChatRequest(BaseModel): + query: str + session_id: str + +class DebugInfo(BaseModel): + intent: str + entities: Dict[str, Any] + tool_calls: List[ToolCallRecord] + +class ChatResponse(BaseModel): + answer: str + debug_info: DebugInfo + +class SessionResponse(BaseModel): + session_id: str + +@app.on_event("startup") +async def startup_event(): + logger.info("--- Ứng dụng Chatbot Phong Thủy đang khởi động ---") + if not test_connection(): + logger.error("!!! CẢNH BÁO: Không thể kết nối đến CSDL. Các chức năng sẽ không hoạt động.") + else: + logger.info(">>> Kết nối CSDL đã sẵn sàng.") + +@app.get("/", include_in_schema=False) +async def root(): + """ + Khi người dùng truy cập trang gốc, tự động chuyển hướng đến trang tài liệu API. + """ + return RedirectResponse(url="/docs") + +@app.post("/session", tags=["General"]) +async def create_session(): + """Tạo một session_id duy nhất cho một cuộc trò chuyện mới.""" + session_id = str(uuid.uuid4()) + # Khởi tạo một context rỗng cho session mới + CONTEXT_STORE[session_id] = ChatContext() + logger.info(f"Đã tạo session mới: {session_id}") + return {"session_id": session_id} + +@app.post("/chat", response_model=ChatResponse, tags=["Chatbot"]) +@app.post("/chat", tags=["Chatbot"]) +async def handle_chat(request: ChatRequest): + """ + Endpoint chính để xử lý yêu cầu chat, với logic quản lý ngữ cảnh được cải tiến. + """ + try: + session_id = request.session_id + logger.info(f"Nhận được query: '{request.query}' cho session_id: {session_id}") + + # --- Giai đoạn 0: Lấy và Hợp nhất Ngữ cảnh (LOGIC MỚI) --- + previous_context = CONTEXT_STORE.get(session_id, ChatContext()) + current_intent_result = await analyze_intent(request.query) + + final_intent_name = current_intent_result.intent + base_entities = ExtractedEntities() # Tạo một entities rỗng + + # Quyết định xem nên giữ lại ngữ cảnh cũ hay bắt đầu mới + is_continuing_conversation = ( + previous_context.missing_info and + previous_context.intent_name not in ["UNKNOWN", "GREETING", None] + ) + + if is_continuing_conversation: + # --- TRƯỜNG HỢP 1: Đang trả lời câu hỏi của chatbot --- + logger.info("Phát hiện đang tiếp tục cuộc trò chuyện.") + # Giữ lại intent của luồng cũ + final_intent_name = previous_context.intent_name + # Lấy entities từ luồng cũ làm nền + base_entities = previous_context.initial_entities + else: + # --- TRƯỜNG HỢP 2: Bắt đầu một chủ đề mới --- + logger.info("Bắt đầu một chủ đề trò chuyện mới.") + # Intent sẽ là intent của câu nói hiện tại + # base_entities là rỗng, bắt đầu lại từ đầu + pass + + # Hợp nhất: Lấy base_entities và cập nhật bằng thông tin mới + merged_entities = base_entities.model_copy( + update=current_intent_result.entities.model_dump(exclude_unset=True, exclude_none=True) + ) + + final_intent_result = IntentResult(intent=final_intent_name, entities=merged_entities) + + logger.info(f"Intent cuối cùng được chọn: '{final_intent_result.intent}'") + logger.info(f"Entities sau khi hợp nhất: {merged_entities.model_dump_json(indent=2)}") + + # --- Giai đoạn 1: Tiền xử lý entities --- + from app.orchestrator.workflow_manager import preprocess_entities + final_intent_result.entities = await preprocess_entities(final_intent_result.entities) + + # --- Giai đoạn 2: Chạy workflow --- + final_context = await run_workflow(final_intent_result) + + # --- Giai đoạn 3: Tổng hợp câu trả lời --- + final_answer = await synthesize_response(final_context) + + # --- Giai đoạn 4: Lưu ngữ cảnh --- + # Nếu workflow đã hoàn thành (không còn missing_info), + # chúng ta có thể cân nhắc xóa bớt entities để chuẩn bị cho lượt sau. + # Tuy nhiên, để đơn giản, cứ lưu lại toàn bộ. + final_context.initial_entities = final_intent_result.entities + CONTEXT_STORE[session_id] = final_context + logger.info(f"Đã cập nhật context cho session_id: {session_id}") + + debug_info = DebugInfo( + intent=final_context.intent_name, + entities=final_context.initial_entities.model_dump(exclude_unset=True, exclude_none=True), + tool_calls=final_context.tool_calls + ) + + return ChatResponse(answer=final_answer, debug_info=debug_info) + + except Exception as e: + logger.exception(f"Lỗi nghiêm trọng trong quá trình xử lý chat cho session {session_id}: {e}") + # Xóa context bị lỗi để tránh ảnh hưởng đến các lần sau + if session_id in CONTEXT_STORE: + del CONTEXT_STORE[session_id] + raise HTTPException(status_code=500, detail="Đã có lỗi xảy ra ở máy chủ. Vui lòng tạo một session mới.") + +# Để chạy ứng dụng, mở terminal và gõ lệnh: +# uvicorn app.main:app --reload \ No newline at end of file diff --git a/app/orchestrator/__init__.py b/app/orchestrator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/orchestrator/workflow_manager.py b/app/orchestrator/workflow_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..cea2ac8b4003c3d2b331e7434153b9ffcbfbca1b --- /dev/null +++ b/app/orchestrator/workflow_manager.py @@ -0,0 +1,99 @@ +# app/orchestrator/workflow_manager.py + +import logging +from app.services.context_manager import ChatContext +from app.services.intent_analyzer import IntentResult + +# Import các lớp workflow cụ thể +from app.orchestrator.workflows.analyze_house import AnalyzeHouseWorkflow +from app.orchestrator.workflows.compare_people import ComparePeopleWorkflow +from app.orchestrator.workflows.lookup_item import LookupItemWorkflow +from app.orchestrator.workflows.lookup_loandau import LookupLoanDauWorkflow +from app.orchestrator.workflows.lookup_namsinh import LookupNamSinhWorkflow +from app.tools import can_chi_helper + +# ... import các workflow khác ở đây khi bạn tạo chúng (ví dụ: ComparePeopleWorkflow) + +logger = logging.getLogger(__name__) + +# Ánh xạ từ tên intent sang lớp workflow tương ứng +WORKFLOW_MAPPING = { + "ANALYZE_HOUSE": AnalyzeHouseWorkflow, + "COMPARE_PEOPLE": ComparePeopleWorkflow, + "LOOKUP_ITEM": LookupItemWorkflow, + "LOOKUP_LOANDAU": LookupLoanDauWorkflow, + "LOOKUP_NAMSINH": LookupNamSinhWorkflow, +} + + +async def preprocess_entities(entities): + """Tiền xử lý entities để giải mã các alias về năm sinh.""" + # Xử lý cho người thứ nhất + if not entities.nam_sinh_1 and entities.nam_sinh_alias_1: + logger.info(f"Tiền xử lý: Đang giải mã alias người 1: '{entities.nam_sinh_alias_1}'") + resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_alias_1) + if resolved_year: + entities.nam_sinh_1 = resolved_year + logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}") + + # Xử lý cho người thứ hai (cho intent COMPARE_PEOPLE) + if not entities.nam_sinh_2 and entities.nam_sinh_alias_2: + logger.info(f"Tiền xử lý: Đang giải mã alias người 2: '{entities.nam_sinh_alias_2}'") + resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_alias_2) + if resolved_year: + entities.nam_sinh_2 = resolved_year + logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}") + pass + + # Xử lý trường hợp LLM trả về năm 2 chữ số (ví dụ: 91) + if entities.nam_sinh_1 and 0 < entities.nam_sinh_1 < 100: + logger.info(f"Tiền xử lý: Đang giải mã năm 2 chữ số: '{entities.nam_sinh_1}'") + resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_1) + if resolved_year: + entities.nam_sinh_1 = resolved_year + logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}") + + return entities + +async def run_workflow(intent_result: IntentResult) -> ChatContext: + """ + Chọn và thực thi workflow phù hợp dựa trên intent đã được phân tích. + + Args: + intent_result: Kết quả từ bộ phân tích ý định. + + Returns: + Đối tượng ChatContext đã được làm giàu thông tin sau khi workflow chạy xong. + """ + intent_name = intent_result.intent + initial_entities = intent_result.entities + + logger.info(f"Đang chọn workflow cho intent: '{intent_name}'") + + # Khởi tạo context ban đầu với các thực thể đã trích xuất + context = ChatContext(initial_entities=initial_entities) + context.intent_name = intent_name + + # Lấy lớp workflow tương ứng từ mapping + workflow_class = WORKFLOW_MAPPING.get(intent_name) + + if workflow_class: + # Nếu tìm thấy workflow phù hợp, khởi tạo và chạy nó + workflow_instance = workflow_class(context) + final_context = await workflow_instance.run() + return final_context + else: + # Xử lý các intent đơn giản hoặc không có workflow riêng + logger.warning(f"Không tìm thấy workflow được định nghĩa cho intent '{intent_name}'.") + # Bạn có thể xử lý các intent đơn giản ở đây (GREETING, UNKNOWN, etc.) + # hoặc chỉ trả về context ban đầu. + response_text = None + if intent_name == "GREETING": + response_text = "Chào bạn, tôi là trợ lý phong thủy. Tôi có thể giúp gì cho bạn?" + elif intent_name == "UNKNOWN": + response_text = "Xin lỗi, tôi chưa hiểu rõ yêu cầu của bạn. Bạn có thể hỏi về phân tích nhà cửa, xem tuổi, hoặc tra cứu vật phẩm phong thủy." + + if response_text: + context.update_context({"direct_response": response_text}) + + return context \ No newline at end of file diff --git a/app/orchestrator/workflows/__init__.py b/app/orchestrator/workflows/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/orchestrator/workflows/analyze_house.py b/app/orchestrator/workflows/analyze_house.py new file mode 100644 index 0000000000000000000000000000000000000000..c1b44f323d2872d1c2766acb82d9c0f4f4c4f043 --- /dev/null +++ b/app/orchestrator/workflows/analyze_house.py @@ -0,0 +1,93 @@ +# app/orchestrator/workflows/analyze_house.py + +import logging +from datetime import datetime + +from app.orchestrator.workflows.base_workflow import BaseWorkflow +from app.services.context_manager import ChatContext +# Import tất cả các tool cần thiết +from app.tools import ngu_hanh_tools, bat_trach_tools, tuong_tac_tools, general_tools + +logger = logging.getLogger(__name__) + + +class AnalyzeHouseWorkflow(BaseWorkflow): + """ + Workflow xử lý yêu cầu phân tích tổng thể về nhà cửa. + """ + + def __init__(self, context: ChatContext): + super().__init__(context) + + async def run(self): + logger.info("--- Bắt đầu Workflow: Phân tích nhà cửa ---") + + # --- Bước 1: Kiểm tra thông tin đầu vào cơ bản --- + if not self.context.is_ready_for_tool(['nam_sinh_1', 'gioi_tinh_1', 'huong_nha']): + logger.warning(f"Thiếu thông tin đầu vào: {self.context.missing_info}") + # Trong ứng dụng thực tế, ở đây sẽ trả về một câu hỏi cho người dùng + return self.context # Trả về context với thông tin bị thiếu + + entities = self.context.initial_entities + nam_sinh = entities.nam_sinh_1 + gioi_tinh = entities.gioi_tinh_1 + huong_nha = entities.huong_nha + + # --- Bước 2: Tra cứu thông tin bản mệnh của gia chủ --- + logger.info("Bước 2: Tra cứu thông tin bản mệnh") + cung_menh_info = await self._call_tool( + ngu_hanh_tools.get_cung_menh_by_year_gender, + nam_sinh=nam_sinh, + gioi_tinh=gioi_tinh + ) + if not cung_menh_info: + logger.error("Không thể tìm thấy cung mệnh, dừng workflow.") + return self.context + self.context.update_context({"cung_menh_info": cung_menh_info}) + + menh_ngu_hanh = cung_menh_info.get('hanhcungmenh') + if menh_ngu_hanh: + menh_info = await self._call_tool(ngu_hanh_tools.get_menh_info, menh=menh_ngu_hanh) + self.context.update_context({"menh_ngu_hanh_info": menh_info}) + + nap_am_info = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=nam_sinh) + self.context.update_context({"nap_am_info": nap_am_info}) + + # --- Bước 3: Phân tích Bát Trạch --- + logger.info("Bước 3: Phân tích Bát Trạch") + cung_menh = cung_menh_info.get('cungmenh') + if cung_menh: + rule_info = await self._call_tool( + bat_trach_tools.get_bat_trach_info, + cung_menh=cung_menh, + huong_nha=huong_nha + ) + self.context.update_context({"bat_trach_rule_info": rule_info}) + + if rule_info: + ten_cung_vi = rule_info.get('tencungvi_taothanh') + if ten_cung_vi: + detail_info = await self._call_tool( + bat_trach_tools.get_cung_vi_detail, + ten_cung_vi=ten_cung_vi + ) + self.context.update_context({"bat_trach_detail_info": detail_info}) + + # --- Bước 4: Phân tích tương tác Mệnh - Hướng --- + logger.info("Bước 4: Phân tích tương tác Mệnh - Hướng") + if menh_ngu_hanh: + interaction_info = await self._call_tool( + tuong_tac_tools.get_menh_huong_interaction, + menh_gia_chu=menh_ngu_hanh, + huong_nha=huong_nha + ) + self.context.update_context({"menh_huong_interaction_info": interaction_info}) + + # --- Bước 5: Phân tích Phi tinh năm hiện tại --- + logger.info("Bước 5: Phân tích Phi tinh") + current_year = datetime.now().year + phi_tinh_info = await self._call_tool(general_tools.get_phi_tinh_info, nam=current_year) + self.context.update_context({"phi_tinh_info": phi_tinh_info}) + + logger.info("--- Hoàn thành Workflow: Phân tích nhà cửa ---") + return self.context \ No newline at end of file diff --git a/app/orchestrator/workflows/base_workflow.py b/app/orchestrator/workflows/base_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..6ee7b958de996a89a08e2580f62b9aa0a37f08cc --- /dev/null +++ b/app/orchestrator/workflows/base_workflow.py @@ -0,0 +1,42 @@ +# app/orchestrator/workflows/base_workflow.py + +from abc import ABC, abstractmethod +from app.services.context_manager import ChatContext +import logging +from typing import Callable, Any, Dict + +logger = logging.getLogger(__name__) + +class BaseWorkflow(ABC): + """ + Lớp cơ sở trừu tượng cho tất cả các workflow. + Mỗi workflow sẽ xử lý một intent cụ thể. + """ + def __init__(self, context: ChatContext): + self.context = context + + async def _call_tool(self, tool_func: Callable, **kwargs) -> Any: + """ + Hàm bọc (wrapper) để gọi một tool, tự động ghi lại lịch sử và xử lý lỗi. + """ + tool_name = tool_func.__name__ + logger.info(f"Workflow đang gọi tool: {tool_name} với params: {kwargs}") + + try: + result = tool_func(**kwargs) + status = "success" if result is not None else "failed (no data)" + self.context.add_tool_call(tool_name=tool_name, params=kwargs, status=status) + return result + except Exception as e: + logger.error(f"Lỗi khi thực thi tool '{tool_name}': {e}") + self.context.add_tool_call(tool_name=tool_name, params=kwargs, status="failed (exception)") + return None + + @abstractmethod + async def run(self): + """ + Phương thức chính để thực thi logic của workflow. + Nó sẽ gọi các tool theo đúng thứ tự, cập nhật context, + và cuối cùng trả về context đã được làm giàu thông tin. + """ + pass \ No newline at end of file diff --git a/app/orchestrator/workflows/compare_people.py b/app/orchestrator/workflows/compare_people.py new file mode 100644 index 0000000000000000000000000000000000000000..021bc29d7d189a30f70ade995a7ca0837e646fa9 --- /dev/null +++ b/app/orchestrator/workflows/compare_people.py @@ -0,0 +1,60 @@ +# app/orchestrator/workflows/compare_people.py + +import logging +from app.orchestrator.workflows.base_workflow import BaseWorkflow +from app.services.context_manager import ChatContext +from app.tools import ngu_hanh_tools, tuong_tac_tools + +logger = logging.getLogger(__name__) + + +class ComparePeopleWorkflow(BaseWorkflow): + """ + Workflow xử lý yêu cầu so sánh sự tương hợp giữa hai người. + """ + + # def __init__(self, context: ChatContext): + # super().__init__(context) + # # Tạo các key mới trong context để lưu thông tin người thứ 2 + # self.context.cung_menh_info_2 = None + # self.context.nap_am_info_2 = None + # self.context.menh_menh_interaction_info = None + + async def run(self): + logger.info("--- Bắt đầu Workflow: So sánh hai người ---") + + entities = self.context.initial_entities + if not (entities.nam_sinh_1 and entities.gioi_tinh_1 and entities.nam_sinh_2 and entities.gioi_tinh_2): + self.context.missing_info = "năm sinh và giới tính của cả hai người" + logger.warning(f"Thiếu thông tin: {self.context.missing_info}") + return self.context + + # --- Bước 1: Tra cứu thông tin người thứ nhất --- + logger.info(f"Bước 1: Tra cứu người 1 (Năm sinh: {entities.nam_sinh_1})") + cung_menh_1 = await self._call_tool(ngu_hanh_tools.get_cung_menh_by_year_gender, nam_sinh=entities.nam_sinh_1, + gioi_tinh=entities.gioi_tinh_1) + nap_am_1 = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=entities.nam_sinh_1) + self.context.update_context({"cung_menh_info": cung_menh_1, "nap_am_info": nap_am_1}) + + # --- Bước 2: Tra cứu thông tin người thứ hai --- + logger.info(f"Bước 2: Tra cứu người 2 (Năm sinh: {entities.nam_sinh_2})") + cung_menh_2 = await self._call_tool(ngu_hanh_tools.get_cung_menh_by_year_gender, nam_sinh=entities.nam_sinh_2, + gioi_tinh=entities.gioi_tinh_2) + nap_am_2 = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=entities.nam_sinh_2) + self.context.update_context({ + "cung_menh_info_2": cung_menh_2, + "nap_am_info_2": nap_am_2 + }) + + # --- Bước 3: Tra cứu sự tương tác --- + if nap_am_1 and nap_am_2: + logger.info("Bước 3: Tra cứu sự tương tác") + nap_am_1_name = nap_am_1.get('tennapam') + nap_am_2_name = nap_am_2.get('tennapam') + if nap_am_1_name and nap_am_2_name: + interaction = await self._call_tool(tuong_tac_tools.get_menh_menh_interaction, nap_am1=nap_am_1_name, + nap_am2=nap_am_2_name) + self.context.update_context({"menh_menh_interaction_info": interaction}) + + logger.info("--- Hoàn thành Workflow: So sánh hai người ---") + return self.context \ No newline at end of file diff --git a/app/orchestrator/workflows/lookup_item.py b/app/orchestrator/workflows/lookup_item.py new file mode 100644 index 0000000000000000000000000000000000000000..16efcb776424ba28c002fe935649d6bd0ca7a95a --- /dev/null +++ b/app/orchestrator/workflows/lookup_item.py @@ -0,0 +1,60 @@ +# app/orchestrator/workflows/lookup_item.py +import logging +from app.orchestrator.workflows.base_workflow import BaseWorkflow +from app.services.context_manager import ChatContext +from app.tools import general_tools, semantic_search_tools + +logger = logging.getLogger(__name__) + + +class LookupItemWorkflow(BaseWorkflow): + """ + Workflow xử lý yêu cầu tra cứu thông tin về một vật phẩm phong thủy. + Sử dụng semantic search để tìm ra tên chính xác trước khi truy vấn CSDL. + """ + + async def run(self): + logger.info("--- Bắt đầu Workflow: Tra cứu Vật phẩm (Nâng cấp) ---") + + # Lấy tên/mô tả vật phẩm từ entities + item_query = self.context.initial_entities.vat_pham + + if not item_query: + self.context.missing_info = "tên hoặc mô tả vật phẩm bạn muốn tìm" + logger.warning(f"Thiếu thông tin: {self.context.missing_info}") + return self.context + + # --- BƯỚC 1: Tìm kiếm ngữ nghĩa để xác định tên vật phẩm chính xác --- + logger.info(f"Bước 1: Tìm kiếm ngữ nghĩa cho query: '{item_query}'.") + similar_item = await self._call_tool( + semantic_search_tools.find_most_similar_item, + query=item_query + ) + + if not similar_item: + logger.warning("Không tìm thấy vật phẩm nào đủ tương đồng qua semantic search.") + self.context.update_context({"lookup_result": None}) + self.context.direct_response = f"Xin lỗi, tôi không tìm thấy thông tin nào khớp với '{item_query}' trong cơ sở dữ liệu vật phẩm." + logger.info("--- Hoàn thành Workflow: Tra cứu Vật phẩm (Không có kết quả) ---") + return self.context + + # Lưu lại kết quả suy luận từ semantic search + self.context.update_context({"semantic_search_result": similar_item}) + + # --- BƯỚC 2: Dùng tên chính xác để truy vấn chi tiết từ CSDL --- + item_name = similar_item.get('name') + if not item_name: + logger.error("Kết quả từ semantic search không có 'name'. Dừng workflow.") + return self.context + + logger.info(f"Bước 2: Dùng tên chính xác '{item_name}' để truy vấn chi tiết.") + + lookup_result = await self._call_tool( + general_tools.get_vat_pham_info, + ten_vat_pham=item_name # Sử dụng tham số mới để tìm kiếm chính xác + ) + + self.context.update_context({"lookup_result": lookup_result}) + + logger.info("--- Hoàn thành Workflow: Tra cứu Vật phẩm ---") + return self.context \ No newline at end of file diff --git a/app/orchestrator/workflows/lookup_loandau.py b/app/orchestrator/workflows/lookup_loandau.py new file mode 100644 index 0000000000000000000000000000000000000000..60bc8cee85f6e2caa1284029e7b17dd74f312d7b --- /dev/null +++ b/app/orchestrator/workflows/lookup_loandau.py @@ -0,0 +1,90 @@ +# app/orchestrator/workflows/lookup_loandau.py +import logging +from app.orchestrator.workflows.base_workflow import BaseWorkflow +from app.services.context_manager import ChatContext +from app.tools import loan_dau_tools, semantic_search_tools, reranker_tools + +logger = logging.getLogger(__name__) + + +class LookupLoanDauWorkflow(BaseWorkflow): + """ + Workflow xử lý yêu cầu tra cứu Loan Đầu (ngoại cảnh) bằng phương pháp 2 giai đoạn: + 1. Retrieval: Dùng semantic search để tìm Top-K ứng viên tiềm năng. + 2. Re-ranking: Dùng LLM để chọn ra ứng viên chính xác nhất từ Top-K. + """ + + async def run(self): + logger.info("--- Bắt đầu Workflow: Tra cứu Loan Đầu (2 giai đoạn) ---") + + keyword = self.context.initial_entities.keyword_loandau + + if not keyword: + self.context.missing_info = "mô tả về ngoại cảnh (ví dụ: đường đâm, sông ôm)" + logger.warning(f"Thiếu thông tin: {self.context.missing_info}") + return self.context + + # --- GIAI ĐOẠN 1: TRUY XUẤT (RETRIEVAL) --- + logger.info("Giai đoạn 1: Tìm kiếm ngữ nghĩa Top-K ứng viên.") + candidate_items = await self._call_tool( + semantic_search_tools.find_most_similar_loandau, + query=keyword, + k=3, # Lấy 3 ứng viên hàng đầu để LLM lựa chọn + similarity_threshold=0.4 # Hạ ngưỡng ở giai đoạn này để có nhiều lựa chọn hơn + ) + + if not candidate_items: + logger.warning("Không tìm thấy ứng viên nào đủ tương đồng qua semantic search.") + self.context.update_context({"lookup_result": None}) + self.context.direct_response = f"Xin lỗi, tôi không tìm thấy thông tin nào khớp với mô tả '{keyword}' của bạn." + logger.info("--- Hoàn thành Workflow: Tra cứu Loan Đầu (Không có ứng viên) ---") + return self.context + + # Nếu chỉ có 1 ứng viên, không cần re-rank, dùng luôn + if len(candidate_items) == 1: + logger.info("Chỉ có 1 ứng viên, bỏ qua giai đoạn re-ranking.") + best_item = candidate_items[0] + else: + # --- GIAI ĐOẠN 2: XẾP HẠNG LẠI (RE-RANKING) --- + logger.info("Giai đoạn 2: Dùng LLM để chọn ứng viên tốt nhất (Re-ranking).") + best_item = await self._call_tool( + reranker_tools.choose_best_loandau_candidate, + user_query=keyword, + candidates=candidate_items + ) + + # Nếu vì lý do nào đó LLM không chọn được, lấy ứng viên có điểm cao nhất làm mặc định + if not best_item: + logger.warning("LLM không chọn được ứng viên, lấy kết quả đầu tiên từ semantic search.") + best_item = candidate_items[0] + + # Lưu lại kết quả suy luận cuối cùng vào context + self.context.update_context({"semantic_search_result": best_item}) + + # --- GIAI ĐOẠN CUỐI: TRUY VẤN DỮ LIỆU CHI TIẾT --- + item_type = best_item.get('type') + item_name = best_item.get('name') + + if not item_type or not item_name: + logger.error("Kết quả lựa chọn cuối cùng không hợp lệ (thiếu type hoặc name).") + self.context.direct_response = "Đã có lỗi xảy ra trong quá trình phân tích. Vui lòng thử lại." + return self.context + + logger.info(f"Giai đoạn cuối: Dùng tên chính xác '{item_name}' để truy vấn chi tiết.") + + lookup_result = None + if item_type == 'sat_khi': + lookup_result = await self._call_tool( + loan_dau_tools.get_sat_khi_info, + ten_sat_khi=item_name + ) + elif item_type == 'the_dat': + lookup_result = await self._call_tool( + loan_dau_tools.get_the_dat_cat_tuong_info, + ten_the_dat=item_name + ) + + self.context.update_context({"lookup_result": lookup_result}) + + logger.info("--- Hoàn thành Workflow: Tra cứu Loan Đầu ---") + return self.context \ No newline at end of file diff --git a/app/orchestrator/workflows/lookup_namsinh.py b/app/orchestrator/workflows/lookup_namsinh.py new file mode 100644 index 0000000000000000000000000000000000000000..403e1c9503ee4481013551283deeff4067296dbd --- /dev/null +++ b/app/orchestrator/workflows/lookup_namsinh.py @@ -0,0 +1,103 @@ +# app/orchestrator/workflows/lookup_namsinh.py + +import logging +from app.orchestrator.workflows.base_workflow import BaseWorkflow +from app.services.context_manager import ChatContext +from app.tools import ngu_hanh_tools, can_chi_helper + +logger = logging.getLogger(__name__) + + +class LookupNamSinhWorkflow(BaseWorkflow): + """ + Workflow xử lý yêu cầu tra cứu thông tin cho một năm sinh. + - Tích hợp bộ giải mã alias (Can Chi, con giáp, năm viết tắt). + - Xử lý logic đa trường hợp: đủ thông tin, thiếu giới tính, hoặc cần làm rõ. + """ + + def __init__(self, context: ChatContext): + super().__init__(context) + + async def run(self): + logger.info("--- Bắt đầu Workflow: Tra cứu Năm sinh (Nâng cấp) ---") + + entities = self.context.initial_entities + nam_sinh = entities.nam_sinh_1 + gioi_tinh = entities.gioi_tinh_1 + nam_sinh_alias = entities.nam_sinh_alias + + # --- Bước 1: Giải mã Alias (nếu có) để tìm ra năm sinh cụ thể --- + if not nam_sinh: + # Nếu vẫn không có nam_sinh, có thể là alias không cụ thể + possible_years = can_chi_helper.resolve_alias_to_year_list(nam_sinh_alias) + if possible_years: + self.context.direct_response = ( + f"Tuổi '{nam_sinh_alias.capitalize()}' có thể ứng với nhiều năm sinh như: " + f"{', '.join(map(str, possible_years[:4]))}... " + "Để có kết quả chính xác, bạn vui lòng cung cấp năm sinh và giới tính cụ thể nhé." + ) + return self.context + else: # Không giải mã được + self.context.missing_info = "năm sinh hoặc tuổi hợp lệ (ví dụ: 1991, Bính Dần)" + return self.context + + # --- Bước 2: Kiểm tra xem đã có đủ thông tin năm sinh để tra cứu chưa --- + if not nam_sinh: + self.context.missing_info = "năm sinh hoặc tuổi (ví dụ: 1991, Bính Dần)" + logger.warning("Không có năm sinh để tra cứu sau bước giải mã.") + return self.context + + # --- Bước 3: Thực hiện các cuộc gọi tool để thu thập dữ liệu --- + # Dictionary để tổng hợp tất cả kết quả tra cứu + combined_result = {} + lookup_successful = False + + # Trường hợp 1: Có đầy đủ năm sinh và giới tính -> Tra cứu Cung Mệnh (Bát Trạch) + if gioi_tinh: + logger.info(f"Tra cứu Cung Mệnh cho {gioi_tinh} {nam_sinh}.") + cung_menh_info = await self._call_tool( + ngu_hanh_tools.get_cung_menh_by_year_gender, + nam_sinh=nam_sinh, + gioi_tinh=gioi_tinh + ) + if cung_menh_info: + combined_result.update(cung_menh_info) + lookup_successful = True + + # Luôn tra cứu Nạp Âm (Ngũ Hành) nếu có năm sinh + logger.info(f"Tra cứu Nạp Âm cho năm sinh {nam_sinh}.") + nap_am_info = await self._call_tool( + ngu_hanh_tools.get_nap_am_info, + nam_sinh=nam_sinh + ) + if nap_am_info: + combined_result.update(nap_am_info) + lookup_successful = True + + # Nếu có Nạp Âm, làm giàu thêm thông tin chi tiết về Mệnh Ngũ Hành + menh_ngu_hanh = nap_am_info.get('hanhnguhanh') + if menh_ngu_hanh: + logger.info(f"Làm giàu thông tin chi tiết cho Mệnh '{menh_ngu_hanh}'.") + menh_info = await self._call_tool( + ngu_hanh_tools.get_menh_info, + menh=menh_ngu_hanh + ) + if menh_info: + combined_result.update(menh_info) + + # --- Bước 4: Tổng hợp kết quả và quyết định phản hồi cuối cùng --- + if lookup_successful: + self.context.update_context({"lookup_result": combined_result}) + # Nếu chỉ tra cứu được Nạp Âm mà thiếu giới tính, gợi ý cho người dùng + if not gioi_tinh: + self.context.direct_response = ( + "Dưới đây là thông tin về Nạp Âm và Mệnh Ngũ Hành. " + "Để xem thêm về Cung Mệnh (Bát Trạch), bạn vui lòng cung cấp thêm giới tính nhé." + ) + else: + # Nếu tất cả các tool đều không tìm thấy dữ liệu + logger.warning(f"Không tìm thấy bất kỳ thông tin nào cho năm sinh {nam_sinh} trong CSDL.") + self.context.direct_response = f"Xin lỗi, tôi không tìm thấy thông tin phong thủy nào cho năm sinh {nam_sinh} trong cơ sở dữ liệu." + + logger.info("--- Hoàn thành Workflow: Tra cứu Năm sinh ---") + return self.context \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/services/context_manager.py b/app/services/context_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..718888287c3785ac3daf2feb3c2e4088a4501ec8 --- /dev/null +++ b/app/services/context_manager.py @@ -0,0 +1,77 @@ +# app/services/context_manager.py + +from typing import Dict, Any, Optional, List # Thêm List +from pydantic import BaseModel, Field + +# Import model entities để tái sử dụng +from app.services.intent_analyzer import ExtractedEntities +import logging + +class ToolCallRecord(BaseModel): + """Một model nhỏ để lưu thông tin về một lần gọi tool.""" + tool_name: str + params: Dict[str, Any] + status: str # "success" hoặc "failed" + # result: Optional[Dict[str, Any]] = None # Bỏ đi để response đỡ cồng kềnh + +class ChatContext(BaseModel): + """ + Lớp quản lý và lưu trữ toàn bộ thông tin thu thập được trong một phiên hội thoại. + Nó hoạt động như một "bộ nhớ tạm" cho workflow. + """ + # --- Thông tin đầu vào ban đầu --- + initial_entities: ExtractedEntities = Field(default_factory=ExtractedEntities) + intent_name: Optional[str] = None + + tool_calls: List[ToolCallRecord] = Field(default_factory=list) + # --- Dữ liệu được làm giàu bởi các tools --- + # Dùng Optional và khởi tạo là None + cung_menh_info: Optional[Dict[str, Any]] = None + menh_ngu_hanh_info: Optional[Dict[str, Any]] = None + nap_am_info: Optional[Dict[str, Any]] = None + bat_trach_rule_info: Optional[Dict[str, Any]] = None + bat_trach_detail_info: Optional[Dict[str, Any]] = None + menh_huong_interaction_info: Optional[Dict[str, Any]] = None + phi_tinh_info: Optional[Dict[str, Any]] = None + + # Cho ComparePeopleWorkflow + workflow_data: Dict[str, Any] = Field(default_factory=dict) + lookup_result: Optional[Dict[str, Any]] = None + + # --- Các thông tin khác có thể cần --- + missing_info: Optional[str] = None # Dùng để hỏi lại người dùng + direct_response: Optional[str] = None + + def update_context(self, data: Dict[str, Any]): + for key, value in data.items(): + if key in self.model_fields: # Kiểm tra xem key có phải là một trường đã định nghĩa không + setattr(self, key, value) + else: + self.workflow_data[key] = value + logging.info(f"Đã cập nhật trường động '{key}' trong workflow_data.") + + def is_ready_for_tool(self, required_fields: list[str]) -> bool: + """ + Kiểm tra xem context đã có đủ thông tin để chạy một tool cụ thể chưa. + """ + entities = self.initial_entities.model_dump() + for field in required_fields: + if field not in entities or entities[field] is None: + # Chuyển đổi tên trường thành dạng thân thiện hơn để hỏi người dùng + missing_map = { + 'nam_sinh_1': 'năm sinh', + 'gioi_tinh_1': 'giới tính', + 'huong_nha': 'hướng nhà', + 'nam_sinh_2': 'năm sinh của người thứ hai', + 'gioi_tinh_2': 'giới tính của người thứ hai' + } + self.missing_info = missing_map.get(field, field) + return False + + self.missing_info = None + return True + + def add_tool_call(self, tool_name: str, params: Dict[str, Any], status: str): + """Thêm một bản ghi về việc gọi tool vào lịch sử.""" + record = ToolCallRecord(tool_name=tool_name, params=params, status=status) + self.tool_calls.append(record) \ No newline at end of file diff --git a/app/services/intent_analyzer.py b/app/services/intent_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..f444691a507120b8ce880effe655523e47e51e95 --- /dev/null +++ b/app/services/intent_analyzer.py @@ -0,0 +1,124 @@ +# app/services/intent_analyzer.py + +import logging +import json +from typing import Dict, Any +from groq import Groq +from pydantic import BaseModel, Field, ValidationError + +from app.core.config import settings +from app.services.prompt_templates import INTENT_ANALYSIS_PROMPT + +logger = logging.getLogger(__name__) + + +# --- Pydantic Models để Validate kết quả từ LLM --- +# Điều này đảm bảo rằng output của LLM luôn có cấu trúc đúng như chúng ta mong đợi. +class ExtractedEntities(BaseModel): + nam_sinh_1: int | None = None + gioi_tinh_1: str | None = None + nam_sinh_alias_1: str | None = None + nam_sinh_2: int | None = None + gioi_tinh_2: str | None = None + nam_sinh_alias_2: str | None = None + huong_nha: str | None = None + vat_pham: str | None = None + keyword_loandau: str | None = None + nam_sinh_alias: str | None = None + + +class IntentResult(BaseModel): + intent: str + entities: ExtractedEntities + + +# --- Khởi tạo client cho Groq --- +try: + groq_client = Groq(api_key=settings.GROQ_API_KEY) +except Exception as e: + logger.error(f"Không thể khởi tạo Groq client: {e}") + groq_client = None + + +async def analyze_intent(user_query: str, max_retries: int = 3) -> IntentResult: + """ + Phân tích câu hỏi của người dùng để xác định ý định và trích xuất thực thể. + + Args: + user_query: Câu hỏi gốc của người dùng. + max_retries: Số lần thử lại nếu LLM trả về kết quả không hợp lệ. + + Returns: + Một đối tượng IntentResult chứa intent và entities đã được validate. + """ + if not groq_client: + logger.error("Groq client chưa được khởi tạo. Không thể phân tích ý định.") + return IntentResult(intent="ERROR", entities=ExtractedEntities()) + + prompt = INTENT_ANALYSIS_PROMPT.format(user_query=user_query) + + for attempt in range(max_retries): + try: + logger.info(f"Đang gửi yêu cầu phân tích ý định đến LLM (Lần thử {attempt + 1})...") + chat_completion = groq_client.chat.completions.create( + messages=[ + { + "role": "user", + "content": prompt, + } + ], + model="gemma2-9b-it", # Hoặc "mixtral-8x7b-32768" + temperature=0, # =0 để kết quả có tính quyết định, ít sáng tạo + max_tokens=256, + response_format={"type": "json_object"}, + ) + + raw_response = chat_completion.choices[0].message.content + logger.info(f"LLM response (raw): {raw_response}") + + # Validate kết quả JSON bằng Pydantic + parsed_json = json.loads(raw_response) + validated_result = IntentResult.model_validate(parsed_json) + + logger.info( + f"Phân tích thành công: Intent='{validated_result.intent}', Entities={validated_result.entities.model_dump_json(indent=2)}") + return validated_result + + except json.JSONDecodeError as e: + logger.warning(f"Lỗi giải mã JSON từ LLM: {e}. Đang thử lại...") + except ValidationError as e: + logger.warning(f"Lỗi validate Pydantic từ LLM: {e}. Đang thử lại...") + except Exception as e: + logger.error(f"Lỗi không xác định khi gọi LLM: {e}") + break # Thoát vòng lặp nếu lỗi nghiêm trọng + + # Nếu tất cả các lần thử đều thất bại + logger.error("Không thể phân tích ý định sau nhiều lần thử.") + return IntentResult(intent="UNKNOWN", entities=ExtractedEntities()) + + +# --- Phần kiểm tra --- +if __name__ == '__main__': + import asyncio + + + async def run_tests(): + test_queries = [ + "xem nhà hướng tây nam cho nữ 1991", + "chồng 1988 vợ 1991 thì sao", + "tác dụng của tỳ hưu là gì", + "nhà tôi đối diện một cái khe hẹp giữa 2 tòa nhà cao tầng", + "chào em", + "1986 mệnh gì", + "hôm nay ăn gì" + ] + + for query in test_queries: + print(f"\n--- Testing query: '{query}' ---") + result = await analyze_intent(query) + print(f"Intent: {result.intent}") + print(f"Entities: {result.entities.model_dump()}") + + + # Chạy các hàm bất đồng bộ để test + asyncio.run(run_tests()) \ No newline at end of file diff --git a/app/services/prompt_templates.py b/app/services/prompt_templates.py new file mode 100644 index 0000000000000000000000000000000000000000..900169448bfda887daa93a41e02aa1238cdb8480 --- /dev/null +++ b/app/services/prompt_templates.py @@ -0,0 +1,93 @@ +# app/services/prompt_templates.py + +# SỬA LỖI: Nhân đôi tất cả các dấu ngoặc nhọn {} để tránh lỗi .format() +# Chỉ giữ lại {user_query} là không nhân đôi. + +INTENT_ANALYSIS_PROMPT = """ +Bạn là một trợ lý AI chuyên phân tích yêu cầu của người dùng về lĩnh vực phong thủy. +Nhiệm vụ của bạn là đọc câu hỏi của người dùng và trả về một đối tượng JSON DUY NHẤT, không giải thích gì thêm. +Đối tượng JSON phải có 2 trường: "intent" và "entities". + +Trường "intent" phải là MỘT trong các giá trị sau: +- "ANALYZE_HOUSE": Người dùng muốn phân tích tổng thể về nhà cửa (hướng nhà, tuổi, ...). +- "COMPARE_PEOPLE": Người dùng muốn xem sự tương hợp giữa hai người (vợ chồng, đối tác). +- "LOOKUP_ITEM": Người dùng hỏi thông tin về một vật phẩm phong thủy cụ thể. +- "LOOKUP_DIRECTION": Người dùng hỏi thông tin về một hướng cụ thể. +- "LOOKUP_NAMSINH": Người dùng chỉ hỏi về thông tin của một năm sinh (cung mệnh, nạp âm...). +- "LOOKUP_LOANDAU": Người dùng mô tả một yếu tố ngoại cảnh (đường đâm, sông ôm, khe hẹp...). +- "GREETING": Người dùng chào hỏi đơn thuần. +- "UNKNOWN": Không thể xác định được ý định rõ ràng. + +Trường "entities" là một đối tượng JSON chứa các thông tin bạn trích xuất được. Các key có thể có: +- "nam_sinh_1", "gioi_tinh_1": Thông tin của người thứ nhất. +- "nam_sinh_2", "gioi_tinh_2": Thông tin của người thứ hai (nếu có). +- "huong_nha": Hướng nhà (ví dụ: "Đông Bắc", "Tây Nam"). +- "vat_pham": Tên vật phẩm phong thủy. +- "keyword_loandau": Từ khóa mô tả ngoại cảnh. +- "nam_sinh_alias": Các cách gọi khác của năm sinh (ví dụ: "Bính Dần", "tuổi chuột", "91"). + +QUY TẮC: +1. Chỉ trả về JSON. Không thêm ```json``` hay bất kỳ văn bản nào khác. +2. Nếu không có thực thể nào, trả về một đối tượng entities rỗng: {{}}. +3. "giới tính" phải là "Nam" hoặc "Nữ". +4. "nam_sinh" phải là số nguyên. + +Dưới đây là các ví dụ: + +--- +User: xem giúp mình nhà hướng đông nam cho nam 1990 +AI: {{"intent": "ANALYZE_HOUSE", "entities": {{"nam_sinh_1": 1990, "gioi_tinh_1": "Nam", "huong_nha": "Đông Nam"}}}} +--- +User: Chồng 1988 vợ 1991 thì sao bạn? +AI: {{"intent": "COMPARE_PEOPLE", "entities": {{"nam_sinh_1": 1988, "gioi_tinh_1": "Nam", "nam_sinh_2": 1991, "gioi_tinh_2": "Nữ"}}}} +--- +User: xem tuổi chồng 88 vợ tân mùi +AI: {{"intent": "COMPARE_PEOPLE", "entities": {{"nam_sinh_alias_1": "88", "gioi_tinh_1": "Nam", "nam_sinh_alias_2": "tân mùi", "gioi_tinh_2": "Nữ"}}}} +--- +User: Tỳ hưu có tác dụng gì? +AI: {{"intent": "LOOKUP_ITEM", "entities": {{"vat_pham": "Tỳ Hưu"}}}} +--- +User: Nhà tôi ở ngay khúc cua con đường nó chĩa vào +AI: {{"intent": "LOOKUP_LOANDAU", "entities": {{"keyword_loandau": "khúc cua đường chĩa vào"}}}} +--- +User: 1995 là mệnh gì +AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_1": 1995}}}} +--- +User: xem mệnh cho tuổi Bính Dần +AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_alias": "Bính Dần"}}}} +--- +User: nữ 91 hợp hướng nào +AI: {{"intent": "ANALYZE_HOUSE", "entities": {{"gioi_tinh_1": "Nữ", "nam_sinh_alias": "91"}}}} +--- +User: người tuổi cọp thì sao +AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_alias": "cọp"}}}} +--- +User: Chào bạn +AI: {{"intent": "GREETING", "entities": {{}}}} +--- +User: thời tiết hôm nay thế nào +AI: {{"intent": "UNKNOWN", "entities": {{}}}} +--- + +Bây giờ, hãy phân tích câu hỏi của người dùng dưới đây. + +User: {user_query} +AI: +""" + +RESPONSE_SYNTHESIS_PROMPT = """ +Bạn là một chuyên gia phong thủy thân thiện và giao tiếp giỏi. Nhiệm vụ của bạn là đọc kỹ phần **DỮ LIỆU PHÂN TÍCH** dưới đây và viết một bài tư vấn hoàn chỉnh cho người dùng với văn phong tự nhiên, dễ hiểu, giống như bạn đang trò chuyện trực tiếp. + +**QUY TẮC TỐI QUAN TRỌNG:** +1. **CHỈ SỬ DỤNG THÔNG TIN CÓ TRONG DỮ LIỆU ĐƯỢC CUNG CẤP.** +2. **Bắt đầu câu trả lời một cách trực tiếp và đi thẳng vào vấn đề.** Tránh sử dụng các đầu mục cứng nhắc như "Báo cáo", "Khách hàng". +3. Khi dữ liệu có ghi chú suy luận (ví dụ: "hệ thống đã suy luận ra đây là..."), hãy khéo léo lồng ghép thông tin này vào phần mở đầu. +4. Trình bày các giải pháp một cách rõ ràng, có thể dùng danh sách (list) hoặc gạch đầu dòng. + +**DỮ LIỆU PHÂN TÍCH:** +--- +{context_data} +--- + +Bây giờ, hãy viết bài tư vấn của bạn. +""" \ No newline at end of file diff --git a/app/services/response_synthesizer.py b/app/services/response_synthesizer.py new file mode 100644 index 0000000000000000000000000000000000000000..189aba156fc9a1a37a3a00c5e70ebdd58bc6b7cb --- /dev/null +++ b/app/services/response_synthesizer.py @@ -0,0 +1,210 @@ +# app/services/response_synthesizer.py + +import logging +import json +from groq import Groq + +from app.core.config import settings +from app.services.context_manager import ChatContext +from app.services.prompt_templates import RESPONSE_SYNTHESIS_PROMPT + +logger = logging.getLogger(__name__) + +# Khởi tạo lại client (hoặc có thể tạo một module client chung) +try: + groq_client = Groq(api_key=settings.GROQ_API_KEY) +except Exception as e: + logger.error(f"Không thể khởi tạo Groq client: {e}") + groq_client = None + + +def _format_dict_to_string(data: dict, title: str) -> list[str]: + """Chuyển một dictionary thành một list các chuỗi có định dạng đẹp.""" + lines = [f"**{title}:**"] + if not data: + lines.append("- Không có thông tin.") + return lines + + # Ánh xạ tên cột xấu xí sang tên đẹp hơn + key_mappings = { + 'tenvatpham': 'Tên Vật Phẩm', + 'congdungchinh_so1': 'Công Dụng Chính', + 'congdungphu_so2': 'Công Dụng Phụ', + 'luy_camky_quantrong': 'Lưu Ý Cấm Kỵ', + 'diengiai_congdung_tailoc': 'Diễn Giải Về Tài Lộc', + 'tenthedat': 'Tên Thế Đất', + 'mucdo_cattuong': 'Mức Độ Tốt', + 'diengiai_tacdong': 'Diễn Giải Tác Động', + 'giaiphap_kichhoat_1': 'Giải Pháp Kích Hoạt', + 'tensatkhi': 'Tên Sát Khí', + 'mucdo_nguyhiem': 'Mức Độ Nguy Hiểm', + 'giaiphap_uutien_1': 'Giải Pháp Hóa Giải', + 'cungmenh': 'Cung Mệnh', + 'hanhcungmenh': 'Hành Cung Mệnh', + 'nhombattrach': 'Nhóm Bát Trạch', + 'tennapam': 'Nạp Âm', + 'diengiai_hinhtuong': 'Diễn Giải Hình Tượng', + } + + found_data = False # Thêm một cờ để kiểm tra + for key, value in data.items(): + # Bỏ qua các cột không cần thiết hoặc giá trị rỗng + if value is None or "id" in key.lower() or "url" in key.lower() or "version" in key.lower() or "sourcefile" in key.lower(): + continue + + # Lấy tên key đẹp từ mapping, nếu không có thì tự tạo + display_key = key_mappings.get(key, key.replace('_', ' ').title()) + + # Chỉ hiển thị các chuỗi không quá ngắn + if isinstance(value, str) and len(value.strip()) > 1 and value.strip() != 'nan' and value.strip() != '(null)': + lines.append(f"- {display_key}: {value.strip()}") + found_data = True + + if not found_data: + return [f"**{title}:**", "- Không tìm thấy dữ liệu chi tiết."] + return lines + + +def format_context_for_prompt(context: ChatContext) -> str: + intent = getattr(context, 'intent_name', 'UNKNOWN') + data_lines = [] + # Lấy dữ liệu từ workflow_data để dễ truy cập + data = context.workflow_data + + match intent: + case "ANALYZE_HOUSE": + data_lines.append("**PHÂN TÍCH TỔNG THỂ NHÀ CỬA**") + entities = context.initial_entities + + # 1. Thông tin gia chủ + gia_chu_info = [f"**1. Thông tin gia chủ:**"] + gia_chu_info.append(f"- Năm sinh: {entities.nam_sinh_1}, Giới tính: {entities.gioi_tinh_1}") + cung_menh_info = data.get('cung_menh_info') + if cung_menh_info: + gia_chu_info.append( + f"- Cung Mệnh: {cung_menh_info.get('cungmenh')} ({cung_menh_info.get('hanhcungmenh')})") + gia_chu_info.append(f"- Nhóm mệnh: {cung_menh_info.get('nhombattrach')}") + + nap_am_info = data.get('nap_am_info') + if nap_am_info: + gia_chu_info.append(f"- Nạp Âm: {nap_am_info.get('tennapam')}") + data_lines.extend(gia_chu_info) + + # 2. Thông tin nhà & Phân tích + data_lines.append(f"\n**2. Thông tin nhà và các phân tích:**") + data_lines.append(f"- Hướng nhà: {entities.huong_nha}") + + rule = data.get('bat_trach_rule_info') + detail = data.get('bat_trach_detail_info') + if rule and detail: + data_lines.append( + f"- Phân tích Bát Trạch: Hướng nhà tạo thành cung **{rule.get('tencungvi_taothanh')}**, là một cung **{detail.get('loaicung')}**.") + data_lines.append(f" + Ý nghĩa: {detail.get('tacdong_tichcuc')}") + + interact = data.get('menh_huong_interaction_info') + if interact: + data_lines.append( + f"- Phân tích Ngũ Hành: Mối quan hệ giữa Mệnh gia chủ và Hướng nhà là **{interact.get('moiquanhe_nguhanh')}**. {interact.get('diengiai_nguhanh')}") + + phi_tinh = data.get('phi_tinh_info') + if phi_tinh: + data_lines.append( + f"- Yếu tố thời vận (Năm {int(phi_tinh.get('nam_duonglich'))}): Cần chú ý đến các sao tốt/xấu của năm. Hướng đại cát là **{phi_tinh.get('phuongvi_daicat_so1')}**, hướng đại hung là **{phi_tinh.get('phuongvi_daihung_so1')}**.") + + case "COMPARE_PEOPLE": + data_lines.append("**PHÂN TÍCH SỰ TƯƠNG HỢP GIỮA HAI NGƯỜI**") + entities = context.initial_entities + + nap_am_1 = data.get('nap_am_info_1') + if nap_am_1: + data_lines.append( + f"- Người 1: {entities.gioi_tinh_1} {entities.nam_sinh_1} (Nạp âm: {nap_am_1.get('tennapam')})") + + nap_am_2 = data.get('nap_am_info_2') + if nap_am_2: + data_lines.append( + f"- Người 2: {entities.gioi_tinh_2} {entities.nam_sinh_2} (Nạp âm: {nap_am_2.get('tennapam')})") + + interact = data.get('menh_menh_interaction_info') + if interact: + data_lines.append(f"\n**Kết quả phân tích:**") + data_lines.extend(_format_dict_to_string(interact, "Chi tiết về mối quan hệ")) + else: + data_lines.append("\n- Không tìm thấy quy tắc tương hợp cụ thể trong cơ sở dữ liệu.") + + case "LOOKUP_ITEM" | "LOOKUP_LOANDAU" | "LOOKUP_NAMSINH": + query_str = "" + entities_dict = context.initial_entities.model_dump(exclude_unset=True, exclude_none=True) + if entities_dict: + # Lấy giá trị đầu tiên trong dict entities làm query string + query_str = next(iter(entities_dict.values())) + + data_lines.append(f"**THÔNG TIN TRA CỨU CHO: '{query_str}'**") + semantic_result = context.workflow_data.get('semantic_search_result') + if semantic_result and semantic_result.get('lookup_method'): + inferred_name = semantic_result.get('name') + similarity = semantic_result.get('similarity_score', 0) + data_lines.append( + f"**Lưu ý:** Dựa trên mô tả của bạn, hệ thống đã suy luận ra đây là **'{inferred_name}'** (độ tương đồng: {similarity:.0%})." + ) + + lookup_result = context.lookup_result # Sửa lại để lấy từ context chính + if lookup_result: + data_lines.append("Dưới đây là dữ liệu chi tiết tìm được từ cơ sở dữ liệu:") + # Chuyển đổi dict thành chuỗi JSON đẹp mắt để LLM đọc + json_data = {k: v for k, v in lookup_result.items() if + v is not None and str(v).strip().lower() not in ['', 'nan', '(null)']} + data_lines.append(json.dumps(json_data, indent=2, ensure_ascii=False)) + else: + # Câu trả lời này sẽ không được dùng nếu workflow đã set direct_response + data_lines.append("- Không tìm thấy thông tin phù hợp trong cơ sở dữ liệu.") + + case _: + return "Không có đủ dữ liệu để tạo báo cáo. Vui lòng cung cấp thêm thông tin." + + return "\n".join(data_lines) + + +async def synthesize_response(context: ChatContext) -> str: + """ + Tổng hợp câu trả lời cuối cùng dựa trên context đã được làm giàu. + """ + if not groq_client: + return "Lỗi: Dịch vụ LLM không khả dụng." + + # Xử lý các trường hợp đơn giản không cần LLM + if context.direct_response: + return context.direct_response + + if context.missing_info: + return f"Để phân tích, tôi cần biết thêm thông tin về {context.missing_info} của bạn." + + # Xây dựng prompt cho các trường hợp phức tạp + formatted_context = format_context_for_prompt(context) + + if "Không có đủ dữ liệu" in formatted_context: + return formatted_context + + prompt = RESPONSE_SYNTHESIS_PROMPT.format(context_data=formatted_context) + + try: + logger.info("Đang gửi yêu cầu tổng hợp câu trả lời đến LLM...") + chat_completion = groq_client.chat.completions.create( + messages=[ + { + "role": "user", + "content": prompt, + } + ], + model="gemma2-9b-it", + temperature=0.7, # Cho phép LLM viết văn mượt mà hơn + max_tokens=2048, + ) + + final_answer = chat_completion.choices[0].message.content + logger.info("Đã nhận được câu trả lời tổng hợp từ LLM.") + return final_answer + + except Exception as e: + logger.error(f"Lỗi khi tổng hợp câu trả lời: {e}") + return "Xin lỗi, đã có lỗi xảy ra trong quá trình tạo câu trả lời. Vui lòng thử lại sau." \ No newline at end of file diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/tools/bat_trach_tools.py b/app/tools/bat_trach_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..311907cc3f18d7b7c8668df9ce21748a009de7d1 --- /dev/null +++ b/app/tools/bat_trach_tools.py @@ -0,0 +1,108 @@ +# app/tools/bat_trach_tools.py + +import pandas as pd +import logging +from typing import Optional, Dict, Any + +from app.database.connection import query_to_dataframe + +logger = logging.getLogger(__name__) + + +def get_bat_trach_info(cung_menh: str, huong_nha: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu cung vị Bát Trạch (Sinh Khí, Tuyệt Mệnh,...) và các thông tin liên quan + từ bảng 'cung_menh_huong_rules' dựa vào Cung Mệnh của gia chủ và Hướng nhà. + + Args: + cung_menh: Cung Mệnh của gia chủ (ví dụ: "Càn", "Khảm"). + huong_nha: Hướng nhà (ví dụ: "Đông Bắc", "Nam"). + + Returns: + Một dictionary chứa thông tin về luật Bát Trạch, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu Bát Trạch cho Cung Mệnh: {cung_menh}, Hướng nhà: {huong_nha}") + + cung_menh_normalized = cung_menh.strip().capitalize() + huong_nha_normalized = huong_nha.strip().title() + + sql_query = """ + SELECT * + FROM cung_menh_huong_rules + WHERE cungmenh_giachu = :cung_menh AND huongnha = :huong_nha + """ + params = {"cung_menh": cung_menh_normalized, "huong_nha": huong_nha_normalized} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning( + f"Không tìm thấy luật Bát Trạch cho Cung Mệnh '{cung_menh_normalized}' và Hướng nhà '{huong_nha_normalized}'.") + return None + + rule_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy cung Bát Trạch: {rule_info.get('tencungvi_taothanh')}") # ten_cung_vi_tao_thanh -> tencungvi_taothanh + return rule_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu luật Bát Trạch: {e}") + return None + + +def get_cung_vi_detail(ten_cung_vi: str) -> Optional[Dict[str, Any]]: + """ + Lấy thông tin mô tả chi tiết về một cung vị Bát Trạch từ bảng 'bat_trach_cung_vi'. + + Args: + ten_cung_vi: Tên của cung vị (ví dụ: "Sinh Khí", "Tuyệt Mệnh"). + + Returns: + Một dictionary chứa thông tin chi tiết, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu chi tiết cho Cung Vị: {ten_cung_vi}") + ten_cung_vi_normalized = ten_cung_vi.strip().title() + + sql_query = "SELECT * FROM bat_trach_cung_vi WHERE tencung = :ten_cung" + params = {"ten_cung": ten_cung_vi_normalized} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy thông tin chi tiết cho Cung Vị '{ten_cung_vi_normalized}'.") + return None + + detail_info = result_df.to_dict('records')[0] + logger.info(f"Đã lấy thông tin chi tiết thành công cho Cung Vị {ten_cung_vi_normalized}.") + return detail_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu chi tiết Cung Vị: {e}") + return None + + +# --- Phần kiểm tra --- +if __name__ == '__main__': + print("--- Đang kiểm tra các tool trong bat_trach_tools.py ---") + + print("\n[Test 1] Tra cứu Bát Trạch cho Cung Mệnh 'Càn', Hướng nhà 'Đông Bắc':") + rule_result = get_bat_trach_info("Càn", "Đông Bắc") # Giữ nguyên "Đông Bắc" + if rule_result: + # SỬA LẠI KEY KHI LẤY DỮ LIỆU + ten_cung = rule_result.get('tencungvi_taothanh') + print(f" - Tên Cung Vị tạo thành: {ten_cung}") + print(f" - Kết luận ngắn gọn: {rule_result.get('ketluan_ngangon')}") # ket_luan_ngan_gon -> ketluan_ngangon + + if ten_cung: + print(f"\n[Test 2] Tra cứu chi tiết cho Cung Vị '{ten_cung}':") + detail_result = get_cung_vi_detail(ten_cung) + if detail_result: + # SỬA LẠI KEY KHI LẤY DỮ LIỆU + print(f" - Loại Cung: {detail_result.get('loaicung')}") + print(f" - Lĩnh vực ảnh hưởng mạnh nhất: {detail_result.get('linhvuc_anhhuong_manhnhat')}") + print(f" - Tác động tích cực: {detail_result.get('tacdong_tichcuc')}") + else: + print(" - Không tìm thấy kết quả chi tiết.") + else: + print(" - Không tìm thấy kết quả.") + + #python -m app.tools.ngu_hanh_tools diff --git a/app/tools/can_chi_helper.py b/app/tools/can_chi_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..2b612ad96b5b71484e83be655cbeae47fcfd397c --- /dev/null +++ b/app/tools/can_chi_helper.py @@ -0,0 +1,139 @@ +import re +from datetime import datetime +from typing import List, Optional + +# --- Phần 1: Định nghĩa các hằng số và dữ liệu gốc --- + +# Ánh xạ các tên gọi phổ biến sang tên con giáp chính tắc +ALIAS_TO_CON_GIAP = { + "chuột": "Tý", "tí": "Tý", "tý": "Tý", + "trâu": "Sửu", "sửu": "Sửu", + "cọp": "Dần", "hổ": "Dần", "dần": "Dần", + "mèo": "Mão", "mẹo": "Mão", "mão": "Mão", + "rồng": "Thìn", "thìn": "Thìn", + "rắn": "Tỵ", "tỵ": "Tỵ", + "ngựa": "Ngọ", "ngọ": "Ngọ", + "dê": "Mùi", "mùi": "Mùi", + "khỉ": "Thân", "thân": "Thân", + "gà": "Dậu", "dậu": "Dậu", + "chó": "Tuất", "tuất": "Tuất", + "heo": "Hợi", "lợn": "Hợi", "hợi": "Hợi", +} + +# Các hằng số cho việc tính toán Can Chi +THIEN_CAN = ["Giáp", "Ất", "Bính", "Đinh", "Mậu", "Kỷ", "Canh", "Tân", "Nhâm", "Quý"] +DIA_CHI = ["Tý", "Sửu", "Dần", "Mão", "Thìn", "Tỵ", "Ngọ", "Mùi", "Thân", "Dậu", "Tuất", "Hợi"] + +# Năm tham chiếu: 1984 là Giáp Tý (Can index 0, Chi index 0) +REFERENCE_YEAR = 1984 +REFERENCE_CAN_INDEX = 0 +REFERENCE_CHI_INDEX = 0 + +# --- Phần 2: Sinh dữ liệu tự động --- + +# Tạo các cấu trúc dữ liệu rỗng để chứa kết quả +CAN_CHI_TO_YEARS = {f"{can} {chi}": [] for can in THIEN_CAN for chi in DIA_CHI} +CON_GIAP_TO_YEARS = {chi: [] for chi in DIA_CHI} + +# Vòng lặp để sinh dữ liệu cho khoảng 120 năm (từ 1924 đến 2043) +for year in range(1924, 2044): + offset = year - REFERENCE_YEAR + can_index = (REFERENCE_CAN_INDEX + offset) % len(THIEN_CAN) + chi_index = (REFERENCE_CHI_INDEX + offset) % len(DIA_CHI) + + can = THIEN_CAN[can_index] + chi = DIA_CHI[chi_index] + + # Thêm năm vào các dictionary tương ứng + CAN_CHI_TO_YEARS[f"{can} {chi}"].append(year) + CON_GIAP_TO_YEARS[chi].append(year) + + +# --- Phần 3: Các hàm chức năng (Tools) --- + +def get_can_chi_from_year(year: int) -> Optional[str]: + """ + Tính toán Can Chi (ví dụ: "Giáp Tý") cho một năm dương lịch bất kỳ. + Sử dụng phương pháp offset từ năm tham chiếu để đảm bảo chính xác. + """ + if not isinstance(year, int) or year <= 0: + return None + + offset = year - REFERENCE_YEAR + can_index = (REFERENCE_CAN_INDEX + offset) % len(THIEN_CAN) + chi_index = (REFERENCE_CHI_INDEX + offset) % len(DIA_CHI) + + can = THIEN_CAN[can_index] + chi = DIA_CHI[chi_index] + + return f"{can} {chi}" + + +def resolve_alias_to_year(alias: str | int) -> Optional[int]: + """ + Cố gắng giải mã một alias thành MỘT năm sinh cụ thể. + Ưu tiên Can Chi và năm viết tắt. + """ + alias_str = str(alias).strip() + alias_normalized = alias_str.title() + + # Trường hợp 1: Can Chi đầy đủ (ví dụ: "Bính Dần") + if alias_normalized in CAN_CHI_TO_YEARS and CAN_CHI_TO_YEARS[alias_normalized]: + possible_years = CAN_CHI_TO_YEARS[alias_normalized] + # Lọc ra các năm trong quá khứ và lấy năm gần nhất + past_years = [y for y in possible_years if y <= datetime.now().year] + if past_years: + return max(past_years) + + # Trường hợp 2: Năm sinh viết tắt (ví dụ: "91", "90") + match = re.match(r'^(\d{2})$', alias_str) + if match: + year_short = int(match.group(1)) + current_year_short = datetime.now().year % 100 + # Logic suy luận thế kỷ: nếu năm viết tắt lớn hơn năm hiện tại -> 19xx, ngược lại -> 20xx + if year_short > current_year_short: + return 1900 + year_short + else: + return 2000 + year_short + + return None + + +def resolve_alias_to_year_list(alias: str) -> List[int]: + """ + Giải mã một alias thành MỘT DANH SÁCH các năm sinh khả thi. + Dùng cho các trường hợp chung chung như "tuổi chuột". + """ + alias_lower = alias.strip().lower() + words = re.split(r'[\s\W]+', alias_lower) + + for word in words: + if word in ALIAS_TO_CON_GIAP: + con_giap = ALIAS_TO_CON_GIAP[word] + return CON_GIAP_TO_YEARS.get(con_giap, []) + + return [] + + +# --- Phần 4: Kiểm tra khi chạy trực tiếp file --- +if __name__ == "__main__": + print("--- CAN_CHI_TO_YEARS (một phần) ---") + print(f"Bính Dần: {CAN_CHI_TO_YEARS.get('Bính Dần')}") + print(f"Kỷ Tỵ: {CAN_CHI_TO_YEARS.get('Kỷ Tỵ')}") + + print("\n--- Kiểm tra hàm get_can_chi_from_year (ĐÃ SỬA LỖI) ---") + print(f"Năm 1990 -> {get_can_chi_from_year(1990)}") # Mong đợi Canh Ngọ + print(f"Năm 1991 -> {get_can_chi_from_year(1991)}") # Mong đợi Tân Mùi + print(f"Năm 2024 -> {get_can_chi_from_year(2024)}") # Mong đợi Giáp Thìn + print(f"Năm 1984 -> {get_can_chi_from_year(1984)}") # Mong đợi Giáp Tý + + print("\n--- Kiểm tra hàm resolve_alias_to_year ---") + print(f"'Bính Dần' -> {resolve_alias_to_year('Bính Dần')}") # Mong đợi 1986 + print(f"'Kỷ Tỵ' -> {resolve_alias_to_year('Kỷ Tỵ')}") # Mong đợi 1989 + print(f"'91' -> {resolve_alias_to_year('91')}") # Mong đợi 1991 + print(f"'05' -> {resolve_alias_to_year('05')}") # Mong đợi 2005 + + print("\n--- Kiểm tra hàm resolve_alias_to_year_list ---") + print(f"'tuổi chuột' -> {resolve_alias_to_year_list('tuổi chuột')}") + print(f"'tuổi mèo' -> {resolve_alias_to_year_list('tuổi mèo')}") + print(f"'Tỵ' -> {resolve_alias_to_year_list('Tỵ')}") \ No newline at end of file diff --git a/app/tools/general_tools.py b/app/tools/general_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..2c3e2ef80c1ff81a22f9645d0b13757f064f9b57 --- /dev/null +++ b/app/tools/general_tools.py @@ -0,0 +1,136 @@ +# app/tools/general_tools.py + +import logging +from typing import Optional, Dict, Any + +from app.database.connection import query_to_dataframe + +logger = logging.getLogger(__name__) + + +def get_huong_info(ten_huong: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu thông tin chi tiết về một hướng cụ thể từ bảng 'huong'. + + Args: + ten_huong: Tên của hướng (ví dụ: "Đông Bắc", "Nam"). + + Returns: + Một dictionary chứa thông tin chi tiết về hướng, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu thông tin cho Hướng: {ten_huong}") + huong_normalized = ten_huong.strip().title() + + sql_query = "SELECT * FROM huong WHERE tenhuong = :ten_huong" + params = {"ten_huong": huong_normalized} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy thông tin cho Hướng '{huong_normalized}'.") + return None + + huong_info = result_df.to_dict('records')[0] + logger.info(f"Đã lấy thông tin thành công cho Hướng {huong_normalized}.") + return huong_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu thông tin Hướng: {e}") + return None + + +def get_vat_pham_info(keyword: str = None, ten_vat_pham: str = None) -> Optional[Dict[str, Any]]: + """ + Tra cứu thông tin chi tiết về một vật phẩm phong thủy. + Ưu tiên tìm kiếm theo tên chính xác (ten_vat_pham), nếu không có sẽ tìm theo keyword (LIKE). + """ + if not ten_vat_pham and not keyword: + logger.warning("get_vat_pham_info được gọi mà không có tham số.") + return None + + if ten_vat_pham: + # --- Ưu tiên tìm kiếm chính xác theo tên --- + logger.info(f"Đang tra cứu vật phẩm theo tên chính xác: '{ten_vat_pham}'") + sql_query = "SELECT * FROM vat_pham_phong_thuy WHERE tenvatpham = :ten_vat_pham" + params = {"ten_vat_pham": ten_vat_pham.strip().title()} + else: + # --- Phương án dự phòng: tìm kiếm tương đối theo keyword --- + logger.info(f"Đang tra cứu vật phẩm theo keyword (LIKE): '{keyword}'") + # Tìm kiếm linh hoạt hơn, ví dụ người dùng gõ "ty huu" vẫn ra "Tỳ Hưu" + sql_query = "SELECT * FROM vat_pham_phong_thuy WHERE tenvatpham LIKE :keyword" + params = {"keyword": f"%{keyword.strip().title()}%"} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy thông tin vật phẩm với params: {params}.") + return None + + # Trả về kết quả đầu tiên tìm được + vat_pham_info = result_df.to_dict('records')[0] + logger.info(f"Đã lấy thông tin thành công cho vật phẩm: {vat_pham_info.get('tenvatpham')}") + return vat_pham_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu thông tin Vật phẩm: {e}") + return None + + +def get_phi_tinh_info(nam: int) -> Optional[Dict[str, Any]]: + """ + Tra cứu thông tin phi tinh lưu niên từ bảng 'phi_tinh_luu_nien'. + """ + # ... (Giữ nguyên code của hàm này) + logger.info(f"Đang tra cứu Phi tinh cho năm: {nam}") + + sql_query = "SELECT * FROM phi_tinh_luu_nien WHERE nam_duonglich = :nam" + params = {"nam": nam} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy thông tin Phi tinh cho năm {nam}.") + return None + + phi_tinh_info = result_df.to_dict('records')[0] + logger.info(f"Đã lấy thông tin Phi tinh thành công cho năm {nam}.") + return phi_tinh_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu thông tin Phi tinh: {e}") + return None + + +# --- Phần kiểm tra --- +if __name__ == '__main__': + # Thêm sys.path để chạy độc lập + import sys, os + + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + + print("--- Đang kiểm tra các tool trong general_tools.py ---") + + print("\n[Test 1] Tra cứu thông tin Hướng 'Đông Bắc':") + huong_result = get_huong_info("Đông Bắc") + if huong_result: + print(f" - Tên Hướng: {huong_result.get('tenhuong')}") + print(f" - Hành Ngũ Hành: {huong_result.get('hanhnguhanh')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 2] Tra cứu thông tin Vật phẩm 'Tỳ Hưu':") + vat_pham_result = get_vat_pham_info("Tỳ Hưu") + if vat_pham_result: + print(f" - Tên Vật phẩm: {vat_pham_result.get('tenvatpham')}") + print(f" - Công dụng chính: {vat_pham_result.get('congdungchinh_so1')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 3] Tra cứu Phi tinh cho năm 2025:") + phi_tinh_result = get_phi_tinh_info(2025) + if phi_tinh_result: + print(f" - Năm Âm lịch: {phi_tinh_result.get('nam_amlich_canchi')}") + print(f" - Hướng Đại Hung: {phi_tinh_result.get('phuongvi_daihung_so1')}") + print(f" - Hướng Đại Cát: {phi_tinh_result.get('phuongvi_daicat_so1')}") + else: + print(" - Không tìm thấy kết quả.") \ No newline at end of file diff --git a/app/tools/loan_dau_tools.py b/app/tools/loan_dau_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..68cd9e942666ce052c3ab93233c48b067b0fb98f --- /dev/null +++ b/app/tools/loan_dau_tools.py @@ -0,0 +1,106 @@ +# app/tools/loan_dau_tools.py + +import logging +from typing import Optional, Dict, Any, List + +from app.database.connection import query_to_dataframe + +logger = logging.getLogger(__name__) + + +def get_the_dat_cat_tuong_info(keyword: str = None, ten_the_dat: str = None) -> Optional[Dict[str, Any]]: + """ + Tìm thông tin về một thế đất tốt dựa vào keyword. + + Args: + keyword: Từ khóa mô tả thế đất (ví dụ: "sông ôm", "tựa núi"). + + Returns: + Thông tin về thế đất hợp nhất, hoặc None. + """ + logger.info(f"Đang tra cứu Thế đất Cát tường với keyword: '{keyword}'") + + if ten_the_dat: + sql_query = "SELECT * FROM loan_dau_cat_tuong WHERE tenthedat = :ten_the_dat" + params = {"ten_the_dat": ten_the_dat} + elif keyword: + sql_query = "SELECT * FROM loan_dau_cat_tuong WHERE keywords_nhandien LIKE :keyword" + params = {"keyword": f"%{keyword}%"} + else: + return None + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy Thế đất Cát tường nào khớp với '{keyword}'.") + return None + + # Trả về kết quả đầu tiên tìm thấy + the_dat_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy Thế đất Cát tường: {the_dat_info.get('tenthedat')}") + return the_dat_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu Thế đất Cát tường: {e}") + return None + + +def get_sat_khi_info(keyword: str = None, ten_sat_khi: str = None) -> Optional[Dict[str, Any]]: + """ + Tìm thông tin về một loại Sát khí ngoại cảnh dựa vào keyword. + + Args: + keyword: Từ khóa mô tả sát khí (ví dụ: "đường đâm", "khe hẹp"). + + Returns: + Thông tin về sát khí hợp nhất, hoặc None. + """ + logger.info(f"Đang tra cứu Sát khí với keyword: '{keyword}'") + + if ten_sat_khi: + sql_query = "SELECT * FROM ngoai_canh_sat_khi WHERE tensatkhi = :ten_sat_khi" + params = {"ten_sat_khi": ten_sat_khi} + elif keyword: + sql_query = "SELECT * FROM ngoai_canh_sat_khi WHERE keywords_nhandien LIKE :keyword" + params = {"keyword": f"%{keyword}%"} + else: + return None + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy Sát khí nào khớp với '{keyword}'.") + return None + + sat_khi_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy Sát khí: {sat_khi_info.get('tensatkhi')}") + return sat_khi_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu Sát khí: {e}") + return None + + +# --- Phần kiểm tra --- +if __name__ == '__main__': + import sys, os + + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + + print("--- Đang kiểm tra các tool trong loan_dau_tools.py ---") + + print("\n[Test 1] Tra cứu Thế đất Cát tường với keyword 'sông ôm':") + cat_tuong_result = get_the_dat_cat_tuong_info("sông ôm") + if cat_tuong_result: + print(f" - Tên Thế đất: {cat_tuong_result.get('tenthedat')}") + print(f" - Mức độ: {cat_tuong_result.get('mucdo_cattuong')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 2] Tra cứu Sát khí với keyword 'đường đâm':") + sat_khi_result = get_sat_khi_info("đường đâm") + if sat_khi_result: + print(f" - Tên Sát khí: {sat_khi_result.get('tensatkhi')}") + print(f" - Mức độ nguy hiểm: {sat_khi_result.get('mucdo_nguyhiem')}") + else: + print(" - Không tìm thấy kết quả.") \ No newline at end of file diff --git a/app/tools/ngu_hanh_tools.py b/app/tools/ngu_hanh_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..a1c9dcdb719cc069e35cea5b44972d0a107f306b --- /dev/null +++ b/app/tools/ngu_hanh_tools.py @@ -0,0 +1,163 @@ +# app/tools/ngu_hanh_tools.py + +import pandas as pd +import logging +from typing import Optional, Dict, Any + +# Import hàm query tiện ích từ module database +from app.database.connection import query_to_dataframe +from app.tools import can_chi_helper + +logger = logging.getLogger(__name__) + + +def get_cung_menh_by_year_gender(nam_sinh: int, gioi_tinh: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu Cung Mệnh, Nhóm Bát Trạch, và các thông tin liên quan từ bảng 'cung_menh_lookup' + dựa vào năm sinh âm lịch và giới tính. + + Args: + nam_sinh: Năm sinh âm lịch (ví dụ: 1991). + gioi_tinh: Giới tính ("Nam" hoặc "Nữ"). + + Returns: + Một dictionary chứa thông tin tra cứu được, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu Cung Mệnh cho năm sinh: {nam_sinh}, giới tính: {gioi_tinh}") + + # Chuẩn hóa đầu vào giới tính để khớp với dữ liệu trong CSDL + gioi_tinh_normalized = gioi_tinh.strip().capitalize() + + # Câu lệnh SQL an toàn với tham số hóa + sql_query = """ + SELECT * + FROM cung_menh_lookup + WHERE namsinh_amlich = :nam_sinh AND gioitinh = :gioi_tinh + """ + params = {"nam_sinh": nam_sinh, "gioi_tinh": gioi_tinh.strip().capitalize()} + + try: + result_df = query_to_dataframe(sql_query, params) + + if result_df.empty: + logger.warning(f"Không tìm thấy Cung Mệnh cho năm sinh {nam_sinh}, giới tính {gioi_tinh_normalized}.") + return None + + # Chuyển dòng đầu tiên của DataFrame thành một dictionary + # to_dict('records') trả về một list, ta lấy phần tử đầu tiên + cung_menh_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy Cung Mệnh: {cung_menh_info.get('cungmenh')}") + return cung_menh_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu Cung Mệnh: {e}") + return None + + +def get_menh_info(menh: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu thông tin chi tiết về một Mệnh Ngũ Hành từ bảng 'menh'. + + Args: + menh: Tên mệnh Ngũ Hành (ví dụ: "Kim", "Thổ"). + + Returns: + Một dictionary chứa thông tin về mệnh, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu thông tin cho Mệnh: {menh}") + menh_normalized = menh.strip().capitalize() + + sql_query = "SELECT * FROM menh WHERE tenmenh = :menh" + params = {"menh": menh.strip().capitalize()} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy thông tin cho Mệnh '{menh_normalized}'.") + return None + + menh_info = result_df.to_dict('records')[0] + logger.info(f"Đã lấy thông tin thành công cho Mệnh {menh_normalized}.") + return menh_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu thông tin Mệnh: {e}") + return None + + +def get_nap_am_info(nam_sinh: int) -> Optional[Dict[str, Any]]: + """ + Tra cứu Nạp Âm và Mệnh Ngũ Hành từ bảng 'nap_am' dựa trên năm sinh. + Lưu ý: Bảng này có thể cần được thiết kế lại tốt hơn, hiện tại đang giả định + bảng `nap_am` có cột `cac_nam_sinh_vi_du` chứa các năm. + + Args: + nam_sinh: Năm sinh âm lịch. + + Returns: + Một dictionary chứa thông tin Nạp Âm, hoặc None nếu không tìm thấy. + """ + logger.info(f"Đang tra cứu Nạp Âm (logic tính toán) cho năm sinh: {nam_sinh}") + + can_chi = can_chi_helper.get_can_chi_from_year(nam_sinh) + if not can_chi: + logger.warning(f"Không thể tính toán Can Chi cho năm {nam_sinh}.") + return None + logger.info(f"Năm {nam_sinh} tương ứng với Can Chi: '{can_chi}'") + + sql_query = "SELECT * FROM nap_am WHERE canchi_tuongung LIKE :can_chi_pattern" + params = {"can_chi_pattern": f"%{can_chi}%"} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy Nạp Âm nào tương ứng với Can Chi '{can_chi}' trong CSDL.") + return None + + # Trả về kết quả đầu tiên tìm được (chắc chắn chỉ có 1) + nap_am_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy Nạp Âm: {nap_am_info.get('tennapam')}") + return nap_am_info + + except Exception as e: + logger.error(f"Lỗi trong quá trình tra cứu Nạp Âm bằng logic tính toán: {e}") + return None + + +# --- Phần kiểm tra (chạy trực tiếp file này để test) --- +if __name__ == '__main__': + print("--- Đang kiểm tra các tool trong ngu_hanh_tools.py ---") + + print("\n[Test 1] Tra cứu Cung Mệnh cho Nữ 1991:") + cung_menh_result = get_cung_menh_by_year_gender(1991, "Nữ") + if cung_menh_result: + print(f" - Cung Mệnh: {cung_menh_result.get('cungmenh')}") + print(f" - Nhóm Bát Trạch: {cung_menh_result.get('nhombattrach')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 2] Tra cứu thông tin Mệnh 'Kim':") + menh_result = get_menh_info("Kim") + if menh_result: + print(f" - Tên Mệnh: {menh_result.get('tenmenh')}") + print(f" - Tính cách tích cực: {menh_result.get('tinhcach_tichcuc_keywords')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 3 - Logic mới] Tra cứu Nạp Âm cho năm 1990:") + nap_am_result = get_nap_am_info(1990) + if nap_am_result: + print(f" - Tên Nạp Âm: {nap_am_result.get('tennapam')}") # Mong đợi Lộ Bàng Thổ + print(f" - Diễn giải hình tượng: {nap_am_result.get('diengiai_hinhtuong')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 4 - Logic mới] Tra cứu Nạp Âm cho năm 2030 (không có trong ví dụ):") + nap_am_result_new = get_nap_am_info(2030) + if nap_am_result_new: + print(f" - Tên Nạp Âm: {nap_am_result_new.get('tennapam')}") # Mong đợi Canh Tuất - Thoa Xuyến Kim + print(f" - Diễn giải hình tượng: {nap_am_result_new.get('diengiai_hinhtuong')}") + else: + print(" - Không tìm thấy kết quả.") + +#python -m app.tools.bat_trach_tools diff --git a/app/tools/reranker_tools.py b/app/tools/reranker_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..b8bc6d7690f7225f09307e96cf0d6ddabb8f287f --- /dev/null +++ b/app/tools/reranker_tools.py @@ -0,0 +1,90 @@ +import logging +from typing import List, Dict, Any, Optional +from groq import Groq +from app.core.config import settings + +logger = logging.getLogger(__name__) + +try: + groq_client = Groq(api_key=settings.GROQ_API_KEY) +except Exception as e: + logger.error(f"Không thể khởi tạo Groq client cho reranker: {e}") + groq_client = None + +# Cần truy vấn CSDL để lấy mô tả chi tiết cho các ứng viên +from app.database.connection import query_to_dataframe + + +def _get_details_for_reranking(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Lấy mô tả chi tiết từ CSDL để LLM có thêm thông tin phán đoán.""" + detailed_candidates = [] + for candidate in candidates: + name = candidate.get('name') + item_type = candidate.get('type') + description = "" + if item_type == 'sat_khi': + df = query_to_dataframe("SELECT mota_nhandien FROM ngoai_canh_sat_khi WHERE tensatkhi = :name", + params={'name': name}) + if not df.empty: + description = df.iloc[0]['mota_nhandien'] + elif item_type == 'the_dat': + df = query_to_dataframe("SELECT mota_nhandien FROM loan_dau_cat_tuong WHERE tenthedat = :name", + params={'name': name}) + if not df.empty: + description = df.iloc[0]['mota_nhandien'] + + detailed_candidates.append({ + "name": name, + "type": item_type, + "description": description + }) + return detailed_candidates + + +def choose_best_loandau_candidate(user_query: str, candidates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """ + Sử dụng LLM để chọn ra ứng viên phù hợp nhất từ một danh sách. + """ + if not groq_client or not candidates: + return None + + # Lấy thêm mô tả chi tiết cho từng ứng viên + detailed_candidates = _get_details_for_reranking(candidates) + + # Xây dựng prompt + prompt = f""" + Bạn là một chuyên gia phân tích. Dựa vào câu hỏi của người dùng và danh sách các lựa chọn có thể, hãy chọn ra lựa chọn phù hợp nhất. + Chỉ trả về tên của lựa chọn đúng nhất dưới dạng JSON, ví dụ: {{"best_choice": "Tên Lựa Chọn"}}. Không giải thích gì thêm. + + Câu hỏi của người dùng: "{user_query}" + + Danh sách các lựa chọn: + """ + for i, candidate in enumerate(detailed_candidates): + prompt += f"\n{i + 1}. Tên: {candidate['name']}\n Mô tả: {candidate['description']}\n" + + prompt += "\nJSON output:" + + try: + logger.info("Gửi yêu cầu re-ranking đến LLM...") + chat_completion = groq_client.chat.completions.create( + messages=[{"role": "user", "content": prompt}], + model="gemma2-9b-it", + temperature=0, + response_format={"type": "json_object"}, + ) + response_str = chat_completion.choices[0].message.content + import json + best_choice_name = json.loads(response_str).get("best_choice") + + logger.info(f"LLM đã chọn: '{best_choice_name}'") + + # Tìm lại thông tin đầy đủ của ứng viên đã được chọn + for candidate in candidates: + if candidate.get("name") == best_choice_name: + return candidate + return None + + except Exception as e: + logger.error(f"Lỗi khi re-ranking với LLM: {e}") + return None \ No newline at end of file diff --git a/app/tools/semantic_search_tools.py b/app/tools/semantic_search_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..4133d12eee470f983aa4a364dea13a82cf1fc0b3 --- /dev/null +++ b/app/tools/semantic_search_tools.py @@ -0,0 +1,146 @@ +import faiss +import pickle +import os +import logging +from sentence_transformers import SentenceTransformer +from typing import Dict, Any, Optional, List + +logger = logging.getLogger(__name__) + +# --- Cấu hình và tải tài nguyên một lần khi module được import --- +# Điều này đảm bảo các file lớn chỉ được load vào bộ nhớ một lần. + +# Biến toàn cục để giữ các tài nguyên đã tải +loandau_index = None +loandau_info = None +item_index = None +item_info = None +model = None + +# Cờ để kiểm tra trạng thái tải +LOANDAU_RESOURCES_LOADED = False +ITEM_RESOURCES_LOADED = False + +try: + # --- Tải mô hình chung --- + PROCESSED_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'processed') + MODEL_NAME = 'bkai-foundation-models/vietnamese-bi-encoder' + + logger.info(f"Đang tải mô hình Sentence Transformer chung: '{MODEL_NAME}'...") + model = SentenceTransformer(MODEL_NAME) + logger.info("Tải mô hình chung thành công!") + + # --- Tải tài nguyên cho Loan Đầu --- + try: + logger.info("Đang tải tài nguyên Semantic Search cho Loan Đầu...") + loandau_index_path = os.path.join(PROCESSED_DATA_DIR, 'loandau.index') + loandau_info_path = os.path.join(PROCESSED_DATA_DIR, 'loandau_info.pkl') + + loandau_index = faiss.read_index(loandau_index_path) + with open(loandau_info_path, 'rb') as f: + loandau_info = pickle.load(f) + + LOANDAU_RESOURCES_LOADED = True + logger.info("Tải tài nguyên Loan Đầu thành công!") + except FileNotFoundError: + logger.warning( + "Không tìm thấy file index/info cho Loan Đầu. Tool 'find_most_similar_loandau' sẽ không hoạt động.") + except Exception as e: + logger.error(f"Lỗi khi tải tài nguyên Loan Đầu: {e}") + + # --- Tải tài nguyên cho Vật Phẩm --- + try: + logger.info("Đang tải tài nguyên Semantic Search cho Vật Phẩm...") + item_index_path = os.path.join(PROCESSED_DATA_DIR, 'item.index') + item_info_path = os.path.join(PROCESSED_DATA_DIR, 'item_info.pkl') + + item_index = faiss.read_index(item_index_path) + with open(item_info_path, 'rb') as f: + item_info = pickle.load(f) + + ITEM_RESOURCES_LOADED = True + logger.info("Tải tài nguyên Vật Phẩm thành công!") + except FileNotFoundError: + logger.warning("Không tìm thấy file index/info cho Vật Phẩm. Tool 'find_most_similar_item' sẽ không hoạt động.") + logger.warning("Vui lòng chạy script 'scripts/create_item_embeddings.py' trước.") + except Exception as e: + logger.error(f"Lỗi khi tải tài nguyên Vật Phẩm: {e}") + +except Exception as e: + logger.error( + f"LỖI NGHIÊM TRỌNG: Không thể tải mô hình embedding chính. Các tool semantic search sẽ thất bại. Lỗi: {e}") + + +def find_most_similar_loandau(query: str, k: int = 3, similarity_threshold: float = 0.5) -> List[Dict[str, Any]]: + """ + Tìm kiếm Top K Sát Khí hoặc Thế Đất Cát Tường tương đồng nhất. + """ + if not LOANDAU_RESOURCES_LOADED or model is None: + logger.error("Tài nguyên Loan Đầu chưa được tải, không thể thực hiện tìm kiếm.") + return [] + + logger.info(f"Đang thực hiện semantic search (Loan Đầu) cho query: '{query}' với K={k}") + + query_embedding = model.encode(query, convert_to_numpy=True).reshape(1, -1) + faiss.normalize_L2(query_embedding) + + # Tìm kiếm K kết quả gần nhất + similarity_scores, indices = loandau_index.search(query_embedding, k=k) + + results = [] + for i in range(k): + idx = indices[0][i] + similarity = similarity_scores[0][i] + + if similarity >= similarity_threshold: + match_info = loandau_info[idx].copy() + match_info['similarity_score'] = float(similarity) + results.append(match_info) + logger.info(f" - Tìm thấy ứng viên: {match_info['name']} (Score: {similarity:.2f})") + + return results + +# --- TOOL MỚI BẠN YÊU CẦU --- +def find_most_similar_item(query: str, similarity_threshold: float = 0.1) -> Optional[Dict[str, Any]]: + """ + Tìm kiếm Vật phẩm phong thủy tương đồng nhất với mô tả hoặc tên gọi khác của người dùng. + + Args: + query (str): Mô tả của người dùng (ví dụ: "cóc ngậm tiền", "vật phẩm chiêu tài"). + similarity_threshold (float): Ngưỡng điểm tương đồng để chấp nhận kết quả. + + Returns: + Optional[Dict[str, Any]]: Một dictionary chứa tên và thông tin của vật phẩm khớp nhất, + hoặc None nếu không tìm thấy kết quả nào đủ tốt. + """ + if not ITEM_RESOURCES_LOADED or model is None: + logger.error("Tài nguyên Vật Phẩm chưa được tải, không thể thực hiện tìm kiếm.") + return None + + logger.info(f"Đang thực hiện semantic search (Vật Phẩm) cho query: '{query}'") + + # 1. Tạo embedding cho câu query và chuẩn hóa nó + query_embedding = model.encode(query, convert_to_numpy=True).reshape(1, -1) + faiss.normalize_L2(query_embedding) + + # 2. Tìm kiếm trong chỉ mục FAISS của vật phẩm + # k=1: chỉ tìm 1 kết quả gần nhất + similarity_scores, indices = item_index.search(query_embedding, k=1) + + best_match_index = indices[0][0] + similarity = similarity_scores[0][0] + + logger.info( + f"Tìm thấy kết quả (Vật Phẩm) gần nhất ở index {best_match_index} với Cosine Similarity: {similarity:.2f}") + + # 3. Kiểm tra ngưỡng tương đồng + if similarity < similarity_threshold: + logger.warning(f"Độ tương đồng ({similarity:.2f}) thấp hơn ngưỡng ({similarity_threshold}). Bỏ qua kết quả.") + return None + + # 4. Trả về thông tin của kết quả khớp nhất từ metadata + best_match_info = item_info[best_match_index].copy() # Dùng copy() để an toàn + best_match_info['similarity_score'] = float(similarity) + best_match_info['lookup_method'] = 'cosine_similarity' + + return best_match_info \ No newline at end of file diff --git a/app/tools/tool_provider.py b/app/tools/tool_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/tools/tuong_tac_tools.py b/app/tools/tuong_tac_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..5c8db246c6ceb7af663ce1595d9ddcc3557ef262 --- /dev/null +++ b/app/tools/tuong_tac_tools.py @@ -0,0 +1,104 @@ +# app/tools/tuong_tac_tools.py + +import logging +from typing import Optional, Dict, Any + +from app.database.connection import query_to_dataframe + +logger = logging.getLogger(__name__) + + +def get_menh_huong_interaction(menh_gia_chu: str, huong_nha: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu quy tắc tương tác Ngũ Hành giữa Mệnh gia chủ và Hướng nhà. + + Args: + menh_gia_chu: Mệnh của gia chủ (ví dụ: "Kim", "Mộc"). + huong_nha: Hướng nhà (ví dụ: "Tây Bắc", "Đông"). + + Returns: + Một dictionary chứa thông tin quy tắc, hoặc None. + """ + logger.info(f"Đang tra cứu tương tác Mệnh-Hướng cho: {menh_gia_chu} - {huong_nha}") + + menh_normalized = menh_gia_chu.strip().capitalize() + huong_normalized = huong_nha.strip().title() + + sql_query = "SELECT * FROM menh_huong_rules WHERE menhgiachu = :menh AND huongnha = :huong" + params = {"menh": menh_normalized, "huong": huong_normalized} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning( + f"Không tìm thấy quy tắc tương tác cho Mệnh '{menh_normalized}' và Hướng '{huong_normalized}'.") + return None + + interaction_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy tương tác Mệnh-Hướng: {interaction_info.get('moiquanhe_nguhanh')}") + return interaction_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu tương tác Mệnh-Hướng: {e}") + return None + + +def get_menh_menh_interaction(nap_am1: str, nap_am2: str) -> Optional[Dict[str, Any]]: + """ + Tra cứu quy tắc tương tác giữa hai người dựa trên Nạp Âm của họ. + Lưu ý: Logic này có thể cần tìm cả hai chiều (A-B và B-A). + + Args: + nap_am1: Nạp âm của người thứ nhất. + nap_am2: Nạp âm của người thứ hai. + + Returns: + Một dictionary chứa thông tin quy tắc, hoặc None. + """ + logger.info(f"Đang tra cứu tương tác Mệnh-Mệnh cho: {nap_am1} - {nap_am2}") + + # Tìm theo cả 2 chiều + sql_query = """ + SELECT * FROM menh_menh_rules + WHERE (napam1 = :na1 AND napam2 = :na2) OR (napam1 = :na2 AND napam2 = :na1) + """ + params = {"na1": nap_am1.strip().title(), "na2": nap_am2.strip().title()} + + try: + result_df = query_to_dataframe(sql_query, params) + if result_df.empty: + logger.warning(f"Không tìm thấy quy tắc tương tác cho Nạp Âm '{nap_am1}' và '{nap_am2}'.") + return None + + interaction_info = result_df.to_dict('records')[0] + logger.info(f"Tìm thấy tương tác Mệnh-Mệnh: {interaction_info.get('moiquanhe_nguhanh')}") + return interaction_info + + except Exception as e: + logger.error(f"Lỗi khi tra cứu tương tác Mệnh-Mệnh: {e}") + return None + + +# --- Phần kiểm tra --- +if __name__ == '__main__': + import sys, os + + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + + print("--- Đang kiểm tra các tool trong tuong_tac_tools.py ---") + + print("\n[Test 1] Tra cứu tương tác Mệnh-Hướng cho Mệnh 'Kim', Hướng 'Tây Bắc':") + menh_huong_result = get_menh_huong_interaction("Kim", "Tây Bắc") + if menh_huong_result: + print(f" - Mối quan hệ: {menh_huong_result.get('moiquanhe_nguhanh')}") + print(f" - Kết luận: {menh_huong_result.get('ketluanchinh')}") + else: + print(" - Không tìm thấy kết quả.") + + print("\n[Test 2] Tra cứu tương tác Mệnh-Mệnh cho 'Kiếm Phong Kim' và 'Tùng Bách Mộc':") + menh_menh_result = get_menh_menh_interaction("Kiếm Phong Kim", "Tùng Bách Mộc") + if menh_menh_result: + print(f" - Mối quan hệ: {menh_menh_result.get('moiquanhe_nguhanh')}") + print(f" - Kết luận: {menh_menh_result.get('ketluanchinh')}") + else: + print(" - Không tìm thấy kết quả.") \ No newline at end of file diff --git a/data/raw/bat_trach_cung_vi.xlsx b/data/raw/bat_trach_cung_vi.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..46e3808eef05e37a4cb6fd9e3d470268cf7305a7 --- /dev/null +++ b/data/raw/bat_trach_cung_vi.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97cb271562de795909b11a6369c00307758741943f1876b753beaae4829774bd +size 14455 diff --git a/data/raw/cung_menh_huong_rules.xlsx b/data/raw/cung_menh_huong_rules.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1908df712e3d6942db6d4bf4421fd08eab373b0f --- /dev/null +++ b/data/raw/cung_menh_huong_rules.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79e4d5d855cbed2d9dcee7577867a1023b7266838156429356d6072090a144bd +size 23433 diff --git a/data/raw/cung_menh_lookup.xlsx b/data/raw/cung_menh_lookup.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b5bb6bb4790432e2cad0d0363db574118e0e8050 --- /dev/null +++ b/data/raw/cung_menh_lookup.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03914f988f74064ac95fb3e90927ac8540afdae2407b5d92564fa7ec28835a7a +size 43300 diff --git a/data/raw/huong.xlsx b/data/raw/huong.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b676bf4a0cbaac0bb1e81731d11e3ee82ac20fbc --- /dev/null +++ b/data/raw/huong.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d5462e49afd2858fa7ed7cdb4fdce4924d18e1adbfc17678bbc60f6fb62117e +size 16296 diff --git a/data/raw/loan_dau_cat_tuong.xlsx b/data/raw/loan_dau_cat_tuong.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4dae39a4ba29e387cc75aaeb9cdff73cd7f901e1 --- /dev/null +++ b/data/raw/loan_dau_cat_tuong.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06638f3aa051aad7f185baac1ee5e410160814588580cd316e473d443948e901 +size 127920 diff --git a/data/raw/menh.xlsx b/data/raw/menh.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5c9a6c65dab86aab7e10d2c4112adbd2529b9f8e --- /dev/null +++ b/data/raw/menh.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11c9ceea6502f4a21c1a3feb21f77d35b95ecd82c497fca55572b326f309665c +size 14492 diff --git a/data/raw/menh_huong_rules.xlsx b/data/raw/menh_huong_rules.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f392339e4d14a6ecd01982c729df944ebdd5bb61 --- /dev/null +++ b/data/raw/menh_huong_rules.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:329503c69eb6dfa2f84fefca42a759cfb6961ef3b3d4e8e20b9c52deaedf059c +size 71024 diff --git a/data/raw/menh_menh_rules.xlsx b/data/raw/menh_menh_rules.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f4b72dcb0be12f1861f4f42c5dc020cdcd44a9ad --- /dev/null +++ b/data/raw/menh_menh_rules.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:428925b602928792a0733c9364e89971da051c09ca9ec75910c5e1908f6b0e46 +size 80776 diff --git a/data/raw/menh_ngu_hanh_lookup.xlsx b/data/raw/menh_ngu_hanh_lookup.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2389639c75345d8faba5d98415e94e0dd0feb286 --- /dev/null +++ b/data/raw/menh_ngu_hanh_lookup.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:152cd3cbb7bfe71b2a23916e69a94cc4cd7d89da30145fb203057a4679a1d4b6 +size 12673 diff --git a/data/raw/nap_am.xlsx b/data/raw/nap_am.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d2da72129e4c271ac4231a7c9f6543fce847f5ad --- /dev/null +++ b/data/raw/nap_am.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:047dbf340f3b6d877b693ea0d78464c44c3656125a0c1788f7a556bf2146e07f +size 33710 diff --git a/data/raw/ngoai_canh_sat_khi.xlsx b/data/raw/ngoai_canh_sat_khi.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..35fd45ea00cc5f93f3425655a30719c32772b15a --- /dev/null +++ b/data/raw/ngoai_canh_sat_khi.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ce3b8eb3182817ad46297617bb2f1c0b03a1df442d6dd4f958006fb3d1e6923 +size 125292 diff --git a/data/raw/phi_tinh_luu_nien.xlsx b/data/raw/phi_tinh_luu_nien.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..78eed08e8872acc2375db05791c3774d11c5c42c --- /dev/null +++ b/data/raw/phi_tinh_luu_nien.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91fb1d6705a14d7d618f80ab7249d8c9064d97405edfaccfc0650093eb896517 +size 19505 diff --git a/data/raw/vat_pham_phong_thuy.xlsx b/data/raw/vat_pham_phong_thuy.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..43189c31574d173005a89fc6a20229554237f9fa --- /dev/null +++ b/data/raw/vat_pham_phong_thuy.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5176ea4e93e77db431ad890539a688606e0f1db052b6c2c63c3b2e1a9c40a05f +size 102580 diff --git a/data/raw/~$menh_menh_rules.xlsx b/data/raw/~$menh_menh_rules.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..234494c0d2189bb0c136041b90051bfcddb1214f --- /dev/null +++ b/data/raw/~$menh_menh_rules.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ba04959837c27019a2349015543802439e152ddc4baf4e8c7b9d2b483362a8 +size 165 diff --git a/data/raw/~$ngoai_canh_sat_khi.xlsx b/data/raw/~$ngoai_canh_sat_khi.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..234494c0d2189bb0c136041b90051bfcddb1214f --- /dev/null +++ b/data/raw/~$ngoai_canh_sat_khi.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ba04959837c27019a2349015543802439e152ddc4baf4e8c7b9d2b483362a8 +size 165 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..61f9d4f86b0c9415e538978a45936947ca5809cc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,178 @@ +accelerate==1.10.1 +aiofiles==24.1.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anyio==4.10.0 +asgiref==3.9.1 +attrs==25.3.0 +backoff==2.2.1 +bcrypt==4.3.0 +beautifulsoup4==4.13.5 +bitsandbytes==0.47.0 +blobfile==3.1.0 +build==1.3.0 +cachetools==5.5.2 +certifi==2025.8.3 +cffi==2.0.0 +charset-normalizer==3.4.3 +chromadb==1.0.21 +click==8.2.1 +colorama==0.4.6 +coloredlogs==15.0.1 +cryptography==45.0.7 +dataclasses-json==0.6.7 +distro==1.9.0 +durationpy==0.10 +emoji==2.14.1 +et_xmlfile==2.0.0 +faiss-cpu==1.12.0 +fastapi==0.116.1 +filelock==3.19.1 +filetype==1.2.0 +flatbuffers==25.2.10 +frozenlist==1.7.0 +fsspec==2025.9.0 +google-auth==2.40.3 +googleapis-common-protos==1.70.0 +greenlet==3.2.4 +groq==0.31.1 +grpcio==1.74.0 +h11==0.16.0 +html5lib==1.1 +httpcore==1.0.9 +httptools==0.6.4 +httpx==0.28.1 +httpx-sse==0.4.1 +huggingface-hub==0.34.4 +humanfriendly==10.0 +idna==3.10 +importlib_metadata==8.7.0 +importlib_resources==6.5.2 +Jinja2==3.1.6 +jiter==0.10.0 +joblib==1.5.2 +jsonpatch==1.33 +jsonpointer==3.0.0 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +kubernetes==33.1.0 +langchain==0.3.27 +langchain-chroma==0.2.6 +langchain-community==0.3.29 +langchain-core==0.3.76 +langchain-groq==0.3.8 +langchain-huggingface==0.3.1 +langchain-ollama==0.3.8 +langchain-openai==0.3.33 +langchain-text-splitters==0.3.11 +langdetect==1.0.9 +langgraph==0.6.8 +langgraph-checkpoint==2.1.1 +langgraph-prebuilt==0.6.4 +langgraph-sdk==0.2.9 +langsmith==0.4.27 +lxml==6.0.1 +Markdown==3.9 +markdown-it-py==4.0.0 +MarkupSafe==3.0.2 +marshmallow==3.26.1 +mdurl==0.1.2 +mmh3==5.2.0 +mpmath==1.3.0 +multidict==6.6.4 +mypy_extensions==1.1.0 +networkx==3.5 +nltk==3.9.1 +numpy==2.3.3 +oauthlib==3.3.1 +olefile==0.47 +ollama==0.5.3 +onnxruntime==1.22.1 +openai==1.107.1 +openpyxl==3.1.5 +opentelemetry-api==1.37.0 +opentelemetry-exporter-otlp-proto-common==1.37.0 +opentelemetry-exporter-otlp-proto-grpc==1.37.0 +opentelemetry-proto==1.37.0 +opentelemetry-sdk==1.37.0 +opentelemetry-semantic-conventions==0.58b0 +orjson==3.11.3 +ormsgpack==1.10.0 +overrides==7.7.0 +packaging==25.0 +pandas==2.3.2 +pillow==11.3.0 +posthog==5.4.0 +propcache==0.3.2 +protobuf==6.32.1 +psutil==7.0.0 +psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pybase64==1.4.2 +pycparser==2.23 +pycryptodomex==3.23.0 +pydantic==2.11.7 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +Pygments==2.19.2 +pypdf==6.0.0 +PyPika==0.48.9 +pyproject_hooks==1.2.0 +pyreadline3==3.5.4 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-iso639==2025.2.18 +python-magic==0.4.27 +python-oxmsg==0.0.2 +pytz==2025.2 +PyYAML==6.0.2 +RapidFuzz==3.14.1 +referencing==0.36.2 +regex==2025.9.1 +requests==2.32.5 +requests-oauthlib==2.0.0 +requests-toolbelt==1.0.0 +rich==14.1.0 +rpds-py==0.27.1 +rsa==4.9.1 +safetensors==0.6.2 +scikit-learn==1.7.2 +scipy==1.16.2 +sentence-transformers==5.1.0 +sentencepiece==0.2.1 +setuptools==80.9.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.8 +SQLAlchemy==2.0.43 +starlette==0.47.3 +sympy==1.14.0 +tenacity==9.1.2 +threadpoolctl==3.6.0 +tiktoken==0.11.0 +tokenizers==0.22.0 +torch==2.8.0 +tqdm==4.67.1 +transformers==4.56.1 +typer==0.17.4 +typing-inspect==0.9.0 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +tzdata==2025.2 +unstructured==0.18.14 +unstructured-client==0.42.3 +urllib3==2.5.0 +uvicorn==0.35.0 +watchfiles==1.1.0 +webencodings==0.5.1 +websocket-client==1.8.0 +websockets==15.0.1 +wrapt==1.17.3 +xxhash==3.6.0 +yarl==1.20.1 +zipp==3.23.0 +zstandard==0.24.0 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/create_embeddings.py b/scripts/create_embeddings.py new file mode 100644 index 0000000000000000000000000000000000000000..b5bfdb381e6ee05e5a251db75a4fc5f9d27a2300 --- /dev/null +++ b/scripts/create_embeddings.py @@ -0,0 +1,65 @@ +# scripts/create_embeddings.py +import sqlite3 +import pandas as pd +import faiss +import numpy as np +import pickle +import os +from sentence_transformers import SentenceTransformer + +# --- Cấu hình --- +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) +DB_PATH = os.path.join(PROJECT_ROOT, 'data', 'processed', 'phongthuy.sqlite') +OUTPUT_DIR = os.path.join(PROJECT_ROOT, 'data', 'processed') +MODEL_NAME = 'bkai-foundation-models/vietnamese-bi-encoder' # Model mới, mạnh hơn + +print("--- Bắt đầu tạo Vector Embeddings ---") + +# --- 1. Tải mô hình Embedding --- +print(f"Đang tải mô hình Sentence Transformer: {MODEL_NAME}...") +model = SentenceTransformer(MODEL_NAME) +print("Tải mô hình thành công.") + +# --- 2. Kết nối và đọc dữ liệu từ SQLite --- +conn = sqlite3.connect(DB_PATH) +df_sat_khi = pd.read_sql_query("SELECT tensatkhi, mota_nhandien, keywords_nhandien FROM ngoai_canh_sat_khi", conn) +df_cat_tuong = pd.read_sql_query("SELECT tenthedat, mota_nhandien, keywords_nhandien FROM loan_dau_cat_tuong", conn) +conn.close() +print(f"Đã đọc {len(df_sat_khi)} Sát Khí và {len(df_cat_tuong)} Thế Đất Cát Tường.") + +# --- 3. Chuẩn bị văn bản để tạo embedding --- +# Kết hợp mô tả và keywords để có ngữ nghĩa phong phú hơn +df_sat_khi['corpus'] = df_sat_khi['mota_nhandien'] + " " + df_sat_khi['keywords_nhandien'] +df_cat_tuong['corpus'] = df_cat_tuong['mota_nhandien'] + " " + df_cat_tuong['keywords_nhandien'] + +all_corpus = pd.concat([df_sat_khi['corpus'], df_cat_tuong['corpus']]).tolist() + +# Lưu lại thông tin gốc để tra cứu ngược +all_data_info = ( + [{'type': 'sat_khi', 'name': row['tensatkhi']} for index, row in df_sat_khi.iterrows()] + + [{'type': 'the_dat', 'name': row['tenthedat']} for index, row in df_cat_tuong.iterrows()] +) + +# --- 4. Tạo Embeddings --- +print("Đang tạo embeddings cho toàn bộ corpus... (việc này có thể mất vài phút)") +corpus_embeddings = model.encode(all_corpus, convert_to_tensor=True, show_progress_bar=True) +corpus_embeddings_np = corpus_embeddings.cpu().numpy() +print(f"Tạo embeddings thành công, shape: {corpus_embeddings_np.shape}") + +# --- 5. Xây dựng chỉ mục FAISS --- +faiss.normalize_L2(corpus_embeddings_np) +d = corpus_embeddings_np.shape[1] # Kích thước của vector +index = faiss.IndexFlatIP(d) # SỬ DỤNG IndexFlatIP +index.add(corpus_embeddings_np) +print(f"Đã xây dựng chỉ mục FAISS (IP) với {index.ntotal} vectors.") + +# --- 6. Lưu tất cả mọi thứ --- +faiss.write_index(index, os.path.join(OUTPUT_DIR, 'loandau.index')) +with open(os.path.join(OUTPUT_DIR, 'loandau_info.pkl'), 'wb') as f: + pickle.dump(all_data_info, f) + +print(f"--- Đã lưu thành công index và data info vào thư mục: {OUTPUT_DIR} ---") + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/scripts/create_item_embeddings.py b/scripts/create_item_embeddings.py new file mode 100644 index 0000000000000000000000000000000000000000..419f91719387f7ba2ccfa10c4bd1f454bedd2534 --- /dev/null +++ b/scripts/create_item_embeddings.py @@ -0,0 +1,114 @@ +import os +import pandas as pd +import sqlite3 +import faiss +import numpy as np +import pickle +import logging +from sentence_transformers import SentenceTransformer + +# --- Cấu hình Logging --- +# Thiết lập hệ thống ghi log để theo dõi tiến trình một cách chi tiết +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +# --- Định nghĩa các đường dẫn và hằng số --- +# Lấy đường dẫn tuyệt đối của thư mục chứa script này (scripts) +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +# Đi ngược lên một cấp để lấy thư mục gốc của project +PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) + +# Định nghĩa các đường dẫn CSDL và thư mục đầu ra +DB_PATH = os.path.join(PROJECT_ROOT, 'data', 'processed', 'phongthuy.sqlite') +OUTPUT_DIR = os.path.join(PROJECT_ROOT, 'data', 'processed') + +# Tên mô hình embedding mạnh mẽ cho tiếng Việt (chuyên cho retrieval) +MODEL_NAME = 'bkai-foundation-models/vietnamese-bi-encoder' + +def main(): + """ + Hàm chính điều phối toàn bộ quá trình tạo embeddings cho vật phẩm phong thủy. + """ + logging.info("--- BẮT ĐẦU QUÁ TRÌNH TẠO EMBEDDINGS CHO VẬT PHẨM ---") + + # --- 1. Tải mô hình Sentence Transformer --- + logging.info(f"Đang tải mô hình Sentence Transformer: '{MODEL_NAME}'...") + try: + model = SentenceTransformer(MODEL_NAME) + logging.info("Tải mô hình thành công.") + except Exception as e: + logging.error(f"Không thể tải mô hình embedding. Lỗi: {e}") + logging.error("Vui lòng kiểm tra kết nối mạng và tên mô hình.") + return + + # --- 2. Kết nối và đọc dữ liệu từ SQLite --- + logging.info(f"Đang kết nối tới CSDL tại: {DB_PATH}") + try: + conn = sqlite3.connect(DB_PATH) + df = pd.read_sql_query("SELECT * FROM vat_pham_phong_thuy", conn) + conn.close() + logging.info(f"Đã đọc thành công {len(df)} vật phẩm từ CSDL.") + except Exception as e: + logging.error(f"Không thể đọc dữ liệu từ bảng 'vat_pham_phong_thuy'. Lỗi: {e}") + return + + if df.empty: + logging.warning("Bảng 'vat_pham_phong_thuy' không có dữ liệu. Dừng quá trình.") + return + + # --- 3. Chuẩn bị Corpus và Dữ liệu tra cứu ngược (Metadata) --- + logging.info("Đang chuẩn bị corpus để tạo embedding...") + # Kết hợp các cột có giá trị ngữ nghĩa để tạo ra một văn bản đại diện phong phú + # .fillna('') để xử lý các ô trống, tránh lỗi + cols_to_combine = ['tenvatpham', 'tengoikhac', 'congdung_keywords', 'mota_truyenthuyet'] + df['corpus'] = df[cols_to_combine].fillna('').astype(str).agg(' '.join, axis=1) + + # Lấy danh sách corpus và metadata theo đúng thứ tự + corpus_list = df['corpus'].tolist() + # Metadata này dùng để tra cứu ngược từ index của vector về tên vật phẩm + metadata_list = [{'name': row['tenvatpham']} for index, row in df.iterrows()] + logging.info(f"Đã tạo {len(corpus_list)} văn bản trong corpus.") + + # --- 4. Tạo Embeddings --- + logging.info("Đang tạo embeddings cho toàn bộ corpus vật phẩm...") + # model.encode sẽ trả về một numpy array + corpus_embeddings = model.encode(corpus_list, convert_to_numpy=True, show_progress_bar=True) + logging.info(f"Tạo embeddings thành công, shape: {corpus_embeddings.shape}") + + # --- 5. Xây dựng chỉ mục FAISS (sử dụng Cosine Similarity) --- + logging.info("Đang xây dựng chỉ mục FAISS...") + # Chuẩn hóa các vector (bước bắt buộc để IndexFlatIP hoạt động như Cosine Similarity) + faiss.normalize_L2(corpus_embeddings) + + # Lấy số chiều của vector + dimension = corpus_embeddings.shape[1] + # Sử dụng IndexFlatIP, tối ưu cho việc tìm kiếm Cosine Similarity + index = faiss.IndexFlatIP(dimension) + # Thêm các vector đã được chuẩn hóa vào chỉ mục + index.add(corpus_embeddings) + logging.info(f"Đã xây dựng chỉ mục FAISS thành công với {index.ntotal} vectors.") + + # --- 6. Lưu các file kết quả --- + # Đảm bảo thư mục đầu ra tồn tại + os.makedirs(OUTPUT_DIR, exist_ok=True) + + index_path = os.path.join(OUTPUT_DIR, 'item.index') + info_path = os.path.join(OUTPUT_DIR, 'item_info.pkl') + + logging.info(f"Đang lưu chỉ mục FAISS vào: {index_path}") + faiss.write_index(index, index_path) + + logging.info(f"Đang lưu thông tin metadata vào: {info_path}") + with open(info_path, 'wb') as f: + pickle.dump(metadata_list, f) + + logging.info("--- HOÀN TẤT! Đã tạo và lưu thành công các file embedding cho vật phẩm. ---") + + +if __name__ == "__main__": + # Dòng này đảm bảo hàm main() chỉ chạy khi script được thực thi trực tiếp + # từ command line bằng lệnh: python scripts/create_item_embeddings.py + main() \ No newline at end of file diff --git a/scripts/preprocess_data.py b/scripts/preprocess_data.py new file mode 100644 index 0000000000000000000000000000000000000000..6fe43027f0f65a9ff962c7f6a67a18b91e7a74cd --- /dev/null +++ b/scripts/preprocess_data.py @@ -0,0 +1,139 @@ +import os +import pandas as pd +import sqlite3 +import glob +import logging +import re +import unicodedata + +# --- Cấu hình Logging --- +# Thiết lập hệ thống ghi log để theo dõi tiến trình một cách chi tiết +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +# --- Định nghĩa các đường dẫn --- +# Lấy đường dẫn tuyệt đối của thư mục chứa script này (scripts) +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +# Đi ngược lên một cấp để lấy thư mục gốc của project +PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) + +# Định nghĩa các thư mục dữ liệu đầu vào và đầu ra +RAW_DATA_DIR = os.path.join(PROJECT_ROOT, 'data', 'raw') +PROCESSED_DATA_DIR = os.path.join(PROJECT_ROOT, 'data', 'processed') +DB_PATH = os.path.join(PROCESSED_DATA_DIR, 'phongthuy.sqlite') + + +def normalize_text(text: str) -> str: + """ + Chuẩn hóa văn bản: chuyển thành chữ thường, bỏ dấu tiếng Việt, + thay thế các ký tự không phải chữ hoặc số bằng gạch dưới. + """ + if not isinstance(text, str): + text = str(text) + # Bỏ dấu tiếng Việt + text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8') + # Thay thế khoảng trắng và các ký tự đặc biệt bằng gạch dưới + text = re.sub(r'[\s\W]+', '_', text.lower()) + # Xóa các gạch dưới thừa ở đầu và cuối chuỗi + return text.strip('_') + + +def process_excel_file(excel_path: str, conn: sqlite3.Connection): + """ + Đọc một file Excel, chuẩn hóa tên cột và ghi dữ liệu vào một bảng trong CSDL SQLite. + Tên bảng sẽ được tạo dựa trên tên file Excel. + """ + try: + # Lấy tên file (không bao gồm phần mở rộng) để làm tên bảng + base_name = os.path.basename(excel_path) + table_name = os.path.splitext(base_name)[0] + logging.info(f"Đang xử lý file: '{base_name}' -> Bảng SQLite: '{table_name}'") + + # Đọc dữ liệu từ file Excel vào một DataFrame của Pandas + df = pd.read_excel(excel_path) + + # Xử lý trường hợp file Excel rỗng + if df.empty: + logging.warning(f"File '{base_name}' rỗng, bỏ qua.") + return + + # Chuẩn hóa tên các cột để tương thích với SQL + # Ví dụ: "1. Bảng tra cứu Bát Trạch (Cung Mệnh vs Hướng)" -> "1_bang_tra_cuu_bat_trach_cung_menh_vs_huong" + original_columns = df.columns.tolist() + df.columns = [normalize_text(col) for col in original_columns] + + # Ghi DataFrame vào bảng SQLite + # - if_exists='replace': Nếu bảng đã tồn tại, xóa đi và tạo lại. Điều này đảm bảo dữ liệu luôn mới nhất. + # - index=False: Không ghi chỉ số của DataFrame vào CSDL. + df.to_sql(table_name, conn, if_exists='replace', index=False) + + logging.info(f"Đã ghi thành công {len(df)} dòng vào bảng '{table_name}'.") + + except FileNotFoundError: + logging.error(f"Không tìm thấy file: {excel_path}") + except Exception as e: + logging.error(f"Gặp lỗi khi xử lý file '{os.path.basename(excel_path)}': {e}") + + +def main(): + """ + Hàm chính điều phối toàn bộ quá trình: + 1. Kiểm tra và tạo các thư mục cần thiết. + 2. Xóa database cũ (nếu có) để tạo mới. + 3. Tìm tất cả các file Excel trong thư mục raw. + 4. Xử lý từng file và ghi vào CSDL SQLite. + """ + logging.info("--- BẮT ĐẦU QUÁ TRÌNH TIỀN XỬ LÝ DỮ LIỆU ---") + + # Kiểm tra xem thư mục dữ liệu thô có tồn tại không + if not os.path.isdir(RAW_DATA_DIR): + logging.error(f"Thư mục dữ liệu thô không tồn tại: {RAW_DATA_DIR}") + logging.error("Vui lòng tạo thư mục 'data/raw' và đặt các file Excel vào đó.") + return + + # Tạo thư mục dữ liệu đã xử lý nếu nó chưa tồn tại + os.makedirs(PROCESSED_DATA_DIR, exist_ok=True) + logging.info(f"Đã đảm bảo thư mục đầu ra tồn tại: {PROCESSED_DATA_DIR}") + + # Xóa file database cũ nếu tồn tại để đảm bảo tạo mới hoàn toàn + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + logging.info(f"Đã xóa database cũ tại: {DB_PATH}") + + # Tìm tất cả các file có đuôi .xlsx trong thư mục dữ liệu thô + excel_files = glob.glob(os.path.join(RAW_DATA_DIR, '*.xlsx')) + + if not excel_files: + logging.warning(f"Không tìm thấy file Excel nào trong thư mục: {RAW_DATA_DIR}") + logging.info("--- KẾT THÚC QUÁ TRÌNH (KHÔNG CÓ GÌ ĐỂ LÀM) ---") + return + + logging.info(f"Tìm thấy {len(excel_files)} file Excel cần xử lý.") + + conn = None + try: + # Tạo kết nối đến file CSDL SQLite + conn = sqlite3.connect(DB_PATH) + logging.info(f"Đã tạo kết nối tới CSDL SQLite tại: {DB_PATH}") + + # Lặp qua từng file Excel và xử lý + for file_path in excel_files: + process_excel_file(file_path, conn) + + except sqlite3.Error as e: + logging.error(f"Lỗi CSDL SQLite: {e}") + finally: + # Đảm bảo kết nối CSDL luôn được đóng, dù có lỗi hay không + if conn: + conn.close() + logging.info("Đã đóng kết nối CSDL.") + + logging.info(f"--- HOÀN TẤT QUÁ TRÌNH TIỀN XỬ LÝ. ĐÃ TẠO DATABASE TẠI: {DB_PATH} ---") + + +if __name__ == "__main__": + # Dòng này đảm bảo hàm main() chỉ chạy khi script được thực thi trực tiếp + main() \ No newline at end of file