Spaces:
Sleeping
Sleeping
anh-khoa-nguyen
commited on
Commit
·
f972c00
1
Parent(s):
571a7d8
first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- .gitignore +209 -0
- Dockerfile +31 -0
- LICENSE +21 -0
- README.md +4 -4
- app/__init__.py +0 -0
- app/core/__init__.py +0 -0
- app/core/config.py +62 -0
- app/core/logging_config.py +0 -0
- app/database/__init__.py +0 -0
- app/database/connection.py +92 -0
- app/database/models.py +0 -0
- app/main.py +167 -0
- app/orchestrator/__init__.py +0 -0
- app/orchestrator/workflow_manager.py +99 -0
- app/orchestrator/workflows/__init__.py +0 -0
- app/orchestrator/workflows/analyze_house.py +93 -0
- app/orchestrator/workflows/base_workflow.py +42 -0
- app/orchestrator/workflows/compare_people.py +60 -0
- app/orchestrator/workflows/lookup_item.py +60 -0
- app/orchestrator/workflows/lookup_loandau.py +90 -0
- app/orchestrator/workflows/lookup_namsinh.py +103 -0
- app/services/__init__.py +0 -0
- app/services/context_manager.py +77 -0
- app/services/intent_analyzer.py +124 -0
- app/services/prompt_templates.py +93 -0
- app/services/response_synthesizer.py +210 -0
- app/tools/__init__.py +0 -0
- app/tools/bat_trach_tools.py +108 -0
- app/tools/can_chi_helper.py +139 -0
- app/tools/general_tools.py +136 -0
- app/tools/loan_dau_tools.py +106 -0
- app/tools/ngu_hanh_tools.py +163 -0
- app/tools/reranker_tools.py +90 -0
- app/tools/semantic_search_tools.py +146 -0
- app/tools/tool_provider.py +0 -0
- app/tools/tuong_tac_tools.py +104 -0
- data/raw/bat_trach_cung_vi.xlsx +3 -0
- data/raw/cung_menh_huong_rules.xlsx +3 -0
- data/raw/cung_menh_lookup.xlsx +3 -0
- data/raw/huong.xlsx +3 -0
- data/raw/loan_dau_cat_tuong.xlsx +3 -0
- data/raw/menh.xlsx +3 -0
- data/raw/menh_huong_rules.xlsx +3 -0
- data/raw/menh_menh_rules.xlsx +3 -0
- data/raw/menh_ngu_hanh_lookup.xlsx +3 -0
- data/raw/nap_am.xlsx +3 -0
- data/raw/ngoai_canh_sat_khi.xlsx +3 -0
- data/raw/phi_tinh_luu_nien.xlsx +3 -0
- 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:
|
| 5 |
-
colorTo:
|
| 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
|