anh-khoa-nguyen commited on
Commit
f972c00
·
1 Parent(s): 571a7d8

first commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +209 -0
  3. Dockerfile +31 -0
  4. LICENSE +21 -0
  5. README.md +4 -4
  6. app/__init__.py +0 -0
  7. app/core/__init__.py +0 -0
  8. app/core/config.py +62 -0
  9. app/core/logging_config.py +0 -0
  10. app/database/__init__.py +0 -0
  11. app/database/connection.py +92 -0
  12. app/database/models.py +0 -0
  13. app/main.py +167 -0
  14. app/orchestrator/__init__.py +0 -0
  15. app/orchestrator/workflow_manager.py +99 -0
  16. app/orchestrator/workflows/__init__.py +0 -0
  17. app/orchestrator/workflows/analyze_house.py +93 -0
  18. app/orchestrator/workflows/base_workflow.py +42 -0
  19. app/orchestrator/workflows/compare_people.py +60 -0
  20. app/orchestrator/workflows/lookup_item.py +60 -0
  21. app/orchestrator/workflows/lookup_loandau.py +90 -0
  22. app/orchestrator/workflows/lookup_namsinh.py +103 -0
  23. app/services/__init__.py +0 -0
  24. app/services/context_manager.py +77 -0
  25. app/services/intent_analyzer.py +124 -0
  26. app/services/prompt_templates.py +93 -0
  27. app/services/response_synthesizer.py +210 -0
  28. app/tools/__init__.py +0 -0
  29. app/tools/bat_trach_tools.py +108 -0
  30. app/tools/can_chi_helper.py +139 -0
  31. app/tools/general_tools.py +136 -0
  32. app/tools/loan_dau_tools.py +106 -0
  33. app/tools/ngu_hanh_tools.py +163 -0
  34. app/tools/reranker_tools.py +90 -0
  35. app/tools/semantic_search_tools.py +146 -0
  36. app/tools/tool_provider.py +0 -0
  37. app/tools/tuong_tac_tools.py +104 -0
  38. data/raw/bat_trach_cung_vi.xlsx +3 -0
  39. data/raw/cung_menh_huong_rules.xlsx +3 -0
  40. data/raw/cung_menh_lookup.xlsx +3 -0
  41. data/raw/huong.xlsx +3 -0
  42. data/raw/loan_dau_cat_tuong.xlsx +3 -0
  43. data/raw/menh.xlsx +3 -0
  44. data/raw/menh_huong_rules.xlsx +3 -0
  45. data/raw/menh_menh_rules.xlsx +3 -0
  46. data/raw/menh_ngu_hanh_lookup.xlsx +3 -0
  47. data/raw/nap_am.xlsx +3 -0
  48. data/raw/ngoai_canh_sat_khi.xlsx +3 -0
  49. data/raw/phi_tinh_luu_nien.xlsx +3 -0
  50. data/raw/vat_pham_phong_thuy.xlsx +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/raw/*.xlsx filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ data/processed/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+ #poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ #pdm.lock
118
+ #pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ #pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .envrc
142
+ .venv
143
+ env/
144
+ venv/
145
+ ENV/
146
+ env.bak/
147
+ venv.bak/
148
+
149
+ # Spyder project settings
150
+ .spyderproject
151
+ .spyproject
152
+
153
+ # Rope project settings
154
+ .ropeproject
155
+
156
+ # mkdocs documentation
157
+ /site
158
+
159
+ # mypy
160
+ .mypy_cache/
161
+ .dmypy.json
162
+ dmypy.json
163
+
164
+ # Pyre type checker
165
+ .pyre/
166
+
167
+ # pytype static type analyzer
168
+ .pytype/
169
+
170
+ # Cython debug symbols
171
+ cython_debug/
172
+
173
+ # PyCharm
174
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
175
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
176
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
177
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
178
+ #.idea/
179
+
180
+ # Abstra
181
+ # Abstra is an AI-powered process automation framework.
182
+ # Ignore directories containing user credentials, local state, and settings.
183
+ # Learn more at https://abstra.io/docs
184
+ .abstra/
185
+
186
+ # Visual Studio Code
187
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
188
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
189
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
190
+ # you could uncomment the following to ignore the entire vscode folder
191
+ # .vscode/
192
+
193
+ # Ruff stuff:
194
+ .ruff_cache/
195
+
196
+ # PyPI configuration file
197
+ .pypirc
198
+
199
+ # Cursor
200
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
201
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
202
+ # refer to https://docs.cursor.com/context/ignore-files
203
+ .cursorignore
204
+ .cursorindexingignore
205
+
206
+ # Marimo
207
+ marimo/_static/
208
+ marimo/_lsp/
209
+ __marimo__/
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sử dụng một ảnh base Python 3.13 chính thức, phiên bản slim
2
+ FROM python:3.13-slim
3
+
4
+ # Thiết lập các biến môi trường để tối ưu hóa Python
5
+ ENV PYTHONDONTWRITEBYTECODE 1
6
+ ENV PYTHONUNBUFFERED 1
7
+
8
+ # Thiết lập thư mục làm việc bên trong container
9
+ WORKDIR /app
10
+
11
+ # Sao chép file requirements.txt vào trước
12
+ COPY ./requirements.txt /app/requirements.txt
13
+
14
+ # Cài đặt các thư viện
15
+ RUN python -m venv /opt/venv
16
+ ENV PATH="/opt/venv/bin:$PATH"
17
+ RUN pip install --no-cache-dir -r /app/requirements.txt
18
+
19
+ # Sao chép toàn bộ code của ứng dụng vào container
20
+ COPY . /app/
21
+
22
+ # --- THÊM BƯỚC NÀY ---
23
+ # Chạy script để chuyển đổi file Excel thành CSDL SQLite
24
+ # Bước này sẽ tạo ra thư mục data/processed/phongthuy.sqlite bên trong container
25
+ RUN python scripts/preprocess_data.py
26
+
27
+ # Expose cổng mà uvicorn sẽ chạy
28
+ EXPOSE 7860
29
+
30
+ # Lệnh để khởi động ứng dụng FastAPI
31
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 anh-khoa-nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: Landify Chatbot V2
3
- emoji: 👁
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: mit
 
1
  ---
2
+ title: Landify Fengshui Chatbot V2
3
+ emoji: 🌖
4
+ colorFrom: red
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  license: mit
app/__init__.py ADDED
File without changes
app/core/__init__.py ADDED
File without changes
app/core/config.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/core/config.py
2
+
3
+ import os
4
+ from pydantic_settings import BaseSettings
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ # --- Xác định đường dẫn gốc của project ---
9
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
10
+
11
+ class Settings(BaseSettings):
12
+ """
13
+ Lớp quản lý cấu hình cho toàn bộ ứng dụng.
14
+ Sử dụng Pydantic để validate và đọc các biến môi trường từ file .env.
15
+ """
16
+ # --- Cấu hình chung cho project ---
17
+ PROJECT_NAME: str = "Phong Thuy Chatbot v2"
18
+ DEBUG: bool = True
19
+
20
+ # --- Cấu hình đường dẫn CSDL ---
21
+ # Ưu tiên đọc DATABASE_URL từ file .env trước.
22
+ # Nếu không có, mới tự động tạo đường dẫn tới file SQLite.
23
+ # Điều này giúp code linh hoạt hơn.
24
+ DATABASE_URL: Optional[str] = None
25
+
26
+ # --- Cấu hình API Keys ---
27
+ # Khai báo tất cả các API key có trong file .env của bạn
28
+ # Pydantic sẽ tự động tìm và nạp các biến này.
29
+ GROQ_API_KEY: str
30
+ OPENAI_API_KEY: str
31
+ HUGGINGFACEHUB_API_TOKEN: str
32
+
33
+ # Cấu hình để Pydantic biết đọc từ file .env
34
+ class Config:
35
+ env_file = os.path.join(PROJECT_ROOT, ".env")
36
+ env_file_encoding = 'utf-8'
37
+
38
+ # Tạo một instance của Settings
39
+ settings = Settings()
40
+
41
+ # --- Xử lý logic cho DATABASE_URL ---
42
+ # Nếu người dùng không cung cấp DATABASE_URL trong .env,
43
+ # chúng ta sẽ tự động gán đường dẫn mặc định tới file SQLite.
44
+ if settings.DATABASE_URL is None:
45
+ default_db_path = os.path.join(PROJECT_ROOT, 'data', 'processed', 'phongthuy.sqlite')
46
+ # Kiểm tra xem file SQLite có thực sự tồn tại không
47
+ if not os.path.exists(default_db_path):
48
+ raise FileNotFoundError(
49
+ 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}. "
50
+ "Vui lòng chạy script 'scripts/preprocess_data.py' trước."
51
+ )
52
+ settings.DATABASE_URL = f"sqlite:///{default_db_path}"
53
+
54
+
55
+ # In ra để kiểm tra khi khởi chạy (chỉ cho mục đích debug)
56
+ if __name__ == "__main__":
57
+ print("--- Cấu hình ứng dụng ---")
58
+ print(f"Tên Project: {settings.PROJECT_NAME}")
59
+ print(f"Đường dẫn CSDL đang sử dụng: {settings.DATABASE_URL}")
60
+ print(f"GROQ API Key đã được load: {'Có' if settings.GROQ_API_KEY else 'Không'}")
61
+ print(f"OpenAI API Key đã được load: {'Có' if settings.OPENAI_API_KEY else 'Không'}")
62
+ print(f"HuggingFace API Token đã được load: {'Có' if settings.HUGGINGFACEHUB_API_TOKEN else 'Không'}")
app/core/logging_config.py ADDED
File without changes
app/database/__init__.py ADDED
File without changes
app/database/connection.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/database/connection.py
2
+
3
+ import sqlite3
4
+ import pandas as pd
5
+ from sqlalchemy import create_engine
6
+ from sqlalchemy.orm import sessionmaker
7
+ from contextlib import contextmanager
8
+ import logging
9
+
10
+ # Import đối tượng settings từ module config
11
+ from app.core.config import settings
12
+
13
+ # --- Thiết lập SQLAlchemy ---
14
+ # create_engine là điểm khởi đầu cho bất kỳ ứng dụng SQLAlchemy nào.
15
+ # Nó thiết lập một "nhà máy" kết nối đến CSDL của chúng ta.
16
+ # connect_args={"check_same_thread": False} là một yêu cầu đặc biệt cho SQLite
17
+ # khi sử dụng trong các ứng dụng đa luồng như FastAPI.
18
+ try:
19
+ engine = create_engine(
20
+ settings.DATABASE_URL,
21
+ connect_args={"check_same_thread": False}
22
+ )
23
+ # SessionLocal là một "nhà máy" tạo ra các phiên làm việc (session) với CSDL.
24
+ # Mỗi instance của SessionLocal sẽ là một session riêng biệt.
25
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
26
+ logging.info("Kết nối SQLAlchemy đến CSDL đã được thiết lập thành công.")
27
+ except Exception as e:
28
+ logging.error(f"Lỗi khi thiết lập kết nối SQLAlchemy: {e}")
29
+ engine = None
30
+ SessionLocal = None
31
+
32
+
33
+ @contextmanager
34
+ def get_db():
35
+ """
36
+ Cung cấp một session CSDL và đảm bảo nó được đóng đúng cách.
37
+ Đây là một Dependency Injection pattern thường dùng trong FastAPI.
38
+ """
39
+ if SessionLocal is None:
40
+ raise ConnectionError("Không thể tạo session do lỗi kết nối CSDL ban đầu.")
41
+
42
+ db = SessionLocal()
43
+ try:
44
+ yield db
45
+ finally:
46
+ db.close()
47
+
48
+
49
+ # --- Hàm tiện ích để truy vấn trực tiếp bằng Pandas (rất hữu ích cho tools) ---
50
+ def query_to_dataframe(query: str, params: dict = None) -> pd.DataFrame:
51
+ """
52
+ Thực thi một câu lệnh SQL và trả về kết quả dưới dạng Pandas DataFrame.
53
+ An toàn hơn khi sử dụng params để tránh SQL Injection.
54
+ """
55
+ if engine is None:
56
+ raise ConnectionError("Không thể thực thi query do lỗi kết nối CSDL ban đầu.")
57
+
58
+ try:
59
+ with engine.connect() as connection:
60
+ df = pd.read_sql(query, connection, params=params)
61
+ return df
62
+ except Exception as e:
63
+ logging.error(f"Lỗi khi thực thi query: {query} với params: {params}. Lỗi: {e}")
64
+ # Trả về DataFrame rỗng nếu có lỗi
65
+ return pd.DataFrame()
66
+
67
+
68
+ # --- Hàm kiểm tra kết nối ---
69
+ def test_connection():
70
+ """Kiểm tra xem có thể kết nối và đọc dữ liệu từ một bảng không."""
71
+ try:
72
+ logging.info("Đang kiểm tra kết nối CSDL...")
73
+ # Thử đọc 1 dòng từ bảng 'menh' (giả sử bảng này tồn tại)
74
+ df = query_to_dataframe("SELECT * FROM menh LIMIT 1")
75
+ if not df.empty:
76
+ logging.info("Kiểm tra kết nối CSDL thành công! Có thể đọc dữ liệu.")
77
+ return True
78
+ else:
79
+ 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.")
80
+ return False
81
+ except Exception as e:
82
+ logging.error(f"Kiểm tra kết nối CSDL thất bại: {e}")
83
+ return False
84
+
85
+
86
+ if __name__ == "__main__":
87
+ test_connection()
88
+ # Ví dụ cách sử dụng
89
+ # sample_query = "SELECT * FROM menh WHERE hanh_ngu_hanh = :menh"
90
+ # result_df = query_to_dataframe(sample_query, params={"menh": "Kim"})
91
+ # print("\nKết quả truy vấn mẫu:")
92
+ # print(result_df)
app/database/models.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/main.py
2
+
3
+ import logging
4
+ from fastapi import FastAPI, HTTPException
5
+ from pydantic import BaseModel
6
+ from typing import List, Dict, Any
7
+ import uuid
8
+
9
+ # Import các module đã tạo
10
+ from app.core.config import settings
11
+ from app.database.connection import test_connection
12
+ from app.services.intent_analyzer import analyze_intent, IntentResult, ExtractedEntities
13
+ from app.orchestrator.workflow_manager import run_workflow, preprocess_entities
14
+ from app.services.response_synthesizer import synthesize_response
15
+ from app.services.context_manager import ToolCallRecord, ChatContext
16
+ from fastapi.responses import RedirectResponse
17
+
18
+ # --- Cấu hình Logging ---
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+ description_md = """
23
+ ### Microservice Chatbot Tư vấn Phong Thủy 🔮
24
+
25
+ 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:
26
+
27
+ 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.
28
+ 2. **Quản lý ngữ cảnh** hội thoại, hỏi lại thông tin còn thiếu.
29
+ 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.
30
+ 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.
31
+
32
+ _API được xây dựng với FastAPI._
33
+ """
34
+
35
+ # --- Khởi tạo ứng dụng FastAPI ---
36
+ app = FastAPI(
37
+ title=settings.PROJECT_NAME,
38
+ debug=settings.DEBUG,
39
+ description=description_md,
40
+ )
41
+
42
+ # Key: session_id (str), Value: ChatContext object
43
+ CONTEXT_STORE: Dict[str, ChatContext] = {}
44
+
45
+ # --- Định nghĩa model cho request body ---
46
+ class ChatRequest(BaseModel):
47
+ query: str
48
+ session_id: str
49
+
50
+ class DebugInfo(BaseModel):
51
+ intent: str
52
+ entities: Dict[str, Any]
53
+ tool_calls: List[ToolCallRecord]
54
+
55
+ class ChatResponse(BaseModel):
56
+ answer: str
57
+ debug_info: DebugInfo
58
+
59
+ class SessionResponse(BaseModel):
60
+ session_id: str
61
+
62
+ @app.on_event("startup")
63
+ async def startup_event():
64
+ logger.info("--- Ứng dụng Chatbot Phong Thủy đang khởi động ---")
65
+ if not test_connection():
66
+ 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.")
67
+ else:
68
+ logger.info(">>> Kết nối CSDL đã sẵn sàng.")
69
+
70
+ @app.get("/", include_in_schema=False)
71
+ async def root():
72
+ """
73
+ Khi người dùng truy cập trang gốc, tự động chuyển hướng đến trang tài liệu API.
74
+ """
75
+ return RedirectResponse(url="/docs")
76
+
77
+ @app.post("/session", tags=["General"])
78
+ async def create_session():
79
+ """Tạo một session_id duy nhất cho một cuộc trò chuyện mới."""
80
+ session_id = str(uuid.uuid4())
81
+ # Khởi tạo một context rỗng cho session mới
82
+ CONTEXT_STORE[session_id] = ChatContext()
83
+ logger.info(f"Đã tạo session mới: {session_id}")
84
+ return {"session_id": session_id}
85
+
86
+ @app.post("/chat", response_model=ChatResponse, tags=["Chatbot"])
87
+ @app.post("/chat", tags=["Chatbot"])
88
+ async def handle_chat(request: ChatRequest):
89
+ """
90
+ Endpoint chính để xử lý yêu cầu chat, với logic quản lý ngữ cảnh được cải tiến.
91
+ """
92
+ try:
93
+ session_id = request.session_id
94
+ logger.info(f"Nhận được query: '{request.query}' cho session_id: {session_id}")
95
+
96
+ # --- Giai đoạn 0: Lấy và Hợp nhất Ngữ cảnh (LOGIC MỚI) ---
97
+ previous_context = CONTEXT_STORE.get(session_id, ChatContext())
98
+ current_intent_result = await analyze_intent(request.query)
99
+
100
+ final_intent_name = current_intent_result.intent
101
+ base_entities = ExtractedEntities() # Tạo một entities rỗng
102
+
103
+ # Quyết định xem nên giữ lại ngữ cảnh cũ hay bắt đầu mới
104
+ is_continuing_conversation = (
105
+ previous_context.missing_info and
106
+ previous_context.intent_name not in ["UNKNOWN", "GREETING", None]
107
+ )
108
+
109
+ if is_continuing_conversation:
110
+ # --- TRƯỜNG HỢP 1: Đang trả lời câu hỏi của chatbot ---
111
+ logger.info("Phát hiện đang tiếp tục cuộc trò chuyện.")
112
+ # Giữ lại intent của luồng cũ
113
+ final_intent_name = previous_context.intent_name
114
+ # Lấy entities từ luồng cũ làm nền
115
+ base_entities = previous_context.initial_entities
116
+ else:
117
+ # --- TRƯỜNG HỢP 2: Bắt đầu một chủ đề mới ---
118
+ logger.info("Bắt đầu một chủ đề trò chuyện mới.")
119
+ # Intent sẽ là intent của câu nói hiện tại
120
+ # base_entities là rỗng, bắt đầu lại từ đầu
121
+ pass
122
+
123
+ # Hợp nhất: Lấy base_entities và cập nhật bằng thông tin mới
124
+ merged_entities = base_entities.model_copy(
125
+ update=current_intent_result.entities.model_dump(exclude_unset=True, exclude_none=True)
126
+ )
127
+
128
+ final_intent_result = IntentResult(intent=final_intent_name, entities=merged_entities)
129
+
130
+ logger.info(f"Intent cuối cùng được chọn: '{final_intent_result.intent}'")
131
+ logger.info(f"Entities sau khi hợp nhất: {merged_entities.model_dump_json(indent=2)}")
132
+
133
+ # --- Giai đoạn 1: Tiền xử lý entities ---
134
+ from app.orchestrator.workflow_manager import preprocess_entities
135
+ final_intent_result.entities = await preprocess_entities(final_intent_result.entities)
136
+
137
+ # --- Giai đoạn 2: Chạy workflow ---
138
+ final_context = await run_workflow(final_intent_result)
139
+
140
+ # --- Giai đoạn 3: Tổng hợp câu trả lời ---
141
+ final_answer = await synthesize_response(final_context)
142
+
143
+ # --- Giai đoạn 4: Lưu ngữ cảnh ---
144
+ # Nếu workflow đã hoàn thành (không còn missing_info),
145
+ # chúng ta có thể cân nhắc xóa bớt entities để chuẩn bị cho lượt sau.
146
+ # Tuy nhiên, để đơn giản, cứ lưu lại toàn bộ.
147
+ final_context.initial_entities = final_intent_result.entities
148
+ CONTEXT_STORE[session_id] = final_context
149
+ logger.info(f"Đã cập nhật context cho session_id: {session_id}")
150
+
151
+ debug_info = DebugInfo(
152
+ intent=final_context.intent_name,
153
+ entities=final_context.initial_entities.model_dump(exclude_unset=True, exclude_none=True),
154
+ tool_calls=final_context.tool_calls
155
+ )
156
+
157
+ return ChatResponse(answer=final_answer, debug_info=debug_info)
158
+
159
+ except Exception as e:
160
+ logger.exception(f"Lỗi nghiêm trọng trong quá trình xử lý chat cho session {session_id}: {e}")
161
+ # Xóa context bị lỗi để tránh ảnh hưởng đến các lần sau
162
+ if session_id in CONTEXT_STORE:
163
+ del CONTEXT_STORE[session_id]
164
+ 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.")
165
+
166
+ # Để chạy ứng dụng, mở terminal và gõ lệnh:
167
+ # uvicorn app.main:app --reload
app/orchestrator/__init__.py ADDED
File without changes
app/orchestrator/workflow_manager.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflow_manager.py
2
+
3
+ import logging
4
+ from app.services.context_manager import ChatContext
5
+ from app.services.intent_analyzer import IntentResult
6
+
7
+ # Import các lớp workflow cụ thể
8
+ from app.orchestrator.workflows.analyze_house import AnalyzeHouseWorkflow
9
+ from app.orchestrator.workflows.compare_people import ComparePeopleWorkflow
10
+ from app.orchestrator.workflows.lookup_item import LookupItemWorkflow
11
+ from app.orchestrator.workflows.lookup_loandau import LookupLoanDauWorkflow
12
+ from app.orchestrator.workflows.lookup_namsinh import LookupNamSinhWorkflow
13
+ from app.tools import can_chi_helper
14
+
15
+ # ... import các workflow khác ở đây khi bạn tạo chúng (ví dụ: ComparePeopleWorkflow)
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Ánh xạ từ tên intent sang lớp workflow tương ứng
20
+ WORKFLOW_MAPPING = {
21
+ "ANALYZE_HOUSE": AnalyzeHouseWorkflow,
22
+ "COMPARE_PEOPLE": ComparePeopleWorkflow,
23
+ "LOOKUP_ITEM": LookupItemWorkflow,
24
+ "LOOKUP_LOANDAU": LookupLoanDauWorkflow,
25
+ "LOOKUP_NAMSINH": LookupNamSinhWorkflow,
26
+ }
27
+
28
+
29
+ async def preprocess_entities(entities):
30
+ """Tiền xử lý entities để giải mã các alias về năm sinh."""
31
+ # Xử lý cho người thứ nhất
32
+ if not entities.nam_sinh_1 and entities.nam_sinh_alias_1:
33
+ logger.info(f"Tiền xử lý: Đang giải mã alias người 1: '{entities.nam_sinh_alias_1}'")
34
+ resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_alias_1)
35
+ if resolved_year:
36
+ entities.nam_sinh_1 = resolved_year
37
+ logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}")
38
+
39
+ # Xử lý cho người thứ hai (cho intent COMPARE_PEOPLE)
40
+ if not entities.nam_sinh_2 and entities.nam_sinh_alias_2:
41
+ logger.info(f"Tiền xử lý: Đang giải mã alias người 2: '{entities.nam_sinh_alias_2}'")
42
+ resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_alias_2)
43
+ if resolved_year:
44
+ entities.nam_sinh_2 = resolved_year
45
+ logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}")
46
+ pass
47
+
48
+ # Xử lý trường hợp LLM trả về năm 2 chữ số (ví dụ: 91)
49
+ if entities.nam_sinh_1 and 0 < entities.nam_sinh_1 < 100:
50
+ logger.info(f"Tiền xử lý: Đang giải mã năm 2 chữ số: '{entities.nam_sinh_1}'")
51
+ resolved_year = can_chi_helper.resolve_alias_to_year(entities.nam_sinh_1)
52
+ if resolved_year:
53
+ entities.nam_sinh_1 = resolved_year
54
+ logger.info(f"Tiền xử lý: Giải mã thành công -> {resolved_year}")
55
+
56
+ return entities
57
+
58
+ async def run_workflow(intent_result: IntentResult) -> ChatContext:
59
+ """
60
+ Chọn và thực thi workflow phù hợp dựa trên intent đã được phân tích.
61
+
62
+ Args:
63
+ intent_result: Kết quả từ bộ phân tích ý định.
64
+
65
+ Returns:
66
+ Đối tượng ChatContext đã được làm giàu thông tin sau khi workflow chạy xong.
67
+ """
68
+ intent_name = intent_result.intent
69
+ initial_entities = intent_result.entities
70
+
71
+ logger.info(f"Đang chọn workflow cho intent: '{intent_name}'")
72
+
73
+ # Khởi tạo context ban đầu với các thực thể đã trích xuất
74
+ context = ChatContext(initial_entities=initial_entities)
75
+ context.intent_name = intent_name
76
+
77
+ # Lấy lớp workflow tương ứng từ mapping
78
+ workflow_class = WORKFLOW_MAPPING.get(intent_name)
79
+
80
+ if workflow_class:
81
+ # Nếu tìm thấy workflow phù hợp, khởi tạo và chạy nó
82
+ workflow_instance = workflow_class(context)
83
+ final_context = await workflow_instance.run()
84
+ return final_context
85
+ else:
86
+ # Xử lý các intent đơn giản hoặc không có workflow riêng
87
+ logger.warning(f"Không tìm thấy workflow được định nghĩa cho intent '{intent_name}'.")
88
+ # Bạn có thể xử lý các intent đơn giản ở đây (GREETING, UNKNOWN, etc.)
89
+ # hoặc chỉ trả về context ban đầu.
90
+ response_text = None
91
+ if intent_name == "GREETING":
92
+ response_text = "Chào bạn, tôi là trợ lý phong thủy. Tôi có thể giúp gì cho bạn?"
93
+ elif intent_name == "UNKNOWN":
94
+ 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."
95
+
96
+ if response_text:
97
+ context.update_context({"direct_response": response_text})
98
+
99
+ return context
app/orchestrator/workflows/__init__.py ADDED
File without changes
app/orchestrator/workflows/analyze_house.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/analyze_house.py
2
+
3
+ import logging
4
+ from datetime import datetime
5
+
6
+ from app.orchestrator.workflows.base_workflow import BaseWorkflow
7
+ from app.services.context_manager import ChatContext
8
+ # Import tất cả các tool cần thiết
9
+ from app.tools import ngu_hanh_tools, bat_trach_tools, tuong_tac_tools, general_tools
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AnalyzeHouseWorkflow(BaseWorkflow):
15
+ """
16
+ Workflow xử lý yêu cầu phân tích tổng thể về nhà cửa.
17
+ """
18
+
19
+ def __init__(self, context: ChatContext):
20
+ super().__init__(context)
21
+
22
+ async def run(self):
23
+ logger.info("--- Bắt đầu Workflow: Phân tích nhà cửa ---")
24
+
25
+ # --- Bước 1: Kiểm tra thông tin đầu vào cơ bản ---
26
+ if not self.context.is_ready_for_tool(['nam_sinh_1', 'gioi_tinh_1', 'huong_nha']):
27
+ logger.warning(f"Thiếu thông tin đầu vào: {self.context.missing_info}")
28
+ # Trong ứng dụng thực tế, ở đây sẽ trả về một câu hỏi cho người dùng
29
+ return self.context # Trả về context với thông tin bị thiếu
30
+
31
+ entities = self.context.initial_entities
32
+ nam_sinh = entities.nam_sinh_1
33
+ gioi_tinh = entities.gioi_tinh_1
34
+ huong_nha = entities.huong_nha
35
+
36
+ # --- Bước 2: Tra cứu thông tin bản mệnh của gia chủ ---
37
+ logger.info("Bước 2: Tra cứu thông tin bản mệnh")
38
+ cung_menh_info = await self._call_tool(
39
+ ngu_hanh_tools.get_cung_menh_by_year_gender,
40
+ nam_sinh=nam_sinh,
41
+ gioi_tinh=gioi_tinh
42
+ )
43
+ if not cung_menh_info:
44
+ logger.error("Không thể tìm thấy cung mệnh, dừng workflow.")
45
+ return self.context
46
+ self.context.update_context({"cung_menh_info": cung_menh_info})
47
+
48
+ menh_ngu_hanh = cung_menh_info.get('hanhcungmenh')
49
+ if menh_ngu_hanh:
50
+ menh_info = await self._call_tool(ngu_hanh_tools.get_menh_info, menh=menh_ngu_hanh)
51
+ self.context.update_context({"menh_ngu_hanh_info": menh_info})
52
+
53
+ nap_am_info = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=nam_sinh)
54
+ self.context.update_context({"nap_am_info": nap_am_info})
55
+
56
+ # --- Bước 3: Phân tích Bát Trạch ---
57
+ logger.info("Bước 3: Phân tích Bát Trạch")
58
+ cung_menh = cung_menh_info.get('cungmenh')
59
+ if cung_menh:
60
+ rule_info = await self._call_tool(
61
+ bat_trach_tools.get_bat_trach_info,
62
+ cung_menh=cung_menh,
63
+ huong_nha=huong_nha
64
+ )
65
+ self.context.update_context({"bat_trach_rule_info": rule_info})
66
+
67
+ if rule_info:
68
+ ten_cung_vi = rule_info.get('tencungvi_taothanh')
69
+ if ten_cung_vi:
70
+ detail_info = await self._call_tool(
71
+ bat_trach_tools.get_cung_vi_detail,
72
+ ten_cung_vi=ten_cung_vi
73
+ )
74
+ self.context.update_context({"bat_trach_detail_info": detail_info})
75
+
76
+ # --- Bước 4: Phân tích tương tác Mệnh - Hướng ---
77
+ logger.info("Bước 4: Phân tích tương tác Mệnh - Hướng")
78
+ if menh_ngu_hanh:
79
+ interaction_info = await self._call_tool(
80
+ tuong_tac_tools.get_menh_huong_interaction,
81
+ menh_gia_chu=menh_ngu_hanh,
82
+ huong_nha=huong_nha
83
+ )
84
+ self.context.update_context({"menh_huong_interaction_info": interaction_info})
85
+
86
+ # --- Bước 5: Phân tích Phi tinh năm hiện tại ---
87
+ logger.info("Bước 5: Phân tích Phi tinh")
88
+ current_year = datetime.now().year
89
+ phi_tinh_info = await self._call_tool(general_tools.get_phi_tinh_info, nam=current_year)
90
+ self.context.update_context({"phi_tinh_info": phi_tinh_info})
91
+
92
+ logger.info("--- Hoàn thành Workflow: Phân tích nhà cửa ---")
93
+ return self.context
app/orchestrator/workflows/base_workflow.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/base_workflow.py
2
+
3
+ from abc import ABC, abstractmethod
4
+ from app.services.context_manager import ChatContext
5
+ import logging
6
+ from typing import Callable, Any, Dict
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class BaseWorkflow(ABC):
11
+ """
12
+ Lớp cơ sở trừu tượng cho tất cả các workflow.
13
+ Mỗi workflow sẽ xử lý một intent cụ thể.
14
+ """
15
+ def __init__(self, context: ChatContext):
16
+ self.context = context
17
+
18
+ async def _call_tool(self, tool_func: Callable, **kwargs) -> Any:
19
+ """
20
+ Hàm bọc (wrapper) để gọi một tool, tự động ghi lại lịch sử và xử lý lỗi.
21
+ """
22
+ tool_name = tool_func.__name__
23
+ logger.info(f"Workflow đang gọi tool: {tool_name} với params: {kwargs}")
24
+
25
+ try:
26
+ result = tool_func(**kwargs)
27
+ status = "success" if result is not None else "failed (no data)"
28
+ self.context.add_tool_call(tool_name=tool_name, params=kwargs, status=status)
29
+ return result
30
+ except Exception as e:
31
+ logger.error(f"Lỗi khi thực thi tool '{tool_name}': {e}")
32
+ self.context.add_tool_call(tool_name=tool_name, params=kwargs, status="failed (exception)")
33
+ return None
34
+
35
+ @abstractmethod
36
+ async def run(self):
37
+ """
38
+ Phương thức chính để thực thi logic của workflow.
39
+ Nó sẽ gọi các tool theo đúng thứ tự, cập nhật context,
40
+ và cuối cùng trả về context đã được làm giàu thông tin.
41
+ """
42
+ pass
app/orchestrator/workflows/compare_people.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/compare_people.py
2
+
3
+ import logging
4
+ from app.orchestrator.workflows.base_workflow import BaseWorkflow
5
+ from app.services.context_manager import ChatContext
6
+ from app.tools import ngu_hanh_tools, tuong_tac_tools
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ComparePeopleWorkflow(BaseWorkflow):
12
+ """
13
+ Workflow xử lý yêu cầu so sánh sự tương hợp giữa hai người.
14
+ """
15
+
16
+ # def __init__(self, context: ChatContext):
17
+ # super().__init__(context)
18
+ # # Tạo các key mới trong context để lưu thông tin người thứ 2
19
+ # self.context.cung_menh_info_2 = None
20
+ # self.context.nap_am_info_2 = None
21
+ # self.context.menh_menh_interaction_info = None
22
+
23
+ async def run(self):
24
+ logger.info("--- Bắt đầu Workflow: So sánh hai người ---")
25
+
26
+ entities = self.context.initial_entities
27
+ if not (entities.nam_sinh_1 and entities.gioi_tinh_1 and entities.nam_sinh_2 and entities.gioi_tinh_2):
28
+ self.context.missing_info = "năm sinh và giới tính của cả hai người"
29
+ logger.warning(f"Thiếu thông tin: {self.context.missing_info}")
30
+ return self.context
31
+
32
+ # --- Bước 1: Tra cứu thông tin người thứ nhất ---
33
+ logger.info(f"Bước 1: Tra cứu người 1 (Năm sinh: {entities.nam_sinh_1})")
34
+ cung_menh_1 = await self._call_tool(ngu_hanh_tools.get_cung_menh_by_year_gender, nam_sinh=entities.nam_sinh_1,
35
+ gioi_tinh=entities.gioi_tinh_1)
36
+ nap_am_1 = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=entities.nam_sinh_1)
37
+ self.context.update_context({"cung_menh_info": cung_menh_1, "nap_am_info": nap_am_1})
38
+
39
+ # --- Bước 2: Tra cứu thông tin người thứ hai ---
40
+ logger.info(f"Bước 2: Tra cứu người 2 (Năm sinh: {entities.nam_sinh_2})")
41
+ cung_menh_2 = await self._call_tool(ngu_hanh_tools.get_cung_menh_by_year_gender, nam_sinh=entities.nam_sinh_2,
42
+ gioi_tinh=entities.gioi_tinh_2)
43
+ nap_am_2 = await self._call_tool(ngu_hanh_tools.get_nap_am_info, nam_sinh=entities.nam_sinh_2)
44
+ self.context.update_context({
45
+ "cung_menh_info_2": cung_menh_2,
46
+ "nap_am_info_2": nap_am_2
47
+ })
48
+
49
+ # --- Bước 3: Tra cứu sự tương tác ---
50
+ if nap_am_1 and nap_am_2:
51
+ logger.info("Bước 3: Tra cứu sự tương tác")
52
+ nap_am_1_name = nap_am_1.get('tennapam')
53
+ nap_am_2_name = nap_am_2.get('tennapam')
54
+ if nap_am_1_name and nap_am_2_name:
55
+ interaction = await self._call_tool(tuong_tac_tools.get_menh_menh_interaction, nap_am1=nap_am_1_name,
56
+ nap_am2=nap_am_2_name)
57
+ self.context.update_context({"menh_menh_interaction_info": interaction})
58
+
59
+ logger.info("--- Hoàn thành Workflow: So sánh hai người ---")
60
+ return self.context
app/orchestrator/workflows/lookup_item.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/lookup_item.py
2
+ import logging
3
+ from app.orchestrator.workflows.base_workflow import BaseWorkflow
4
+ from app.services.context_manager import ChatContext
5
+ from app.tools import general_tools, semantic_search_tools
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class LookupItemWorkflow(BaseWorkflow):
11
+ """
12
+ Workflow xử lý yêu cầu tra cứu thông tin về một vật phẩm phong thủy.
13
+ Sử dụng semantic search để tìm ra tên chính xác trước khi truy vấn CSDL.
14
+ """
15
+
16
+ async def run(self):
17
+ logger.info("--- Bắt đầu Workflow: Tra cứu Vật phẩm (Nâng cấp) ---")
18
+
19
+ # Lấy tên/mô tả vật phẩm từ entities
20
+ item_query = self.context.initial_entities.vat_pham
21
+
22
+ if not item_query:
23
+ self.context.missing_info = "tên hoặc mô tả vật phẩm bạn muốn tìm"
24
+ logger.warning(f"Thiếu thông tin: {self.context.missing_info}")
25
+ return self.context
26
+
27
+ # --- BƯỚC 1: Tìm kiếm ngữ nghĩa để xác định tên vật phẩm chính xác ---
28
+ logger.info(f"Bước 1: Tìm kiếm ngữ nghĩa cho query: '{item_query}'.")
29
+ similar_item = await self._call_tool(
30
+ semantic_search_tools.find_most_similar_item,
31
+ query=item_query
32
+ )
33
+
34
+ if not similar_item:
35
+ logger.warning("Không tìm thấy vật phẩm nào đủ tương đồng qua semantic search.")
36
+ self.context.update_context({"lookup_result": None})
37
+ 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."
38
+ logger.info("--- Hoàn thành Workflow: Tra cứu Vật phẩm (Không có kết quả) ---")
39
+ return self.context
40
+
41
+ # Lưu lại kết quả suy luận từ semantic search
42
+ self.context.update_context({"semantic_search_result": similar_item})
43
+
44
+ # --- BƯỚC 2: Dùng tên chính xác để truy vấn chi tiết từ CSDL ---
45
+ item_name = similar_item.get('name')
46
+ if not item_name:
47
+ logger.error("Kết quả từ semantic search không có 'name'. Dừng workflow.")
48
+ return self.context
49
+
50
+ logger.info(f"Bước 2: Dùng tên chính xác '{item_name}' để truy vấn chi tiết.")
51
+
52
+ lookup_result = await self._call_tool(
53
+ general_tools.get_vat_pham_info,
54
+ ten_vat_pham=item_name # Sử dụng tham số mới để tìm kiếm chính xác
55
+ )
56
+
57
+ self.context.update_context({"lookup_result": lookup_result})
58
+
59
+ logger.info("--- Hoàn thành Workflow: Tra cứu Vật phẩm ---")
60
+ return self.context
app/orchestrator/workflows/lookup_loandau.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/lookup_loandau.py
2
+ import logging
3
+ from app.orchestrator.workflows.base_workflow import BaseWorkflow
4
+ from app.services.context_manager import ChatContext
5
+ from app.tools import loan_dau_tools, semantic_search_tools, reranker_tools
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class LookupLoanDauWorkflow(BaseWorkflow):
11
+ """
12
+ 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:
13
+ 1. Retrieval: Dùng semantic search để tìm Top-K ứng viên tiềm năng.
14
+ 2. Re-ranking: Dùng LLM để chọn ra ứng viên chính xác nhất từ Top-K.
15
+ """
16
+
17
+ async def run(self):
18
+ logger.info("--- Bắt đầu Workflow: Tra cứu Loan Đầu (2 giai đoạn) ---")
19
+
20
+ keyword = self.context.initial_entities.keyword_loandau
21
+
22
+ if not keyword:
23
+ self.context.missing_info = "mô tả về ngoại cảnh (ví dụ: đường đâm, sông ôm)"
24
+ logger.warning(f"Thiếu thông tin: {self.context.missing_info}")
25
+ return self.context
26
+
27
+ # --- GIAI ĐOẠN 1: TRUY XUẤT (RETRIEVAL) ---
28
+ logger.info("Giai đoạn 1: Tìm kiếm ngữ nghĩa Top-K ứng viên.")
29
+ candidate_items = await self._call_tool(
30
+ semantic_search_tools.find_most_similar_loandau,
31
+ query=keyword,
32
+ k=3, # Lấy 3 ứng viên hàng đầu để LLM lựa chọn
33
+ similarity_threshold=0.4 # Hạ ngưỡng ở giai đoạn này để có nhiều lựa chọn hơn
34
+ )
35
+
36
+ if not candidate_items:
37
+ logger.warning("Không tìm thấy ứng viên nào đủ tương đồng qua semantic search.")
38
+ self.context.update_context({"lookup_result": None})
39
+ 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."
40
+ logger.info("--- Hoàn thành Workflow: Tra cứu Loan Đầu (Không có ứng viên) ---")
41
+ return self.context
42
+
43
+ # Nếu chỉ có 1 ứng viên, không cần re-rank, dùng luôn
44
+ if len(candidate_items) == 1:
45
+ logger.info("Chỉ có 1 ứng viên, bỏ qua giai đoạn re-ranking.")
46
+ best_item = candidate_items[0]
47
+ else:
48
+ # --- GIAI ĐOẠN 2: XẾP HẠNG LẠI (RE-RANKING) ---
49
+ logger.info("Giai đoạn 2: Dùng LLM để chọn ứng viên tốt nhất (Re-ranking).")
50
+ best_item = await self._call_tool(
51
+ reranker_tools.choose_best_loandau_candidate,
52
+ user_query=keyword,
53
+ candidates=candidate_items
54
+ )
55
+
56
+ # 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
57
+ if not best_item:
58
+ logger.warning("LLM không chọn được ứng viên, lấy kết quả đầu tiên từ semantic search.")
59
+ best_item = candidate_items[0]
60
+
61
+ # Lưu lại kết quả suy luận cuối cùng vào context
62
+ self.context.update_context({"semantic_search_result": best_item})
63
+
64
+ # --- GIAI ĐOẠN CUỐI: TRUY VẤN DỮ LIỆU CHI TIẾT ---
65
+ item_type = best_item.get('type')
66
+ item_name = best_item.get('name')
67
+
68
+ if not item_type or not item_name:
69
+ logger.error("Kết quả lựa chọn cuối cùng không hợp lệ (thiếu type hoặc name).")
70
+ self.context.direct_response = "Đã có lỗi xảy ra trong quá trình phân tích. Vui lòng thử lại."
71
+ return self.context
72
+
73
+ logger.info(f"Giai đoạn cuối: Dùng tên chính xác '{item_name}' để truy vấn chi tiết.")
74
+
75
+ lookup_result = None
76
+ if item_type == 'sat_khi':
77
+ lookup_result = await self._call_tool(
78
+ loan_dau_tools.get_sat_khi_info,
79
+ ten_sat_khi=item_name
80
+ )
81
+ elif item_type == 'the_dat':
82
+ lookup_result = await self._call_tool(
83
+ loan_dau_tools.get_the_dat_cat_tuong_info,
84
+ ten_the_dat=item_name
85
+ )
86
+
87
+ self.context.update_context({"lookup_result": lookup_result})
88
+
89
+ logger.info("--- Hoàn thành Workflow: Tra cứu Loan Đầu ---")
90
+ return self.context
app/orchestrator/workflows/lookup_namsinh.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/orchestrator/workflows/lookup_namsinh.py
2
+
3
+ import logging
4
+ from app.orchestrator.workflows.base_workflow import BaseWorkflow
5
+ from app.services.context_manager import ChatContext
6
+ from app.tools import ngu_hanh_tools, can_chi_helper
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class LookupNamSinhWorkflow(BaseWorkflow):
12
+ """
13
+ Workflow xử lý yêu cầu tra cứu thông tin cho một năm sinh.
14
+ - Tích hợp bộ giải mã alias (Can Chi, con giáp, năm viết tắt).
15
+ - Xử lý logic đa trường hợp: đủ thông tin, thiếu giới tính, hoặc cần làm rõ.
16
+ """
17
+
18
+ def __init__(self, context: ChatContext):
19
+ super().__init__(context)
20
+
21
+ async def run(self):
22
+ logger.info("--- Bắt đầu Workflow: Tra cứu Năm sinh (Nâng cấp) ---")
23
+
24
+ entities = self.context.initial_entities
25
+ nam_sinh = entities.nam_sinh_1
26
+ gioi_tinh = entities.gioi_tinh_1
27
+ nam_sinh_alias = entities.nam_sinh_alias
28
+
29
+ # --- Bước 1: Giải mã Alias (nếu có) để tìm ra năm sinh cụ thể ---
30
+ if not nam_sinh:
31
+ # Nếu vẫn không có nam_sinh, có thể là alias không cụ thể
32
+ possible_years = can_chi_helper.resolve_alias_to_year_list(nam_sinh_alias)
33
+ if possible_years:
34
+ self.context.direct_response = (
35
+ f"Tuổi '{nam_sinh_alias.capitalize()}' có thể ứng với nhiều năm sinh như: "
36
+ f"{', '.join(map(str, possible_years[:4]))}... "
37
+ "Để 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é."
38
+ )
39
+ return self.context
40
+ else: # Không giải mã được
41
+ self.context.missing_info = "năm sinh hoặc tuổi hợp lệ (ví dụ: 1991, Bính Dần)"
42
+ return self.context
43
+
44
+ # --- Bước 2: Kiểm tra xem đã có đủ thông tin năm sinh để tra cứu chưa ---
45
+ if not nam_sinh:
46
+ self.context.missing_info = "năm sinh hoặc tuổi (ví dụ: 1991, Bính Dần)"
47
+ logger.warning("Không có năm sinh để tra cứu sau bước giải mã.")
48
+ return self.context
49
+
50
+ # --- Bước 3: Thực hiện các cuộc gọi tool để thu thập dữ liệu ---
51
+ # Dictionary để tổng hợp tất cả kết quả tra cứu
52
+ combined_result = {}
53
+ lookup_successful = False
54
+
55
+ # 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)
56
+ if gioi_tinh:
57
+ logger.info(f"Tra cứu Cung Mệnh cho {gioi_tinh} {nam_sinh}.")
58
+ cung_menh_info = await self._call_tool(
59
+ ngu_hanh_tools.get_cung_menh_by_year_gender,
60
+ nam_sinh=nam_sinh,
61
+ gioi_tinh=gioi_tinh
62
+ )
63
+ if cung_menh_info:
64
+ combined_result.update(cung_menh_info)
65
+ lookup_successful = True
66
+
67
+ # Luôn tra cứu Nạp Âm (Ngũ Hành) nếu có năm sinh
68
+ logger.info(f"Tra cứu Nạp Âm cho năm sinh {nam_sinh}.")
69
+ nap_am_info = await self._call_tool(
70
+ ngu_hanh_tools.get_nap_am_info,
71
+ nam_sinh=nam_sinh
72
+ )
73
+ if nap_am_info:
74
+ combined_result.update(nap_am_info)
75
+ lookup_successful = True
76
+
77
+ # Nếu có Nạp Âm, làm giàu thêm thông tin chi tiết về Mệnh Ngũ Hành
78
+ menh_ngu_hanh = nap_am_info.get('hanhnguhanh')
79
+ if menh_ngu_hanh:
80
+ logger.info(f"Làm giàu thông tin chi tiết cho Mệnh '{menh_ngu_hanh}'.")
81
+ menh_info = await self._call_tool(
82
+ ngu_hanh_tools.get_menh_info,
83
+ menh=menh_ngu_hanh
84
+ )
85
+ if menh_info:
86
+ combined_result.update(menh_info)
87
+
88
+ # --- Bước 4: Tổng hợp kết quả và quyết định phản hồi cuối cùng ---
89
+ if lookup_successful:
90
+ self.context.update_context({"lookup_result": combined_result})
91
+ # Nếu chỉ tra cứu được Nạp Âm mà thiếu giới tính, gợi ý cho người dùng
92
+ if not gioi_tinh:
93
+ self.context.direct_response = (
94
+ "Dưới đây là thông tin về Nạp Âm và Mệnh Ngũ Hành. "
95
+ "Để 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é."
96
+ )
97
+ else:
98
+ # Nếu tất cả các tool đều không tìm thấy dữ liệu
99
+ 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.")
100
+ 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."
101
+
102
+ logger.info("--- Hoàn thành Workflow: Tra cứu Năm sinh ---")
103
+ return self.context
app/services/__init__.py ADDED
File without changes
app/services/context_manager.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/context_manager.py
2
+
3
+ from typing import Dict, Any, Optional, List # Thêm List
4
+ from pydantic import BaseModel, Field
5
+
6
+ # Import model entities để tái sử dụng
7
+ from app.services.intent_analyzer import ExtractedEntities
8
+ import logging
9
+
10
+ class ToolCallRecord(BaseModel):
11
+ """Một model nhỏ để lưu thông tin về một lần gọi tool."""
12
+ tool_name: str
13
+ params: Dict[str, Any]
14
+ status: str # "success" hoặc "failed"
15
+ # result: Optional[Dict[str, Any]] = None # Bỏ đi để response đỡ cồng kềnh
16
+
17
+ class ChatContext(BaseModel):
18
+ """
19
+ 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.
20
+ Nó hoạt động như một "bộ nhớ tạm" cho workflow.
21
+ """
22
+ # --- Thông tin đầu vào ban đầu ---
23
+ initial_entities: ExtractedEntities = Field(default_factory=ExtractedEntities)
24
+ intent_name: Optional[str] = None
25
+
26
+ tool_calls: List[ToolCallRecord] = Field(default_factory=list)
27
+ # --- Dữ liệu được làm giàu bởi các tools ---
28
+ # Dùng Optional và khởi tạo là None
29
+ cung_menh_info: Optional[Dict[str, Any]] = None
30
+ menh_ngu_hanh_info: Optional[Dict[str, Any]] = None
31
+ nap_am_info: Optional[Dict[str, Any]] = None
32
+ bat_trach_rule_info: Optional[Dict[str, Any]] = None
33
+ bat_trach_detail_info: Optional[Dict[str, Any]] = None
34
+ menh_huong_interaction_info: Optional[Dict[str, Any]] = None
35
+ phi_tinh_info: Optional[Dict[str, Any]] = None
36
+
37
+ # Cho ComparePeopleWorkflow
38
+ workflow_data: Dict[str, Any] = Field(default_factory=dict)
39
+ lookup_result: Optional[Dict[str, Any]] = None
40
+
41
+ # --- Các thông tin khác có thể cần ---
42
+ missing_info: Optional[str] = None # Dùng để hỏi lại người dùng
43
+ direct_response: Optional[str] = None
44
+
45
+ def update_context(self, data: Dict[str, Any]):
46
+ for key, value in data.items():
47
+ if key in self.model_fields: # Kiểm tra xem key có phải là một trường đã định nghĩa không
48
+ setattr(self, key, value)
49
+ else:
50
+ self.workflow_data[key] = value
51
+ logging.info(f"Đã cập nhật trường động '{key}' trong workflow_data.")
52
+
53
+ def is_ready_for_tool(self, required_fields: list[str]) -> bool:
54
+ """
55
+ Kiểm tra xem context đã có đủ thông tin để chạy một tool cụ thể chưa.
56
+ """
57
+ entities = self.initial_entities.model_dump()
58
+ for field in required_fields:
59
+ if field not in entities or entities[field] is None:
60
+ # Chuyển đổi tên trường thành dạng thân thiện hơn để hỏi người dùng
61
+ missing_map = {
62
+ 'nam_sinh_1': 'năm sinh',
63
+ 'gioi_tinh_1': 'giới tính',
64
+ 'huong_nha': 'hướng nhà',
65
+ 'nam_sinh_2': 'năm sinh của người thứ hai',
66
+ 'gioi_tinh_2': 'giới tính của người thứ hai'
67
+ }
68
+ self.missing_info = missing_map.get(field, field)
69
+ return False
70
+
71
+ self.missing_info = None
72
+ return True
73
+
74
+ def add_tool_call(self, tool_name: str, params: Dict[str, Any], status: str):
75
+ """Thêm một bản ghi về việc gọi tool vào lịch sử."""
76
+ record = ToolCallRecord(tool_name=tool_name, params=params, status=status)
77
+ self.tool_calls.append(record)
app/services/intent_analyzer.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/intent_analyzer.py
2
+
3
+ import logging
4
+ import json
5
+ from typing import Dict, Any
6
+ from groq import Groq
7
+ from pydantic import BaseModel, Field, ValidationError
8
+
9
+ from app.core.config import settings
10
+ from app.services.prompt_templates import INTENT_ANALYSIS_PROMPT
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ # --- Pydantic Models để Validate kết quả từ LLM ---
16
+ # Đ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.
17
+ class ExtractedEntities(BaseModel):
18
+ nam_sinh_1: int | None = None
19
+ gioi_tinh_1: str | None = None
20
+ nam_sinh_alias_1: str | None = None
21
+ nam_sinh_2: int | None = None
22
+ gioi_tinh_2: str | None = None
23
+ nam_sinh_alias_2: str | None = None
24
+ huong_nha: str | None = None
25
+ vat_pham: str | None = None
26
+ keyword_loandau: str | None = None
27
+ nam_sinh_alias: str | None = None
28
+
29
+
30
+ class IntentResult(BaseModel):
31
+ intent: str
32
+ entities: ExtractedEntities
33
+
34
+
35
+ # --- Khởi tạo client cho Groq ---
36
+ try:
37
+ groq_client = Groq(api_key=settings.GROQ_API_KEY)
38
+ except Exception as e:
39
+ logger.error(f"Không thể khởi tạo Groq client: {e}")
40
+ groq_client = None
41
+
42
+
43
+ async def analyze_intent(user_query: str, max_retries: int = 3) -> IntentResult:
44
+ """
45
+ 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ể.
46
+
47
+ Args:
48
+ user_query: Câu hỏi gốc của người dùng.
49
+ max_retries: Số lần thử lại nếu LLM trả về kết quả không hợp lệ.
50
+
51
+ Returns:
52
+ Một đối tượng IntentResult chứa intent và entities đã được validate.
53
+ """
54
+ if not groq_client:
55
+ logger.error("Groq client chưa được khởi tạo. Không thể phân tích ý định.")
56
+ return IntentResult(intent="ERROR", entities=ExtractedEntities())
57
+
58
+ prompt = INTENT_ANALYSIS_PROMPT.format(user_query=user_query)
59
+
60
+ for attempt in range(max_retries):
61
+ try:
62
+ logger.info(f"Đang gửi yêu cầu phân tích ý định đến LLM (Lần thử {attempt + 1})...")
63
+ chat_completion = groq_client.chat.completions.create(
64
+ messages=[
65
+ {
66
+ "role": "user",
67
+ "content": prompt,
68
+ }
69
+ ],
70
+ model="gemma2-9b-it", # Hoặc "mixtral-8x7b-32768"
71
+ temperature=0, # =0 để kết quả có tính quyết định, ít sáng tạo
72
+ max_tokens=256,
73
+ response_format={"type": "json_object"},
74
+ )
75
+
76
+ raw_response = chat_completion.choices[0].message.content
77
+ logger.info(f"LLM response (raw): {raw_response}")
78
+
79
+ # Validate kết quả JSON bằng Pydantic
80
+ parsed_json = json.loads(raw_response)
81
+ validated_result = IntentResult.model_validate(parsed_json)
82
+
83
+ logger.info(
84
+ f"Phân tích thành công: Intent='{validated_result.intent}', Entities={validated_result.entities.model_dump_json(indent=2)}")
85
+ return validated_result
86
+
87
+ except json.JSONDecodeError as e:
88
+ logger.warning(f"Lỗi giải mã JSON từ LLM: {e}. Đang thử lại...")
89
+ except ValidationError as e:
90
+ logger.warning(f"Lỗi validate Pydantic từ LLM: {e}. Đang thử lại...")
91
+ except Exception as e:
92
+ logger.error(f"Lỗi không xác định khi gọi LLM: {e}")
93
+ break # Thoát vòng lặp nếu lỗi nghiêm trọng
94
+
95
+ # Nếu tất cả các lần thử đều thất bại
96
+ logger.error("Không thể phân tích ý định sau nhiều lần thử.")
97
+ return IntentResult(intent="UNKNOWN", entities=ExtractedEntities())
98
+
99
+
100
+ # --- Phần kiểm tra ---
101
+ if __name__ == '__main__':
102
+ import asyncio
103
+
104
+
105
+ async def run_tests():
106
+ test_queries = [
107
+ "xem nhà hướng tây nam cho nữ 1991",
108
+ "chồng 1988 vợ 1991 thì sao",
109
+ "tác dụng của tỳ hưu là gì",
110
+ "nhà tôi đối diện một cái khe hẹp giữa 2 tòa nhà cao tầng",
111
+ "chào em",
112
+ "1986 mệnh gì",
113
+ "hôm nay ăn gì"
114
+ ]
115
+
116
+ for query in test_queries:
117
+ print(f"\n--- Testing query: '{query}' ---")
118
+ result = await analyze_intent(query)
119
+ print(f"Intent: {result.intent}")
120
+ print(f"Entities: {result.entities.model_dump()}")
121
+
122
+
123
+ # Chạy các hàm bất đồng bộ để test
124
+ asyncio.run(run_tests())
app/services/prompt_templates.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/prompt_templates.py
2
+
3
+ # SỬA LỖI: Nhân đôi tất cả các dấu ngoặc nhọn {} để tránh lỗi .format()
4
+ # Chỉ giữ lại {user_query} là không nhân đôi.
5
+
6
+ INTENT_ANALYSIS_PROMPT = """
7
+ 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.
8
+ 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.
9
+ Đối tượng JSON phải có 2 trường: "intent" và "entities".
10
+
11
+ Trường "intent" phải là MỘT trong các giá trị sau:
12
+ - "ANALYZE_HOUSE": Người dùng muốn phân tích tổng thể về nhà cửa (hướng nhà, tuổi, ...).
13
+ - "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).
14
+ - "LOOKUP_ITEM": Người dùng hỏi thông tin về một vật phẩm phong thủy cụ thể.
15
+ - "LOOKUP_DIRECTION": Người dùng hỏi thông tin về một hướng cụ thể.
16
+ - "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...).
17
+ - "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...).
18
+ - "GREETING": Người dùng chào hỏi đơn thuần.
19
+ - "UNKNOWN": Không thể xác định được ý định rõ ràng.
20
+
21
+ 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ó:
22
+ - "nam_sinh_1", "gioi_tinh_1": Thông tin của người thứ nhất.
23
+ - "nam_sinh_2", "gioi_tinh_2": Thông tin của người thứ hai (nếu có).
24
+ - "huong_nha": Hướng nhà (ví dụ: "Đông Bắc", "Tây Nam").
25
+ - "vat_pham": Tên vật phẩm phong thủy.
26
+ - "keyword_loandau": Từ khóa mô tả ngoại cảnh.
27
+ - "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").
28
+
29
+ QUY TẮC:
30
+ 1. Chỉ trả về JSON. Không thêm ```json``` hay bất kỳ văn bản nào khác.
31
+ 2. Nếu không có thực thể nào, trả về một đối tượng entities rỗng: {{}}.
32
+ 3. "giới tính" phải là "Nam" hoặc "Nữ".
33
+ 4. "nam_sinh" phải là số nguyên.
34
+
35
+ Dưới đây là các ví dụ:
36
+
37
+ ---
38
+ User: xem giúp mình nhà hướng đông nam cho nam 1990
39
+ AI: {{"intent": "ANALYZE_HOUSE", "entities": {{"nam_sinh_1": 1990, "gioi_tinh_1": "Nam", "huong_nha": "Đông Nam"}}}}
40
+ ---
41
+ User: Chồng 1988 vợ 1991 thì sao bạn?
42
+ AI: {{"intent": "COMPARE_PEOPLE", "entities": {{"nam_sinh_1": 1988, "gioi_tinh_1": "Nam", "nam_sinh_2": 1991, "gioi_tinh_2": "Nữ"}}}}
43
+ ---
44
+ User: xem tuổi chồng 88 vợ tân mùi
45
+ 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ữ"}}}}
46
+ ---
47
+ User: Tỳ hưu có tác dụng gì?
48
+ AI: {{"intent": "LOOKUP_ITEM", "entities": {{"vat_pham": "Tỳ Hưu"}}}}
49
+ ---
50
+ User: Nhà tôi ở ngay khúc cua con đường nó chĩa vào
51
+ AI: {{"intent": "LOOKUP_LOANDAU", "entities": {{"keyword_loandau": "khúc cua đường chĩa vào"}}}}
52
+ ---
53
+ User: 1995 là mệnh gì
54
+ AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_1": 1995}}}}
55
+ ---
56
+ User: xem mệnh cho tuổi Bính Dần
57
+ AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_alias": "Bính Dần"}}}}
58
+ ---
59
+ User: nữ 91 hợp hướng nào
60
+ AI: {{"intent": "ANALYZE_HOUSE", "entities": {{"gioi_tinh_1": "Nữ", "nam_sinh_alias": "91"}}}}
61
+ ---
62
+ User: người tuổi cọp thì sao
63
+ AI: {{"intent": "LOOKUP_NAMSINH", "entities": {{"nam_sinh_alias": "cọp"}}}}
64
+ ---
65
+ User: Chào bạn
66
+ AI: {{"intent": "GREETING", "entities": {{}}}}
67
+ ---
68
+ User: thời tiết hôm nay thế nào
69
+ AI: {{"intent": "UNKNOWN", "entities": {{}}}}
70
+ ---
71
+
72
+ Bây giờ, hãy phân tích câu hỏi của người dùng dưới đây.
73
+
74
+ User: {user_query}
75
+ AI:
76
+ """
77
+
78
+ RESPONSE_SYNTHESIS_PROMPT = """
79
+ 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.
80
+
81
+ **QUY TẮC TỐI QUAN TRỌNG:**
82
+ 1. **CHỈ SỬ DỤNG THÔNG TIN CÓ TRONG DỮ LIỆU ĐƯỢC CUNG CẤP.**
83
+ 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".
84
+ 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.
85
+ 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.
86
+
87
+ **DỮ LIỆU PHÂN TÍCH:**
88
+ ---
89
+ {context_data}
90
+ ---
91
+
92
+ Bây giờ, hãy viết bài tư vấn của bạn.
93
+ """
app/services/response_synthesizer.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/response_synthesizer.py
2
+
3
+ import logging
4
+ import json
5
+ from groq import Groq
6
+
7
+ from app.core.config import settings
8
+ from app.services.context_manager import ChatContext
9
+ from app.services.prompt_templates import RESPONSE_SYNTHESIS_PROMPT
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Khởi tạo lại client (hoặc có thể tạo một module client chung)
14
+ try:
15
+ groq_client = Groq(api_key=settings.GROQ_API_KEY)
16
+ except Exception as e:
17
+ logger.error(f"Không thể khởi tạo Groq client: {e}")
18
+ groq_client = None
19
+
20
+
21
+ def _format_dict_to_string(data: dict, title: str) -> list[str]:
22
+ """Chuyển một dictionary thành một list các chuỗi có định dạng đẹp."""
23
+ lines = [f"**{title}:**"]
24
+ if not data:
25
+ lines.append("- Không có thông tin.")
26
+ return lines
27
+
28
+ # Ánh xạ tên cột xấu xí sang tên đẹp hơn
29
+ key_mappings = {
30
+ 'tenvatpham': 'Tên Vật Phẩm',
31
+ 'congdungchinh_so1': 'Công Dụng Chính',
32
+ 'congdungphu_so2': 'Công Dụng Phụ',
33
+ 'luy_camky_quantrong': 'Lưu Ý Cấm Kỵ',
34
+ 'diengiai_congdung_tailoc': 'Diễn Giải Về Tài Lộc',
35
+ 'tenthedat': 'Tên Thế Đất',
36
+ 'mucdo_cattuong': 'Mức Độ Tốt',
37
+ 'diengiai_tacdong': 'Diễn Giải Tác Động',
38
+ 'giaiphap_kichhoat_1': 'Giải Pháp Kích Hoạt',
39
+ 'tensatkhi': 'Tên Sát Khí',
40
+ 'mucdo_nguyhiem': 'Mức Độ Nguy Hiểm',
41
+ 'giaiphap_uutien_1': 'Giải Pháp Hóa Giải',
42
+ 'cungmenh': 'Cung Mệnh',
43
+ 'hanhcungmenh': 'Hành Cung Mệnh',
44
+ 'nhombattrach': 'Nhóm Bát Trạch',
45
+ 'tennapam': 'Nạp Âm',
46
+ 'diengiai_hinhtuong': 'Diễn Giải Hình Tượng',
47
+ }
48
+
49
+ found_data = False # Thêm một cờ để kiểm tra
50
+ for key, value in data.items():
51
+ # Bỏ qua các cột không cần thiết hoặc giá trị rỗng
52
+ if value is None or "id" in key.lower() or "url" in key.lower() or "version" in key.lower() or "sourcefile" in key.lower():
53
+ continue
54
+
55
+ # Lấy tên key đẹp từ mapping, nếu không có thì tự tạo
56
+ display_key = key_mappings.get(key, key.replace('_', ' ').title())
57
+
58
+ # Chỉ hiển thị các chuỗi không quá ngắn
59
+ if isinstance(value, str) and len(value.strip()) > 1 and value.strip() != 'nan' and value.strip() != '(null)':
60
+ lines.append(f"- {display_key}: {value.strip()}")
61
+ found_data = True
62
+
63
+ if not found_data:
64
+ return [f"**{title}:**", "- Không tìm thấy dữ liệu chi tiết."]
65
+ return lines
66
+
67
+
68
+ def format_context_for_prompt(context: ChatContext) -> str:
69
+ intent = getattr(context, 'intent_name', 'UNKNOWN')
70
+ data_lines = []
71
+ # Lấy dữ liệu từ workflow_data để dễ truy cập
72
+ data = context.workflow_data
73
+
74
+ match intent:
75
+ case "ANALYZE_HOUSE":
76
+ data_lines.append("**PHÂN TÍCH TỔNG THỂ NHÀ CỬA**")
77
+ entities = context.initial_entities
78
+
79
+ # 1. Thông tin gia chủ
80
+ gia_chu_info = [f"**1. Thông tin gia chủ:**"]
81
+ gia_chu_info.append(f"- Năm sinh: {entities.nam_sinh_1}, Giới tính: {entities.gioi_tinh_1}")
82
+ cung_menh_info = data.get('cung_menh_info')
83
+ if cung_menh_info:
84
+ gia_chu_info.append(
85
+ f"- Cung Mệnh: {cung_menh_info.get('cungmenh')} ({cung_menh_info.get('hanhcungmenh')})")
86
+ gia_chu_info.append(f"- Nhóm mệnh: {cung_menh_info.get('nhombattrach')}")
87
+
88
+ nap_am_info = data.get('nap_am_info')
89
+ if nap_am_info:
90
+ gia_chu_info.append(f"- Nạp Âm: {nap_am_info.get('tennapam')}")
91
+ data_lines.extend(gia_chu_info)
92
+
93
+ # 2. Thông tin nhà & Phân tích
94
+ data_lines.append(f"\n**2. Thông tin nhà và các phân tích:**")
95
+ data_lines.append(f"- Hướng nhà: {entities.huong_nha}")
96
+
97
+ rule = data.get('bat_trach_rule_info')
98
+ detail = data.get('bat_trach_detail_info')
99
+ if rule and detail:
100
+ data_lines.append(
101
+ 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')}**.")
102
+ data_lines.append(f" + Ý nghĩa: {detail.get('tacdong_tichcuc')}")
103
+
104
+ interact = data.get('menh_huong_interaction_info')
105
+ if interact:
106
+ data_lines.append(
107
+ 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')}")
108
+
109
+ phi_tinh = data.get('phi_tinh_info')
110
+ if phi_tinh:
111
+ data_lines.append(
112
+ 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')}**.")
113
+
114
+ case "COMPARE_PEOPLE":
115
+ data_lines.append("**PHÂN TÍCH SỰ TƯƠNG HỢP GIỮA HAI NGƯỜI**")
116
+ entities = context.initial_entities
117
+
118
+ nap_am_1 = data.get('nap_am_info_1')
119
+ if nap_am_1:
120
+ data_lines.append(
121
+ f"- Người 1: {entities.gioi_tinh_1} {entities.nam_sinh_1} (Nạp âm: {nap_am_1.get('tennapam')})")
122
+
123
+ nap_am_2 = data.get('nap_am_info_2')
124
+ if nap_am_2:
125
+ data_lines.append(
126
+ f"- Người 2: {entities.gioi_tinh_2} {entities.nam_sinh_2} (Nạp âm: {nap_am_2.get('tennapam')})")
127
+
128
+ interact = data.get('menh_menh_interaction_info')
129
+ if interact:
130
+ data_lines.append(f"\n**Kết quả phân tích:**")
131
+ data_lines.extend(_format_dict_to_string(interact, "Chi tiết về mối quan hệ"))
132
+ else:
133
+ 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.")
134
+
135
+ case "LOOKUP_ITEM" | "LOOKUP_LOANDAU" | "LOOKUP_NAMSINH":
136
+ query_str = ""
137
+ entities_dict = context.initial_entities.model_dump(exclude_unset=True, exclude_none=True)
138
+ if entities_dict:
139
+ # Lấy giá trị đầu tiên trong dict entities làm query string
140
+ query_str = next(iter(entities_dict.values()))
141
+
142
+ data_lines.append(f"**THÔNG TIN TRA CỨU CHO: '{query_str}'**")
143
+ semantic_result = context.workflow_data.get('semantic_search_result')
144
+ if semantic_result and semantic_result.get('lookup_method'):
145
+ inferred_name = semantic_result.get('name')
146
+ similarity = semantic_result.get('similarity_score', 0)
147
+ data_lines.append(
148
+ 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%})."
149
+ )
150
+
151
+ lookup_result = context.lookup_result # Sửa lại để lấy từ context chính
152
+ if lookup_result:
153
+ data_lines.append("Dưới đây là dữ liệu chi tiết tìm được từ cơ sở dữ liệu:")
154
+ # Chuyển đổi dict thành chuỗi JSON đẹp mắt để LLM đọc
155
+ json_data = {k: v for k, v in lookup_result.items() if
156
+ v is not None and str(v).strip().lower() not in ['', 'nan', '(null)']}
157
+ data_lines.append(json.dumps(json_data, indent=2, ensure_ascii=False))
158
+ else:
159
+ # Câu trả lời này sẽ không được dùng nếu workflow đã set direct_response
160
+ data_lines.append("- Không tìm thấy thông tin phù hợp trong cơ sở dữ liệu.")
161
+
162
+ case _:
163
+ return "Không có đủ dữ liệu để tạo báo cáo. Vui lòng cung cấp thêm thông tin."
164
+
165
+ return "\n".join(data_lines)
166
+
167
+
168
+ async def synthesize_response(context: ChatContext) -> str:
169
+ """
170
+ Tổng hợp câu trả lời cuối cùng dựa trên context đã được làm giàu.
171
+ """
172
+ if not groq_client:
173
+ return "Lỗi: Dịch vụ LLM không khả dụng."
174
+
175
+ # Xử lý các trường hợp đơn giản không cần LLM
176
+ if context.direct_response:
177
+ return context.direct_response
178
+
179
+ if context.missing_info:
180
+ 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."
181
+
182
+ # Xây dựng prompt cho các trường hợp phức tạp
183
+ formatted_context = format_context_for_prompt(context)
184
+
185
+ if "Không có đủ dữ liệu" in formatted_context:
186
+ return formatted_context
187
+
188
+ prompt = RESPONSE_SYNTHESIS_PROMPT.format(context_data=formatted_context)
189
+
190
+ try:
191
+ logger.info("Đang gửi yêu cầu tổng hợp câu trả lời đến LLM...")
192
+ chat_completion = groq_client.chat.completions.create(
193
+ messages=[
194
+ {
195
+ "role": "user",
196
+ "content": prompt,
197
+ }
198
+ ],
199
+ model="gemma2-9b-it",
200
+ temperature=0.7, # Cho phép LLM viết văn mượt mà hơn
201
+ max_tokens=2048,
202
+ )
203
+
204
+ final_answer = chat_completion.choices[0].message.content
205
+ logger.info("Đã nhận được câu trả lời tổng hợp từ LLM.")
206
+ return final_answer
207
+
208
+ except Exception as e:
209
+ logger.error(f"Lỗi khi tổng hợp câu trả lời: {e}")
210
+ 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."
app/tools/__init__.py ADDED
File without changes
app/tools/bat_trach_tools.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/tools/bat_trach_tools.py
2
+
3
+ import pandas as pd
4
+ import logging
5
+ from typing import Optional, Dict, Any
6
+
7
+ from app.database.connection import query_to_dataframe
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def get_bat_trach_info(cung_menh: str, huong_nha: str) -> Optional[Dict[str, Any]]:
13
+ """
14
+ Tra cứu cung vị Bát Trạch (Sinh Khí, Tuyệt Mệnh,...) và các thông tin liên quan
15
+ từ bảng 'cung_menh_huong_rules' dựa vào Cung Mệnh của gia chủ và Hướng nhà.
16
+
17
+ Args:
18
+ cung_menh: Cung Mệnh của gia chủ (ví dụ: "Càn", "Khảm").
19
+ huong_nha: Hướng nhà (ví dụ: "Đông Bắc", "Nam").
20
+
21
+ Returns:
22
+ 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.
23
+ """
24
+ logger.info(f"Đang tra cứu Bát Trạch cho Cung Mệnh: {cung_menh}, Hướng nhà: {huong_nha}")
25
+
26
+ cung_menh_normalized = cung_menh.strip().capitalize()
27
+ huong_nha_normalized = huong_nha.strip().title()
28
+
29
+ sql_query = """
30
+ SELECT *
31
+ FROM cung_menh_huong_rules
32
+ WHERE cungmenh_giachu = :cung_menh AND huongnha = :huong_nha
33
+ """
34
+ params = {"cung_menh": cung_menh_normalized, "huong_nha": huong_nha_normalized}
35
+
36
+ try:
37
+ result_df = query_to_dataframe(sql_query, params)
38
+ if result_df.empty:
39
+ logger.warning(
40
+ 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}'.")
41
+ return None
42
+
43
+ rule_info = result_df.to_dict('records')[0]
44
+ logger.info(f"Tìm thấy cung Bát Trạch: {rule_info.get('tencungvi_taothanh')}") # ten_cung_vi_tao_thanh -> tencungvi_taothanh
45
+ return rule_info
46
+
47
+ except Exception as e:
48
+ logger.error(f"Lỗi khi tra cứu luật Bát Trạch: {e}")
49
+ return None
50
+
51
+
52
+ def get_cung_vi_detail(ten_cung_vi: str) -> Optional[Dict[str, Any]]:
53
+ """
54
+ 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'.
55
+
56
+ Args:
57
+ ten_cung_vi: Tên của cung vị (ví dụ: "Sinh Khí", "Tuyệt Mệnh").
58
+
59
+ Returns:
60
+ Một dictionary chứa thông tin chi tiết, hoặc None nếu không tìm thấy.
61
+ """
62
+ logger.info(f"Đang tra cứu chi tiết cho Cung Vị: {ten_cung_vi}")
63
+ ten_cung_vi_normalized = ten_cung_vi.strip().title()
64
+
65
+ sql_query = "SELECT * FROM bat_trach_cung_vi WHERE tencung = :ten_cung"
66
+ params = {"ten_cung": ten_cung_vi_normalized}
67
+
68
+ try:
69
+ result_df = query_to_dataframe(sql_query, params)
70
+ if result_df.empty:
71
+ logger.warning(f"Không tìm thấy thông tin chi tiết cho Cung Vị '{ten_cung_vi_normalized}'.")
72
+ return None
73
+
74
+ detail_info = result_df.to_dict('records')[0]
75
+ logger.info(f"Đã lấy thông tin chi tiết thành công cho Cung Vị {ten_cung_vi_normalized}.")
76
+ return detail_info
77
+
78
+ except Exception as e:
79
+ logger.error(f"Lỗi khi tra cứu chi tiết Cung Vị: {e}")
80
+ return None
81
+
82
+
83
+ # --- Phần kiểm tra ---
84
+ if __name__ == '__main__':
85
+ print("--- Đang kiểm tra các tool trong bat_trach_tools.py ---")
86
+
87
+ print("\n[Test 1] Tra cứu Bát Trạch cho Cung Mệnh 'Càn', Hướng nhà 'Đông Bắc':")
88
+ rule_result = get_bat_trach_info("Càn", "Đông Bắc") # Giữ nguyên "Đông Bắc"
89
+ if rule_result:
90
+ # SỬA LẠI KEY KHI LẤY DỮ LIỆU
91
+ ten_cung = rule_result.get('tencungvi_taothanh')
92
+ print(f" - Tên Cung Vị tạo thành: {ten_cung}")
93
+ print(f" - Kết luận ngắn gọn: {rule_result.get('ketluan_ngangon')}") # ket_luan_ngan_gon -> ketluan_ngangon
94
+
95
+ if ten_cung:
96
+ print(f"\n[Test 2] Tra cứu chi tiết cho Cung Vị '{ten_cung}':")
97
+ detail_result = get_cung_vi_detail(ten_cung)
98
+ if detail_result:
99
+ # SỬA LẠI KEY KHI LẤY DỮ LIỆU
100
+ print(f" - Loại Cung: {detail_result.get('loaicung')}")
101
+ print(f" - Lĩnh vực ảnh hưởng mạnh nhất: {detail_result.get('linhvuc_anhhuong_manhnhat')}")
102
+ print(f" - Tác động tích cực: {detail_result.get('tacdong_tichcuc')}")
103
+ else:
104
+ print(" - Không tìm thấy kết quả chi tiết.")
105
+ else:
106
+ print(" - Không tìm thấy kết quả.")
107
+
108
+ #python -m app.tools.ngu_hanh_tools
app/tools/can_chi_helper.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from datetime import datetime
3
+ from typing import List, Optional
4
+
5
+ # --- Phần 1: Định nghĩa các hằng số và dữ liệu gốc ---
6
+
7
+ # Ánh xạ các tên gọi phổ biến sang tên con giáp chính tắc
8
+ ALIAS_TO_CON_GIAP = {
9
+ "chuột": "Tý", "tí": "Tý", "tý": "Tý",
10
+ "trâu": "Sửu", "sửu": "Sửu",
11
+ "cọp": "Dần", "hổ": "Dần", "dần": "Dần",
12
+ "mèo": "Mão", "mẹo": "Mão", "mão": "Mão",
13
+ "rồng": "Thìn", "thìn": "Thìn",
14
+ "rắn": "Tỵ", "tỵ": "Tỵ",
15
+ "ngựa": "Ngọ", "ngọ": "Ngọ",
16
+ "dê": "Mùi", "mùi": "Mùi",
17
+ "khỉ": "Thân", "thân": "Thân",
18
+ "gà": "Dậu", "dậu": "Dậu",
19
+ "chó": "Tuất", "tuất": "Tuất",
20
+ "heo": "Hợi", "lợn": "Hợi", "hợi": "Hợi",
21
+ }
22
+
23
+ # Các hằng số cho việc tính toán Can Chi
24
+ THIEN_CAN = ["Giáp", "Ất", "Bính", "Đinh", "Mậu", "Kỷ", "Canh", "Tân", "Nhâm", "Quý"]
25
+ DIA_CHI = ["Tý", "Sửu", "Dần", "Mão", "Thìn", "Tỵ", "Ngọ", "Mùi", "Thân", "Dậu", "Tuất", "Hợi"]
26
+
27
+ # Năm tham chiếu: 1984 là Giáp Tý (Can index 0, Chi index 0)
28
+ REFERENCE_YEAR = 1984
29
+ REFERENCE_CAN_INDEX = 0
30
+ REFERENCE_CHI_INDEX = 0
31
+
32
+ # --- Phần 2: Sinh dữ liệu tự động ---
33
+
34
+ # Tạo các cấu trúc dữ liệu rỗng để chứa kết quả
35
+ CAN_CHI_TO_YEARS = {f"{can} {chi}": [] for can in THIEN_CAN for chi in DIA_CHI}
36
+ CON_GIAP_TO_YEARS = {chi: [] for chi in DIA_CHI}
37
+
38
+ # Vòng lặp để sinh dữ liệu cho khoảng 120 năm (từ 1924 đến 2043)
39
+ for year in range(1924, 2044):
40
+ offset = year - REFERENCE_YEAR
41
+ can_index = (REFERENCE_CAN_INDEX + offset) % len(THIEN_CAN)
42
+ chi_index = (REFERENCE_CHI_INDEX + offset) % len(DIA_CHI)
43
+
44
+ can = THIEN_CAN[can_index]
45
+ chi = DIA_CHI[chi_index]
46
+
47
+ # Thêm năm vào các dictionary tương ứng
48
+ CAN_CHI_TO_YEARS[f"{can} {chi}"].append(year)
49
+ CON_GIAP_TO_YEARS[chi].append(year)
50
+
51
+
52
+ # --- Phần 3: Các hàm chức năng (Tools) ---
53
+
54
+ def get_can_chi_from_year(year: int) -> Optional[str]:
55
+ """
56
+ Tính toán Can Chi (ví dụ: "Giáp Tý") cho một năm dương lịch bất kỳ.
57
+ Sử dụng phương pháp offset từ năm tham chiếu để đảm bảo chính xác.
58
+ """
59
+ if not isinstance(year, int) or year <= 0:
60
+ return None
61
+
62
+ offset = year - REFERENCE_YEAR
63
+ can_index = (REFERENCE_CAN_INDEX + offset) % len(THIEN_CAN)
64
+ chi_index = (REFERENCE_CHI_INDEX + offset) % len(DIA_CHI)
65
+
66
+ can = THIEN_CAN[can_index]
67
+ chi = DIA_CHI[chi_index]
68
+
69
+ return f"{can} {chi}"
70
+
71
+
72
+ def resolve_alias_to_year(alias: str | int) -> Optional[int]:
73
+ """
74
+ Cố gắng giải mã một alias thành MỘT năm sinh cụ thể.
75
+ Ưu tiên Can Chi và năm viết tắt.
76
+ """
77
+ alias_str = str(alias).strip()
78
+ alias_normalized = alias_str.title()
79
+
80
+ # Trường hợp 1: Can Chi đầy đủ (ví dụ: "Bính Dần")
81
+ if alias_normalized in CAN_CHI_TO_YEARS and CAN_CHI_TO_YEARS[alias_normalized]:
82
+ possible_years = CAN_CHI_TO_YEARS[alias_normalized]
83
+ # Lọc ra các năm trong quá khứ và lấy năm gần nhất
84
+ past_years = [y for y in possible_years if y <= datetime.now().year]
85
+ if past_years:
86
+ return max(past_years)
87
+
88
+ # Trường hợp 2: Năm sinh viết tắt (ví dụ: "91", "90")
89
+ match = re.match(r'^(\d{2})$', alias_str)
90
+ if match:
91
+ year_short = int(match.group(1))
92
+ current_year_short = datetime.now().year % 100
93
+ # 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
94
+ if year_short > current_year_short:
95
+ return 1900 + year_short
96
+ else:
97
+ return 2000 + year_short
98
+
99
+ return None
100
+
101
+
102
+ def resolve_alias_to_year_list(alias: str) -> List[int]:
103
+ """
104
+ Giải mã một alias thành MỘT DANH SÁCH các năm sinh khả thi.
105
+ Dùng cho các trường hợp chung chung như "tuổi chuột".
106
+ """
107
+ alias_lower = alias.strip().lower()
108
+ words = re.split(r'[\s\W]+', alias_lower)
109
+
110
+ for word in words:
111
+ if word in ALIAS_TO_CON_GIAP:
112
+ con_giap = ALIAS_TO_CON_GIAP[word]
113
+ return CON_GIAP_TO_YEARS.get(con_giap, [])
114
+
115
+ return []
116
+
117
+
118
+ # --- Phần 4: Kiểm tra khi chạy trực tiếp file ---
119
+ if __name__ == "__main__":
120
+ print("--- CAN_CHI_TO_YEARS (một phần) ---")
121
+ print(f"Bính Dần: {CAN_CHI_TO_YEARS.get('Bính Dần')}")
122
+ print(f"Kỷ Tỵ: {CAN_CHI_TO_YEARS.get('Kỷ Tỵ')}")
123
+
124
+ print("\n--- Kiểm tra hàm get_can_chi_from_year (ĐÃ SỬA LỖI) ---")
125
+ print(f"Năm 1990 -> {get_can_chi_from_year(1990)}") # Mong đợi Canh Ngọ
126
+ print(f"Năm 1991 -> {get_can_chi_from_year(1991)}") # Mong đợi Tân Mùi
127
+ print(f"Năm 2024 -> {get_can_chi_from_year(2024)}") # Mong đợi Giáp Thìn
128
+ print(f"Năm 1984 -> {get_can_chi_from_year(1984)}") # Mong đợi Giáp Tý
129
+
130
+ print("\n--- Kiểm tra hàm resolve_alias_to_year ---")
131
+ print(f"'Bính Dần' -> {resolve_alias_to_year('Bính Dần')}") # Mong đợi 1986
132
+ print(f"'Kỷ Tỵ' -> {resolve_alias_to_year('Kỷ Tỵ')}") # Mong đợi 1989
133
+ print(f"'91' -> {resolve_alias_to_year('91')}") # Mong đợi 1991
134
+ print(f"'05' -> {resolve_alias_to_year('05')}") # Mong đợi 2005
135
+
136
+ print("\n--- Kiểm tra hàm resolve_alias_to_year_list ---")
137
+ print(f"'tuổi chuột' -> {resolve_alias_to_year_list('tuổi chuột')}")
138
+ print(f"'tuổi mèo' -> {resolve_alias_to_year_list('tuổi mèo')}")
139
+ print(f"'Tỵ' -> {resolve_alias_to_year_list('Tỵ')}")
app/tools/general_tools.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/tools/general_tools.py
2
+
3
+ import logging
4
+ from typing import Optional, Dict, Any
5
+
6
+ from app.database.connection import query_to_dataframe
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def get_huong_info(ten_huong: str) -> Optional[Dict[str, Any]]:
12
+ """
13
+ Tra cứu thông tin chi tiết về một hướng cụ thể từ bảng 'huong'.
14
+
15
+ Args:
16
+ ten_huong: Tên của hướng (ví dụ: "Đông Bắc", "Nam").
17
+
18
+ Returns:
19
+ 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.
20
+ """
21
+ logger.info(f"Đang tra cứu thông tin cho Hướng: {ten_huong}")
22
+ huong_normalized = ten_huong.strip().title()
23
+
24
+ sql_query = "SELECT * FROM huong WHERE tenhuong = :ten_huong"
25
+ params = {"ten_huong": huong_normalized}
26
+
27
+ try:
28
+ result_df = query_to_dataframe(sql_query, params)
29
+ if result_df.empty:
30
+ logger.warning(f"Không tìm thấy thông tin cho Hướng '{huong_normalized}'.")
31
+ return None
32
+
33
+ huong_info = result_df.to_dict('records')[0]
34
+ logger.info(f"Đã lấy thông tin thành công cho Hướng {huong_normalized}.")
35
+ return huong_info
36
+
37
+ except Exception as e:
38
+ logger.error(f"Lỗi khi tra cứu thông tin Hướng: {e}")
39
+ return None
40
+
41
+
42
+ def get_vat_pham_info(keyword: str = None, ten_vat_pham: str = None) -> Optional[Dict[str, Any]]:
43
+ """
44
+ Tra cứu thông tin chi tiết về một vật phẩm phong thủy.
45
+ Ư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).
46
+ """
47
+ if not ten_vat_pham and not keyword:
48
+ logger.warning("get_vat_pham_info được gọi mà không có tham số.")
49
+ return None
50
+
51
+ if ten_vat_pham:
52
+ # --- Ưu tiên tìm kiếm chính xác theo tên ---
53
+ logger.info(f"Đang tra cứu vật phẩm theo tên chính xác: '{ten_vat_pham}'")
54
+ sql_query = "SELECT * FROM vat_pham_phong_thuy WHERE tenvatpham = :ten_vat_pham"
55
+ params = {"ten_vat_pham": ten_vat_pham.strip().title()}
56
+ else:
57
+ # --- Phương án dự phòng: tìm kiếm tương đối theo keyword ---
58
+ logger.info(f"Đang tra cứu vật phẩm theo keyword (LIKE): '{keyword}'")
59
+ # Tìm kiếm linh hoạt hơn, ví dụ người dùng gõ "ty huu" vẫn ra "Tỳ Hưu"
60
+ sql_query = "SELECT * FROM vat_pham_phong_thuy WHERE tenvatpham LIKE :keyword"
61
+ params = {"keyword": f"%{keyword.strip().title()}%"}
62
+
63
+ try:
64
+ result_df = query_to_dataframe(sql_query, params)
65
+ if result_df.empty:
66
+ logger.warning(f"Không tìm thấy thông tin vật phẩm với params: {params}.")
67
+ return None
68
+
69
+ # Trả về kết quả đầu tiên tìm được
70
+ vat_pham_info = result_df.to_dict('records')[0]
71
+ logger.info(f"Đã lấy thông tin thành công cho vật phẩm: {vat_pham_info.get('tenvatpham')}")
72
+ return vat_pham_info
73
+
74
+ except Exception as e:
75
+ logger.error(f"Lỗi khi tra cứu thông tin Vật phẩm: {e}")
76
+ return None
77
+
78
+
79
+ def get_phi_tinh_info(nam: int) -> Optional[Dict[str, Any]]:
80
+ """
81
+ Tra cứu thông tin phi tinh lưu niên từ bảng 'phi_tinh_luu_nien'.
82
+ """
83
+ # ... (Giữ nguyên code của hàm này)
84
+ logger.info(f"Đang tra cứu Phi tinh cho năm: {nam}")
85
+
86
+ sql_query = "SELECT * FROM phi_tinh_luu_nien WHERE nam_duonglich = :nam"
87
+ params = {"nam": nam}
88
+
89
+ try:
90
+ result_df = query_to_dataframe(sql_query, params)
91
+ if result_df.empty:
92
+ logger.warning(f"Không tìm thấy thông tin Phi tinh cho năm {nam}.")
93
+ return None
94
+
95
+ phi_tinh_info = result_df.to_dict('records')[0]
96
+ logger.info(f"Đã lấy thông tin Phi tinh thành công cho năm {nam}.")
97
+ return phi_tinh_info
98
+
99
+ except Exception as e:
100
+ logger.error(f"Lỗi khi tra cứu thông tin Phi tinh: {e}")
101
+ return None
102
+
103
+
104
+ # --- Phần kiểm tra ---
105
+ if __name__ == '__main__':
106
+ # Thêm sys.path để chạy độc lập
107
+ import sys, os
108
+
109
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
110
+
111
+ print("--- Đang kiểm tra các tool trong general_tools.py ---")
112
+
113
+ print("\n[Test 1] Tra cứu thông tin Hướng 'Đông Bắc':")
114
+ huong_result = get_huong_info("Đông Bắc")
115
+ if huong_result:
116
+ print(f" - Tên Hướng: {huong_result.get('tenhuong')}")
117
+ print(f" - Hành Ngũ Hành: {huong_result.get('hanhnguhanh')}")
118
+ else:
119
+ print(" - Không tìm thấy kết quả.")
120
+
121
+ print("\n[Test 2] Tra cứu thông tin Vật phẩm 'Tỳ Hưu':")
122
+ vat_pham_result = get_vat_pham_info("Tỳ Hưu")
123
+ if vat_pham_result:
124
+ print(f" - Tên Vật phẩm: {vat_pham_result.get('tenvatpham')}")
125
+ print(f" - Công dụng chính: {vat_pham_result.get('congdungchinh_so1')}")
126
+ else:
127
+ print(" - Không tìm thấy kết quả.")
128
+
129
+ print("\n[Test 3] Tra cứu Phi tinh cho năm 2025:")
130
+ phi_tinh_result = get_phi_tinh_info(2025)
131
+ if phi_tinh_result:
132
+ print(f" - Năm Âm lịch: {phi_tinh_result.get('nam_amlich_canchi')}")
133
+ print(f" - Hướng Đại Hung: {phi_tinh_result.get('phuongvi_daihung_so1')}")
134
+ print(f" - Hướng Đại Cát: {phi_tinh_result.get('phuongvi_daicat_so1')}")
135
+ else:
136
+ print(" - Không tìm thấy kết quả.")
app/tools/loan_dau_tools.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/tools/loan_dau_tools.py
2
+
3
+ import logging
4
+ from typing import Optional, Dict, Any, List
5
+
6
+ from app.database.connection import query_to_dataframe
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def get_the_dat_cat_tuong_info(keyword: str = None, ten_the_dat: str = None) -> Optional[Dict[str, Any]]:
12
+ """
13
+ Tìm thông tin về một thế đất tốt dựa vào keyword.
14
+
15
+ Args:
16
+ keyword: Từ khóa mô tả thế đất (ví dụ: "sông ôm", "tựa núi").
17
+
18
+ Returns:
19
+ Thông tin về thế đất hợp nhất, hoặc None.
20
+ """
21
+ logger.info(f"Đang tra cứu Thế đất Cát tường với keyword: '{keyword}'")
22
+
23
+ if ten_the_dat:
24
+ sql_query = "SELECT * FROM loan_dau_cat_tuong WHERE tenthedat = :ten_the_dat"
25
+ params = {"ten_the_dat": ten_the_dat}
26
+ elif keyword:
27
+ sql_query = "SELECT * FROM loan_dau_cat_tuong WHERE keywords_nhandien LIKE :keyword"
28
+ params = {"keyword": f"%{keyword}%"}
29
+ else:
30
+ return None
31
+
32
+ try:
33
+ result_df = query_to_dataframe(sql_query, params)
34
+ if result_df.empty:
35
+ logger.warning(f"Không tìm thấy Thế đất Cát tường nào khớp với '{keyword}'.")
36
+ return None
37
+
38
+ # Trả về kết quả đầu tiên tìm thấy
39
+ the_dat_info = result_df.to_dict('records')[0]
40
+ logger.info(f"Tìm thấy Thế đất Cát tường: {the_dat_info.get('tenthedat')}")
41
+ return the_dat_info
42
+
43
+ except Exception as e:
44
+ logger.error(f"Lỗi khi tra cứu Thế đất Cát tường: {e}")
45
+ return None
46
+
47
+
48
+ def get_sat_khi_info(keyword: str = None, ten_sat_khi: str = None) -> Optional[Dict[str, Any]]:
49
+ """
50
+ Tìm thông tin về một loại Sát khí ngoại cảnh dựa vào keyword.
51
+
52
+ Args:
53
+ keyword: Từ khóa mô tả sát khí (ví dụ: "đường đâm", "khe hẹp").
54
+
55
+ Returns:
56
+ Thông tin về sát khí hợp nhất, hoặc None.
57
+ """
58
+ logger.info(f"Đang tra cứu Sát khí với keyword: '{keyword}'")
59
+
60
+ if ten_sat_khi:
61
+ sql_query = "SELECT * FROM ngoai_canh_sat_khi WHERE tensatkhi = :ten_sat_khi"
62
+ params = {"ten_sat_khi": ten_sat_khi}
63
+ elif keyword:
64
+ sql_query = "SELECT * FROM ngoai_canh_sat_khi WHERE keywords_nhandien LIKE :keyword"
65
+ params = {"keyword": f"%{keyword}%"}
66
+ else:
67
+ return None
68
+
69
+ try:
70
+ result_df = query_to_dataframe(sql_query, params)
71
+ if result_df.empty:
72
+ logger.warning(f"Không tìm thấy Sát khí nào khớp với '{keyword}'.")
73
+ return None
74
+
75
+ sat_khi_info = result_df.to_dict('records')[0]
76
+ logger.info(f"Tìm thấy Sát khí: {sat_khi_info.get('tensatkhi')}")
77
+ return sat_khi_info
78
+
79
+ except Exception as e:
80
+ logger.error(f"Lỗi khi tra cứu Sát khí: {e}")
81
+ return None
82
+
83
+
84
+ # --- Phần kiểm tra ---
85
+ if __name__ == '__main__':
86
+ import sys, os
87
+
88
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
89
+
90
+ print("--- Đang kiểm tra các tool trong loan_dau_tools.py ---")
91
+
92
+ print("\n[Test 1] Tra cứu Thế đất Cát tường với keyword 'sông ôm':")
93
+ cat_tuong_result = get_the_dat_cat_tuong_info("sông ôm")
94
+ if cat_tuong_result:
95
+ print(f" - Tên Thế đất: {cat_tuong_result.get('tenthedat')}")
96
+ print(f" - Mức độ: {cat_tuong_result.get('mucdo_cattuong')}")
97
+ else:
98
+ print(" - Không tìm thấy kết quả.")
99
+
100
+ print("\n[Test 2] Tra cứu Sát khí với keyword 'đường đâm':")
101
+ sat_khi_result = get_sat_khi_info("đường đâm")
102
+ if sat_khi_result:
103
+ print(f" - Tên Sát khí: {sat_khi_result.get('tensatkhi')}")
104
+ print(f" - Mức độ nguy hiểm: {sat_khi_result.get('mucdo_nguyhiem')}")
105
+ else:
106
+ print(" - Không tìm thấy kết quả.")
app/tools/ngu_hanh_tools.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/tools/ngu_hanh_tools.py
2
+
3
+ import pandas as pd
4
+ import logging
5
+ from typing import Optional, Dict, Any
6
+
7
+ # Import hàm query tiện ích từ module database
8
+ from app.database.connection import query_to_dataframe
9
+ from app.tools import can_chi_helper
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def get_cung_menh_by_year_gender(nam_sinh: int, gioi_tinh: str) -> Optional[Dict[str, Any]]:
15
+ """
16
+ 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'
17
+ dựa vào năm sinh âm lịch và giới tính.
18
+
19
+ Args:
20
+ nam_sinh: Năm sinh âm lịch (ví dụ: 1991).
21
+ gioi_tinh: Giới tính ("Nam" hoặc "Nữ").
22
+
23
+ Returns:
24
+ Một dictionary chứa thông tin tra cứu được, hoặc None nếu không tìm thấy.
25
+ """
26
+ logger.info(f"Đang tra cứu Cung Mệnh cho năm sinh: {nam_sinh}, giới tính: {gioi_tinh}")
27
+
28
+ # Chuẩn hóa đầu vào giới tính để khớp với dữ liệu trong CSDL
29
+ gioi_tinh_normalized = gioi_tinh.strip().capitalize()
30
+
31
+ # Câu lệnh SQL an toàn với tham số hóa
32
+ sql_query = """
33
+ SELECT *
34
+ FROM cung_menh_lookup
35
+ WHERE namsinh_amlich = :nam_sinh AND gioitinh = :gioi_tinh
36
+ """
37
+ params = {"nam_sinh": nam_sinh, "gioi_tinh": gioi_tinh.strip().capitalize()}
38
+
39
+ try:
40
+ result_df = query_to_dataframe(sql_query, params)
41
+
42
+ if result_df.empty:
43
+ 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}.")
44
+ return None
45
+
46
+ # Chuyển dòng đầu tiên của DataFrame thành một dictionary
47
+ # to_dict('records') trả về một list, ta lấy phần tử đầu tiên
48
+ cung_menh_info = result_df.to_dict('records')[0]
49
+ logger.info(f"Tìm thấy Cung Mệnh: {cung_menh_info.get('cungmenh')}")
50
+ return cung_menh_info
51
+
52
+ except Exception as e:
53
+ logger.error(f"Lỗi khi tra cứu Cung Mệnh: {e}")
54
+ return None
55
+
56
+
57
+ def get_menh_info(menh: str) -> Optional[Dict[str, Any]]:
58
+ """
59
+ Tra cứu thông tin chi tiết về một Mệnh Ngũ Hành từ bảng 'menh'.
60
+
61
+ Args:
62
+ menh: Tên mệnh Ngũ Hành (ví dụ: "Kim", "Thổ").
63
+
64
+ Returns:
65
+ Một dictionary chứa thông tin về mệnh, hoặc None nếu không tìm thấy.
66
+ """
67
+ logger.info(f"Đang tra cứu thông tin cho Mệnh: {menh}")
68
+ menh_normalized = menh.strip().capitalize()
69
+
70
+ sql_query = "SELECT * FROM menh WHERE tenmenh = :menh"
71
+ params = {"menh": menh.strip().capitalize()}
72
+
73
+ try:
74
+ result_df = query_to_dataframe(sql_query, params)
75
+ if result_df.empty:
76
+ logger.warning(f"Không tìm thấy thông tin cho Mệnh '{menh_normalized}'.")
77
+ return None
78
+
79
+ menh_info = result_df.to_dict('records')[0]
80
+ logger.info(f"Đã lấy thông tin thành công cho Mệnh {menh_normalized}.")
81
+ return menh_info
82
+
83
+ except Exception as e:
84
+ logger.error(f"Lỗi khi tra cứu thông tin Mệnh: {e}")
85
+ return None
86
+
87
+
88
+ def get_nap_am_info(nam_sinh: int) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Tra cứu Nạp Âm và Mệnh Ngũ Hành từ bảng 'nap_am' dựa trên năm sinh.
91
+ 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
92
+ bảng `nap_am` có cột `cac_nam_sinh_vi_du` chứa các năm.
93
+
94
+ Args:
95
+ nam_sinh: Năm sinh âm lịch.
96
+
97
+ Returns:
98
+ Một dictionary chứa thông tin Nạp Âm, hoặc None nếu không tìm thấy.
99
+ """
100
+ logger.info(f"Đang tra cứu Nạp Âm (logic tính toán) cho năm sinh: {nam_sinh}")
101
+
102
+ can_chi = can_chi_helper.get_can_chi_from_year(nam_sinh)
103
+ if not can_chi:
104
+ logger.warning(f"Không thể tính toán Can Chi cho năm {nam_sinh}.")
105
+ return None
106
+ logger.info(f"Năm {nam_sinh} tương ứng với Can Chi: '{can_chi}'")
107
+
108
+ sql_query = "SELECT * FROM nap_am WHERE canchi_tuongung LIKE :can_chi_pattern"
109
+ params = {"can_chi_pattern": f"%{can_chi}%"}
110
+
111
+ try:
112
+ result_df = query_to_dataframe(sql_query, params)
113
+ if result_df.empty:
114
+ 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.")
115
+ return None
116
+
117
+ # Trả về kết quả đầu tiên tìm được (chắc chắn chỉ có 1)
118
+ nap_am_info = result_df.to_dict('records')[0]
119
+ logger.info(f"Tìm thấy Nạp Âm: {nap_am_info.get('tennapam')}")
120
+ return nap_am_info
121
+
122
+ except Exception as e:
123
+ logger.error(f"Lỗi trong quá trình tra cứu Nạp Âm bằng logic tính toán: {e}")
124
+ return None
125
+
126
+
127
+ # --- Phần kiểm tra (chạy trực tiếp file này để test) ---
128
+ if __name__ == '__main__':
129
+ print("--- Đang kiểm tra các tool trong ngu_hanh_tools.py ---")
130
+
131
+ print("\n[Test 1] Tra cứu Cung Mệnh cho Nữ 1991:")
132
+ cung_menh_result = get_cung_menh_by_year_gender(1991, "Nữ")
133
+ if cung_menh_result:
134
+ print(f" - Cung Mệnh: {cung_menh_result.get('cungmenh')}")
135
+ print(f" - Nhóm Bát Trạch: {cung_menh_result.get('nhombattrach')}")
136
+ else:
137
+ print(" - Không tìm thấy kết quả.")
138
+
139
+ print("\n[Test 2] Tra cứu thông tin Mệnh 'Kim':")
140
+ menh_result = get_menh_info("Kim")
141
+ if menh_result:
142
+ print(f" - Tên Mệnh: {menh_result.get('tenmenh')}")
143
+ print(f" - Tính cách tích cực: {menh_result.get('tinhcach_tichcuc_keywords')}")
144
+ else:
145
+ print(" - Không tìm thấy kết quả.")
146
+
147
+ print("\n[Test 3 - Logic mới] Tra cứu Nạp Âm cho năm 1990:")
148
+ nap_am_result = get_nap_am_info(1990)
149
+ if nap_am_result:
150
+ print(f" - Tên Nạp Âm: {nap_am_result.get('tennapam')}") # Mong đợi Lộ Bàng Thổ
151
+ print(f" - Diễn giải hình tượng: {nap_am_result.get('diengiai_hinhtuong')}")
152
+ else:
153
+ print(" - Không tìm thấy kết quả.")
154
+
155
+ print("\n[Test 4 - Logic mới] Tra cứu Nạp Âm cho năm 2030 (không có trong ví dụ):")
156
+ nap_am_result_new = get_nap_am_info(2030)
157
+ if nap_am_result_new:
158
+ print(f" - Tên Nạp Âm: {nap_am_result_new.get('tennapam')}") # Mong đợi Canh Tuất - Thoa Xuyến Kim
159
+ print(f" - Diễn giải hình tượng: {nap_am_result_new.get('diengiai_hinhtuong')}")
160
+ else:
161
+ print(" - Không tìm thấy kết quả.")
162
+
163
+ #python -m app.tools.bat_trach_tools
app/tools/reranker_tools.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Dict, Any, Optional
3
+ from groq import Groq
4
+ from app.core.config import settings
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ try:
9
+ groq_client = Groq(api_key=settings.GROQ_API_KEY)
10
+ except Exception as e:
11
+ logger.error(f"Không thể khởi tạo Groq client cho reranker: {e}")
12
+ groq_client = None
13
+
14
+ # Cần truy vấn CSDL để lấy mô tả chi tiết cho các ứng viên
15
+ from app.database.connection import query_to_dataframe
16
+
17
+
18
+ def _get_details_for_reranking(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
19
+ """Lấy mô tả chi tiết từ CSDL để LLM có thêm thông tin phán đoán."""
20
+ detailed_candidates = []
21
+ for candidate in candidates:
22
+ name = candidate.get('name')
23
+ item_type = candidate.get('type')
24
+ description = ""
25
+ if item_type == 'sat_khi':
26
+ df = query_to_dataframe("SELECT mota_nhandien FROM ngoai_canh_sat_khi WHERE tensatkhi = :name",
27
+ params={'name': name})
28
+ if not df.empty:
29
+ description = df.iloc[0]['mota_nhandien']
30
+ elif item_type == 'the_dat':
31
+ df = query_to_dataframe("SELECT mota_nhandien FROM loan_dau_cat_tuong WHERE tenthedat = :name",
32
+ params={'name': name})
33
+ if not df.empty:
34
+ description = df.iloc[0]['mota_nhandien']
35
+
36
+ detailed_candidates.append({
37
+ "name": name,
38
+ "type": item_type,
39
+ "description": description
40
+ })
41
+ return detailed_candidates
42
+
43
+
44
+ def choose_best_loandau_candidate(user_query: str, candidates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
45
+ """
46
+ Sử dụng LLM để chọn ra ứng viên phù hợp nhất từ một danh sách.
47
+ """
48
+ if not groq_client or not candidates:
49
+ return None
50
+
51
+ # Lấy thêm mô tả chi tiết cho từng ứng viên
52
+ detailed_candidates = _get_details_for_reranking(candidates)
53
+
54
+ # Xây dựng prompt
55
+ prompt = f"""
56
+ 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.
57
+ 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.
58
+
59
+ Câu hỏi của người dùng: "{user_query}"
60
+
61
+ Danh sách các lựa chọn:
62
+ """
63
+ for i, candidate in enumerate(detailed_candidates):
64
+ prompt += f"\n{i + 1}. Tên: {candidate['name']}\n Mô tả: {candidate['description']}\n"
65
+
66
+ prompt += "\nJSON output:"
67
+
68
+ try:
69
+ logger.info("Gửi yêu cầu re-ranking đến LLM...")
70
+ chat_completion = groq_client.chat.completions.create(
71
+ messages=[{"role": "user", "content": prompt}],
72
+ model="gemma2-9b-it",
73
+ temperature=0,
74
+ response_format={"type": "json_object"},
75
+ )
76
+ response_str = chat_completion.choices[0].message.content
77
+ import json
78
+ best_choice_name = json.loads(response_str).get("best_choice")
79
+
80
+ logger.info(f"LLM đã chọn: '{best_choice_name}'")
81
+
82
+ # Tìm lại thông tin đầy đủ của ứng viên đã được chọn
83
+ for candidate in candidates:
84
+ if candidate.get("name") == best_choice_name:
85
+ return candidate
86
+ return None
87
+
88
+ except Exception as e:
89
+ logger.error(f"Lỗi khi re-ranking với LLM: {e}")
90
+ return None
app/tools/semantic_search_tools.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import pickle
3
+ import os
4
+ import logging
5
+ from sentence_transformers import SentenceTransformer
6
+ from typing import Dict, Any, Optional, List
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # --- Cấu hình và tải tài nguyên một lần khi module được import ---
11
+ # Điều này đảm bảo các file lớn chỉ được load vào bộ nhớ một lần.
12
+
13
+ # Biến toàn cục để giữ các tài nguyên đã tải
14
+ loandau_index = None
15
+ loandau_info = None
16
+ item_index = None
17
+ item_info = None
18
+ model = None
19
+
20
+ # Cờ để kiểm tra trạng thái tải
21
+ LOANDAU_RESOURCES_LOADED = False
22
+ ITEM_RESOURCES_LOADED = False
23
+
24
+ try:
25
+ # --- Tải mô hình chung ---
26
+ PROCESSED_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'processed')
27
+ MODEL_NAME = 'bkai-foundation-models/vietnamese-bi-encoder'
28
+
29
+ logger.info(f"Đang tải mô hình Sentence Transformer chung: '{MODEL_NAME}'...")
30
+ model = SentenceTransformer(MODEL_NAME)
31
+ logger.info("Tải mô hình chung thành công!")
32
+
33
+ # --- Tải tài nguyên cho Loan Đầu ---
34
+ try:
35
+ logger.info("Đang tải tài nguyên Semantic Search cho Loan Đầu...")
36
+ loandau_index_path = os.path.join(PROCESSED_DATA_DIR, 'loandau.index')
37
+ loandau_info_path = os.path.join(PROCESSED_DATA_DIR, 'loandau_info.pkl')
38
+
39
+ loandau_index = faiss.read_index(loandau_index_path)
40
+ with open(loandau_info_path, 'rb') as f:
41
+ loandau_info = pickle.load(f)
42
+
43
+ LOANDAU_RESOURCES_LOADED = True
44
+ logger.info("Tải tài nguyên Loan Đầu thành công!")
45
+ except FileNotFoundError:
46
+ logger.warning(
47
+ "Không tìm thấy file index/info cho Loan Đầu. Tool 'find_most_similar_loandau' sẽ không hoạt động.")
48
+ except Exception as e:
49
+ logger.error(f"Lỗi khi tải tài nguyên Loan Đầu: {e}")
50
+
51
+ # --- Tải tài nguyên cho Vật Phẩm ---
52
+ try:
53
+ logger.info("Đang tải tài nguyên Semantic Search cho Vật Phẩm...")
54
+ item_index_path = os.path.join(PROCESSED_DATA_DIR, 'item.index')
55
+ item_info_path = os.path.join(PROCESSED_DATA_DIR, 'item_info.pkl')
56
+
57
+ item_index = faiss.read_index(item_index_path)
58
+ with open(item_info_path, 'rb') as f:
59
+ item_info = pickle.load(f)
60
+
61
+ ITEM_RESOURCES_LOADED = True
62
+ logger.info("Tải tài nguyên Vật Phẩm thành công!")
63
+ except FileNotFoundError:
64
+ 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.")
65
+ logger.warning("Vui lòng chạy script 'scripts/create_item_embeddings.py' trước.")
66
+ except Exception as e:
67
+ logger.error(f"Lỗi khi tải tài nguyên Vật Phẩm: {e}")
68
+
69
+ except Exception as e:
70
+ logger.error(
71
+ 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}")
72
+
73
+
74
+ def find_most_similar_loandau(query: str, k: int = 3, similarity_threshold: float = 0.5) -> List[Dict[str, Any]]:
75
+ """
76
+ Tìm kiếm Top K Sát Khí hoặc Thế Đất Cát Tường tương đồng nhất.
77
+ """
78
+ if not LOANDAU_RESOURCES_LOADED or model is None:
79
+ logger.error("Tài nguyên Loan Đầu chưa được tải, không thể thực hiện tìm kiếm.")
80
+ return []
81
+
82
+ logger.info(f"Đang thực hiện semantic search (Loan Đầu) cho query: '{query}' với K={k}")
83
+
84
+ query_embedding = model.encode(query, convert_to_numpy=True).reshape(1, -1)
85
+ faiss.normalize_L2(query_embedding)
86
+
87
+ # Tìm kiếm K kết quả gần nhất
88
+ similarity_scores, indices = loandau_index.search(query_embedding, k=k)
89
+
90
+ results = []
91
+ for i in range(k):
92
+ idx = indices[0][i]
93
+ similarity = similarity_scores[0][i]
94
+
95
+ if similarity >= similarity_threshold:
96
+ match_info = loandau_info[idx].copy()
97
+ match_info['similarity_score'] = float(similarity)
98
+ results.append(match_info)
99
+ logger.info(f" - Tìm thấy ứng viên: {match_info['name']} (Score: {similarity:.2f})")
100
+
101
+ return results
102
+
103
+ # --- TOOL MỚI BẠN YÊU CẦU ---
104
+ def find_most_similar_item(query: str, similarity_threshold: float = 0.1) -> Optional[Dict[str, Any]]:
105
+ """
106
+ 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.
107
+
108
+ Args:
109
+ 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").
110
+ similarity_threshold (float): Ngưỡng điểm tương đồng để chấp nhận kết quả.
111
+
112
+ Returns:
113
+ 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,
114
+ hoặc None nếu không tìm thấy kết quả nào đủ tốt.
115
+ """
116
+ if not ITEM_RESOURCES_LOADED or model is None:
117
+ 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.")
118
+ return None
119
+
120
+ logger.info(f"Đang thực hiện semantic search (Vật Phẩm) cho query: '{query}'")
121
+
122
+ # 1. Tạo embedding cho câu query và chuẩn hóa nó
123
+ query_embedding = model.encode(query, convert_to_numpy=True).reshape(1, -1)
124
+ faiss.normalize_L2(query_embedding)
125
+
126
+ # 2. Tìm kiếm trong chỉ mục FAISS của vật phẩm
127
+ # k=1: chỉ tìm 1 kết quả gần nhất
128
+ similarity_scores, indices = item_index.search(query_embedding, k=1)
129
+
130
+ best_match_index = indices[0][0]
131
+ similarity = similarity_scores[0][0]
132
+
133
+ logger.info(
134
+ 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}")
135
+
136
+ # 3. Kiểm tra ngưỡng tương đồng
137
+ if similarity < similarity_threshold:
138
+ logger.warning(f"Độ tương đồng ({similarity:.2f}) thấp hơn ngưỡng ({similarity_threshold}). Bỏ qua kết quả.")
139
+ return None
140
+
141
+ # 4. Trả về thông tin của kết quả khớp nhất từ metadata
142
+ best_match_info = item_info[best_match_index].copy() # Dùng copy() để an toàn
143
+ best_match_info['similarity_score'] = float(similarity)
144
+ best_match_info['lookup_method'] = 'cosine_similarity'
145
+
146
+ return best_match_info
app/tools/tool_provider.py ADDED
File without changes
app/tools/tuong_tac_tools.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/tools/tuong_tac_tools.py
2
+
3
+ import logging
4
+ from typing import Optional, Dict, Any
5
+
6
+ from app.database.connection import query_to_dataframe
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def get_menh_huong_interaction(menh_gia_chu: str, huong_nha: str) -> Optional[Dict[str, Any]]:
12
+ """
13
+ Tra cứu quy tắc tương tác Ngũ Hành giữa Mệnh gia chủ và Hướng nhà.
14
+
15
+ Args:
16
+ menh_gia_chu: Mệnh của gia chủ (ví dụ: "Kim", "Mộc").
17
+ huong_nha: Hướng nhà (ví dụ: "Tây Bắc", "Đông").
18
+
19
+ Returns:
20
+ Một dictionary chứa thông tin quy tắc, hoặc None.
21
+ """
22
+ logger.info(f"Đang tra cứu tương tác Mệnh-Hướng cho: {menh_gia_chu} - {huong_nha}")
23
+
24
+ menh_normalized = menh_gia_chu.strip().capitalize()
25
+ huong_normalized = huong_nha.strip().title()
26
+
27
+ sql_query = "SELECT * FROM menh_huong_rules WHERE menhgiachu = :menh AND huongnha = :huong"
28
+ params = {"menh": menh_normalized, "huong": huong_normalized}
29
+
30
+ try:
31
+ result_df = query_to_dataframe(sql_query, params)
32
+ if result_df.empty:
33
+ logger.warning(
34
+ f"Không tìm thấy quy tắc tương tác cho Mệnh '{menh_normalized}' và Hướng '{huong_normalized}'.")
35
+ return None
36
+
37
+ interaction_info = result_df.to_dict('records')[0]
38
+ logger.info(f"Tìm thấy tương tác Mệnh-Hướng: {interaction_info.get('moiquanhe_nguhanh')}")
39
+ return interaction_info
40
+
41
+ except Exception as e:
42
+ logger.error(f"Lỗi khi tra cứu tương tác Mệnh-Hướng: {e}")
43
+ return None
44
+
45
+
46
+ def get_menh_menh_interaction(nap_am1: str, nap_am2: str) -> Optional[Dict[str, Any]]:
47
+ """
48
+ 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ọ.
49
+ Lưu ý: Logic này có thể cần tìm cả hai chiều (A-B và B-A).
50
+
51
+ Args:
52
+ nap_am1: Nạp âm của người thứ nhất.
53
+ nap_am2: Nạp âm của người thứ hai.
54
+
55
+ Returns:
56
+ Một dictionary chứa thông tin quy tắc, hoặc None.
57
+ """
58
+ logger.info(f"Đang tra cứu tương tác Mệnh-Mệnh cho: {nap_am1} - {nap_am2}")
59
+
60
+ # Tìm theo cả 2 chiều
61
+ sql_query = """
62
+ SELECT * FROM menh_menh_rules
63
+ WHERE (napam1 = :na1 AND napam2 = :na2) OR (napam1 = :na2 AND napam2 = :na1)
64
+ """
65
+ params = {"na1": nap_am1.strip().title(), "na2": nap_am2.strip().title()}
66
+
67
+ try:
68
+ result_df = query_to_dataframe(sql_query, params)
69
+ if result_df.empty:
70
+ 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}'.")
71
+ return None
72
+
73
+ interaction_info = result_df.to_dict('records')[0]
74
+ logger.info(f"Tìm thấy tương tác Mệnh-Mệnh: {interaction_info.get('moiquanhe_nguhanh')}")
75
+ return interaction_info
76
+
77
+ except Exception as e:
78
+ logger.error(f"Lỗi khi tra cứu tương tác Mệnh-Mệnh: {e}")
79
+ return None
80
+
81
+
82
+ # --- Phần kiểm tra ---
83
+ if __name__ == '__main__':
84
+ import sys, os
85
+
86
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
87
+
88
+ print("--- Đang kiểm tra các tool trong tuong_tac_tools.py ---")
89
+
90
+ 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':")
91
+ menh_huong_result = get_menh_huong_interaction("Kim", "Tây Bắc")
92
+ if menh_huong_result:
93
+ print(f" - Mối quan hệ: {menh_huong_result.get('moiquanhe_nguhanh')}")
94
+ print(f" - Kết luận: {menh_huong_result.get('ketluanchinh')}")
95
+ else:
96
+ print(" - Không tìm thấy kết quả.")
97
+
98
+ 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':")
99
+ menh_menh_result = get_menh_menh_interaction("Kiếm Phong Kim", "Tùng Bách Mộc")
100
+ if menh_menh_result:
101
+ print(f" - Mối quan hệ: {menh_menh_result.get('moiquanhe_nguhanh')}")
102
+ print(f" - Kết luận: {menh_menh_result.get('ketluanchinh')}")
103
+ else:
104
+ print(" - Không tìm thấy kết quả.")
data/raw/bat_trach_cung_vi.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97cb271562de795909b11a6369c00307758741943f1876b753beaae4829774bd
3
+ size 14455
data/raw/cung_menh_huong_rules.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79e4d5d855cbed2d9dcee7577867a1023b7266838156429356d6072090a144bd
3
+ size 23433
data/raw/cung_menh_lookup.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03914f988f74064ac95fb3e90927ac8540afdae2407b5d92564fa7ec28835a7a
3
+ size 43300
data/raw/huong.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d5462e49afd2858fa7ed7cdb4fdce4924d18e1adbfc17678bbc60f6fb62117e
3
+ size 16296
data/raw/loan_dau_cat_tuong.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06638f3aa051aad7f185baac1ee5e410160814588580cd316e473d443948e901
3
+ size 127920
data/raw/menh.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:11c9ceea6502f4a21c1a3feb21f77d35b95ecd82c497fca55572b326f309665c
3
+ size 14492
data/raw/menh_huong_rules.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:329503c69eb6dfa2f84fefca42a759cfb6961ef3b3d4e8e20b9c52deaedf059c
3
+ size 71024
data/raw/menh_menh_rules.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:428925b602928792a0733c9364e89971da051c09ca9ec75910c5e1908f6b0e46
3
+ size 80776
data/raw/menh_ngu_hanh_lookup.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:152cd3cbb7bfe71b2a23916e69a94cc4cd7d89da30145fb203057a4679a1d4b6
3
+ size 12673
data/raw/nap_am.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:047dbf340f3b6d877b693ea0d78464c44c3656125a0c1788f7a556bf2146e07f
3
+ size 33710
data/raw/ngoai_canh_sat_khi.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ce3b8eb3182817ad46297617bb2f1c0b03a1df442d6dd4f958006fb3d1e6923
3
+ size 125292
data/raw/phi_tinh_luu_nien.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91fb1d6705a14d7d618f80ab7249d8c9064d97405edfaccfc0650093eb896517
3
+ size 19505
data/raw/vat_pham_phong_thuy.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5176ea4e93e77db431ad890539a688606e0f1db052b6c2c63c3b2e1a9c40a05f
3
+ size 102580