Nguyen Trong Lap commited on
Commit
eeb0f9c
·
0 Parent(s):

Recreate history without binary blobs

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .gitattributes +36 -0
  3. README.md +212 -0
  4. agents/AGENT_ARCHITECTURE.md +1235 -0
  5. agents/README.md +312 -0
  6. agents/__init__.py +40 -0
  7. agents/core/__init__.py +14 -0
  8. agents/core/base_agent.py +602 -0
  9. agents/core/context_analyzer.py +260 -0
  10. agents/core/coordinator.py +579 -0
  11. agents/core/orchestrator.py +212 -0
  12. agents/core/response_validator.py +182 -0
  13. agents/core/router.py +657 -0
  14. agents/core/unified_tone.py +155 -0
  15. agents/specialized/__init__.py +35 -0
  16. agents/specialized/exercise_agent.py +413 -0
  17. agents/specialized/general_health_agent.py +194 -0
  18. agents/specialized/mental_health_agent.py +368 -0
  19. agents/specialized/nutrition_agent.py +598 -0
  20. agents/specialized/symptom_agent.py +854 -0
  21. app.py +31 -0
  22. assets/bot-avatar.png +3 -0
  23. auth/auth.py +103 -0
  24. auth/db.py +50 -0
  25. chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/data_level0.bin +3 -0
  26. chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/header.bin +0 -0
  27. chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/index_metadata.pickle +3 -0
  28. chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/length.bin +0 -0
  29. chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/link_lists.bin +0 -0
  30. config/settings.py +22 -0
  31. data_mining/__init__.py +2 -0
  32. data_mining/mining_fitness.py +134 -0
  33. data_mining/mining_medical_qa.py +291 -0
  34. data_mining/mining_mentalchat.py +178 -0
  35. data_mining/mining_nutrition.py +144 -0
  36. data_mining/mining_vietnamese_nutrition.py +120 -0
  37. data_mining/mining_vimedical.py +180 -0
  38. data_mining/vn_food_db.py +162 -0
  39. examples/feedback_loop_example.py +267 -0
  40. examples/multilingual_example.py +239 -0
  41. examples/pydantic_validation_example.py +231 -0
  42. examples/session_persistence_example.py +103 -0
  43. examples/summarization_example.py +128 -0
  44. feedback/__init__.py +25 -0
  45. feedback/feedback_analyzer.py +333 -0
  46. feedback/feedback_system.py +425 -0
  47. fine_tuning/README.md +297 -0
  48. fine_tuning/__init__.py +14 -0
  49. fine_tuning/data_collector.py +217 -0
  50. fine_tuning/trainer.py +244 -0
.DS_Store ADDED
Binary file (10.2 kB). View file
 
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
+ *.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Healthcare AI Chatbot
2
+
3
+ Trợ lý sức khỏe cá nhân thông minh sử dụng OpenAI API và Gradio.
4
+
5
+ ## ✨ Key Features
6
+
7
+ - 🤖 **Multi-Agent Architecture** - Specialized agents for nutrition, exercise, symptoms, mental health
8
+ - 🧠 **Conversation Memory** - Remembers user data, no repeated questions
9
+ - 🔄 **Agent Handoffs** - Smooth transitions between specialists
10
+ - 💬 **Agent Communication** - Agents share context and collaborate
11
+ - 📚 **RAG Integration** - Medical knowledge from WHO, CDC, NIMH
12
+ - 🎯 **Context-Aware Routing** - Intelligent query understanding
13
+
14
+ See [agents/AGENT_ARCHITECTURE.md](agents/AGENT_ARCHITECTURE.md) for detailed architecture documentation.
15
+
16
+ ## Setup
17
+
18
+ ### 1. Create Virtual Environment
19
+
20
+ First, create a virtual environment to isolate project dependencies:
21
+
22
+ **macOS/Linux:**
23
+ ```bash
24
+ python3 -m venv venv
25
+ ```
26
+
27
+ **Windows:**
28
+ ```bash
29
+ python -m venv venv
30
+ ```
31
+
32
+ ### 2. Activate Virtual Environment
33
+
34
+ **macOS/Linux:**
35
+ ```bash
36
+ source venv/bin/activate
37
+ ```
38
+
39
+ **Windows:**
40
+ ```bash
41
+ venv\Scripts\activate
42
+ ```
43
+
44
+ ### 3. Install Dependencies
45
+
46
+ ```bash
47
+ pip install -r requirements.txt
48
+ ```
49
+
50
+ ### 4. Configure Environment Variables
51
+
52
+ Create a `.env` file in the project root:
53
+
54
+ ```bash
55
+ OPENAI_API_KEY=your_api_key_here
56
+ ```
57
+
58
+ ### 5. Setup RAG System & Fine-tuning (One-time)
59
+
60
+ **IMPORTANT:** Before running the app, setup the complete system with one command:
61
+
62
+ ```bash
63
+ # One command to setup everything (15-20 minutes)
64
+ bash scripts/setup_rag.sh
65
+ ```
66
+
67
+ **What this does:**
68
+
69
+ **Phase 1: RAG Databases (10-15 minutes)**
70
+ - ✅ Downloads and processes medical datasets from HuggingFace
71
+ - ✅ Builds ChromaDB vector databases for each domain
72
+ - ✅ Total: ~160 MB, 6 specialized databases
73
+
74
+ **Datasets by Domain:**
75
+ - **Symptoms/Diseases**: ViMedical_Disease (603 diseases, ~50 MB)
76
+ - **Mental Health**: MentalChat16K (16K conversations, 33 topics, ~80 MB)
77
+ - **Nutrition**: LLM_Dietary_Recommendation (50 patient profiles + diet plans, ~20 MB)
78
+ - **Vietnamese Food**: Vietnamese_Nutrition (73 foods with nutrition facts, ~5 MB)
79
+ - **Fitness**: GYM-Exercise (1.66K exercises, ~10 MB)
80
+ - **Medical Q&A**: Vietnamese_Medical_QA (9.3K Q&A pairs, ~15 MB)
81
+
82
+ **Phase 2: Training Data Generation (2-3 minutes)**
83
+ - ✅ Generates 200 synthetic conversations using GPT-4o-mini
84
+ - ✅ 50 scenarios per agent (nutrition, symptom, exercise, mental_health)
85
+ - ✅ Cost: ~$0.50 from your API budget
86
+ - ✅ Saved to `fine_tuning/training_data/` (NOT committed to git)
87
+
88
+ **Phase 3: Fine-tuning (Optional, 30-60 minutes)**
89
+ - ❓ Prompts: "Do you want to fine-tune now? (y/N)" (10 sec timeout)
90
+ - ✅ If yes: Uploads data, creates fine-tuning jobs, waits for completion
91
+ - ✅ If no: Skip, you can fine-tune later with `python scripts/auto_finetune.py`
92
+ - ✅ Cost: ~$2.00 from your API budget
93
+ - ✅ Creates `config/fine_tuned_models.py` (NOT committed to git)
94
+
95
+ **Total Cost:** ~$2.50 from your API budget (if you choose to fine-tune)
96
+
97
+ **Fine-tune Later (Optional):**
98
+ ```bash
99
+ # If you skipped fine-tuning during setup
100
+ python scripts/auto_finetune.py
101
+ ```
102
+
103
+ **Manual Setup (Alternative):**
104
+ ```bash
105
+ # RAG only (no training data generation)
106
+ python data_mining/mining_vimedical.py
107
+ python data_mining/mining_mentalchat.py
108
+ # ... other mining scripts
109
+
110
+ # Training data only
111
+ python scripts/generate_training_data.py
112
+
113
+ # Fine-tuning only
114
+ python scripts/auto_finetune.py
115
+ ```
116
+
117
+ **Team Sharing:**
118
+ - ✅ Each team member runs `bash scripts/setup_rag.sh` once
119
+ - ✅ Everyone generates their own data with their API key
120
+ - ❌ RAG databases and training data are NOT committed to git (too large)
121
+ - ✅ Scripts and code are committed for easy sharing
122
+
123
+ ## Run the Application
124
+
125
+ You have multiple options to run the application:
126
+
127
+ **Option 1: Using the shell script (recommended):**
128
+ ```bash
129
+ bash run.sh
130
+ ```
131
+
132
+ **Option 2: Using Gradio CLI:**
133
+ ```bash
134
+ gradio app.py
135
+ ```
136
+
137
+ **Option 3: Using Python directly:**
138
+ ```bash
139
+ python app.py
140
+ ```
141
+
142
+ **Notes:**
143
+ - The app will launch with a local URL and a public shareable link
144
+ - Ensure your virtual environment is activated before running
145
+ - Make sure `OPENAI_API_KEY` is set in your `.env` file
146
+
147
+ ### 5. Deactivate Virtual Environment (when done)
148
+
149
+ ```bash
150
+ deactivate
151
+ ```
152
+
153
+ ## Project Structure
154
+
155
+ ```
156
+ healthcare_bot/
157
+ ├── app.py # File chính (Gradio UI)
158
+ ├── rag/
159
+ │ ├── ingest.py # Ingest tài liệu vào ChromaDB
160
+ │ ├── query_engine.py # LangChain Retrieval QA
161
+ │ └── data/ # Nguồn PDF/CSV/MD
162
+ ├── modules/
163
+ │ ├── nutrition.py # Module dinh dưỡng
164
+ │ ├── exercise.py # Module bài tập
165
+ │ └── rules.json # Quy tắc cơ bản
166
+ ├── utils/
167
+ │ └── helpers.py # Hàm hỗ trợ, tính BMI, format output
168
+ ├── config/
169
+ │ └── settings.py # Config env + API + model
170
+ └── requirements.txt
171
+ ```
172
+
173
+ ## Technologies
174
+
175
+ - Python 3.9+
176
+ - OpenAI API (GPT-4o-mini)
177
+ - Gradio 5.49.0
178
+ - python-dotenv 1.1.1
179
+ - Virtual Environment (venv)
180
+
181
+ ## Features
182
+
183
+ - 💬 **Chat interface với AI** - Giao diện trò chuyện thân thiện
184
+ - 🏥 **Tư vấn sức khỏe toàn diện** - Hỏi kỹ thông tin trước khi tư vấn
185
+ - 🔍 **Thu thập thông tin chi tiết** - Hỏi về triệu chứng, bệnh nền, thuốc đang dùng
186
+ - 📊 **Đánh giá tổng quan** - Phân tích dựa trên nhiều yếu tố sức khỏe
187
+ - 🌐 **Public shareable link** - Chia sẻ dễ dàng
188
+ - 📱 **Responsive UI** - Giao diện đẹp, hiện đại
189
+ - 💾 **Lưu lịch sử hội thoại** - Nhớ ngữ cảnh cuộc trò chuyện
190
+
191
+ ## Cách chatbot hoạt động
192
+
193
+ Khi bạn chia sẻ triệu chứng hoặc thông tin sức khỏe, chatbot sẽ:
194
+
195
+ 1. **Hỏi thông tin cá nhân**: Tuổi, giới tính, cân nặng, chiều cao (nếu cần)
196
+ 2. **Hỏi về triệu chứng**: Thời gian, mức độ nghiêm trọng, triệu chứng kèm theo
197
+ 3. **Hỏi về bệnh nền**: Tiểu đường, huyết áp cao, tim mạch, v.v.
198
+ 4. **Hỏi về thuốc**: Thuốc đang dùng, liệu pháp điều trị
199
+ 5. **Hỏi về lối sống**: Chế độ ăn, tập luyện, giấc ngủ, stress
200
+ 6. **Đưa ra tư vấn**: Sau khi có đủ thông tin, đưa ra lời khuyên toàn diện và chính xác
201
+
202
+ **Ví dụ:**
203
+ ```
204
+ User: "Tôi bị đau đầu"
205
+ Bot: "Tôi hiểu bạn đang bị đau đầu. Để tư vấn chính xác hơn, cho tôi hỏi thêm:
206
+ - Bạn bao nhiêu tuổi?
207
+ - Đau đầu kéo dài bao lâu rồi?
208
+ - Mức độ đau (nhẹ/vừa/nặng)?
209
+ - Có triệu chứng kèm theo không (buồn nôn, chóng mặt)?
210
+ - Bạn có bệnh nền gì không?
211
+ - Đang dùng thuốc gì không?"
212
+ ```
agents/AGENT_ARCHITECTURE.md ADDED
@@ -0,0 +1,1235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent-Based Architecture Documentation 🏗️
2
+
3
+ ## Overview
4
+
5
+ This system uses an **agent-based architecture** with **OpenAI function calling** for intelligent healthcare assistance.
6
+
7
+ ### Why Agent-Based Architecture?
8
+
9
+ **Advantages over Monolithic:**
10
+ 1. **Token Efficiency** - Each agent loads only necessary prompts (60-70% reduction)
11
+ 2. **Scalability** - Easy to add new specialized agents
12
+ 3. **Accuracy** - Domain-specific expertise per agent
13
+ 4. **Maintainability** - Clear separation of concerns
14
+ 5. **Context Awareness** - Intelligent routing with conversation history
15
+
16
+ ### Core Capabilities
17
+
18
+ - **Specialized Agents** - Nutrition, Exercise, Symptoms, Mental Health, General Health
19
+ - **Conversation Memory** - Persistent user data across conversation
20
+ - **Agent Handoffs** - Smooth transitions between specialists
21
+ - **Agent Communication** - Cross-agent data sharing and collaboration
22
+ - **Multi-Agent Responses** - Coordinate multiple agents for complex queries
23
+ - **Context-Aware Routing** - Understand conversation flow and intent
24
+
25
+ ---
26
+
27
+ ## 📊 System Architecture
28
+
29
+ ```
30
+ User Input
31
+
32
+ Agent Coordinator
33
+
34
+ ┌─────────────────────────────────────────────┐
35
+ │ Shared Conversation Memory │
36
+ │ ┌────────────────────────────────────┐ │
37
+ │ │ • User Profile (age, gender, etc.) │ │
38
+ │ │ • Agent-specific Data │ │
39
+ │ │ • Conversation State │ │
40
+ │ │ • Pending Questions │ │
41
+ │ └────────────────────────────────────┘ │
42
+ └─────────────────────────────────────────────┘
43
+
44
+ Router (Function Calling) + Context Analysis
45
+
46
+ ┌─────────────────────────────────────┐
47
+ │ Chọn Agent(s) Phù Hợp │
48
+ ├─────────────────────────────────────┤
49
+ │ • Nutrition Agent │
50
+ │ • Exercise Agent │
51
+ │ • Symptom Agent │
52
+ │ • Mental Health Agent │
53
+ │ • General Health Agent (default) │
54
+ └─────────────────────────────────────┘
55
+
56
+ ┌─ Single Agent Response
57
+ ├─ Agent Handoff (smooth transition)
58
+ └─ Multi-Agent Combined Response
59
+
60
+ Response (with full context awareness)
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🤖 Các Agent
66
+
67
+ ### 1. **Router** (`agents/core/router.py`)
68
+
69
+ **Chức năng:** Phân tích user input và route đến agent phù hợp
70
+
71
+ **Công nghệ:** OpenAI Function Calling
72
+
73
+ **Available Functions:**
74
+ ```python
75
+ - nutrition_agent: Dinh dưỡng, BMI, calo, thực đơn
76
+ - exercise_agent: Tập luyện, gym, yoga, cardio
77
+ - symptom_agent: Triệu chứng bệnh, đau đầu, sốt
78
+ - mental_health_agent: Stress, lo âu, trầm cảm
79
+ - general_health_agent: Câu hỏi chung về sức khỏe
80
+ ```
81
+
82
+ **🆕 Context-Aware Features:**
83
+
84
+ 1. **Extended Context Window:**
85
+ - OLD: 3 exchanges
86
+ - NEW: **10 exchanges** (+233%)
87
+ - Hiểu conversation flow tốt hơn
88
+
89
+ 2. **Last Agent Tracking:**
90
+ - Track agent nào vừa được dùng
91
+ - Giúp xử lý follow-up questions
92
+ - Example: "Vậy nên ăn gì?" → biết đang nói về giảm cân
93
+
94
+ 3. **Enhanced Routing Prompt:**
95
+ - Hướng dẫn rõ ràng về câu hỏi mơ hồ
96
+ - Ví dụ cụ thể về follow-up questions
97
+ - Detect topic switching
98
+
99
+ 4. **Improved System Prompt:**
100
+ - Nhấn mạnh khả năng hiểu ngữ cảnh
101
+ - Xử lý ambiguous questions
102
+ - Recognize follow-up patterns (vậy, còn, thì sao)
103
+
104
+ **Routing Accuracy:**
105
+ - Clear questions: **90-95%**
106
+ - Follow-up questions: **80-85%** (improved from ~60%)
107
+ - Topic switching: **85-90%**
108
+ - Multi-topic: **70-75%**
109
+
110
+ **Ví dụ:**
111
+ ```python
112
+ from agents import route_to_agent
113
+
114
+ # Example 1: Clear question
115
+ result = route_to_agent("Tôi muốn giảm cân", chat_history)
116
+ # Returns: {
117
+ # "agent": "nutrition_agent",
118
+ # "parameters": {"user_query": "Tôi muốn giảm cân"},
119
+ # "confidence": 0.9
120
+ # }
121
+
122
+ # Example 2: Ambiguous follow-up (NEW - context-aware)
123
+ chat_history = [
124
+ ["Tôi muốn giảm cân", "Response from nutrition_agent..."]
125
+ ]
126
+ result = route_to_agent("Vậy nên ăn gì?", chat_history)
127
+ # Returns: {
128
+ # "agent": "nutrition_agent", # ✅ Understands context!
129
+ # "parameters": {"user_query": "Vậy nên ăn gì?"},
130
+ # "confidence": 0.9
131
+ # }
132
+
133
+ # Example 3: Topic switch
134
+ chat_history = [
135
+ ["Tôi muốn giảm cân", "Response..."],
136
+ ["Vậy nên ăn gì?", "Response..."]
137
+ ]
138
+ result = route_to_agent("À mà tôi bị đau đầu", chat_history)
139
+ # Returns: {
140
+ # "agent": "symptom_agent", # ✅ Detects topic switch!
141
+ # "parameters": {"user_query": "À mà tôi bị đau đầu"},
142
+ # "confidence": 0.9
143
+ # }
144
+ ```
145
+
146
+ **Context Handling Examples:**
147
+
148
+ | User Message | Context | Routed To | Why |
149
+ |--------------|---------|-----------|-----|
150
+ | "Tôi muốn giảm cân" | None | nutrition_agent | Clear question |
151
+ | "Vậy nên ăn gì?" | After giảm cân | nutrition_agent | Follow-up with context |
152
+ | "Tôi nên tập gì?" | After giảm cân | exercise_agent | Clear topic |
153
+ | "Còn về dinh dưỡng?" | After tập gym | nutrition_agent | Explicit topic mention |
154
+ | "À mà tôi bị đau đầu" | Any | symptom_agent | Clear topic switch |
155
+ | "Nó có ảnh hưởng gì?" | After đau đầu | symptom_agent | Pronoun resolution |
156
+
157
+ ---
158
+
159
+ ### 2. **Nutrition Agent** (`agents/specialized/nutrition_agent.py`)
160
+
161
+ **Chuyên môn:**
162
+ - Tính BMI, phân tích thể trạng
163
+ - Tính calo, macro (protein/carb/fat)
164
+ - Gợi ý thực đơn
165
+ - Thực phẩm bổ sung
166
+
167
+ **System Prompt:** ~500 tokens (thay vì 3000+ tokens của monolithic)
168
+
169
+ **Data Flow:**
170
+ ```
171
+ User: "Tôi muốn giảm cân"
172
+
173
+ Router → nutrition_agent
174
+
175
+ Agent hỏi: tuổi, giới tính, cân nặng, chiều cao
176
+
177
+ User cung cấp thông tin
178
+
179
+ Agent tính BMI → Gọi NutritionAdvisor
180
+
181
+ Response: BMI + Calo + Thực đơn + Lời khuyên
182
+ ```
183
+
184
+ **Ví dụ Response:**
185
+ ```
186
+ 🥗 Tư Vấn Dinh Dưỡng Cá Nhân Hóa
187
+
188
+ 📊 Phân tích BMI:
189
+ - BMI: 24.5 (normal)
190
+ - Lời khuyên: Duy trì cân nặng
191
+
192
+ 🎯 Mục tiêu hàng ngày:
193
+ - 🔥 Calo: 1800 kcal
194
+ - 🥩 Protein: 112g
195
+ - 🍚 Carb: 202g
196
+ - 🥑 Chất béo: 50g
197
+
198
+ 🍽️ Gợi ý thực đơn:
199
+ [Chi tiết món ăn...]
200
+ ```
201
+
202
+ ---
203
+
204
+ ### 3. **Exercise Agent** (`agents/specialized/exercise_agent.py`)
205
+
206
+ **Chuyên môn:**
207
+ - Tạo lịch tập 7 ngày
208
+ - Tư vấn bài tập theo mục tiêu
209
+ - Hướng dẫn kỹ thuật an toàn
210
+ - Progression (tuần 1, 2, 3...)
211
+
212
+ **System Prompt:** ~400 tokens
213
+
214
+ **Data Flow:**
215
+ ```
216
+ User: "Tôi muốn tập gym"
217
+
218
+ Router → exercise_agent
219
+
220
+ Agent hỏi: tuổi, giới tính, thể lực, mục tiêu, thời gian
221
+
222
+ User cung cấp thông tin
223
+
224
+ Agent gọi generate_exercise_plan()
225
+
226
+ Response: Lịch tập 7 ngày chi tiết
227
+ ```
228
+
229
+ ---
230
+
231
+ ### 4. **Symptom Agent** (`agents/specialized/symptom_agent.py`)
232
+
233
+ **Chuyên môn:**
234
+ - Đánh giá triệu chứng bằng OPQRST method
235
+ - Phát hiện red flags
236
+ - Tư vấn xử lý tại nhà
237
+ - Khuyên khi nào cần gặp bác sĩ
238
+
239
+ **System Prompt:** ~600 tokens
240
+
241
+ **OPQRST Method:**
242
+ - **O**nset: Khi nào bắt đầu?
243
+ - **P**rovocation/Palliation: Gì làm tệ/đỡ hơn?
244
+ - **Q**uality: Mô tả cảm giác?
245
+ - **R**egion/Radiation: Vị trí?
246
+ - **S**everity: Mức độ 1-10?
247
+ - **T**iming: Lúc nào xuất hiện?
248
+
249
+ **Red Flags Detection:**
250
+ ```python
251
+ - Đau ngực + khó thở → Heart attack warning
252
+ - Đau đầu + cứng gáy + sốt → Meningitis warning
253
+ - Yếu một bên cơ thể → Stroke warning
254
+ ```
255
+
256
+ **Data Flow:**
257
+ ```
258
+ User: "Tôi bị đau đầu"
259
+
260
+ Router → symptom_agent
261
+
262
+ Agent check red flags → Không có
263
+
264
+ Agent hỏi OPQRST (6 rounds)
265
+
266
+ User trả lời từng round
267
+
268
+ Agent phân tích → Đưa ra lời khuyên
269
+ ```
270
+
271
+ ---
272
+
273
+ ### 5. **Mental Health Agent** (`agents/specialized/mental_health_agent.py`)
274
+
275
+ **Chuyên môn:**
276
+ - Hỗ trợ stress, lo âu, trầm cảm
277
+ - Kỹ thuật thư giãn, mindfulness
278
+ - Cải thiện giấc ngủ
279
+ - Quản lý cảm xúc
280
+
281
+ **System Prompt:** ~500 tokens
282
+
283
+ **Crisis Detection:**
284
+ ```python
285
+ - Ý định tự tử → Hotline khẩn cấp:
286
+ • 115 - Cấp cứu y tế (Trung tâm Cấp cứu 115 TP.HCM)
287
+ • 1900 1267 - Chuyên gia tâm thần (Bệnh viện Tâm Thần TP.HCM)
288
+ • 0909 65 80 35 - Tư vấn tâm lý miễn phí (Davipharm)
289
+ - Tự gây thương tích → Same hotlines
290
+ - ONLY show hotlines for serious mental health crises
291
+ ```
292
+
293
+ **Phong cách:**
294
+ - Ấm áp, đồng cảm 💙
295
+ - Validate cảm xúc
296
+ - Không phán xét
297
+ - Khuyến khích tìm kiếm sự hỗ trợ
298
+
299
+ ---
300
+
301
+ ### 6. **General Health Agent** (`agents/specialized/general_health_agent.py`)
302
+
303
+ **Chuyên môn:**
304
+ - Câu hỏi chung về sức khỏe
305
+ - Phòng bệnh
306
+ - Lối sống lành mạnh
307
+ - Default fallback agent
308
+
309
+ **System Prompt:** ~2000 tokens (comprehensive prompt từ helpers.py)
310
+
311
+ **Khi nào dùng:**
312
+ - Câu hỏi không rõ ràng
313
+ - Không match với agent chuyên môn
314
+ - Routing thất bại
315
+
316
+ ---
317
+
318
+ ## 🧠 Memory & Coordination Components
319
+
320
+ ### 7. **Conversation Memory** (`utils/memory.py`) - ✨ NEW!
321
+
322
+ **Chức năng:** Shared memory system cho tất cả agents
323
+
324
+ **Core Features:**
325
+
326
+ 1. **User Profile Storage**
327
+ ```python
328
+ memory.update_profile('age', 25)
329
+ memory.update_profile('weight', 70)
330
+ memory.get_profile('age') # → 25
331
+ ```
332
+
333
+ 2. **Missing Fields Detection**
334
+ ```python
335
+ missing = memory.get_missing_fields(['age', 'gender', 'weight', 'height'])
336
+ # → ['gender', 'height']
337
+ ```
338
+
339
+ 3. **Agent-Specific Data**
340
+ ```python
341
+ memory.add_agent_data('nutrition', 'goal', 'weight_loss')
342
+ memory.get_agent_data('nutrition', 'goal') # → 'weight_loss'
343
+ ```
344
+
345
+ 4. **Conversation State Tracking**
346
+ ```python
347
+ memory.set_current_agent('nutrition_agent')
348
+ memory.get_current_agent() # → 'nutrition_agent'
349
+ memory.get_previous_agent() # → 'symptom_agent'
350
+ ```
351
+
352
+ 5. **Context Summary**
353
+ ```python
354
+ memory.get_context_summary()
355
+ # → "User: 25 tuổi, nam | 70kg, 175cm | Topic: giảm cân"
356
+ ```
357
+
358
+ **Benefits:**
359
+ - ✅ No repeated questions
360
+ - ✅ Full conversation context
361
+ - ✅ Agent coordination
362
+ - ✅ Persistent user data
363
+
364
+ ---
365
+
366
+ ### 8. **Base Agent Class** (`agents/core/base_agent.py`) - ✨ NEW!
367
+
368
+ **Chức năng:** Parent class cho tất cả agents với memory support
369
+
370
+ **Core Methods:**
371
+
372
+ 1. **Memory Access**
373
+ ```python
374
+ class MyAgent(BaseAgent):
375
+ def handle(self, parameters, chat_history):
376
+ # Get user profile
377
+ profile = self.get_user_profile()
378
+
379
+ # Update profile
380
+ self.update_user_profile('age', 25)
381
+
382
+ # Check missing fields
383
+ missing = self.get_missing_profile_fields(['age', 'weight'])
384
+ ```
385
+
386
+ 2. **Handoff Detection**
387
+ ```python
388
+ # Check if should hand off
389
+ if self.should_handoff(user_query, chat_history):
390
+ next_agent = self.suggest_next_agent(user_query)
391
+ return self.create_handoff_message(next_agent)
392
+ ```
393
+
394
+ 3. **Multi-Agent Collaboration**
395
+ ```python
396
+ # Detect if multiple agents needed
397
+ agents_needed = self.needs_collaboration(user_query)
398
+ # → ['nutrition_agent', 'exercise_agent']
399
+ ```
400
+
401
+ 4. **Context Awareness**
402
+ ```python
403
+ # Get conversation context
404
+ context = self.get_context_summary()
405
+ previous_agent = self.get_previous_agent()
406
+ current_topic = self.get_current_topic()
407
+ ```
408
+
409
+ **Benefits:**
410
+ - ✅ Unified interface for all agents
411
+ - ✅ Built-in memory access
412
+ - ✅ Automatic handoff logic
413
+ - ✅ Context awareness
414
+
415
+ ---
416
+
417
+ ### 9. **Agent Coordinator** (`agents/core/coordinator.py`) - ✨ NEW!
418
+
419
+ **Chức năng:** Orchestrates all agents with shared memory
420
+
421
+ **Core Features:**
422
+
423
+ 1. **Shared Memory Management**
424
+ - All agents share same memory instance
425
+ - Automatic memory updates from chat history
426
+ - Persistent user data across turns
427
+
428
+ 2. **Single Agent Routing**
429
+ ```python
430
+ coordinator = AgentCoordinator()
431
+ response = coordinator.handle_query(
432
+ "Tôi muốn giảm cân",
433
+ chat_history
434
+ )
435
+ # → Routes to nutrition_agent with memory
436
+ ```
437
+
438
+ 3. **Agent Handoff**
439
+ ```python
440
+ # User: "Tôi muốn giảm cân nhưng bị đau đầu"
441
+ # Nutrition agent detects symptom keyword
442
+ # → Smooth handoff to symptom_agent
443
+ ```
444
+
445
+ 4. **Multi-Agent Collaboration**
446
+ ```python
447
+ # User: "Tôi muốn giảm cân, nên ăn gì và tập gì?"
448
+ # Coordinator detects need for both agents
449
+ # → Combined response from nutrition + exercise
450
+ ```
451
+
452
+ 5. **Memory Persistence**
453
+ ```python
454
+ # Turn 1
455
+ coordinator.handle_query("Tôi 25 tuổi, nam, 70kg", [])
456
+
457
+ # Turn 2 - Memory persists!
458
+ coordinator.handle_query("Tôi muốn giảm cân", chat_history)
459
+ # → Agent knows age=25, gender=male, weight=70
460
+ ```
461
+
462
+ **Response Types:**
463
+
464
+ 1. **Single Agent Response**
465
+ ```
466
+ User: "Tôi muốn giảm cân"
467
+ → Nutrition agent handles
468
+ ```
469
+
470
+ 2. **Handoff Response**
471
+ ```
472
+ User: "Tôi muốn giảm cân nhưng bị đau đầu"
473
+ → Nutrition agent → Handoff → Symptom agent
474
+ ```
475
+
476
+ 3. **Multi-Agent Response**
477
+ ```
478
+ User: "Tôi muốn giảm cân, nên ăn gì và tập gì?"
479
+
480
+ Response:
481
+ ---
482
+ ## 🥗 Tư Vấn Dinh Dưỡng
483
+ [Nutrition advice]
484
+
485
+ ---
486
+ ## 💪 Tư Vấn Tập Luyện
487
+ [Exercise advice]
488
+ ---
489
+ ```
490
+
491
+ **Benefits:**
492
+ - ✅ Seamless agent coordination
493
+ - ✅ No repeated questions
494
+ - ✅ Multi-agent support
495
+ - ✅ Smooth handoffs
496
+ - ✅ Full context awareness
497
+
498
+ ---
499
+
500
+
501
+
502
+ ## 🔄 Flow Hoàn Chỉnh
503
+
504
+ ### Example 1: Nutrition Request (with Memory) ✨ NEW!
505
+
506
+ ```
507
+ User: "Tôi 25 tuổi, nam, 70kg, 175cm, muốn giảm cân"
508
+
509
+ helpers.chat_logic() → USE_COORDINATOR = True
510
+
511
+ AgentCoordinator.handle_query()
512
+
513
+ Update Shared Memory from chat history
514
+ → memory.update_profile('age', 25)
515
+ → memory.update_profile('gender', 'male')
516
+ → memory.update_profile('weight', 70)
517
+ → memory.update_profile('height', 175)
518
+
519
+ route_to_agent() → Function Calling
520
+
521
+ OpenAI returns: nutrition_agent
522
+
523
+ memory.set_current_agent('nutrition_agent')
524
+
525
+ NutritionAgent.handle() [with memory access]
526
+
527
+ Check memory for user data
528
+ → user_data = memory.get_full_profile()
529
+ → {age: 25, gender: 'male', weight: 70, height: 175}
530
+
531
+ NutritionAdvisor.generate_nutrition_advice(user_data)
532
+
533
+ Calculate BMI: 22.9 (normal)
534
+ Calculate targets: 1800 kcal, 112g protein...
535
+ Generate meal suggestions
536
+
537
+ Save agent data to memory
538
+ → memory.add_agent_data('nutrition', 'goal', 'weight_loss')
539
+ → memory.add_agent_data('nutrition', 'bmi', 22.9)
540
+
541
+ Format response
542
+
543
+ Return to user
544
+ ```
545
+
546
+ **Next Turn:**
547
+ ```
548
+ User: "Vậy tôi nên tập gì?"
549
+
550
+ AgentCoordinator.handle_query()
551
+
552
+ Memory already has: age=25, gender=male, weight=70, height=175
553
+
554
+ route_to_agent() → exercise_agent
555
+
556
+ ExerciseAgent.handle() [with memory access]
557
+
558
+ Get user data from memory (no need to ask again!)
559
+ → profile = memory.get_full_profile()
560
+ → nutrition_goal = memory.get_agent_data('nutrition', 'goal')
561
+
562
+ Generate exercise plan based on profile + nutrition goal
563
+
564
+ Return personalized exercise advice
565
+ ```
566
+
567
+ **Token Usage:**
568
+ - Router: ~200 tokens
569
+ - Nutrition Agent prompt: ~500 tokens
570
+ - Memory operations: negligible
571
+ - Total: ~700 tokens (vs 3000+ monolithic)
572
+
573
+ **Key Improvement:** ✅ No repeated questions!
574
+
575
+ ---
576
+
577
+ ### Example 2: Symptom Assessment
578
+
579
+ ```
580
+ User: "Tôi bị đau đầu"
581
+
582
+ route_to_agent() → symptom_agent
583
+
584
+ SymptomAgent.handle()
585
+
586
+ Check red flags: None
587
+
588
+ Assess OPQRST progress: onset not asked
589
+
590
+ Ask: "Đau từ khi nào? Đột ngột hay từ từ?"
591
+
592
+ User: "Đau từ 2 ngày trước, đột ngột"
593
+
594
+ Assess OPQRST: quality not asked
595
+
596
+ Ask: "Mô tả cảm giác? Mức độ 1-10?"
597
+
598
+ ... (continue 6 rounds)
599
+
600
+ All OPQRST collected → Provide assessment
601
+ ```
602
+
603
+ **Token Usage:**
604
+ - Each round: ~300-400 tokens
605
+ - Total: ~2000 tokens across conversation (vs 3000+ per message)
606
+
607
+ ---
608
+
609
+ ### Example 3: Agent Handoff ✨ NEW!
610
+
611
+ ```
612
+ User: "Tôi muốn giảm cân nhưng bị đau đầu"
613
+
614
+ AgentCoordinator.handle_query()
615
+
616
+ route_to_agent() → nutrition_agent (primary intent)
617
+
618
+ NutritionAgent.handle()
619
+
620
+ Detect symptom keyword: "đau đầu"
621
+
622
+ should_handoff() → True
623
+
624
+ suggest_next_agent() → 'symptom_agent'
625
+
626
+ create_handoff_message()
627
+
628
+ Response: "Mình thấy bạn có triệu chứng đau đầu.
629
+ Để tư vấn chính xác hơn, mình sẽ chuyển bạn
630
+ sang chuyên gia đánh giá triệu chứng nhé! 😊"
631
+
632
+ memory.set_current_agent('symptom_agent')
633
+
634
+ Next turn: SymptomAgent handles with full context
635
+ ```
636
+
637
+ **Benefits:**
638
+ - ✅ Smooth transition between agents
639
+ - ✅ Context preserved
640
+ - ✅ User-friendly handoff message
641
+
642
+ ---
643
+
644
+ ### Example 4: Multi-Agent Collaboration ✨ NEW!
645
+
646
+ ```
647
+ User: "Tôi muốn giảm cân, nên ăn gì và tập gì?"
648
+
649
+ AgentCoordinator.handle_query()
650
+
651
+ _detect_required_agents()
652
+ → ['nutrition_agent', 'exercise_agent']
653
+
654
+ _needs_multi_agent() → True
655
+
656
+ _handle_multi_agent_query()
657
+
658
+ Get response from nutrition_agent
659
+ → "Để giảm cân, bạn nên ăn..."
660
+
661
+ Get response from exercise_agent
662
+ → "Bạn nên tập cardio..."
663
+
664
+ _combine_responses()
665
+
666
+ Response:
667
+ ---
668
+ ## 🥗 Tư Vấn Dinh Dưỡng
669
+
670
+ Để giảm cân hiệu quả, bạn nên:
671
+ - Giảm 300-500 kcal/ngày
672
+ - Tăng protein, giảm carb tinh chế
673
+ - Ăn nhiều rau xanh, trái cây
674
+ [...]
675
+
676
+ ---
677
+ ## 💪 Tư Vấn Tập Luyện
678
+
679
+ Bạn nên tập:
680
+ - Cardio 30-45 phút/ngày (chạy bộ, đạp xe)
681
+ - Strength training 2-3 lần/tuần
682
+ - HIIT 2 lần/tuần
683
+ [...]
684
+
685
+ ---
686
+ 💬 Bạn có câu hỏi gì thêm không?
687
+ ```
688
+
689
+ **Benefits:**
690
+ - ✅ Comprehensive response
691
+ - ✅ Multiple expert perspectives
692
+ - ✅ Well-organized output
693
+ - ✅ Single response instead of multiple turns
694
+
695
+ ---
696
+
697
+ ## 💾 Data Structure
698
+
699
+ ### Unified User Data Format
700
+
701
+ ```python
702
+ {
703
+ # Common fields
704
+ "age": int,
705
+ "gender": str, # "male" or "female"
706
+ "weight": float, # kg
707
+ "height": float, # cm
708
+
709
+ # Nutrition specific
710
+ "goal": str, # "weight_loss", "weight_gain", "muscle_building", "maintenance"
711
+ "activity_level": str, # "low", "moderate", "high"
712
+ "dietary_restrictions": list,
713
+ "health_conditions": list,
714
+
715
+ # Exercise specific
716
+ "fitness_level": str, # "beginner", "intermediate", "advanced"
717
+ "available_time": int, # minutes per day
718
+
719
+ # Symptom specific
720
+ "symptom_type": str,
721
+ "duration": str,
722
+ "severity": int, # 1-10
723
+ "location": str,
724
+
725
+ # Mental health specific
726
+ "stress_level": str,
727
+ "triggers": list
728
+ }
729
+ ```
730
+
731
+ ---
732
+
733
+ ## 📈 Performance Comparison
734
+
735
+ ### Monolithic (helpers.py - OLD)
736
+
737
+ ```
738
+ ❌ Token per request: 3000-4000 tokens
739
+ ❌ Response time: 3-5 seconds
740
+ ❌ Cost: $0.03-0.04 per request
741
+ ❌ Maintainability: Low (1 file, 600+ lines)
742
+ ❌ Scalability: Hard to add new features
743
+ ```
744
+
745
+ ### Agent-Based (NEW)
746
+
747
+ ```
748
+ ✅ Token per request: 700-1500 tokens (50-70% reduction)
749
+ ✅ Response time: 1-3 seconds
750
+ ✅ Cost: $0.007-0.015 per request (70% cheaper)
751
+ ✅ Maintainability: High (modular, clear separation)
752
+ ✅ Scalability: Easy to add new agents
753
+ ```
754
+
755
+ ---
756
+
757
+ ## 🚀 Cách Sử Dụng
758
+
759
+ ### 0. Import Structure (NEW!)
760
+
761
+ **Option 1: Import from main package (Recommended)**
762
+ ```python
763
+ from agents import (
764
+ route_to_agent, # Router function
765
+ AgentCoordinator, # Coordinator class
766
+ BaseAgent, # Base agent class
767
+ NutritionAgent, # Specialized agents
768
+ ExerciseAgent,
769
+ get_agent # Agent factory
770
+ )
771
+ ```
772
+
773
+ **Option 2: Import from subpackages (Explicit)**
774
+ ```python
775
+ from agents.core import route_to_agent, AgentCoordinator, BaseAgent
776
+ from agents.specialized import NutritionAgent, ExerciseAgent
777
+ ```
778
+
779
+ **Option 3: Import specific modules**
780
+ ```python
781
+ from agents.core.router import route_to_agent
782
+ from agents.core.coordinator import AgentCoordinator
783
+ from agents.specialized.nutrition_agent import NutritionAgent
784
+ ```
785
+
786
+ ### 1. Basic Usage
787
+
788
+ ```python
789
+ from utils.helpers import chat_logic
790
+
791
+ message = "Tôi muốn giảm cân"
792
+ chat_history = []
793
+
794
+ _, updated_history = chat_logic(message, chat_history)
795
+ ```
796
+
797
+ ### 2. Add New Agent
798
+
799
+ ```python
800
+ # Step 1: Create new agent file
801
+ # agents/new_agent.py
802
+
803
+ class NewAgent:
804
+ def __init__(self):
805
+ self.system_prompt = "..."
806
+
807
+ def handle(self, parameters, chat_history):
808
+ # Your logic here
809
+ return response
810
+
811
+ # Step 2: Register in router.py
812
+ AVAILABLE_FUNCTIONS.append({
813
+ "name": "new_agent",
814
+ "description": "...",
815
+ "parameters": {...}
816
+ })
817
+
818
+ # Step 3: Register in __init__.py
819
+ AGENTS["new_agent"] = NewAgent
820
+ ```
821
+
822
+ ### 3. Test Specific Agent
823
+
824
+ ```python
825
+ from agents import get_agent
826
+
827
+ agent = get_agent("nutrition_agent")
828
+ response = agent.handle({
829
+ "user_query": "Tôi muốn giảm cân",
830
+ "user_data": {
831
+ "age": 25,
832
+ "gender": "male",
833
+ "weight": 70,
834
+ "height": 175
835
+ }
836
+ }, chat_history=[])
837
+
838
+ print(response)
839
+ ```
840
+
841
+ ---
842
+
843
+ ## 🧪 Testing
844
+
845
+ ### Test Router
846
+
847
+ ```python
848
+ from agents import route_to_agent
849
+
850
+ # Test nutrition routing
851
+ result = route_to_agent("Tôi muốn giảm cân")
852
+ assert result['agent'] == 'nutrition_agent'
853
+
854
+ # Test exercise routing
855
+ result = route_to_agent("Tôi muốn tập gym")
856
+ assert result['agent'] == 'exercise_agent'
857
+
858
+ # Test symptom routing
859
+ result = route_to_agent("Tôi bị đau đầu")
860
+ assert result['agent'] == 'symptom_agent'
861
+ ```
862
+
863
+ ### Test Individual Agent
864
+
865
+ ```python
866
+ from agents import NutritionAgent
867
+
868
+ agent = NutritionAgent()
869
+ response = agent.handle({
870
+ "user_query": "Tôi muốn giảm cân",
871
+ "user_data": {
872
+ "age": 25,
873
+ "gender": "male",
874
+ "weight": 70,
875
+ "height": 175,
876
+ "goal": "weight_loss"
877
+ }
878
+ })
879
+
880
+ assert "BMI" in response
881
+ assert "Calo" in response
882
+ ```
883
+
884
+ ---
885
+
886
+ ## 📁 File Structure
887
+
888
+ ```
889
+ heocare-chatbot/
890
+ ├── agents/ # NEW: Agent system
891
+ │ ├── __init__.py # Agent registry
892
+ │ ├── router.py # Function calling router
893
+ │ ├── nutrition_agent.py # Nutrition specialist
894
+ │ ├── exercise_agent.py # Exercise specialist
895
+ │ ├── symptom_agent.py # Symptom assessment
896
+ │ ├── mental_health_agent.py # Mental health support
897
+ │ └── general_health_agent.py # General health (fallback)
898
+
899
+ ├── utils/
900
+ │ ├── helpers.py # NEW: Clean chat logic
901
+ │ └── helpers.py # OLD: Monolithic (deprecated)
902
+
903
+ ├── modules/
904
+ │ ├── nutrition.py # Nutrition calculations
905
+ │ ├── exercise/ # Exercise planning
906
+ │ └── rules.json # Business rules
907
+
908
+ ├── app.py # Gradio UI (updated)
909
+ └── config/
910
+ └── settings.py # OpenAI client
911
+ ```
912
+
913
+ ---
914
+
915
+ ## 🔧 Configuration
916
+
917
+ ### Environment Variables
918
+
919
+ ```bash
920
+ # .env
921
+ OPENAI_API_KEY=your_key_here
922
+ MODEL=gpt-4o-mini # or gpt-4
923
+ ```
924
+
925
+ ### Model Selection
926
+
927
+ ```python
928
+ # config/settings.py
929
+ MODEL = "gpt-4o-mini" # Fast, cheap, good for routing
930
+ # MODEL = "gpt-4" # More accurate, expensive
931
+ ```
932
+
933
+ ---
934
+
935
+ ## 💡 Best Practices
936
+
937
+ ### 1. Token Optimization
938
+
939
+ ```python
940
+ # ✅ GOOD: Only load necessary prompt
941
+ agent = get_agent("nutrition_agent") # ~500 tokens
942
+
943
+ # ❌ BAD: Load entire monolithic prompt
944
+ # ~3000 tokens every time
945
+ ```
946
+
947
+ ### 2. Error Handling
948
+
949
+ ```python
950
+ try:
951
+ result = route_to_agent(message, chat_history)
952
+ agent = get_agent(result['agent'])
953
+ response = agent.handle(result['parameters'], chat_history)
954
+ except Exception as e:
955
+ # Fallback to general health agent
956
+ agent = GeneralHealthAgent()
957
+ response = agent.handle({"user_query": message}, chat_history)
958
+ ```
959
+
960
+ ### 3. Context Management (NEW)
961
+
962
+ ```python
963
+ # ✅ GOOD: Pass full chat history for context
964
+ result = route_to_agent(message, chat_history) # Uses last 10 exchanges
965
+
966
+ # ⚠️ CAUTION: Don't truncate history too early
967
+ # Router needs context to handle ambiguous questions
968
+
969
+ # 💡 TIP: For very long conversations (50+ exchanges)
970
+ # Consider keeping only relevant exchanges or summarizing
971
+ ```
972
+
973
+ ### 4. Caching
974
+
975
+ ```python
976
+ # Cache agent instances (optional optimization)
977
+ _agent_cache = {}
978
+
979
+ def get_cached_agent(agent_name):
980
+ if agent_name not in _agent_cache:
981
+ _agent_cache[agent_name] = get_agent(agent_name)
982
+ return _agent_cache[agent_name]
983
+ ```
984
+
985
+ ---
986
+
987
+ ## 📊 Monitoring
988
+
989
+ ### Log Routing Decisions
990
+
991
+ ```python
992
+ # In helpers.py
993
+ routing_result = route_to_agent(message, chat_history)
994
+ print(f"Routed to: {routing_result['agent']}, Confidence: {routing_result['confidence']}")
995
+ ```
996
+
997
+ ### Track Token Usage
998
+
999
+ ```python
1000
+ # In each agent
1001
+ response = client.chat.completions.create(...)
1002
+ print(f"Tokens used: {response.usage.total_tokens}")
1003
+ ```
1004
+
1005
+ ---
1006
+
1007
+ ## 🤝 Contributing
1008
+
1009
+ ### Để thêm agent mới (with Memory Support):
1010
+
1011
+ **Option 1: Extend BaseAgent (Recommended)** ✨
1012
+ ```python
1013
+ # agents/specialized/your_agent.py
1014
+ from agents.core.base_agent import BaseAgent
1015
+
1016
+ class YourAgent(BaseAgent):
1017
+ def __init__(self, memory=None):
1018
+ super().__init__(memory)
1019
+ self.agent_name = 'your_agent'
1020
+ self.system_prompt = "Your specialized prompt..."
1021
+
1022
+ def handle(self, parameters, chat_history=None):
1023
+ user_query = parameters.get('user_query', '')
1024
+
1025
+ # Access shared memory
1026
+ user_profile = self.get_user_profile()
1027
+
1028
+ # Check missing fields
1029
+ missing = self.get_missing_profile_fields(['age', 'weight'])
1030
+ if missing:
1031
+ return f"Cho mình biết {', '.join(missing)} nhé!"
1032
+
1033
+ # Your logic here
1034
+ response = self._generate_response(user_query, user_profile)
1035
+
1036
+ # Save agent data
1037
+ self.save_agent_data('key', 'value')
1038
+
1039
+ return response
1040
+ ```
1041
+
1042
+ **Option 2: Standalone Agent (Legacy)**
1043
+ ```python
1044
+ # agents/specialized/your_agent.py
1045
+ class YourAgent:
1046
+ def handle(self, parameters, chat_history=None):
1047
+ # Your logic without memory
1048
+ return "Response"
1049
+ ```
1050
+
1051
+ **Steps:**
1052
+ 1. Create `agents/specialized/your_agent.py`
1053
+ 2. Extend `BaseAgent` for memory support (recommended)
1054
+ 3. Register in `agents/core/router.py` AVAILABLE_FUNCTIONS
1055
+ 4. Register in `agents/specialized/__init__.py` AGENTS
1056
+ 5. Add to `agents/core/coordinator.py` if using coordinator
1057
+ 6. Test thoroughly
1058
+
1059
+ **Example Registration:**
1060
+ ```python
1061
+ # agents/core/router.py
1062
+ AVAILABLE_FUNCTIONS = [
1063
+ {
1064
+ "name": "your_agent",
1065
+ "description": "Your agent description",
1066
+ "parameters": {...}
1067
+ }
1068
+ ]
1069
+
1070
+ # agents/specialized/__init__.py
1071
+ from .your_agent import YourAgent
1072
+
1073
+ AGENTS = {
1074
+ # ... existing agents
1075
+ 'your_agent': YourAgent()
1076
+ }
1077
+
1078
+ # agents/core/coordinator.py (if using)
1079
+ from agents.specialized.your_agent import YourAgent
1080
+
1081
+ self.agents = {
1082
+ # ... existing agents
1083
+ 'your_agent': YourAgent()
1084
+ }
1085
+ ```
1086
+
1087
+ ---
1088
+
1089
+ ## 📚 RAG System (Retrieval-Augmented Generation)
1090
+
1091
+ ### Smart RAG Decision (Performance Optimization)
1092
+
1093
+ **Problem:** Always calling RAG adds 4-6s latency, even for simple queries.
1094
+
1095
+ **Solution:** Conditional RAG based on query complexity.
1096
+
1097
+ ```python
1098
+ # BaseAgent.should_use_rag() - Shared by all agents
1099
+ def should_use_rag(self, user_query, chat_history):
1100
+ # Skip RAG for:
1101
+ # - Greetings: "xin chào", "hello"
1102
+ # - Acknowledgments: "cảm ơn", "ok"
1103
+ # - Meta questions: "bạn là ai"
1104
+ # - Simple responses: "có", "không"
1105
+
1106
+ # Use RAG for:
1107
+ # - Complex medical terms: "nguyên nhân", "điều trị"
1108
+ # - Specific diseases: "bệnh", "viêm", "ung thư"
1109
+ # - Detailed questions: "chi tiết", "cụ thể"
1110
+
1111
+ return True/False # Smart decision
1112
+ ```
1113
+
1114
+ **Performance Impact:**
1115
+ - Simple queries: **2-3s** (was 8-10s) → **3x faster** ⚡
1116
+ - Complex queries: **6-8s** (was 8-10s) → **1.3x faster** ⚡
1117
+ - Model & DB cached at startup (save 2-3s per query)
1118
+
1119
+ ### Architecture: Separate Collections (Option A)
1120
+
1121
+ Each agent has its own dedicated vector database for fast, focused retrieval:
1122
+
1123
+ ```
1124
+ rag/vector_store/
1125
+ ├── medical_diseases/ # SymptomAgent
1126
+ ├── mental_health/ # MentalHealthAgent
1127
+ ├── nutrition/ # NutritionAgent
1128
+ ├── fitness/ # FitnessAgent
1129
+ └── general/ # SymptomAgent (COVID, general health)
1130
+ ```
1131
+
1132
+ ### Datasets by Agent
1133
+
1134
+ | Agent | Dataset | Source | Size | Records |
1135
+ |-------|---------|--------|------|---------|
1136
+ | **SymptomAgent** | ViMedical_Disease | HuggingFace | 50 MB | 603 diseases, 12K examples |
1137
+ | **SymptomAgent** | COVID_QA_Castorini | HuggingFace | 5 MB | 124 COVID-19 Q&A |
1138
+ | **MentalHealthAgent** | MentalChat16K | HuggingFace | 80 MB | 16K conversations, 33 topics |
1139
+ | **NutritionAgent** | LLM_Dietary_Recommendation | HuggingFace | 20 MB | 50 patient profiles + diet plans |
1140
+ | **FitnessAgent** | GYM-Exercise | HuggingFace | 10 MB | 1,660 gym exercises |
1141
+
1142
+ **Total:** ~165 MB across 5 vector stores
1143
+
1144
+ ### How Agents Use RAG
1145
+
1146
+ ```python
1147
+ class SymptomAgent:
1148
+ def __init__(self):
1149
+ # Load domain-specific vector stores
1150
+ self.symptoms_db = ChromaDB("rag/vector_store/medical_diseases")
1151
+ self.general_db = ChromaDB("rag/vector_store/general")
1152
+
1153
+ def process(self, user_query):
1154
+ # 1. Search symptoms database
1155
+ results = self.symptoms_db.query(user_query, n_results=5)
1156
+
1157
+ # 2. If not enough, search general database
1158
+ if len(results) < 3:
1159
+ general_results = self.general_db.query(user_query, n_results=3)
1160
+ results.extend(general_results)
1161
+
1162
+ # 3. Use results in response generation
1163
+ context = self.format_context(results)
1164
+ response = self.generate_response(user_query, context)
1165
+ return response
1166
+ ```
1167
+
1168
+ ### Benefits
1169
+
1170
+ - **Fast Retrieval**: Each agent searches only its domain (~10-50ms)
1171
+ - **High Relevance**: Domain-specific results, no noise from other topics
1172
+ - **Scalable**: Easy to add new datasets per agent
1173
+ - **Maintainable**: Update one domain without affecting others
1174
+
1175
+ ### Setup
1176
+
1177
+ ```bash
1178
+ # One command sets up all RAG databases
1179
+ bash scripts/setup_rag.sh
1180
+
1181
+ # Automatically:
1182
+ # 1. Downloads 5 datasets from HuggingFace
1183
+ # 2. Processes and builds ChromaDB for each
1184
+ # 3. Moves to rag/vector_store/
1185
+ # 4. Total time: 10-15 minutes
1186
+ ```
1187
+
1188
+ See `data_mining/README.md` for detailed dataset information.
1189
+
1190
+ ---
1191
+
1192
+ ## ✅ Implemented Features
1193
+
1194
+ - **Fine-tuning System** - Automatic data collection and model training (`fine_tuning/`)
1195
+ - Conversation logging for all agents
1196
+ - OpenAI fine-tuning API integration
1197
+ - Quality filtering and export tools
1198
+ - Training scripts and management
1199
+
1200
+ - **Session Persistence** - Save conversation memory across sessions (`utils/session_store.py`)
1201
+ - Automatic session save/load
1202
+ - User-specific memory storage
1203
+ - Multi-user support
1204
+ - Session cleanup utilities
1205
+
1206
+ - **Conversation Summarization** - Automatic summarization of long conversations (`utils/conversation_summarizer.py`)
1207
+ - LLM-powered summarization
1208
+ - Automatic trigger when conversation exceeds threshold
1209
+ - Keeps recent turns + summary
1210
+ - Token usage optimization
1211
+ - Context preservation
1212
+
1213
+ - **Feedback Loop** - Learn from user ratings and corrections (`feedback/`)
1214
+ - Collect ratings (1-5 stars, thumbs up/down)
1215
+ - User corrections and reports
1216
+ - Performance analytics per agent
1217
+ - Actionable insights generation
1218
+ - Export for fine-tuning
1219
+ - Agent comparison and ranking
1220
+
1221
+ - **Multi-language Support** - Vietnamese and English support (`i18n/`)
1222
+ - Automatic language detection
1223
+ - Bilingual translations (UI messages, prompts)
1224
+ - Language-specific agent system prompts
1225
+ - Seamless language switching
1226
+ - User language preferences
1227
+ - Language usage statistics
1228
+
1229
+ ## 🔮 Future Enhancements
1230
+
1231
+ - **Centralized Database** - Migrate health data storage from JSON to PostgreSQL for multi-user scalability
1232
+ - **Admin Dashboard** - Monitor agent performance, routing accuracy, user metrics
1233
+ - **Analytics & Monitoring** - Track response quality, token usage, user satisfaction
1234
+ - **A/B Testing** - Test different prompts and routing strategies
1235
+ - **Voice Interface** - Speech-to-text and text-to-speech capabilities
agents/README.md ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agents Package Structure 🏗️
2
+
3
+ ## 📁 Directory Structure
4
+
5
+ ```
6
+ agents/
7
+ ├── README.md # This file
8
+ ├── AGENT_ARCHITECTURE.md # Full architecture documentation
9
+
10
+ ├── core/ # Core infrastructure
11
+ │ ├── __init__.py
12
+ │ ├── router.py # OpenAI function calling router
13
+ │ ├── coordinator.py # Multi-agent coordinator
14
+ │ └── base_agent.py # Base class for all agents
15
+
16
+ └── specialized/ # Domain-specific agents
17
+ ├── __init__.py
18
+ ├── nutrition_agent.py # Nutrition & diet advice
19
+ ├── exercise_agent.py # Exercise & fitness plans
20
+ ├── symptom_agent.py # Symptom assessment
21
+ ├── mental_health_agent.py # Mental health support
22
+ └── general_health_agent.py # General health queries
23
+ ```
24
+
25
+ ---
26
+
27
+ ## 🎯 Purpose of Each Component
28
+
29
+ ### **Core Components** (`core/`)
30
+
31
+ #### 1. `router.py`
32
+ - **Purpose:** Routes user queries to appropriate agents
33
+ - **Technology:** OpenAI Function Calling
34
+ - **Key Features:**
35
+ - Context-aware routing (10 exchanges history)
36
+ - Last agent tracking
37
+ - Improved accuracy: 80-85% for ambiguous questions
38
+
39
+ #### 2. `coordinator.py`
40
+ - **Purpose:** Orchestrates multiple agents with shared memory
41
+ - **Key Features:**
42
+ - Shared conversation memory
43
+ - Agent handoffs
44
+ - Multi-agent collaboration
45
+ - Memory persistence across turns
46
+
47
+ #### 3. `base_agent.py`
48
+ - **Purpose:** Base class providing common functionality
49
+ - **Key Features:**
50
+ - Memory access helpers
51
+ - Agent handoff detection**
52
+ - Agent-to-agent communication
53
+ - Context awareness methods
54
+ - Handoff detection logic
55
+ - Context awareness methods
56
+ - User data extraction
57
+
58
+ ---
59
+
60
+ ### **Specialized Agents** (`specialized/`)
61
+
62
+ #### 1. `nutrition_agent.py`
63
+ - **Domain:** Nutrition, diet, BMI, calories
64
+ - **Capabilities:**
65
+ - Calculate BMI and calorie needs
66
+ - Generate meal plans
67
+ - Provide dietary advice
68
+ - Handle weight loss/gain goals
69
+
70
+ #### 2. `exercise_agent.py`
71
+ - **Domain:** Exercise, fitness, workout plans
72
+ - **Capabilities:**
73
+ - Create personalized workout plans
74
+ - Suggest exercises based on fitness level
75
+ - Provide form guidance
76
+ - Track progress
77
+
78
+ #### 3. `symptom_agent.py`
79
+ - **Domain:** Symptom assessment, health concerns
80
+ - **Capabilities:**
81
+ - OPQRST symptom assessment
82
+ - Red flag detection
83
+ - Triage recommendations
84
+ - Medical advice (when to see doctor)
85
+
86
+ #### 4. `mental_health_agent.py`
87
+ - **Domain:** Mental health, stress, anxiety
88
+ - **Capabilities:**
89
+ - Stress assessment
90
+ - Coping strategies
91
+ - Mindfulness techniques
92
+ - Crisis detection
93
+
94
+ #### 5. `general_health_agent.py`
95
+ - **Domain:** General health queries
96
+ - **Capabilities:**
97
+ - Answer general health questions
98
+ - Provide health tips
99
+ - Fallback for unclear queries
100
+
101
+ ---
102
+
103
+ ## 🔄 How It Works
104
+
105
+ ### 1. **User Query Flow**
106
+
107
+ ```
108
+ User Input
109
+
110
+ helpers.py (chat_logic)
111
+
112
+ AgentCoordinator
113
+
114
+ ┌─────────────────────────┐
115
+ │ Shared Memory │
116
+ │ - User Profile │
117
+ │ - Conversation State │
118
+ └─────────────────────────┘
119
+
120
+ Router (Function Calling)
121
+
122
+ Specialized Agent(s)
123
+
124
+ Response (with memory)
125
+ ```
126
+
127
+ ### 2. **Import Structure**
128
+
129
+ ```python
130
+ # From outside agents package
131
+ from agents import (
132
+ route_to_agent, # Router function
133
+ AgentCoordinator, # Coordinator class
134
+ BaseAgent, # Base agent class
135
+ NutritionAgent, # Specialized agents
136
+ get_agent # Agent factory
137
+ )
138
+
139
+ # Within agents package
140
+ from agents.core import router, coordinator, base_agent
141
+ from agents.specialized import nutrition_agent, exercise_agent
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 🚀 Usage Examples
147
+
148
+ ### Example 1: Using Coordinator (Recommended)
149
+
150
+ ```python
151
+ from agents import AgentCoordinator
152
+
153
+ coordinator = AgentCoordinator()
154
+
155
+ # Handle query with memory
156
+ response = coordinator.handle_query(
157
+ "Tôi 25 tuổi, muốn giảm cân",
158
+ chat_history
159
+ )
160
+
161
+ # Memory persists!
162
+ response2 = coordinator.handle_query(
163
+ "Tôi nên ăn gì?", # Knows age=25, goal=weight_loss
164
+ chat_history
165
+ )
166
+ ```
167
+
168
+ ### Example 2: Using Router Directly
169
+
170
+ ```python
171
+ from agents import route_to_agent, get_agent
172
+
173
+ # Route to agent
174
+ routing = route_to_agent("Tôi muốn giảm cân", chat_history)
175
+ # → {'agent': 'nutrition_agent', 'parameters': {...}}
176
+
177
+ # Get agent instance
178
+ agent = get_agent(routing['agent'])
179
+
180
+ # Handle request
181
+ response = agent.handle(routing['parameters'], chat_history)
182
+ ```
183
+
184
+ ### Example 3: Creating Custom Agent
185
+
186
+ ```python
187
+ from agents.core import BaseAgent
188
+
189
+ class MyCustomAgent(BaseAgent):
190
+ def __init__(self, memory=None):
191
+ super().__init__(memory)
192
+ self.agent_name = 'my_custom_agent'
193
+ self.system_prompt = "Your custom prompt..."
194
+
195
+ def handle(self, parameters, chat_history=None):
196
+ # Access shared memory
197
+ user_profile = self.get_user_profile()
198
+
199
+ # Your logic here
200
+ response = self._generate_response(parameters)
201
+
202
+ # Save to memory
203
+ self.save_agent_data('key', 'value')
204
+
205
+ return response
206
+ ```
207
+
208
+ ---
209
+
210
+ ## 📊 Key Benefits of This Structure
211
+
212
+ ### ✅ **Separation of Concerns**
213
+ - Core infrastructure separate from domain logic
214
+ - Easy to maintain and test
215
+
216
+ ### ✅ **Scalability**
217
+ - Add new agents without touching core
218
+ - Easy to extend functionality
219
+
220
+ ### ✅ **Reusability**
221
+ - BaseAgent provides common functionality
222
+ - Coordinator handles all agents uniformly
223
+
224
+ ### ✅ **Memory Management**
225
+ - Shared memory across all agents
226
+ - No repeated questions
227
+ - Full context awareness
228
+
229
+ ### ✅ **Clean Imports**
230
+ - Clear import paths
231
+ - No circular dependencies
232
+ - Well-organized namespaces
233
+
234
+ ---
235
+
236
+ ## 🔧 Adding a New Agent
237
+
238
+ ### Step 1: Create Agent File
239
+
240
+ ```python
241
+ # agents/specialized/my_agent.py
242
+ from agents.core import BaseAgent
243
+
244
+ class MyAgent(BaseAgent):
245
+ def __init__(self, memory=None):
246
+ super().__init__(memory)
247
+ self.agent_name = 'my_agent'
248
+ self.system_prompt = "..."
249
+
250
+ def handle(self, parameters, chat_history=None):
251
+ # Your implementation
252
+ return "Response"
253
+ ```
254
+
255
+ ### Step 2: Register in `specialized/__init__.py`
256
+
257
+ ```python
258
+ from .my_agent import MyAgent
259
+
260
+ AGENTS = {
261
+ # ... existing agents
262
+ "my_agent": MyAgent,
263
+ }
264
+ ```
265
+
266
+ ### Step 3: Register in `core/router.py`
267
+
268
+ ```python
269
+ AVAILABLE_FUNCTIONS = [
270
+ # ... existing functions
271
+ {
272
+ "name": "my_agent",
273
+ "description": "Your agent description",
274
+ "parameters": {...}
275
+ }
276
+ ]
277
+ ```
278
+
279
+ ### Step 4: Add to Coordinator
280
+
281
+ ```python
282
+ # agents/core/coordinator.py
283
+ from agents.specialized.my_agent import MyAgent
284
+
285
+ self.agents = {
286
+ # ... existing agents
287
+ 'my_agent': MyAgent()
288
+ }
289
+ ```
290
+
291
+ ---
292
+
293
+ ## 📚 Documentation
294
+
295
+ - **Full Architecture:** See `AGENT_ARCHITECTURE.md`
296
+ - **Implementation Guide:** See `PART1_IMPLEMENTATION.md` (if exists)
297
+ - **API Reference:** See individual agent files
298
+
299
+ ---
300
+
301
+ ## 🎯 Best Practices
302
+
303
+ 1. **Always extend BaseAgent** for new agents (unless you have a good reason not to)
304
+ 2. **Use coordinator** for production (enables memory & multi-agent)
305
+ 3. **Keep agents focused** - One domain per agent
306
+ 4. **Document your prompts** - Clear system prompts are crucial
307
+ 5. **Test thoroughly** - Test routing, memory, and handoffs
308
+
309
+ ---
310
+
311
+ **Last Updated:** Oct 11, 2025
312
+ **Version:** 2.0 (with Memory & Coordination)
agents/__init__.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agents Package - Healthcare AI Agent System
3
+
4
+ Structure:
5
+ - core/: Router, Coordinator, Base Agent
6
+ - specialized/: Domain-specific agents (Nutrition, Exercise, Symptom, etc.)
7
+ """
8
+
9
+ # Core components
10
+ from .core import route_to_agent, get_agent_description, AgentCoordinator, BaseAgent
11
+
12
+ # Specialized agents
13
+ from .specialized import (
14
+ NutritionAgent,
15
+ ExerciseAgent,
16
+ SymptomAgent,
17
+ MentalHealthAgent,
18
+ GeneralHealthAgent,
19
+ AGENTS,
20
+ get_agent
21
+ )
22
+
23
+ __all__ = [
24
+ # Core
25
+ 'route_to_agent',
26
+ 'get_agent_description',
27
+ 'AgentCoordinator',
28
+ 'BaseAgent',
29
+
30
+ # Specialized agents
31
+ 'NutritionAgent',
32
+ 'ExerciseAgent',
33
+ 'SymptomAgent',
34
+ 'MentalHealthAgent',
35
+ 'GeneralHealthAgent',
36
+
37
+ # Utilities
38
+ 'AGENTS',
39
+ 'get_agent'
40
+ ]
agents/core/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core agents package - Router, Coordinator, and Base Agent
3
+ """
4
+
5
+ from .router import route_to_agent, get_agent_description
6
+ from .coordinator import AgentCoordinator
7
+ from .base_agent import BaseAgent
8
+
9
+ __all__ = [
10
+ 'route_to_agent',
11
+ 'get_agent_description',
12
+ 'AgentCoordinator',
13
+ 'BaseAgent'
14
+ ]
agents/core/base_agent.py ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base Agent - Parent class for all specialized agents
3
+ Provides shared functionality: memory access, handoff logic, coordination
4
+ """
5
+
6
+ from typing import Dict, Any, Optional, List
7
+ from utils.memory import ConversationMemory
8
+
9
+
10
+ class BaseAgent:
11
+ """
12
+ Base class for all agents
13
+ Provides common functionality and interface
14
+ """
15
+
16
+ def __init__(self, memory: Optional[ConversationMemory] = None):
17
+ """
18
+ Initialize base agent
19
+
20
+ Args:
21
+ memory: Shared conversation memory (optional)
22
+ """
23
+ self.memory = memory or ConversationMemory()
24
+ self.agent_name = self.__class__.__name__.replace('Agent', '').lower()
25
+ self.system_prompt = ""
26
+
27
+ # Handoff configuration
28
+ self.can_handoff = True
29
+ self.handoff_triggers = []
30
+
31
+ # ===== Core Interface =====
32
+
33
+ def handle(self, parameters: Dict[str, Any], chat_history: Optional[List] = None) -> str:
34
+ """
35
+ Handle user request (must be implemented by subclasses)
36
+
37
+ Args:
38
+ parameters: Request parameters from router
39
+ chat_history: Conversation history
40
+
41
+ Returns:
42
+ str: Response message
43
+ """
44
+ raise NotImplementedError("Subclasses must implement handle()")
45
+
46
+ # ===== Memory Access Helpers =====
47
+
48
+ def get_user_profile(self) -> Dict[str, Any]:
49
+ """Get complete user profile from memory"""
50
+ return self.memory.get_full_profile()
51
+
52
+ # ===== Smart RAG Helper =====
53
+
54
+ def should_use_rag(self, user_query: str, chat_history: Optional[List] = None) -> bool:
55
+ """
56
+ Smart RAG Decision - Skip RAG for simple queries to improve performance
57
+
58
+ Performance Impact:
59
+ - Simple queries: 2-3s (was 8-10s) - 3x faster
60
+ - Complex queries: 6-8s (was 8-10s) - 1.3x faster
61
+
62
+ Args:
63
+ user_query: User's message
64
+ chat_history: Conversation history
65
+
66
+ Returns:
67
+ bool: True if RAG needed, False for simple conversational queries
68
+ """
69
+ query_lower = user_query.lower().strip()
70
+
71
+ # 1. Greetings & acknowledgments (no RAG needed)
72
+ greetings = [
73
+ 'xin chào', 'hello', 'hi', 'chào', 'hey',
74
+ 'cảm ơn', 'thanks', 'thank you', 'tks',
75
+ 'ok', 'được', 'vâng', 'ừ', 'uhm', 'uh huh',
76
+ 'bye', 'tạm biệt', 'hẹn gặp lại'
77
+ ]
78
+ if any(g in query_lower for g in greetings):
79
+ return False
80
+
81
+ # 2. Very short responses (usually conversational)
82
+ if len(query_lower) < 10:
83
+ short_responses = ['có', 'không', 'rồi', 'ạ', 'dạ', 'yes', 'no', 'nope', 'yep']
84
+ if any(r == query_lower or query_lower.startswith(r + ' ') for r in short_responses):
85
+ return False
86
+
87
+ # 3. Meta questions about the bot (no RAG needed)
88
+ meta_questions = [
89
+ 'bạn là ai', 'bạn tên gì', 'bạn có thể', 'bạn làm gì',
90
+ 'who are you', 'what can you', 'what do you'
91
+ ]
92
+ if any(m in query_lower for m in meta_questions):
93
+ return False
94
+
95
+ # 4. Complex medical/health questions (NEED RAG)
96
+ complex_patterns = [
97
+ # Medical terms
98
+ 'nguyên nhân', 'tại sao', 'why', 'how', 'làm sao',
99
+ 'cách nào', 'phương pháp', 'điều trị', 'chữa',
100
+ 'thuốc', 'medicine', 'phòng ngừa', 'prevention',
101
+ 'biến chứng', 'complication', 'nghiên cứu', 'research',
102
+ # Specific diseases
103
+ 'bệnh', 'disease', 'viêm', 'ung thư', 'cancer',
104
+ 'tiểu đường', 'diabetes', 'huyết áp', 'blood pressure',
105
+ # Detailed questions
106
+ 'chi tiết', 'cụ thể', 'specific', 'detail',
107
+ 'khoa học', 'scientific', 'evidence', 'hướng dẫn',
108
+ 'guideline', 'recommendation', 'chuyên gia', 'expert'
109
+ ]
110
+ if any(p in query_lower for p in complex_patterns):
111
+ return True
112
+
113
+ # 5. Default: Simple first-turn questions don't need RAG
114
+ # Agent can ask clarifying questions first
115
+ if not chat_history or len(chat_history) == 0:
116
+ # Simple initial statements
117
+ simple_starts = [
118
+ 'tôi muốn', 'tôi cần', 'giúp tôi', 'tôi bị',
119
+ 'i want', 'i need', 'help me', 'i have', 'i feel'
120
+ ]
121
+ if any(s in query_lower for s in simple_starts):
122
+ # Let agent gather info first, use RAG later
123
+ return False
124
+
125
+ # 6. Default: Use RAG for safety (medical context)
126
+ return True
127
+
128
+ def update_user_profile(self, key: str, value: Any) -> None:
129
+ """Update user profile in shared memory"""
130
+ self.memory.update_profile(key, value)
131
+
132
+ def get_missing_profile_fields(self, required_fields: List[str]) -> List[str]:
133
+ """Check what profile fields are missing"""
134
+ return self.memory.get_missing_fields(required_fields)
135
+
136
+ def save_agent_data(self, key: str, value: Any) -> None:
137
+ """Save agent-specific data to memory"""
138
+ self.memory.add_agent_data(self.agent_name, key, value)
139
+
140
+ def get_agent_data(self, key: str = None) -> Any:
141
+ """Get agent-specific data from memory"""
142
+ return self.memory.get_agent_data(self.agent_name, key)
143
+
144
+ def get_other_agent_data(self, agent_name: str, key: str = None) -> Any:
145
+ """Get data from another agent"""
146
+ return self.memory.get_agent_data(agent_name, key)
147
+
148
+ # ===== Context Awareness =====
149
+
150
+ def get_context_summary(self) -> str:
151
+ """Get summary of current conversation context"""
152
+ return self.memory.get_context_summary()
153
+
154
+ def get_previous_agent(self) -> Optional[str]:
155
+ """Get name of previous agent"""
156
+ return self.memory.get_previous_agent()
157
+
158
+ def get_current_topic(self) -> Optional[str]:
159
+ """Get current conversation topic"""
160
+ return self.memory.get_current_topic()
161
+
162
+ def set_current_topic(self, topic: str) -> None:
163
+ """Set current conversation topic"""
164
+ self.memory.set_current_topic(topic)
165
+
166
+ def generate_natural_opening(self, user_query: str, chat_history: Optional[List] = None) -> str:
167
+ """
168
+ Generate natural conversation opening based on context
169
+ Avoids robotic prefixes like "Thông tin đã tư vấn:"
170
+
171
+ Args:
172
+ user_query: Current user query
173
+ chat_history: Conversation history
174
+
175
+ Returns:
176
+ str: Natural opening phrase (empty if not needed)
177
+ """
178
+ # Check if this is a topic transition
179
+ previous_agent = self.get_previous_agent()
180
+ is_new_topic = previous_agent and previous_agent != self.agent_name
181
+
182
+ # If continuing same topic, no special opening needed
183
+ if not is_new_topic:
184
+ return ""
185
+
186
+ # Generate natural transition based on agent type
187
+ query_lower = user_query.lower()
188
+
189
+ # Enthusiastic transitions for new requests
190
+ if any(word in query_lower for word in ['muốn', 'cần', 'giúp', 'tư vấn']):
191
+ openings = [
192
+ "Ah, bây giờ bạn đang cần",
193
+ "Được rồi, để mình",
194
+ "Tuyệt! Mình sẽ",
195
+ "Ok, cùng",
196
+ ]
197
+ import random
198
+ return random.choice(openings) + " "
199
+
200
+ # Default: no prefix, just natural response
201
+ return ""
202
+
203
+ # ===== Handoff Logic =====
204
+
205
+ def should_handoff(self, user_query: str, chat_history: Optional[List] = None) -> bool:
206
+ """
207
+ Determine if this agent should hand off to another agent
208
+
209
+ Args:
210
+ user_query: User's current query
211
+ chat_history: Conversation history
212
+
213
+ Returns:
214
+ bool: True if handoff is needed
215
+ """
216
+ if not self.can_handoff:
217
+ return False
218
+
219
+ # Check for handoff trigger keywords
220
+ query_lower = user_query.lower()
221
+ for trigger in self.handoff_triggers:
222
+ if trigger in query_lower:
223
+ return True
224
+
225
+ return False
226
+
227
+ def suggest_next_agent(self, user_query: str) -> Optional[str]:
228
+ """
229
+ Suggest which agent to hand off to
230
+
231
+ Args:
232
+ user_query: User's current query
233
+
234
+ Returns:
235
+ str: Name of suggested agent, or None
236
+ """
237
+ query_lower = user_query.lower()
238
+
239
+ # Symptom keywords
240
+ symptom_keywords = ['đau', 'sốt', 'ho', 'buồn nôn', 'chóng mặt', 'mệt']
241
+ if any(kw in query_lower for kw in symptom_keywords):
242
+ return 'symptom_agent'
243
+
244
+ # Nutrition keywords
245
+ nutrition_keywords = ['ăn', 'thực đơn', 'calo', 'giảm cân', 'tăng cân']
246
+ if any(kw in query_lower for kw in nutrition_keywords):
247
+ return 'nutrition_agent'
248
+
249
+ # Exercise keywords
250
+ exercise_keywords = ['tập', 'gym', 'cardio', 'yoga', 'chạy bộ']
251
+ if any(kw in query_lower for kw in exercise_keywords):
252
+ return 'exercise_agent'
253
+
254
+ # Mental health keywords
255
+ mental_keywords = ['stress', 'lo âu', 'trầm cảm', 'mất ngủ', 'burnout']
256
+ if any(kw in query_lower for kw in mental_keywords):
257
+ return 'mental_health_agent'
258
+
259
+ return None
260
+
261
+ def create_handoff_message(self, next_agent: str, context: str = "", user_query: str = "") -> str:
262
+ """
263
+ Create a SEAMLESS topic transition (not explicit handoff)
264
+
265
+ Args:
266
+ next_agent: Name of agent to hand off to
267
+ context: Additional context for handoff
268
+ user_query: User's query to understand intent
269
+
270
+ Returns:
271
+ str: Natural transition message (NOT "chuyển sang chuyên gia")
272
+ """
273
+ # Map agents to topic areas
274
+ topic_map = {
275
+ 'symptom_agent': {
276
+ 'topic': 'triệu chứng',
277
+ 'action': 'đánh giá',
278
+ 'info_needed': ['triệu chứng cụ thể', 'thời gian xuất hiện']
279
+ },
280
+ 'nutrition_agent': {
281
+ 'topic': 'dinh dưỡng',
282
+ 'action': 'tư vấn chế độ ăn',
283
+ 'info_needed': ['mục tiêu', 'cân nặng', 'chiều cao', 'tuổi']
284
+ },
285
+ 'exercise_agent': {
286
+ 'topic': 'tập luyện',
287
+ 'action': 'lên lịch tập',
288
+ 'info_needed': ['mục tiêu', 'thời gian có thể tập', 'thiết bị']
289
+ },
290
+ 'mental_health_agent': {
291
+ 'topic': 'sức khỏe tinh thần',
292
+ 'action': 'hỗ trợ',
293
+ 'info_needed': ['cảm giác hiện tại', 'thời gian kéo dài']
294
+ }
295
+ }
296
+
297
+ topic_info = topic_map.get(next_agent, {
298
+ 'topic': 'vấn đề này',
299
+ 'action': 'tư vấn',
300
+ 'info_needed': []
301
+ })
302
+
303
+ # SEAMLESS transition - acknowledge topic change naturally
304
+ message = f"{context}\n\n" if context else ""
305
+
306
+ # Natural acknowledgment based on query
307
+ if 'tập' in user_query.lower() or 'gym' in user_query.lower():
308
+ message += f"Ah, bây giờ bạn đang cần về {topic_info['topic']}! "
309
+ elif 'ăn' in user_query.lower() or 'thực đơn' in user_query.lower():
310
+ message += f"Okii, giờ chuyển sang {topic_info['topic']} nhé! "
311
+ else:
312
+ message += f"Được, mình giúp bạn về {topic_info['topic']}! "
313
+
314
+ # Ask for info if needed (natural, not formal)
315
+ if topic_info['info_needed']:
316
+ info_list = ', '.join(topic_info['info_needed'][:2]) # Max 2 items
317
+ message += f"Để {topic_info['action']} phù hợp, cho mình biết thêm về {info_list} nhé!"
318
+
319
+ return message
320
+
321
+ # ===== Multi-Agent Coordination =====
322
+
323
+ def needs_collaboration(self, user_query: str) -> List[str]:
324
+ """
325
+ Determine if multiple agents are needed
326
+
327
+ Args:
328
+ user_query: User's query
329
+
330
+ Returns:
331
+ List[str]: List of agent names needed
332
+ """
333
+ agents_needed = []
334
+ query_lower = user_query.lower()
335
+
336
+ # Check for each agent's keywords
337
+ if any(kw in query_lower for kw in ['đau', 'sốt', 'ho', 'triệu chứng']):
338
+ agents_needed.append('symptom_agent')
339
+
340
+ if any(kw in query_lower for kw in ['ăn', 'thực đơn', 'calo', 'dinh dưỡng']):
341
+ agents_needed.append('nutrition_agent')
342
+
343
+ if any(kw in query_lower for kw in ['tập', 'gym', 'cardio', 'exercise']):
344
+ agents_needed.append('exercise_agent')
345
+
346
+ if any(kw in query_lower for kw in ['stress', 'lo âu', 'trầm cảm', 'mental']):
347
+ agents_needed.append('mental_health_agent')
348
+
349
+ return agents_needed
350
+
351
+ # ===== Utility Methods =====
352
+
353
+ def extract_user_data_from_history(self, chat_history: List) -> Dict[str, Any]:
354
+ """
355
+ Extract user data from conversation history
356
+ (Can be overridden by subclasses for specific extraction)
357
+
358
+ Args:
359
+ chat_history: List of [user_msg, bot_msg] pairs
360
+
361
+ Returns:
362
+ Dict: Extracted user data
363
+ """
364
+ import re
365
+
366
+ if not chat_history:
367
+ return {}
368
+
369
+ all_messages = " ".join([msg[0] for msg in chat_history if msg[0]])
370
+ extracted = {}
371
+
372
+ # Extract age
373
+ age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|tôi\s*(\d+)', all_messages.lower())
374
+ if age_match:
375
+ extracted['age'] = int([g for g in age_match.groups() if g][0])
376
+
377
+ # Extract gender
378
+ if re.search(r'\bnam\b|male|đàn ông', all_messages.lower()):
379
+ extracted['gender'] = 'male'
380
+ elif re.search(r'\bnữ\b|female|đàn bà|phụ nữ', all_messages.lower()):
381
+ extracted['gender'] = 'female'
382
+
383
+ # Extract weight
384
+ weight_match = re.search(r'(\d+)\s*kg|nặng\s*(\d+)|cân\s*(\d+)', all_messages.lower())
385
+ if weight_match:
386
+ extracted['weight'] = float([g for g in weight_match.groups() if g][0])
387
+
388
+ # Extract height
389
+ height_match = re.search(r'(\d+)\s*cm|cao\s*(\d+)|chiều cao\s*(\d+)', all_messages.lower())
390
+ if height_match:
391
+ extracted['height'] = float([g for g in height_match.groups() if g][0])
392
+
393
+ return extracted
394
+
395
+ def update_memory_from_history(self, chat_history: List) -> None:
396
+ """Extract and update memory from chat history"""
397
+ extracted = self.extract_user_data_from_history(chat_history)
398
+
399
+ for key, value in extracted.items():
400
+ # Always update with latest info (user may correct themselves)
401
+ self.memory.update_profile(key, value)
402
+
403
+ def extract_and_save_user_info(self, user_message: str) -> Dict[str, Any]:
404
+ """
405
+ Extract user info from a single message using LLM (flexible, handles typos)
406
+ Saves to memory immediately
407
+
408
+ Args:
409
+ user_message: Single user message (any format, any order)
410
+
411
+ Returns:
412
+ Dict: Extracted data
413
+ """
414
+ from config.settings import client, MODEL
415
+ import json
416
+
417
+ # Use LLM to extract structured data (handles typos, any order, extra info)
418
+ extraction_prompt = f"""Extract health information from this user message. Handle typos and variations.
419
+
420
+ User message: "{user_message}"
421
+
422
+ Extract these fields if present (return null if not found):
423
+ - age: integer (tuổi, age, years old)
424
+ - gender: "male" or "female" (nam, nữ, male, female, đàn ông, phụ nữ)
425
+ - weight: float in kg (nặng, cân, weight, kg)
426
+ - height: float in cm (cao, chiều cao, height, cm, m)
427
+ IMPORTANT: Height MUST be in cm (50-300 range)
428
+ - If user says "1.75m" or "1.78m" → convert to cm (175, 178)
429
+ - If user says "175cm" or "178cm" → use as is (175, 178)
430
+ - NEVER return values like 1.0, 1.5, 1.75 for height!
431
+ - body_fat_percentage: float (tỉ lệ mỡ, body fat, %, optional)
432
+
433
+ Return ONLY valid JSON with these exact keys. Example:
434
+ {{"age": 30, "gender": "male", "weight": 70.5, "height": 175, "body_fat_percentage": 25}}
435
+
436
+ CRITICAL: Height must be 50-300 (in cm). If user says "1.78m", return 178, not 1.78!
437
+ If a field is not found, use null. Be flexible with typos and word order."""
438
+
439
+ try:
440
+ response = client.chat.completions.create(
441
+ model=MODEL,
442
+ messages=[
443
+ {"role": "system", "content": "You are a data extraction assistant. Extract structured health data from user messages. Handle typos and variations. Return only valid JSON."},
444
+ {"role": "user", "content": extraction_prompt}
445
+ ],
446
+ temperature=0.1, # Low temp for consistent extraction
447
+ max_tokens=150
448
+ )
449
+
450
+ result_text = response.choices[0].message.content.strip()
451
+
452
+ # Parse JSON response
453
+ # Remove markdown code blocks if present
454
+ if "```json" in result_text:
455
+ result_text = result_text.split("```json")[1].split("```")[0].strip()
456
+ elif "```" in result_text:
457
+ result_text = result_text.split("```")[1].split("```")[0].strip()
458
+
459
+ extracted = json.loads(result_text)
460
+
461
+ # Auto-correct obvious errors before saving
462
+ extracted = self._auto_correct_health_data(extracted)
463
+
464
+ # Save to memory (only non-null values)
465
+ allowed_fields = ['age', 'gender', 'weight', 'height', 'body_fat_percentage']
466
+ for key, value in extracted.items():
467
+ if value is not None and key in allowed_fields:
468
+ self.update_user_profile(key, value)
469
+
470
+ return {k: v for k, v in extracted.items() if v is not None}
471
+
472
+ except Exception as e:
473
+ # Fallback to regex if LLM fails
474
+ print(f"LLM extraction failed: {e}, using regex fallback")
475
+ return self._extract_with_regex_fallback(user_message)
476
+
477
+ def _auto_correct_health_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
478
+ """
479
+ Auto-correct obvious errors in health data (typos, wrong units)
480
+
481
+ Examples:
482
+ - height: 200 → 200cm ✅ (likely meant 200cm, not 200m)
483
+ - height: 1.75 → 175cm ✅ (convert m to cm)
484
+ - weight: 75 → 75kg ✅ (assume kg if reasonable)
485
+ - weight: 75000 → 75kg ✅ (likely meant 75kg, not 75000g)
486
+ """
487
+ corrected = data.copy()
488
+
489
+ # Auto-correct height
490
+ if 'height' in corrected and corrected['height'] is not None:
491
+ height = float(corrected['height'])
492
+
493
+ # If height is very small (< 10), likely in meters → convert to cm
494
+ if 0 < height < 10:
495
+ corrected['height'] = height * 100
496
+ print(f"Auto-corrected height: {height}m → {corrected['height']}cm")
497
+
498
+ # If height is reasonable (50-300), assume cm
499
+ elif 50 <= height <= 300:
500
+ corrected['height'] = height
501
+
502
+ # If height is very large (> 1000), likely in mm → convert to cm
503
+ elif height > 1000:
504
+ corrected['height'] = height / 10
505
+ print(f"Auto-corrected height: {height}mm → {corrected['height']}cm")
506
+
507
+ # Otherwise invalid, set to None
508
+ else:
509
+ print(f"Invalid height: {height}, setting to None")
510
+ corrected['height'] = None
511
+
512
+ # Auto-correct weight
513
+ if 'weight' in corrected and corrected['weight'] is not None:
514
+ weight = float(corrected['weight'])
515
+
516
+ # If weight is very large (> 500), likely in grams → convert to kg
517
+ if weight > 500:
518
+ corrected['weight'] = weight / 1000
519
+ print(f"Auto-corrected weight: {weight}g → {corrected['weight']}kg")
520
+
521
+ # If weight is reasonable (20-300), assume kg
522
+ elif 20 <= weight <= 300:
523
+ corrected['weight'] = weight
524
+
525
+ # If weight is very small (< 20), might be wrong unit
526
+ elif 0 < weight < 20:
527
+ # Could be in different unit or child weight
528
+ # Keep as is but flag
529
+ corrected['weight'] = weight
530
+
531
+ # Otherwise invalid
532
+ else:
533
+ print(f"Invalid weight: {weight}, setting to None")
534
+ corrected['weight'] = None
535
+
536
+ # Auto-correct age
537
+ if 'age' in corrected and corrected['age'] is not None:
538
+ age = int(corrected['age'])
539
+
540
+ # Reasonable age range: 1-120
541
+ if not (1 <= age <= 120):
542
+ print(f"Invalid age: {age}, setting to None")
543
+ corrected['age'] = None
544
+
545
+ # Auto-correct body fat percentage
546
+ if 'body_fat_percentage' in corrected and corrected['body_fat_percentage'] is not None:
547
+ bf = float(corrected['body_fat_percentage'])
548
+
549
+ # Reasonable body fat: 3-60%
550
+ if not (3 <= bf <= 60):
551
+ print(f"Invalid body fat: {bf}%, setting to None")
552
+ corrected['body_fat_percentage'] = None
553
+
554
+ return corrected
555
+
556
+ def _extract_with_regex_fallback(self, user_message: str) -> Dict[str, Any]:
557
+ """Fallback regex extraction (less flexible but reliable)"""
558
+ import re
559
+ extracted = {}
560
+ msg_lower = user_message.lower()
561
+
562
+ # Extract age
563
+ age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|age\s*(\d+)', msg_lower)
564
+ if age_match:
565
+ age = int([g for g in age_match.groups() if g][0])
566
+ extracted['age'] = age
567
+ self.update_user_profile('age', age)
568
+
569
+ # Extract gender
570
+ if re.search(r'\bnam\b|male|đàn ông', msg_lower):
571
+ extracted['gender'] = 'male'
572
+ self.update_user_profile('gender', 'male')
573
+ elif re.search(r'\bnữ\b|female|đàn bà|phụ nữ', msg_lower):
574
+ extracted['gender'] = 'female'
575
+ self.update_user_profile('gender', 'female')
576
+
577
+ # Extract weight
578
+ weight_match = re.search(r'(?:nặng|cân|weight)?\s*(\d+(?:\.\d+)?)\s*kg', msg_lower)
579
+ if weight_match:
580
+ weight = float(weight_match.group(1))
581
+ extracted['weight'] = weight
582
+ self.update_user_profile('weight', weight)
583
+
584
+ # Extract height
585
+ height_cm_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+(?:\.\d+)?)\s*cm', msg_lower)
586
+ if height_cm_match:
587
+ height = float(height_cm_match.group(1))
588
+ extracted['height'] = height
589
+ self.update_user_profile('height', height)
590
+ else:
591
+ height_m_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+\.?\d*)\s*m\b', msg_lower)
592
+ if height_m_match:
593
+ height = float(height_m_match.group(1))
594
+ if height < 3:
595
+ height = height * 100
596
+ extracted['height'] = height
597
+ self.update_user_profile('height', height)
598
+
599
+ return extracted
600
+
601
+ def __repr__(self) -> str:
602
+ return f"<{self.__class__.__name__}: {self.get_context_summary()}>"
agents/core/context_analyzer.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Context Analyzer - Understands user intent and needs
3
+ Determines what type of response is most appropriate
4
+ """
5
+
6
+ from typing import Dict, List, Optional
7
+
8
+ class ContextAnalyzer:
9
+ """
10
+ Optimized Context Analyzer - Balanced between simplicity and coverage
11
+ Handles 90% of scenarios with 50% less complexity
12
+ """
13
+
14
+ # Define patterns once as class variables (better performance)
15
+ # Use compound patterns to reduce false positives
16
+ URGENT_PATTERNS = [
17
+ 'đang đau', 'đau quá', 'khó chịu', 'không chịu nổi',
18
+ 'cấp cứu', 'khẩn cấp', 'gấp', 'ngay',
19
+ 'đau không thể', 'không aguanta được' # Strong pain indicators
20
+ ]
21
+
22
+ # Context-specific urgent combinations (must have BOTH)
23
+ URGENT_COMBINATIONS = [
24
+ ('làm sao', ['hết đau', 'giảm đau', 'đỡ', 'ngay']),
25
+ ('giúp tôi', ['đau', 'khó chịu', 'không chịu']),
26
+ ('phải làm gì', ['đau', 'khó chịu', 'ngay', 'gấp']),
27
+ ('cần gì', ['giảm đau', 'hết đau', 'cấp cứu'])
28
+ ]
29
+
30
+ INFO_PATTERNS = [
31
+ 'tại sao', 'nguyên nhân', 'có phải', 'bị gì',
32
+ 'là gì', 'thế nào', 'như thế nào'
33
+ ]
34
+
35
+ PREVENTION_PATTERNS = [
36
+ 'phòng ngừa', 'tránh', 'không bị', 'hạn chế',
37
+ 'làm sao để không', 'để khỏi'
38
+ ]
39
+
40
+ @staticmethod
41
+ def is_vague_query(user_query: str) -> bool:
42
+ """
43
+ Detect if user query is too vague/ambiguous
44
+ """
45
+ vague_patterns = [
46
+ 'không khỏe', 'mệt', 'khó chịu', 'không ổn',
47
+ 'giúp tôi', 'cần giúp', 'phải làm sao',
48
+ 'không biết', 'chẳng hiểu', 'làm gì bây giờ'
49
+ ]
50
+
51
+ query_lower = user_query.lower()
52
+
53
+ # Check if query is very short and vague
54
+ if len(user_query.split()) <= 3:
55
+ if any(pattern in query_lower for pattern in vague_patterns):
56
+ return True
57
+
58
+ # Check if query has no specific symptom/goal
59
+ has_specifics = any(word in query_lower for word in [
60
+ 'đau', 'sốt', 'ho', 'buồn nôn', # Symptoms
61
+ 'giảm cân', 'tăng cân', 'tập', # Goals
62
+ 'ăn', 'thực đơn', 'calo' # Nutrition
63
+ ])
64
+
65
+ if not has_specifics and any(pattern in query_lower for pattern in vague_patterns):
66
+ return True
67
+
68
+ return False
69
+
70
+ @staticmethod
71
+ def analyze_user_intent(user_query: str, chat_history: List) -> Dict:
72
+ """
73
+ Simplified but effective intent analysis with context awareness
74
+ Returns only what's needed for response generation
75
+ """
76
+ query_lower = user_query.lower()
77
+
78
+ # Check if query is too vague
79
+ is_vague = ContextAnalyzer.is_vague_query(user_query)
80
+
81
+ # Smart urgency check with context
82
+ urgency = 'medium' # default
83
+
84
+ # Check direct urgent patterns
85
+ if any(p in query_lower for p in ContextAnalyzer.URGENT_PATTERNS):
86
+ urgency = 'high'
87
+ # Check combination patterns (need both parts)
88
+ else:
89
+ for trigger, contexts in ContextAnalyzer.URGENT_COMBINATIONS:
90
+ if trigger in query_lower:
91
+ if any(ctx in query_lower for ctx in contexts):
92
+ urgency = 'high'
93
+ break
94
+
95
+ # If not urgent, check if it's informational
96
+ if urgency != 'high' and any(p in query_lower for p in ContextAnalyzer.INFO_PATTERNS):
97
+ urgency = 'low'
98
+
99
+ # Determine primary intent (simplified)
100
+ intent = 'general' # default
101
+ if urgency == 'high':
102
+ intent = 'immediate_relief'
103
+ elif any(p in query_lower for p in ContextAnalyzer.INFO_PATTERNS):
104
+ intent = 'information'
105
+ elif any(p in query_lower for p in ContextAnalyzer.PREVENTION_PATTERNS):
106
+ intent = 'prevention'
107
+
108
+ # Simple decision: solution vs education
109
+ needs_solution = urgency in ['high', 'medium']
110
+ needs_education = urgency == 'low' or intent in ['information', 'prevention']
111
+
112
+ # Add conversation stage for compatibility
113
+ conversation_stage = len(chat_history) if chat_history else 0
114
+
115
+ return {
116
+ 'intent': intent,
117
+ 'urgency': urgency,
118
+ 'needs_solution': needs_solution,
119
+ 'needs_education': needs_education,
120
+ 'is_vague': is_vague,
121
+ 'needs_clarification': is_vague,
122
+ 'conversation_stage': conversation_stage
123
+ }
124
+
125
+ @staticmethod
126
+ def determine_response_structure(context: Dict) -> Dict[str, any]:
127
+ """
128
+ Determine how to structure the response based on context
129
+
130
+ Returns dict with:
131
+ - structure: 'solution_first', 'assessment_first', 'education_first'
132
+ - include_immediate: bool
133
+ - include_prevention: bool
134
+ - include_referral: bool
135
+ """
136
+
137
+ if context['urgency'] == 'high':
138
+ return {
139
+ 'structure': 'solution_first',
140
+ 'include_immediate': True,
141
+ 'include_prevention': True, # But after solution
142
+ 'include_referral': True,
143
+ 'tone': 'supportive_urgent'
144
+ }
145
+
146
+ elif context['intent'] == 'diagnosis':
147
+ return {
148
+ 'structure': 'assessment_first',
149
+ 'include_immediate': False,
150
+ 'include_prevention': True,
151
+ 'include_referral': False,
152
+ 'tone': 'informative'
153
+ }
154
+
155
+ elif context['intent'] == 'prevention':
156
+ return {
157
+ 'structure': 'education_first',
158
+ 'include_immediate': False,
159
+ 'include_prevention': True,
160
+ 'include_referral': False,
161
+ 'tone': 'educational'
162
+ }
163
+
164
+ else: # Default balanced
165
+ return {
166
+ 'structure': 'solution_first',
167
+ 'include_immediate': True,
168
+ 'include_prevention': True,
169
+ 'include_referral': True,
170
+ 'tone': 'balanced'
171
+ }
172
+
173
+ @staticmethod
174
+ def format_contextual_response(
175
+ symptom_assessment: str,
176
+ solutions: List[str],
177
+ preventions: List[str],
178
+ response_structure: Dict
179
+ ) -> str:
180
+ """
181
+ Format response based on context and user needs
182
+ """
183
+ response = ""
184
+
185
+ if response_structure['structure'] == 'solution_first':
186
+ # IMMEDIATE RELIEF FIRST
187
+ if response_structure['include_immediate'] and solutions:
188
+ response += "**Để giảm triệu chứng ngay, bạn có thể:**\n"
189
+ for i, solution in enumerate(solutions[:3], 1): # Top 3 immediate actions
190
+ response += f"{i}. {solution}\n"
191
+ response += "\n"
192
+
193
+ # MEDICATION OPTIONS (if urgent)
194
+ if response_structure['tone'] == 'supportive_urgent':
195
+ response += "**Về thuốc:**\n"
196
+ response += "- Có thể dùng thuốc kháng acid (Maalox, Gaviscon) nếu đau do acid\n"
197
+ response += "- Thuốc chống co thắt (Buscopan) nếu đau quặn\n"
198
+ response += "⚠️ *Nên tham khảo dược sĩ trước khi dùng*\n\n"
199
+
200
+ # WARNING SIGNS
201
+ if response_structure['include_referral']:
202
+ response += "**⚠️ Đi khám ngay nếu:**\n"
203
+ response += "- Đau không giảm sau 2 giờ\n"
204
+ response += "- Kèm sốt, nôn, tiêu chảy\n"
205
+ response += "- Đau dữ dội tăng dần\n\n"
206
+
207
+ # PREVENTION (after immediate care)
208
+ if response_structure['include_prevention'] and preventions:
209
+ response += "**Sau khi đỡ, để phòng tránh:**\n"
210
+ for prevention in preventions[:3]:
211
+ response += f"• {prevention}\n"
212
+
213
+ elif response_structure['structure'] == 'assessment_first':
214
+ # Start with assessment/diagnosis
215
+ response += symptom_assessment + "\n\n"
216
+
217
+ # Then solutions
218
+ if solutions:
219
+ response += "**Cách xử lý:**\n"
220
+ for solution in solutions:
221
+ response += f"• {solution}\n"
222
+
223
+ elif response_structure['structure'] == 'education_first':
224
+ # Start with education/prevention
225
+ if preventions:
226
+ response += "**Để phòng ngừa hiệu quả:**\n"
227
+ for prevention in preventions:
228
+ response += f"• {prevention}\n"
229
+
230
+ return response
231
+
232
+ @staticmethod
233
+ def should_ask_followup(context: Dict, chat_history: List) -> bool:
234
+ """
235
+ Determine if we should ask follow-up questions
236
+
237
+ Rules:
238
+ - Don't ask if urgency is high (give solutions first)
239
+ - Don't ask if we already have enough info
240
+ - Don't ask more than 2 questions total
241
+ """
242
+
243
+ # High urgency = no questions, give help
244
+ if context['urgency'] == 'high':
245
+ return False
246
+
247
+ # Already asked 2+ questions = enough
248
+ if chat_history and len(chat_history) >= 2:
249
+ bot_questions = 0
250
+ for _, bot_msg in chat_history:
251
+ if bot_msg and '?' in bot_msg:
252
+ bot_questions += 1
253
+ if bot_questions >= 2:
254
+ return False
255
+
256
+ # First interaction and not urgent = can ask 1 question
257
+ if context['conversation_stage'] == 0:
258
+ return True
259
+
260
+ return False
agents/core/coordinator.py ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Coordinator - Manages agent collaboration and handoffs
3
+ Enables multi-agent responses and smooth transitions
4
+ """
5
+
6
+ from typing import Dict, List, Optional, Any
7
+ import asyncio
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from utils.memory import ConversationMemory
10
+ from utils.session_store import get_session_store
11
+ from utils.conversation_summarizer import get_summarizer
12
+ from agents.core.router import route_to_agent, get_router
13
+ from fine_tuning import get_data_collector
14
+ from health_data import HealthContext, HealthDataStore
15
+ import hashlib
16
+ import json
17
+
18
+
19
+ class AgentCoordinator:
20
+ """
21
+ Coordinates multiple agents and manages handoffs
22
+ Provides multi-agent collaboration capabilities
23
+ """
24
+
25
+ def __init__(self, user_id: Optional[str] = None, use_embedding_router=True, enable_cache=True, enable_data_collection=True, enable_session_persistence=True):
26
+ """
27
+ Initialize coordinator with shared memory and data store
28
+
29
+ Args:
30
+ user_id: Unique user identifier for session persistence
31
+ use_embedding_router: Use embedding-based routing (faster)
32
+ enable_cache: Enable response caching
33
+ enable_data_collection: Enable conversation logging for fine-tuning
34
+ enable_session_persistence: Enable session persistence across restarts
35
+ """
36
+ # Session persistence
37
+ self.user_id = user_id
38
+ self.session_store = get_session_store() if enable_session_persistence else None
39
+
40
+ # Initialize memory with session persistence
41
+ self.memory = ConversationMemory(
42
+ user_id=user_id,
43
+ session_store=self.session_store
44
+ )
45
+
46
+ self.data_store = HealthDataStore()
47
+ self.health_context = None
48
+ self.agents = {}
49
+
50
+ # Enable embedding router (faster than LLM routing)
51
+ self.use_embedding_router = use_embedding_router
52
+ if use_embedding_router:
53
+ self.router = get_router(use_embeddings=True)
54
+ else:
55
+ self.router = None
56
+
57
+ # Enable response cache
58
+ self.enable_cache = enable_cache
59
+ self.response_cache = {} if enable_cache else None
60
+
61
+ # Enable data collection for fine-tuning
62
+ self.enable_data_collection = enable_data_collection
63
+ if enable_data_collection:
64
+ self.data_collector = get_data_collector()
65
+ else:
66
+ self.data_collector = None
67
+
68
+ # Conversation summarizer
69
+ self.summarizer = get_summarizer()
70
+
71
+ self._initialize_agents()
72
+
73
+ def _initialize_agents(self) -> None:
74
+ """Initialize all agents with shared memory"""
75
+ # Import agents (lazy import to avoid circular dependencies)
76
+ from agents.specialized.nutrition_agent import NutritionAgent
77
+ from agents.specialized.exercise_agent import ExerciseAgent
78
+ from agents.specialized.symptom_agent import SymptomAgent
79
+ from agents.specialized.mental_health_agent import MentalHealthAgent
80
+ from agents.specialized.general_health_agent import GeneralHealthAgent
81
+
82
+ # Create agents with shared memory
83
+ self.agents = {
84
+ 'nutrition_agent': NutritionAgent(memory=self.memory),
85
+ 'exercise_agent': ExerciseAgent(memory=self.memory),
86
+ 'symptom_agent': SymptomAgent(memory=self.memory),
87
+ 'mental_health_agent': MentalHealthAgent(memory=self.memory),
88
+ 'general_health_agent': GeneralHealthAgent(memory=self.memory)
89
+ }
90
+
91
+ def handle_query(self, message: str, chat_history: Optional[List] = None, user_id: Optional[str] = None) -> str:
92
+ """
93
+ Main entry point - handles user query with coordination
94
+
95
+ Args:
96
+ message: User's message
97
+ chat_history: Conversation history
98
+ user_id: User ID for data persistence
99
+
100
+ Returns:
101
+ str: Response (possibly from multiple agents)
102
+ """
103
+ chat_history = chat_history or []
104
+
105
+ # Create or update health context for user
106
+ if user_id:
107
+ self.health_context = HealthContext(user_id, self.data_store)
108
+ # Inject health context into all agents
109
+ for agent in self.agents.values():
110
+ if hasattr(agent, 'set_health_context'):
111
+ agent.set_health_context(self.health_context)
112
+
113
+ # Update memory from chat history
114
+ self._update_memory_from_history(chat_history)
115
+
116
+ # Summarize if conversation is too long
117
+ if self.summarizer.should_summarize(chat_history):
118
+ chat_history = self._summarize_if_needed(chat_history)
119
+
120
+ # Check if multi-agent collaboration is needed
121
+ if self._needs_multi_agent(message):
122
+ return self._handle_multi_agent_query(message, chat_history)
123
+
124
+ # Single agent routing
125
+ return self._handle_single_agent_query(message, chat_history)
126
+
127
+ def _get_cache_key(self, message: str, chat_history: List) -> str:
128
+ """Generate cache key from message and recent history"""
129
+ # Include last 2 exchanges for context
130
+ recent_history = chat_history[-4:] if len(chat_history) > 4 else chat_history
131
+ cache_data = {
132
+ "message": message.lower().strip(),
133
+ "history": [(h[0].lower().strip() if h[0] else "", h[1][:50] if len(h) > 1 else "") for h in recent_history]
134
+ }
135
+ cache_str = json.dumps(cache_data, sort_keys=True)
136
+ return hashlib.md5(cache_str.encode()).hexdigest()
137
+
138
+ def _handle_single_agent_query(self, message: str, chat_history: List, file_data: Optional[Dict] = None) -> str:
139
+ """Handle query with single agent (with potential handoff)"""
140
+ # Check cache first
141
+ if self.enable_cache:
142
+ cache_key = self._get_cache_key(message, chat_history)
143
+ if cache_key in self.response_cache:
144
+ # print("[CACHE HIT] Returning cached response")
145
+ return self.response_cache[cache_key]
146
+
147
+ # Route to appropriate agent (use embedding router if available)
148
+ if self.router:
149
+ routing_result = self.router.route(message, chat_history)
150
+ else:
151
+ routing_result = route_to_agent(message, chat_history)
152
+
153
+ agent_name = routing_result['agent']
154
+ parameters = routing_result['parameters']
155
+
156
+ # Update current agent in memory
157
+ self.memory.set_current_agent(agent_name)
158
+
159
+ # Get agent
160
+ agent = self.agents.get(agent_name)
161
+ if not agent:
162
+ return "Xin lỗi, không tìm thấy agent phù hợp."
163
+
164
+ # Let agent handle the request
165
+ response = agent.handle(parameters, chat_history)
166
+
167
+ # Log conversation for fine-tuning (with cleaned data)
168
+ if self.enable_data_collection and self.data_collector:
169
+ user_data = self.memory.get_full_profile()
170
+
171
+ # Clean user data before logging to prevent learning from errors
172
+ cleaned_user_data = self._clean_user_data_for_training(user_data)
173
+
174
+ self.data_collector.log_conversation(
175
+ agent_name=agent_name,
176
+ user_message=message,
177
+ agent_response=response,
178
+ user_data=cleaned_user_data,
179
+ metadata={'data_cleaned': True} # Flag that data was cleaned
180
+ )
181
+
182
+ # Cache the response
183
+ if self.enable_cache:
184
+ cache_key = self._get_cache_key(message, chat_history)
185
+ self.response_cache[cache_key] = response
186
+ # Limit cache size to 100 entries
187
+ if len(self.response_cache) > 100:
188
+ # Remove oldest entry (simple FIFO)
189
+ self.response_cache.pop(next(iter(self.response_cache)))
190
+
191
+ # Check if handoff is needed
192
+ if hasattr(agent, 'should_handoff') and agent.should_handoff(message, chat_history):
193
+ next_agent_name = agent.suggest_next_agent(message)
194
+ if next_agent_name and next_agent_name in self.agents:
195
+ return self._perform_handoff(agent, next_agent_name, response, message, chat_history)
196
+
197
+ return response
198
+
199
+ def _handle_multi_agent_query(self, message: str, chat_history: List) -> str:
200
+ """Handle query that needs multiple agents (with parallel execution)"""
201
+ # Detect which agents are needed
202
+ agents_needed = self._detect_required_agents(message)
203
+
204
+ if len(agents_needed) <= 1:
205
+ # Fallback to single agent
206
+ return self._handle_single_agent_query(message, chat_history)
207
+
208
+ # Use async for parallel execution (faster!)
209
+ try:
210
+ loop = asyncio.new_event_loop()
211
+ asyncio.set_event_loop(loop)
212
+ responses = loop.run_until_complete(
213
+ self._handle_multi_agent_async(message, chat_history, agents_needed)
214
+ )
215
+ loop.close()
216
+ except Exception as e:
217
+ print(f"Async multi-agent failed, falling back to sequential: {e}")
218
+ # Fallback to sequential if async fails
219
+ responses = {}
220
+ for agent_name in agents_needed:
221
+ agent = self.agents.get(agent_name)
222
+ if agent:
223
+ parameters = {'user_query': message}
224
+ responses[agent_name] = agent.handle(parameters, chat_history)
225
+
226
+ # Combine responses
227
+ return self._combine_responses(responses, agents_needed)
228
+
229
+ async def _handle_multi_agent_async(self, message: str, chat_history: List, agents_needed: List[str]) -> Dict[str, str]:
230
+ """Execute multiple agents in parallel using asyncio"""
231
+ async def call_agent(agent_name: str):
232
+ """Async wrapper for agent.handle()"""
233
+ agent = self.agents.get(agent_name)
234
+ if not agent:
235
+ return None
236
+
237
+ # Run in thread pool (since agent.handle is sync)
238
+ loop = asyncio.get_event_loop()
239
+ with ThreadPoolExecutor() as pool:
240
+ parameters = {'user_query': message}
241
+ response = await loop.run_in_executor(
242
+ pool,
243
+ agent.handle,
244
+ parameters,
245
+ chat_history
246
+ )
247
+ return response
248
+
249
+ # Create tasks for all agents
250
+ tasks = {agent_name: call_agent(agent_name) for agent_name in agents_needed}
251
+
252
+ # Execute in parallel
253
+ results = await asyncio.gather(*tasks.values(), return_exceptions=True)
254
+
255
+ # Map results back to agent names
256
+ responses = {}
257
+ for agent_name, result in zip(tasks.keys(), results):
258
+ if isinstance(result, Exception):
259
+ print(f"Agent {agent_name} failed: {result}")
260
+ responses[agent_name] = f"Xin lỗi, {agent_name} gặp lỗi."
261
+ elif result:
262
+ responses[agent_name] = result
263
+
264
+ return responses
265
+
266
+ def _perform_handoff(
267
+ self,
268
+ from_agent: Any,
269
+ to_agent_name: str,
270
+ current_response: str,
271
+ message: str,
272
+ chat_history: List
273
+ ) -> str:
274
+ """
275
+ Perform smooth handoff between agents
276
+
277
+ Args:
278
+ from_agent: Current agent
279
+ to_agent_name: Name of agent to hand off to
280
+ current_response: Current agent's response
281
+ message: User's message
282
+ chat_history: Conversation history
283
+
284
+ Returns:
285
+ str: Combined response with handoff
286
+ """
287
+ # Create handoff message
288
+ handoff_msg = from_agent.create_handoff_message(to_agent_name, current_response)
289
+
290
+ # Update memory
291
+ self.memory.set_current_agent(to_agent_name)
292
+
293
+ return handoff_msg
294
+
295
+ def _needs_multi_agent(self, message: str) -> bool:
296
+ """
297
+ Determine if query needs multiple agents
298
+
299
+ Args:
300
+ message: User's message
301
+
302
+ Returns:
303
+ bool: True if multiple agents needed
304
+ """
305
+ agents_needed = self._detect_required_agents(message)
306
+ return len(agents_needed) > 1
307
+
308
+ def _detect_required_agents(self, message: str) -> List[str]:
309
+ """
310
+ Detect which agents are needed for this query
311
+
312
+ Args:
313
+ message: User's message
314
+
315
+ Returns:
316
+ List[str]: List of agent names needed
317
+ """
318
+ agents_needed = []
319
+ message_lower = message.lower()
320
+
321
+ # PRIORITY 1: Symptom keywords (highest priority - health emergencies)
322
+ symptom_keywords = ['đau', 'sốt', 'ho', 'buồn nôn', 'chóng mặt', 'triệu chứng', 'khó tiêu', 'đầy bụng', 'ợ hơi']
323
+ has_symptoms = any(kw in message_lower for kw in symptom_keywords)
324
+
325
+ # PRIORITY 2: Nutrition keywords (but NOT if it's a symptom context)
326
+ nutrition_keywords = ['thực đơn', 'calo', 'giảm cân', 'tăng cân', 'dinh dưỡng', 'rau củ', 'thực phẩm']
327
+ # Special handling: 'ăn' only counts as nutrition if NOT in symptom context
328
+ has_nutrition = any(kw in message_lower for kw in nutrition_keywords)
329
+ if not has_symptoms and 'ăn' in message_lower:
330
+ has_nutrition = True
331
+
332
+ # PRIORITY 3: Exercise keywords
333
+ exercise_keywords = ['tập', 'gym', 'cardio', 'yoga', 'chạy bộ', 'exercise', 'workout']
334
+ has_exercise = any(kw in message_lower for kw in exercise_keywords)
335
+
336
+ # PRIORITY 4: Mental health keywords
337
+ mental_keywords = ['stress', 'lo âu', 'trầm cảm', 'mất ngủ', 'burnout', 'mental']
338
+ has_mental = any(kw in message_lower for kw in mental_keywords)
339
+
340
+ # IMPORTANT: Only trigger multi-agent if CLEARLY needs multiple domains
341
+ # Example: "Tôi bị đau bụng, nên ăn gì?" -> symptom + nutrition
342
+ # But: "WHO khuyến nghị ăn bao nhiêu rau củ?" -> ONLY nutrition
343
+
344
+ # Count how many domains are triggered
345
+ domain_count = sum([has_symptoms, has_nutrition, has_exercise, has_mental])
346
+
347
+ # If only 1 domain -> single agent (no multi-agent)
348
+ if domain_count <= 1:
349
+ if has_symptoms:
350
+ agents_needed.append('symptom_agent')
351
+ elif has_nutrition:
352
+ agents_needed.append('nutrition_agent')
353
+ elif has_exercise:
354
+ agents_needed.append('exercise_agent')
355
+ elif has_mental:
356
+ agents_needed.append('mental_health_agent')
357
+ else:
358
+ # Multiple domains detected
359
+ # Check if it's a REAL multi-domain question or false positive
360
+
361
+ # False positive patterns (should be single agent)
362
+ false_positives = [
363
+ 'who khuyến nghị', # WHO recommendations -> single domain
364
+ 'bao nhiêu', # Quantitative questions -> single domain
365
+ 'khó tiêu', # Digestive issues -> symptom only
366
+ 'đầy bụng', # Bloating -> symptom only
367
+ 'đau bụng', # Stomach pain -> symptom only
368
+ 'ợ hơi', # Burping -> symptom only
369
+ ]
370
+
371
+ is_false_positive = any(pattern in message_lower for pattern in false_positives)
372
+
373
+ if is_false_positive:
374
+ # Use primary domain only
375
+ if has_nutrition:
376
+ agents_needed.append('nutrition_agent')
377
+ elif has_exercise:
378
+ agents_needed.append('exercise_agent')
379
+ elif has_symptoms:
380
+ agents_needed.append('symptom_agent')
381
+ elif has_mental:
382
+ agents_needed.append('mental_health_agent')
383
+ else:
384
+ # Real multi-domain question
385
+ if has_symptoms:
386
+ agents_needed.append('symptom_agent')
387
+ if has_nutrition:
388
+ agents_needed.append('nutrition_agent')
389
+ if has_exercise:
390
+ agents_needed.append('exercise_agent')
391
+ if has_mental:
392
+ agents_needed.append('mental_health_agent')
393
+
394
+ return agents_needed
395
+
396
+ def _combine_responses(self, responses: Dict[str, str], agents_order: List[str]) -> str:
397
+ """
398
+ Combine responses from multiple agents
399
+
400
+ Args:
401
+ responses: Dict of agent_name -> response
402
+ agents_order: Order of agents
403
+
404
+ Returns:
405
+ str: Combined response
406
+ """
407
+ # For natural flow, just combine responses without headers
408
+ # Make it feel like ONE person giving comprehensive advice
409
+
410
+ responses_list = [responses[agent] for agent in agents_order if agent in responses]
411
+
412
+ if len(responses_list) == 1:
413
+ # Single agent - return as is
414
+ return responses_list[0]
415
+
416
+ # Multiple agents - combine naturally
417
+ combined = ""
418
+
419
+ # First response (usually symptom assessment)
420
+ combined += responses_list[0]
421
+
422
+ # Add other responses with smooth transitions
423
+ for i in range(1, len(responses_list)):
424
+ # Natural transition phrases
425
+ transitions = [
426
+ "\n\nNgoài ra, ",
427
+ "\n\nBên cạnh đó, ",
428
+ "\n\nĐồng thời, ",
429
+ "\n\nVề mặt khác, "
430
+ ]
431
+ transition = transitions[min(i-1, len(transitions)-1)]
432
+ combined += transition + responses_list[i]
433
+
434
+ # Natural closing (not too formal)
435
+ combined += "\n\nBạn thử làm theo xem có đỡ không nhé. Có gì thắc mắc cứ hỏi mình!"
436
+
437
+ return combined
438
+
439
+ def _update_memory_from_history(self, chat_history: List) -> None:
440
+ """Extract and update SHARED memory from chat history to prevent duplicate questions"""
441
+ if not chat_history:
442
+ return
443
+
444
+ # Extract user info from ALL conversations (not just current agent)
445
+ user_info = self._extract_user_info_from_all_history(chat_history)
446
+
447
+ # Update SHARED memory that ALL agents can access
448
+ if user_info:
449
+ for key, value in user_info.items():
450
+ self.memory.update_profile(key, value)
451
+
452
+ def _extract_user_info_from_all_history(self, chat_history: List) -> Dict:
453
+ """Extract user information from entire conversation history"""
454
+ user_info = {}
455
+
456
+ # Common patterns to extract
457
+ patterns = {
458
+ 'age': [r'(\d+)\s*tuổi', r'tôi\s*(\d+)', r'(\d+)\s*years?\s*old'],
459
+ 'gender': [r'tôi là (nam|nữ)', r'giới tính[:\s]*(nam|nữ)', r'(male|female|nam|nữ)'],
460
+ 'weight': [r'(\d+)\s*kg', r'nặng\s*(\d+)', r'cân nặng[:\s]*(\d+)'],
461
+ 'height': [r'(\d+)\s*cm', r'cao\s*(\d+)', r'chiều cao[:\s]*(\d+)'],
462
+ 'goal': [r'muốn\s*(giảm cân|tăng cân|tăng cơ|khỏe mạnh)', r'mục tiêu[:\s]*(.+)']
463
+ }
464
+
465
+ # Search through all user messages
466
+ import re
467
+ for user_msg, _ in chat_history:
468
+ if not user_msg:
469
+ continue
470
+
471
+ for field, field_patterns in patterns.items():
472
+ if field not in user_info: # Only extract if not already found
473
+ for pattern in field_patterns:
474
+ match = re.search(pattern, user_msg.lower())
475
+ if match:
476
+ user_info[field] = match.group(1)
477
+ break
478
+
479
+ return user_info
480
+
481
+ # Extract gender
482
+ if not self.memory.get_profile('gender'):
483
+ if re.search(r'\bnam\b|male', all_messages.lower()):
484
+ self.memory.update_profile('gender', 'male')
485
+ elif re.search(r'\bnữ\b|female', all_messages.lower()):
486
+ self.memory.update_profile('gender', 'female')
487
+
488
+ # Extract weight
489
+ if not self.memory.get_profile('weight'):
490
+ weight_match = re.search(r'(\d+)\s*kg|nặng\s*(\d+)', all_messages.lower())
491
+ if weight_match:
492
+ weight = float([g for g in weight_match.groups() if g][0])
493
+ self.memory.update_profile('weight', weight)
494
+
495
+ # Extract height
496
+ if not self.memory.get_profile('height'):
497
+ height_match = re.search(r'(\d+)\s*cm|cao\s*(\d+)', all_messages.lower())
498
+ if height_match:
499
+ height = float([g for g in height_match.groups() if g][0])
500
+ self.memory.update_profile('height', height)
501
+
502
+ def _summarize_if_needed(self, chat_history: List) -> List:
503
+ """
504
+ Summarize conversation if it's too long
505
+
506
+ Args:
507
+ chat_history: Full conversation history
508
+
509
+ Returns:
510
+ Compressed history with summary
511
+ """
512
+ user_profile = self.memory.get_full_profile()
513
+ compressed = self.summarizer.compress_history(
514
+ chat_history,
515
+ target_turns=10 # Keep last 10 turns + summary
516
+ )
517
+
518
+ # print(f"📝 Summarized {len(chat_history)} turns → {len(compressed)} turns")
519
+ return compressed
520
+
521
+ def get_conversation_stats(self, chat_history: List) -> Dict[str, Any]:
522
+ """Get statistics about current conversation"""
523
+ return self.summarizer.get_summary_stats(chat_history)
524
+
525
+ def get_memory_summary(self) -> str:
526
+ """Get summary of current memory state"""
527
+ return self.memory.get_context_summary()
528
+
529
+ def _clean_user_data_for_training(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
530
+ """
531
+ Clean user data before logging for training
532
+ Ensures only valid, corrected data is used for fine-tuning
533
+
534
+ This prevents the model from learning bad patterns like:
535
+ - "cao 200m" (should be 200cm)
536
+ - "nặng 75g" (should be 75kg)
537
+ - Invalid BMI values
538
+ """
539
+ cleaned = user_data.copy()
540
+
541
+ # Validate and clean height (should be 50-300 cm)
542
+ if 'height' in cleaned and cleaned['height'] is not None:
543
+ height = float(cleaned['height'])
544
+ if not (50 <= height <= 300):
545
+ # Invalid height - don't log it
546
+ cleaned['height'] = None
547
+
548
+ # Validate and clean weight (should be 20-300 kg)
549
+ if 'weight' in cleaned and cleaned['weight'] is not None:
550
+ weight = float(cleaned['weight'])
551
+ if not (20 <= weight <= 300):
552
+ # Invalid weight - don't log it
553
+ cleaned['weight'] = None
554
+
555
+ # Validate and clean age (should be 1-120)
556
+ if 'age' in cleaned and cleaned['age'] is not None:
557
+ age = int(cleaned['age'])
558
+ if not (1 <= age <= 120):
559
+ # Invalid age - don't log it
560
+ cleaned['age'] = None
561
+
562
+ # Validate and clean body fat (should be 3-60%)
563
+ if 'body_fat_percentage' in cleaned and cleaned['body_fat_percentage'] is not None:
564
+ bf = float(cleaned['body_fat_percentage'])
565
+ if not (3 <= bf <= 60):
566
+ # Invalid body fat - don't log it
567
+ cleaned['body_fat_percentage'] = None
568
+
569
+ # Remove any None values to keep training data clean
570
+ cleaned = {k: v for k, v in cleaned.items() if v is not None}
571
+
572
+ return cleaned
573
+
574
+ def clear_memory(self) -> None:
575
+ """Clear all memory (start fresh)"""
576
+ self.memory.clear()
577
+
578
+ def __repr__(self) -> str:
579
+ return f"<AgentCoordinator: {self.get_memory_summary()}>"
agents/core/orchestrator.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Agent Orchestrator
3
+ Coordinates multiple agents to handle complex queries
4
+ """
5
+
6
+ from typing import List, Dict, Any
7
+ import json
8
+
9
+ class MultiAgentOrchestrator:
10
+ """
11
+ Orchestrates multiple agents to handle complex queries
12
+ that require expertise from multiple domains
13
+ """
14
+
15
+ def __init__(self, agents: Dict[str, Any]):
16
+ """
17
+ Initialize orchestrator with available agents
18
+
19
+ Args:
20
+ agents: Dictionary of agent_name -> agent_instance
21
+ """
22
+ self.agents = agents
23
+
24
+ def orchestrate(self, query: str, agent_names: List[str], chat_history: List = None) -> str:
25
+ """
26
+ Orchestrate multiple agents to answer a complex query
27
+
28
+ Args:
29
+ query: User query
30
+ agent_names: List of agents to use
31
+ chat_history: Conversation history
32
+
33
+ Returns:
34
+ Combined response from all agents
35
+ """
36
+ if len(agent_names) == 1:
37
+ # Single agent - just call it
38
+ return self._call_single_agent(agent_names[0], query, chat_history)
39
+
40
+ # Multi-agent orchestration
41
+ return self._orchestrate_multi_agent(query, agent_names, chat_history)
42
+
43
+ def _call_single_agent(self, agent_name: str, query: str, chat_history: List) -> str:
44
+ """Call a single agent"""
45
+ agent = self.agents.get(agent_name)
46
+ if not agent:
47
+ return f"Agent {agent_name} not found"
48
+
49
+ try:
50
+ response = agent.handle(
51
+ parameters={"user_query": query},
52
+ chat_history=chat_history
53
+ )
54
+ return response
55
+ except Exception as e:
56
+ return f"Error calling {agent_name}: {str(e)}"
57
+
58
+ def _orchestrate_multi_agent(self, query: str, agent_names: List[str], chat_history: List) -> str:
59
+ """
60
+ Orchestrate multiple agents
61
+
62
+ Strategy:
63
+ 1. Analyze query to determine what each agent should focus on
64
+ 2. Call each agent with specific sub-query
65
+ 3. Combine responses intelligently
66
+ """
67
+ # Decompose query into sub-queries for each agent
68
+ sub_queries = self._decompose_query(query, agent_names)
69
+
70
+ # Call each agent with their sub-query
71
+ responses = {}
72
+ for agent_name, sub_query in sub_queries.items():
73
+ if agent_name in self.agents:
74
+ try:
75
+ response = self.agents[agent_name].handle(
76
+ parameters={"user_query": sub_query},
77
+ chat_history=chat_history
78
+ )
79
+ responses[agent_name] = response
80
+ except Exception as e:
81
+ responses[agent_name] = f"Error: {str(e)}"
82
+
83
+ # Combine responses
84
+ return self._combine_responses(query, responses, agent_names)
85
+
86
+ def _decompose_query(self, query: str, agent_names: List[str]) -> Dict[str, str]:
87
+ """
88
+ Decompose complex query into sub-queries for each agent
89
+
90
+ Example:
91
+ Query: "Tôi muốn giảm cân, nên ăn gì và tập gì?"
92
+
93
+ nutrition_agent: "Tư vấn chế độ ăn để giảm cân"
94
+ exercise_agent: "Tư vấn lịch tập để giảm cân"
95
+ """
96
+ sub_queries = {}
97
+
98
+ # Simple heuristic-based decomposition
99
+ query_lower = query.lower()
100
+
101
+ for agent_name in agent_names:
102
+ if agent_name == "nutrition_agent":
103
+ if "ăn" in query_lower or "dinh dưỡng" in query_lower or "calo" in query_lower:
104
+ sub_queries[agent_name] = f"Tư vấn dinh dưỡng cho: {query}"
105
+ else:
106
+ sub_queries[agent_name] = query
107
+
108
+ elif agent_name == "exercise_agent":
109
+ if "tập" in query_lower or "gym" in query_lower or "luyện" in query_lower:
110
+ sub_queries[agent_name] = f"Tư vấn tập luyện cho: {query}"
111
+ else:
112
+ sub_queries[agent_name] = query
113
+
114
+ elif agent_name == "mental_health_agent":
115
+ if "stress" in query_lower or "lo âu" in query_lower:
116
+ sub_queries[agent_name] = f"Tư vấn sức khỏe tinh thần cho: {query}"
117
+ else:
118
+ sub_queries[agent_name] = query
119
+
120
+ else:
121
+ # Default: use original query
122
+ sub_queries[agent_name] = query
123
+
124
+ return sub_queries
125
+
126
+ def _combine_responses(self, original_query: str, responses: Dict[str, str], agent_names: List[str]) -> str:
127
+ """
128
+ Combine responses from multiple agents into a coherent answer
129
+
130
+ Strategy:
131
+ 1. Identify the main topic
132
+ 2. Structure response logically
133
+ 3. Avoid redundancy
134
+ """
135
+ if not responses:
136
+ return "Xin lỗi, không thể xử lý câu hỏi này."
137
+
138
+ # Build combined response
139
+ combined = []
140
+
141
+ # Add intro
142
+ if len(responses) > 1:
143
+ combined.append("Để giải đáp câu hỏi của bạn, tôi sẽ tư vấn từ nhiều góc độ:\n")
144
+
145
+ # Add each agent's response with clear sections
146
+ agent_labels = {
147
+ "nutrition_agent": "📊 Dinh Dưỡng",
148
+ "exercise_agent": "💪 Tập Luyện",
149
+ "mental_health_agent": "🧠 Sức Khỏe Tinh Thần",
150
+ "symptom_agent": "🩺 Đánh Giá Triệu Chứng",
151
+ "general_health_agent": "🏥 Tổng Quan"
152
+ }
153
+
154
+ for agent_name in agent_names:
155
+ if agent_name in responses:
156
+ label = agent_labels.get(agent_name, agent_name)
157
+ response = responses[agent_name]
158
+
159
+ # Clean up response (remove redundant intro)
160
+ response = self._clean_response(response)
161
+
162
+ combined.append(f"\n{label}:\n{response}\n")
163
+
164
+ # Add conclusion
165
+ if len(responses) > 1:
166
+ combined.append("\n---\n")
167
+ combined.append("💡 Lưu ý: Để đạt kết quả tốt nhất, hãy kết hợp cả dinh dưỡng, tập luyện và nghỉ ngơi hợp lý.")
168
+
169
+ return "".join(combined)
170
+
171
+ def _clean_response(self, response: str) -> str:
172
+ """Clean up response by removing redundant intros"""
173
+ # Remove common intro phrases
174
+ intros_to_remove = [
175
+ "Chào bạn!",
176
+ "Xin chào!",
177
+ "Để giải đáp câu hỏi của bạn",
178
+ "Mình sẽ giúp bạn",
179
+ ]
180
+
181
+ for intro in intros_to_remove:
182
+ if response.startswith(intro):
183
+ response = response[len(intro):].strip()
184
+
185
+ return response
186
+
187
+
188
+ # Example usage
189
+ if __name__ == "__main__":
190
+ # Mock agents for testing
191
+ class MockAgent:
192
+ def __init__(self, name):
193
+ self.name = name
194
+
195
+ def handle(self, parameters, chat_history=None):
196
+ query = parameters.get("user_query", "")
197
+ return f"[{self.name}] Response to: {query}"
198
+
199
+ agents = {
200
+ "nutrition_agent": MockAgent("Nutrition"),
201
+ "exercise_agent": MockAgent("Exercise"),
202
+ "mental_health_agent": MockAgent("Mental Health")
203
+ }
204
+
205
+ orchestrator = MultiAgentOrchestrator(agents)
206
+
207
+ # Test multi-agent
208
+ query = "Tôi muốn giảm cân, nên ăn gì và tập gì?"
209
+ agent_names = ["nutrition_agent", "exercise_agent"]
210
+
211
+ response = orchestrator.orchestrate(query, agent_names)
212
+ print(response)
agents/core/response_validator.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Response Validator - Ensures LLM responses follow quality standards
3
+ Shared validation logic for all agents
4
+ """
5
+
6
+ from typing import Dict, List, Tuple
7
+
8
+ class ResponseValidator:
9
+ """
10
+ Base validator with common rules + agent-specific rules
11
+ """
12
+
13
+ # Common bad phrases across all agents
14
+ COMMON_BAD_PHRASES = [
15
+ "Dựa trên thông tin bạn cung cấp",
16
+ "Dựa vào thông tin",
17
+ "Theo thông tin bạn đưa ra",
18
+ "Từ thông tin trên",
19
+ "Với tư cách",
20
+ "Tôi là chuyên gia"
21
+ ]
22
+
23
+ @staticmethod
24
+ def validate_common(response: str) -> Tuple[bool, List[str]]:
25
+ """
26
+ Common validation rules for all agents
27
+ Returns: (is_valid, list_of_issues)
28
+ """
29
+ issues = []
30
+
31
+ # Check for formal phrases
32
+ for phrase in ResponseValidator.COMMON_BAD_PHRASES:
33
+ if phrase.lower() in response.lower():
34
+ issues.append(f"Formal phrase: '{phrase}'")
35
+ break
36
+
37
+ # Check for excessive length (>500 words)
38
+ word_count = len(response.split())
39
+ if word_count > 500:
40
+ issues.append(f"Too long: {word_count} words (max 500)")
41
+
42
+ # Check for empty response
43
+ if len(response.strip()) < 10:
44
+ issues.append("Response too short or empty")
45
+
46
+ return len(issues) == 0, issues
47
+
48
+ @staticmethod
49
+ def validate_symptom_response(response: str, context: Dict) -> Tuple[bool, List[str]]:
50
+ """
51
+ Symptom-specific validation
52
+ """
53
+ issues = []
54
+ stage = context.get('conversation_stage', 0)
55
+
56
+ # Assessment phase: should ask, not advise
57
+ if stage <= 1:
58
+ advice_indicators = [
59
+ "khuyến nghị", "nên", "hãy", "bạn thử",
60
+ "giải pháp", "cách xử lý"
61
+ ]
62
+ has_advice = any(ind in response.lower() for ind in advice_indicators)
63
+ has_question = '?' in response
64
+
65
+ if has_advice and not has_question:
66
+ issues.append("Đưa lời khuyên quá sớm (assessment phase)")
67
+
68
+ # Check if both asking and advising
69
+ if '?' in response:
70
+ advice_count = sum(1 for ind in ["khuyến nghị", "nên", "hãy thử"]
71
+ if ind in response.lower())
72
+ if advice_count >= 2:
73
+ issues.append("Vừa hỏi vừa khuyên")
74
+
75
+ return len(issues) == 0, issues
76
+
77
+ @staticmethod
78
+ def validate_nutrition_response(response: str, context: Dict, chat_history: List) -> Tuple[bool, List[str]]:
79
+ """
80
+ Nutrition-specific validation
81
+ """
82
+ issues = []
83
+
84
+ # Check if asking for info already provided
85
+ if chat_history:
86
+ all_user_text = " ".join([msg[0].lower() for msg in chat_history if msg[0]])
87
+
88
+ # Check if asking for age when already provided
89
+ if "tuổi" in all_user_text or "năm" in all_user_text:
90
+ if "bao nhiêu tuổi" in response.lower() or "tuổi của bạn" in response.lower():
91
+ issues.append("Hỏi lại tuổi đã được cung cấp")
92
+
93
+ # Check if asking for weight when already provided
94
+ if "kg" in all_user_text or "cân nặng" in all_user_text:
95
+ if "cân nặng" in response.lower() and "?" in response:
96
+ issues.append("Hỏi lại cân nặng đã được cung cấp")
97
+
98
+ # Check for too theoretical (should be practical)
99
+ theory_indicators = ["lý thuyết", "nghiên cứu cho thấy", "theo khoa học"]
100
+ if any(ind in response.lower() for ind in theory_indicators):
101
+ practical_indicators = ["bạn thử", "có thể", "ví dụ", "thực đơn"]
102
+ if not any(ind in response.lower() for ind in practical_indicators):
103
+ issues.append("Quá lý thuyết, thiếu practical advice")
104
+
105
+ return len(issues) == 0, issues
106
+
107
+ @staticmethod
108
+ def validate_exercise_response(response: str, context: Dict) -> Tuple[bool, List[str]]:
109
+ """
110
+ Exercise-specific validation
111
+ """
112
+ issues = []
113
+
114
+ # Check if workout plan is too generic
115
+ if "lịch tập" in response.lower() or "kế hoạch" in response.lower():
116
+ # Should have specific days or progression
117
+ has_specifics = any(word in response.lower() for word in [
118
+ "thứ", "ngày", "tuần 1", "tuần 2", "tháng"
119
+ ])
120
+ if not has_specifics:
121
+ issues.append("Lịch tập quá generic, thiếu chi tiết")
122
+
123
+ # Check for progression
124
+ if "tập" in response.lower():
125
+ has_progression = any(word in response.lower() for word in [
126
+ "tăng dần", "progression", "tuần 1", "giai đoạn"
127
+ ])
128
+ if not has_progression and len(response) > 200:
129
+ issues.append("Thiếu hướng dẫn progression")
130
+
131
+ return len(issues) == 0, issues
132
+
133
+ @staticmethod
134
+ def validate_mental_health_response(response: str, context: Dict) -> Tuple[bool, List[str]]:
135
+ """
136
+ Mental health-specific validation
137
+ """
138
+ issues = []
139
+
140
+ # Should have empathy/validation
141
+ empathy_indicators = [
142
+ "cảm giác", "hiểu", "bình thường", "nhiều người",
143
+ "không phải lỗi của bạn"
144
+ ]
145
+
146
+ if len(response) > 100: # Only check for longer responses
147
+ has_empathy = any(ind in response.lower() for ind in empathy_indicators)
148
+ if not has_empathy:
149
+ issues.append("Thiếu empathy/validation")
150
+
151
+ # Check for too clinical
152
+ clinical_indicators = ["chẩn đoán", "bệnh", "rối loạn"]
153
+ if any(ind in response.lower() for ind in clinical_indicators):
154
+ if "không phải bác sĩ" not in response.lower():
155
+ issues.append("Quá clinical, cần disclaimer")
156
+
157
+ return len(issues) == 0, issues
158
+
159
+ @staticmethod
160
+ def validate_response(response: str, agent_type: str, context: Dict, chat_history: List = None) -> Tuple[bool, List[str]]:
161
+ """
162
+ Main validation method - routes to appropriate validator
163
+ """
164
+ # Common validation first
165
+ is_valid_common, common_issues = ResponseValidator.validate_common(response)
166
+
167
+ # Agent-specific validation
168
+ agent_issues = []
169
+ if agent_type == 'symptom':
170
+ _, agent_issues = ResponseValidator.validate_symptom_response(response, context)
171
+ elif agent_type == 'nutrition':
172
+ _, agent_issues = ResponseValidator.validate_nutrition_response(response, context, chat_history or [])
173
+ elif agent_type == 'exercise':
174
+ _, agent_issues = ResponseValidator.validate_exercise_response(response, context)
175
+ elif agent_type == 'mental_health':
176
+ _, agent_issues = ResponseValidator.validate_mental_health_response(response, context)
177
+
178
+ # Combine all issues
179
+ all_issues = common_issues + agent_issues
180
+ is_valid = len(all_issues) == 0
181
+
182
+ return is_valid, all_issues
agents/core/router.py ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Router - Routes user requests to appropriate specialized agents
3
+
4
+ Supports two routing strategies:
5
+ 1. Embedding-based routing (primary) - Automatic, scalable
6
+ 2. LLM-based routing (fallback) - Manual, explicit
7
+ """
8
+
9
+ from config.settings import client, MODEL
10
+ from typing import List, Dict, Tuple, Optional
11
+ import numpy as np
12
+
13
+ # Try to import embedding model (optional)
14
+ try:
15
+ from sentence_transformers import SentenceTransformer
16
+ from sklearn.metrics.pairwise import cosine_similarity
17
+ EMBEDDINGS_AVAILABLE = True
18
+ except ImportError:
19
+ EMBEDDINGS_AVAILABLE = False
20
+ print("[WARNING] sentence-transformers not installed. Using LLM-based routing only.")
21
+ print("Install with: pip install sentence-transformers scikit-learn")
22
+
23
+ # Define available functions/agents
24
+ AVAILABLE_FUNCTIONS = [
25
+ {
26
+ "name": "nutrition_agent",
27
+ "description": """Tư vấn dinh dưỡng và chế độ ăn uống:
28
+ - Tính BMI, calo, macro (protein/carb/fat)
29
+ - Lập thực đơn, meal plan
30
+ - Tư vấn thực phẩm nên ăn/tránh
31
+ - Giảm cân, tăng cân, tăng cơ
32
+ - Bổ sung dinh dưỡng, vitamin
33
+
34
+ KHÔNG dùng cho: triệu chứng bệnh (đau bụng, buồn nôn, tiêu chảy)
35
+ → Triệu chứng bệnh → dùng symptom_agent""",
36
+ "parameters": {
37
+ "type": "object",
38
+ "properties": {
39
+ "user_query": {
40
+ "type": "string",
41
+ "description": "Câu hỏi của người dùng về dinh dưỡng"
42
+ },
43
+ "user_data": {
44
+ "type": "object",
45
+ "description": "Thông tin người dùng (tuổi, giới tính, cân nặng, chiều cao, mục tiêu)",
46
+ "properties": {
47
+ "age": {"type": "integer"},
48
+ "gender": {"type": "string"},
49
+ "weight": {"type": "number"},
50
+ "height": {"type": "number"},
51
+ "goal": {"type": "string"}
52
+ }
53
+ }
54
+ },
55
+ "required": ["user_query"]
56
+ }
57
+ },
58
+ {
59
+ "name": "exercise_agent",
60
+ "description": "Tư vấn tập luyện, lịch tập gym, bài tập thể dục, kế hoạch tập luyện, yoga, cardio",
61
+ "parameters": {
62
+ "type": "object",
63
+ "properties": {
64
+ "user_query": {
65
+ "type": "string",
66
+ "description": "Câu hỏi của người dùng về tập luyện"
67
+ },
68
+ "user_data": {
69
+ "type": "object",
70
+ "description": "Thông tin người dùng (tuổi, giới tính, thể lực, mục tiêu, thời gian)",
71
+ "properties": {
72
+ "age": {"type": "integer"},
73
+ "gender": {"type": "string"},
74
+ "fitness_level": {"type": "string"},
75
+ "goal": {"type": "string"},
76
+ "available_time": {"type": "integer"}
77
+ }
78
+ }
79
+ },
80
+ "required": ["user_query"]
81
+ }
82
+ },
83
+ {
84
+ "name": "symptom_agent",
85
+ "description": """CLINICAL SYMPTOM ASSESSMENT - Đánh giá triệu chứng bệnh CỤ THỂ:
86
+
87
+ ✅ USE FOR (Specific symptoms):
88
+ - Pain: đau đầu, đau bụng, đau lưng, đau ngực, đau khớp
89
+ - Fever/Infection: sốt, ho, cảm cúm, viêm họng, viêm phổi
90
+ - Digestive: buồn nôn, nôn, tiêu chảy, táo bón, đầy hơi
91
+ - Neurological: chóng mặt, đau nửa đầu, mất thăng bằng
92
+ - Acute symptoms: triệu chứng đột ngột, bất thường, nghiêm trọng
93
+
94
+ ✅ WHEN TO USE:
95
+ - User describes SPECIFIC symptom: "Tôi bị đau bụng"
96
+ - User feels sick/unwell: "Tôi không khỏe", "Tôi bị ốm"
97
+ - Medical concern: "Tôi sợ bị bệnh X"
98
+
99
+ ❌ DO NOT USE FOR:
100
+ - General wellness: "Làm sao để khỏe?" → general_health_agent
101
+ - Prevention: "Phòng ngừa bệnh" → general_health_agent
102
+ - Lifestyle: "Sống khỏe mạnh" → general_health_agent
103
+ - Nutrition: "Nên ăn gì?" → nutrition_agent
104
+ - Exercise: "Tập gì?" → exercise_agent""",
105
+ "parameters": {
106
+ "type": "object",
107
+ "properties": {
108
+ "user_query": {
109
+ "type": "string",
110
+ "description": "Mô tả triệu chứng của người dùng"
111
+ },
112
+ "symptom_data": {
113
+ "type": "object",
114
+ "description": "Thông tin triệu chứng (onset, location, severity, duration)",
115
+ "properties": {
116
+ "symptom_type": {"type": "string"},
117
+ "duration": {"type": "string"},
118
+ "severity": {"type": "integer"},
119
+ "location": {"type": "string"}
120
+ }
121
+ }
122
+ },
123
+ "required": ["user_query"]
124
+ }
125
+ },
126
+ {
127
+ "name": "mental_health_agent",
128
+ "description": "Tư vấn sức khỏe tinh thần, stress, lo âu, trầm cảm, burnout, giấc ngủ, cảm xúc",
129
+ "parameters": {
130
+ "type": "object",
131
+ "properties": {
132
+ "user_query": {
133
+ "type": "string",
134
+ "description": "Câu hỏi về sức khỏe tinh thần"
135
+ },
136
+ "context": {
137
+ "type": "object",
138
+ "description": "Ngữ cảnh (công việc, gia đình, stress level)",
139
+ "properties": {
140
+ "stress_level": {"type": "string"},
141
+ "duration": {"type": "string"},
142
+ "triggers": {"type": "array", "items": {"type": "string"}}
143
+ }
144
+ }
145
+ },
146
+ "required": ["user_query"]
147
+ }
148
+ },
149
+ {
150
+ "name": "general_health_agent",
151
+ "description": """GENERAL WELLNESS & LIFESTYLE - Tư vấn sức khỏe TỔNG QUÁT:
152
+
153
+ ✅ USE FOR (General health & wellness):
154
+ - Wellness: "Làm sao để khỏe mạnh?", "Sống khỏe"
155
+ - Prevention: "Phòng ngừa bệnh", "Tăng sức đề kháng"
156
+ - Lifestyle: "Lối sống lành mạnh", "Thói quen tốt"
157
+ - General advice: "Tư vấn sức khỏe", "Chăm sóc sức khỏe"
158
+ - Health education: "Kiến thức sức khỏe", "Hiểu về cơ thể"
159
+ - Check-ups: "Khám sức khỏe định kỳ", "Xét nghiệm gì?"
160
+
161
+ ✅ WHEN TO USE:
162
+ - Broad health questions: "Tôi muốn khỏe hơn"
163
+ - No specific symptom: "Tư vấn sức khỏe tổng quát"
164
+ - Prevention focus: "Làm gì để không bị ốm?"
165
+ - Lifestyle optimization: "Cải thiện sức khỏe"
166
+
167
+ ❌ DO NOT USE FOR:
168
+ - Specific symptoms: "Tôi bị đau bụng" → symptom_agent
169
+ - Nutrition details: "Lập thực đơn" → nutrition_agent
170
+ - Exercise plans: "Lịch tập gym" → exercise_agent
171
+ - Mental health: "Stress, lo âu" → mental_health_agent""",
172
+ "parameters": {
173
+ "type": "object",
174
+ "properties": {
175
+ "user_query": {
176
+ "type": "string",
177
+ "description": "Câu hỏi chung về sức khỏe"
178
+ }
179
+ },
180
+ "required": ["user_query"]
181
+ }
182
+ }
183
+ ]
184
+
185
+ def route_to_agent(message, chat_history=None):
186
+ """
187
+ Route user message to appropriate specialized agent using function calling
188
+
189
+ Args:
190
+ message (str): User's message
191
+ chat_history (list): Conversation history for context
192
+
193
+ Returns:
194
+ dict: {
195
+ "agent": str, # Agent name
196
+ "parameters": dict, # Extracted parameters
197
+ "confidence": float # Routing confidence (0-1)
198
+ }
199
+ """
200
+
201
+ # Build context from chat history (increased from 3 to 10 for better context)
202
+ context = ""
203
+ last_agent = None
204
+
205
+ if chat_history:
206
+ recent_messages = chat_history[-10:] # Last 10 exchanges (was 3)
207
+
208
+ # Extract last agent from bot response
209
+ if recent_messages:
210
+ last_bot_msg = recent_messages[-1][1] if len(recent_messages[-1]) > 1 else ""
211
+ # Try to detect agent from debug info
212
+ if "Agent used:" in last_bot_msg:
213
+ import re
214
+ match = re.search(r'Agent used: `(\w+)`', last_bot_msg)
215
+ if match:
216
+ last_agent = match.group(1)
217
+
218
+ # Build context with turn numbers for clarity
219
+ context_lines = []
220
+ for i, (user_msg, bot_msg) in enumerate(recent_messages, 1):
221
+ # Truncate long messages
222
+ user_short = user_msg[:80] + "..." if len(user_msg) > 80 else user_msg
223
+ bot_short = bot_msg[:80] + "..." if len(bot_msg) > 80 else bot_msg
224
+ context_lines.append(f"Turn {i}:\n User: {user_short}\n Bot: {bot_short}")
225
+
226
+ context = "\n".join(context_lines)
227
+
228
+ # Create enhanced routing prompt with context awareness
229
+ routing_prompt = f"""Phân tích câu hỏi của người dùng và xác định agent phù hợp nhất.
230
+
231
+ LỊCH SỬ HỘI THOẠI (10 exchanges gần nhất):
232
+ {context if context else "Đây là câu hỏi đầu tiên"}
233
+
234
+ AGENT TRƯỚC ĐÓ: {last_agent if last_agent else "Chưa có"}
235
+
236
+ CÂU HỎI HIỆN TẠI: {message}
237
+
238
+ HƯỚNG DẪN QUAN TRỌNG:
239
+
240
+ 1. **TRIỆU CHỨNG BỆNH CỤ THỂ → symptom_agent (ưu tiên cao nhất)**
241
+ - User MÔ TẢ triệu chứng CỤ THỂ: "tôi bị đau...", "tôi bị sốt", "buồn nôn"
242
+ - Ví dụ: "đau bụng", "đau đầu", "sốt cao", "ho ra máu", "chóng mặt"
243
+ - LUÔN ưu tiên symptom_agent khi có triệu chứng CỤ THỂ!
244
+
245
+ ⚠️ EDGE CASES - KHÔNG PHẢI symptom_agent:
246
+ - "Làm sao để KHÔNG bị đau đầu?" → general_health_agent (phòng ngừa)
247
+ - "Ăn gì để hết đau bụng?" → nutrition_agent (dinh dưỡng)
248
+ - "Tập gì để hết đau lưng?" → exercise_agent (tập luyện)
249
+ - "Làm sao để khỏe?" → general_health_agent (tổng quát)
250
+
251
+ 2. **DINH DƯỠNG → nutrition_agent**
252
+ - Hỏi về thực phẩm, chế độ ăn, calo, BMI, thực đơn
253
+ - KHÔNG phải triệu chứng bệnh
254
+ - Ví dụ: "nên ăn gì", "giảm cân", "thực đơn"
255
+
256
+ 3. **TẬP LUYỆN → exercise_agent**
257
+ - Hỏi về bài tập, lịch tập, gym, cardio, dụng cụ tập
258
+ - Follow-up về giáo án tập: "không có dụng cụ", "tập tại nhà", "không có tạ"
259
+ - Ví dụ: "nên tập gì", "lịch tập gym", "không có dụng cụ gym"
260
+ - **QUAN TRỌNG:** Nếu đang nói về tập luyện → TIẾP TỤC exercise_agent
261
+
262
+ 4. **SỨC KHỎE TINH THẦN → mental_health_agent**
263
+ - Stress, lo âu, trầm cảm, burnout, giấc ngủ
264
+ - Ví dụ: "tôi stress", "lo âu", "mất ngủ"
265
+
266
+ 5. **SỨC KHỎE TỔNG QUÁT → general_health_agent**
267
+ - Câu hỏi CHUNG về sức khỏe, wellness, lifestyle
268
+ - Phòng ngừa, tăng cường sức khỏe
269
+ - Ví dụ: "làm sao để khỏe?", "phòng bệnh", "sống khỏe"
270
+
271
+ ⚠️ EDGE CASES - Phân biệt với symptom_agent:
272
+ - "Tôi BỊ đau bụng" → symptom_agent (có triệu chứng)
273
+ - "Làm sao để KHÔNG bị đau bụng?" → general_health_agent (phòng ngừa)
274
+ - "Tôi không khỏe" (mơ hồ) → general_health_agent (chung chung)
275
+ - "Tôi bị sốt cao" → symptom_agent (triệu chứng cụ thể)
276
+
277
+ VÍ DỤ ROUTING (Bao gồm edge cases):
278
+
279
+ **Symptom Agent (có triệu chứng CỤ THỂ):**
280
+ ✅ "Tôi bị đau bụng" → symptom_agent
281
+ ✅ "Đau đầu từ sáng" → symptom_agent
282
+ ✅ "Buồn nôn, muốn làm sao cho hết" → symptom_agent
283
+ ✅ "Tôi bị sốt cao 39 độ" → symptom_agent
284
+
285
+ **General Health Agent (phòng ngừa, tổng quát):**
286
+ ✅ "Làm sao để khỏe mạnh?" → general_health_agent
287
+ ✅ "Phòng ngừa đau đầu" → general_health_agent (phòng ngừa!)
288
+ ✅ "Tôi muốn sống khỏe hơn" → general_health_agent
289
+ ✅ "Tư vấn sức khỏe tổng quát" → general_health_agent
290
+
291
+ **Nutrition Agent (dinh dưỡng):**
292
+ ✅ "Tôi muốn giảm cân" → nutrition_agent
293
+ ✅ "Nên ăn gì để khỏe?" → nutrition_agent
294
+ ✅ "Ăn gì để hết đau bụng?" → nutrition_agent (dinh dưỡng!)
295
+
296
+ **Exercise Agent (tập luyện):**
297
+ ✅ "Tôi nên tập gì?" → exercise_agent
298
+ ✅ "Tập gì để hết đau lưng?" → exercise_agent (tập luyện!)
299
+ ✅ "Không có dụng cụ gym thì sao?" (context: tập) → exercise_agent
300
+
301
+ **Mental Health Agent:**
302
+ ✅ "Tôi stress quá" → mental_health_agent
303
+
304
+ **QUAN TRỌNG - CONTEXT AWARENESS:**
305
+ - Nếu last_agent = "exercise_agent" và câu hỏi về "dụng cụ", "tạ", "gym", "tại nhà"
306
+ → TIẾP TỤC exercise_agent (đây là follow-up!)
307
+ - Nếu last_agent = "nutrition_agent" và câu hỏi về "món ăn", "thực đơn", "calo"
308
+ → TIẾP TỤC nutrition_agent (đây là follow-up!)
309
+
310
+ Hãy chọn agent phù hợp nhất dựa trên CẢ câu hỏi hiện tại VÀ ngữ cảnh hội thoại."""
311
+
312
+ try:
313
+ response = client.chat.completions.create(
314
+ model=MODEL,
315
+ messages=[
316
+ {
317
+ "role": "system",
318
+ "content": """Bạn là hệ thống định tuyến thông minh với khả năng HIỂU NGỮ CẢNH hội thoại.
319
+
320
+ NHIỆM VỤ: Phân tích câu hỏi trong NGỮCẢNH cuộc hội thoại và chọn agent phù hợp.
321
+
322
+ KỸ NĂNG QUAN TRỌNG:
323
+ 1. Nhận biết câu hỏi follow-up (vậy, còn, thì sao, nữa)
324
+ 2. Hiểu context từ lịch sử hội thoại
325
+ 3. Phát hiện topic switching (chuyển đề rõ ràng)
326
+ 4. Xử lý câu hỏi mơ hồ bằng cách xem context
327
+
328
+ NGUYÊN TẮC:
329
+ - Câu hỏi RÕ RÀNG → agent trực tiếp
330
+ - Câu hỏi MƠ HỒ → xem lịch sử, last agent
331
+ - Follow-up question → có thể tiếp tục agent cũ
332
+ - Topic switch rõ ràng → agent mới"""
333
+ },
334
+ {
335
+ "role": "user",
336
+ "content": routing_prompt
337
+ }
338
+ ],
339
+ functions=AVAILABLE_FUNCTIONS,
340
+ function_call="auto",
341
+ temperature=0.3 # Lower temperature for more consistent routing
342
+ )
343
+
344
+ # Check if function was called
345
+ if response.choices[0].message.function_call:
346
+ function_call = response.choices[0].message.function_call
347
+
348
+ import json
349
+ parameters = json.loads(function_call.arguments)
350
+
351
+ return {
352
+ "agent": function_call.name,
353
+ "parameters": parameters,
354
+ "confidence": 0.9, # High confidence when function is called
355
+ "raw_response": response
356
+ }
357
+ else:
358
+ # No function called, default to general health agent
359
+ return {
360
+ "agent": "general_health_agent",
361
+ "parameters": {"user_query": message},
362
+ "confidence": 0.5,
363
+ "raw_response": response
364
+ }
365
+
366
+ except Exception as e:
367
+ print(f"Routing error: {e}")
368
+ # Fallback to general health agent
369
+ return {
370
+ "agent": "general_health_agent",
371
+ "parameters": {"user_query": message},
372
+ "confidence": 0.3,
373
+ "error": str(e)
374
+ }
375
+
376
+ def get_agent_description(agent_name):
377
+ """Get description of an agent"""
378
+ for func in AVAILABLE_FUNCTIONS:
379
+ if func["name"] == agent_name:
380
+ return func["description"]
381
+ return "Unknown agent"
382
+
383
+
384
+ # ============================================================
385
+ # Embedding-Based Router (New, Scalable Approach)
386
+ # ============================================================
387
+
388
+ class EmbeddingRouter:
389
+ """
390
+ Embedding-based router that automatically matches queries to agents
391
+ without manual rules. More scalable than LLM-based routing.
392
+ """
393
+
394
+ def __init__(self, use_embeddings=True):
395
+ """
396
+ Initialize router
397
+
398
+ Args:
399
+ use_embeddings: If False, falls back to LLM-based routing
400
+ """
401
+ self.use_embeddings = use_embeddings and EMBEDDINGS_AVAILABLE
402
+
403
+ if self.use_embeddings:
404
+ # Load embedding model
405
+ self.embedder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
406
+
407
+ # Agent descriptions for embedding matching
408
+ self.agent_descriptions = {
409
+ "symptom_agent": """
410
+ Đánh giá triệu chứng bệnh khi BỊ ĐAU hoặc KHÔNG KHỎE:
411
+ đau đầu, đau bụng, đau lưng, sốt, ho, buồn nôn, chóng mặt,
412
+ mệt mỏi bất thường, khó thở, đau ngực, bị bệnh, cảm thấy đau,
413
+ đau nhức, triệu chứng bệnh lý, không khỏe, ốm, bệnh,
414
+ đang bị gì, bị gì vậy, triệu chứng gì
415
+ """,
416
+ "nutrition_agent": """
417
+ Tư vấn dinh dưỡng, ăn uống healthy, chế độ ăn:
418
+ giảm cân, tăng cân, giảm mỡ, muốn gầy, muốn béo,
419
+ ăn gì để giảm cân, ăn gì để tăng cân, calo, BMI,
420
+ thực đơn, chế độ ăn kiêng, thực phẩm, protein, carb, fat,
421
+ vitamin, khoáng chất, dinh dưỡng lành mạnh, ăn uống khoa học,
422
+ setup plan ăn uống, kế hoạch dinh dưỡng, healthy eating,
423
+ ăn healthy, ăn sạch, clean eating
424
+ """,
425
+ "exercise_agent": """
426
+ Tập luyện, gym, workout, fitness, thể hình:
427
+ tập luyện, luyện tập, gym, cardio, bài tập, lịch tập,
428
+ dụng cụ tập, tạ, thanh đòn, tập tại nhà, không có dụng cụ,
429
+ squat, push-up, plank, chạy bộ, yoga, thể dục, thể hình,
430
+ rèn luyện cơ thể, tăng cơ, giảm mỡ, build muscle, lose fat,
431
+ setup plan tập luyện, kế hoạch tập luyện, lịch tập 7 ngày,
432
+ tập gym, tập thể hình, workout plan, fitness plan
433
+ """,
434
+ "mental_health_agent": """
435
+ Sức khỏe tinh thần, stress, lo âu, trầm cảm, burnout,
436
+ mất ngủ, giấc ngủ, căng thẳng, áp lực, tâm lý, cảm xúc,
437
+ buồn bã, mệt mỏi tinh thần
438
+ """,
439
+ "general_health_agent": """
440
+ Câu hỏi chung về sức khỏe, lời khuyên sức khỏe,
441
+ phòng bệnh, chăm sóc sức khỏe, kiểm tra sức khỏe
442
+ """
443
+ }
444
+
445
+ # Pre-compute agent embeddings
446
+ print("[INFO] Pre-computing agent embeddings...")
447
+ self.agent_embeddings = {
448
+ agent: self.embedder.encode(desc)
449
+ for agent, desc in self.agent_descriptions.items()
450
+ }
451
+ print("[INFO] Embedding router ready!")
452
+ else:
453
+ print("[INFO] Using LLM-based routing (embeddings not available)")
454
+
455
+ def route(self, message: str, chat_history: List[Tuple[str, str]] = None) -> Dict:
456
+ """
457
+ Route message to appropriate agent
458
+
459
+ Args:
460
+ message: User message
461
+ chat_history: Conversation history
462
+
463
+ Returns:
464
+ {
465
+ "agent": agent_name,
466
+ "parameters": {...},
467
+ "confidence": float,
468
+ "method": "embedding" or "llm"
469
+ }
470
+ """
471
+ if self.use_embeddings:
472
+ return self._route_with_embeddings(message, chat_history)
473
+ else:
474
+ # Fallback to LLM-based routing
475
+ return route_to_agent(message, chat_history)
476
+
477
+ def _route_with_embeddings(self, message: str, chat_history: List[Tuple[str, str]]) -> Dict:
478
+ """Route using embedding similarity with topic change detection"""
479
+ # Embed query
480
+ query_embedding = self.embedder.encode(message)
481
+
482
+ # Calculate similarity with each agent
483
+ similarities = {}
484
+ for agent, agent_embedding in self.agent_embeddings.items():
485
+ similarity = cosine_similarity(
486
+ query_embedding.reshape(1, -1),
487
+ agent_embedding.reshape(1, -1)
488
+ )[0][0]
489
+ similarities[agent] = similarity
490
+
491
+ # Detect topic change vs follow-up
492
+ is_topic_change = self._detect_topic_change(message, chat_history)
493
+
494
+ # Context boost ONLY for genuine follow-ups (NOT topic changes)
495
+ if not is_topic_change and chat_history and len(chat_history) > 0:
496
+ # Determine CURRENT context from recent conversation (not just last message)
497
+ current_context_agent = self._get_current_context_agent(chat_history)
498
+
499
+ # Simple heuristic: if query is short and has follow-up indicators
500
+ if len(message.split()) < 10 and any(word in message.lower() for word in ["thì sao", "còn", "nữa", "thế", "vậy", "không", "khác", "nếu"]):
501
+ # Boost ONLY the current context agent (not all agents)
502
+ if current_context_agent and current_context_agent in similarities:
503
+ similarities[current_context_agent] += 0.15
504
+ print(f"[ROUTER] Boosting current context agent: {current_context_agent}")
505
+
506
+ # Get best agent
507
+ best_agent = max(similarities, key=similarities.get)
508
+ confidence = float(similarities[best_agent])
509
+
510
+ # Debug logging (disabled for cleaner output)
511
+ # print(f"\n[ROUTER DEBUG] Message: {message[:50]}...")
512
+ # print(f"[ROUTER DEBUG] Topic change detected: {is_topic_change}")
513
+ # print(f"[ROUTER DEBUG] Similarities:")
514
+ # for agent, score in sorted(similarities.items(), key=lambda x: x[1], reverse=True):
515
+ # print(f" - {agent}: {score:.4f}")
516
+ # print(f"[ROUTER DEBUG] Selected: {best_agent} (confidence: {confidence:.4f})\n")
517
+
518
+ return {
519
+ "agent": best_agent,
520
+ "parameters": {"user_query": message},
521
+ "confidence": confidence,
522
+ "method": "embedding",
523
+ "all_scores": {k: float(v) for k, v in similarities.items()},
524
+ "topic_change": is_topic_change
525
+ }
526
+
527
+ def _get_current_context_agent(self, chat_history: List[Tuple[str, str]]) -> Optional[str]:
528
+ """
529
+ Determine which agent is handling the CURRENT context
530
+ by analyzing recent conversation (last 3-5 turns)
531
+
532
+ Returns:
533
+ Agent name that's currently active, or None
534
+ """
535
+ if not chat_history or len(chat_history) == 0:
536
+ return None
537
+
538
+ # Check last 3-5 turns for dominant domain
539
+ recent_turns = chat_history[-5:] if len(chat_history) >= 5 else chat_history
540
+
541
+ domain_keywords = {
542
+ 'nutrition_agent': ['ăn', 'dinh dưỡng', 'thực đơn', 'calo', 'bmi', 'giảm cân', 'tăng cân', 'protein', 'carb', 'meal', 'bữa'],
543
+ 'exercise_agent': ['tập', 'luyện', 'gym', 'cardio', 'yoga', 'vận động', 'tăng cơ', 'giảm mỡ', 'workout', 'bài tập'],
544
+ 'symptom_agent': ['đau', 'bệnh', 'triệu chứng', 'khó chịu', 'buồn nôn', 'sốt', 'ốm'],
545
+ 'mental_health_agent': ['stress', 'lo âu', 'mất ngủ', 'trầm cảm', 'tâm lý']
546
+ }
547
+
548
+ # Count domain occurrences in recent turns
549
+ domain_counts = {agent: 0 for agent in domain_keywords.keys()}
550
+
551
+ for user_msg, bot_msg in recent_turns:
552
+ combined = (user_msg + " " + bot_msg).lower()
553
+ for agent, keywords in domain_keywords.items():
554
+ for keyword in keywords:
555
+ if keyword in combined:
556
+ domain_counts[agent] += 1
557
+ break # Count once per turn
558
+
559
+ # Return agent with highest count (if significant)
560
+ if domain_counts:
561
+ max_agent = max(domain_counts, key=domain_counts.get)
562
+ max_count = domain_counts[max_agent]
563
+
564
+ # Need at least 2 occurrences in recent turns to be considered "current context"
565
+ if max_count >= 2:
566
+ return max_agent
567
+
568
+ return None
569
+
570
+ def _detect_topic_change(self, message: str, chat_history: List[Tuple[str, str]]) -> bool:
571
+ """
572
+ Detect if user is changing topics vs following up
573
+
574
+ Topic change indicators:
575
+ - Explicit new requests: "tôi muốn", "giúp tôi", "tư vấn về"
576
+ - Different domain keywords: nutrition → exercise, symptom → nutrition
577
+ - Long, detailed new questions
578
+
579
+ Returns:
580
+ bool: True if topic change, False if follow-up
581
+ """
582
+ msg_lower = message.lower()
583
+
584
+ # Strong topic change indicators
585
+ topic_change_phrases = [
586
+ 'tôi muốn', 'tôi cần', 'giúp tôi', 'tư vấn về', 'cho tôi',
587
+ 'bây giờ', 'còn về', 'chuyển sang', 'ngoài ra',
588
+ 'setup', 'tạo plan', 'lập kế hoạch'
589
+ ]
590
+
591
+ if any(phrase in msg_lower for phrase in topic_change_phrases):
592
+ # Likely a new request
593
+ return True
594
+
595
+ # Check for domain-specific keywords that indicate topic change
596
+ domain_keywords = {
597
+ 'nutrition': ['ăn', 'dinh dưỡng', 'thực đơn', 'calo', 'bmi', 'giảm cân', 'tăng cân'],
598
+ 'exercise': ['tập', 'luyện', 'gym', 'cardio', 'yoga', 'vận động', 'tăng cơ', 'giảm mỡ'],
599
+ 'symptom': ['đau', 'bệnh', 'triệu chứng', 'khó chịu', 'buồn nôn'],
600
+ 'mental': ['stress', 'lo âu', 'mất ngủ', 'trầm cảm', 'tâm lý']
601
+ }
602
+
603
+ # Detect current message domain
604
+ current_domains = []
605
+ for domain, keywords in domain_keywords.items():
606
+ if any(kw in msg_lower for kw in keywords):
607
+ current_domains.append(domain)
608
+
609
+ # If no chat history, it's a new topic
610
+ if not chat_history or len(chat_history) == 0:
611
+ return True
612
+
613
+ # Check last few messages for domain
614
+ recent_messages = chat_history[-3:] if len(chat_history) >= 3 else chat_history
615
+ previous_domains = []
616
+
617
+ for user_msg, bot_msg in recent_messages:
618
+ combined = (user_msg + " " + bot_msg).lower()
619
+ for domain, keywords in domain_keywords.items():
620
+ if any(kw in combined for kw in keywords):
621
+ if domain not in previous_domains:
622
+ previous_domains.append(domain)
623
+
624
+ # If current domain is different from previous, it's a topic change
625
+ if current_domains and previous_domains:
626
+ # Check if there's overlap
627
+ overlap = set(current_domains) & set(previous_domains)
628
+ if not overlap:
629
+ # No overlap = topic change
630
+ return True
631
+
632
+ # Long messages (>15 words) with new content are likely topic changes
633
+ if len(message.split()) > 15:
634
+ # Check if it's not just elaborating on previous topic
635
+ follow_up_words = ['vì', 'do', 'bởi vì', 'là do', 'nghĩa là']
636
+ if not any(word in msg_lower for word in follow_up_words):
637
+ return True
638
+
639
+ # Default: assume follow-up
640
+ return False
641
+
642
+
643
+ # Global router instance (lazy initialization)
644
+ _router_instance = None
645
+
646
+ def get_router(use_embeddings=True, force_reload=False) -> EmbeddingRouter:
647
+ """
648
+ Get global router instance
649
+
650
+ Args:
651
+ use_embeddings: Use embedding-based routing
652
+ force_reload: Force reload router (useful after updating agent descriptions)
653
+ """
654
+ global _router_instance
655
+ if _router_instance is None or force_reload:
656
+ _router_instance = EmbeddingRouter(use_embeddings=use_embeddings)
657
+ return _router_instance
agents/core/unified_tone.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unified Tone System - Ensures consistent personality across all agents
3
+ Makes the AI feel like ONE knowledgeable assistant, not multiple people
4
+ """
5
+
6
+ class UnifiedTone:
7
+ """
8
+ Provides consistent tone and personality across all agents
9
+ The AI is ONE medical professional with multiple specialties
10
+ """
11
+
12
+ # Base personality - applies to ALL agents
13
+ BASE_PERSONALITY = """
14
+ Bạn là một trợ lý sức khỏe AI thông minh và đa năng.
15
+
16
+ QUAN TRỌNG - Tính nhất quán:
17
+ - Bạn là MỘT NGƯỜI duy nhất với nhiều chuyên môn
18
+ - KHÔNG tự giới thiệu là "chuyên gia dinh dưỡng" hay "huấn luyện viên"
19
+ - Chỉ nói "Tôi sẽ tư vấn về [lĩnh vực]" khi cần
20
+ - Giữ giọng điệu nhất quán xuyên suốt
21
+
22
+ SMART GREETING - Câu đầu tiên:
23
+ - Nếu user CHỈ chào (vd: "chào", "hello") → Chào đầy đủ + giới thiệu
24
+ - Nếu user VÀO THẲNG VẤN ĐỀ (vd: "đau lưng", "tôi muốn giảm cân") → Chào ngắn gọn + trả lời luôn
25
+ * Ví dụ: "Chào bạn! Để giúp bạn về vấn đề đau lưng..."
26
+ * KHÔNG greeting dài dòng khi user đã có vấn đề cụ thể
27
+
28
+ Phong cách chung:
29
+ - Thân thiện nhưng chuyên nghiệp
30
+ - Rõ ràng, súc tích, dễ hiểu
31
+ - Quan tâm nhưng không quá emotional
32
+ - Thực tế, có căn cứ khoa học
33
+ - Tránh emoji quá nhiều (chỉ dùng khi cần thiết)
34
+ """
35
+
36
+ # Smooth transitions between specialties
37
+ TRANSITION_PHRASES = {
38
+ 'to_nutrition': "Về mặt dinh dưỡng, ",
39
+ 'to_exercise': "Về vận động và tập luyện, ",
40
+ 'to_symptom': "Về triệu chứng bạn đang gặp, ",
41
+ 'to_mental': "Về mặt tinh thần và cảm xúc, ",
42
+ 'general': "Dựa trên thông tin bạn cung cấp, "
43
+ }
44
+
45
+ @staticmethod
46
+ def apply_unified_tone(response: str, agent_type: str) -> str:
47
+ """
48
+ Apply unified tone to agent response
49
+ Ensures consistency across all agents
50
+ """
51
+ # Remove agent-specific introductions
52
+ replacements = [
53
+ ("Tôi là chuyên gia dinh dưỡng", "Về dinh dưỡng"),
54
+ ("Tôi là huấn luyện viên", "Về tập luyện"),
55
+ ("Với tư cách bác sĩ", "Theo y học"),
56
+ ("Là chuyên gia tâm lý", "Về mặt tâm lý"),
57
+ ]
58
+
59
+ for old, new in replacements:
60
+ response = response.replace(old, new)
61
+
62
+ # Moderate emoji usage
63
+ if agent_type == 'symptom':
64
+ # Remove excessive medical emojis
65
+ response = response.replace('🏥', '').replace('💊', '')
66
+ elif agent_type == 'exercise':
67
+ # Keep motivational but not excessive
68
+ response = response.replace('💪💪💪', '💪')
69
+ response = response.replace('🔥🔥🔥', '🔥')
70
+
71
+ return response
72
+
73
+ @staticmethod
74
+ def create_smooth_handoff(from_agent: str, to_agent: str, context: str) -> str:
75
+ """
76
+ Create smooth transition between agent specialties
77
+ Makes it feel like one person switching topics, not different people
78
+ """
79
+ transition = UnifiedTone.TRANSITION_PHRASES.get(f'to_{to_agent}', '')
80
+
81
+ # Don't say "I'm handing you over to..."
82
+ # Instead, smoothly transition topics
83
+ handoff_message = f"{context}\n\n{transition}"
84
+
85
+ return handoff_message
86
+
87
+ @staticmethod
88
+ def check_information_before_asking(chat_history: list, field: str) -> bool:
89
+ """
90
+ Check if information already exists in chat history
91
+ Prevents duplicate questions across agents
92
+ """
93
+ import re
94
+
95
+ # Patterns to check for each field
96
+ patterns = {
97
+ 'age': r'\d+\s*tuổi',
98
+ 'gender': r'(nam|nữ|male|female)',
99
+ 'weight': r'\d+\s*kg',
100
+ 'height': r'\d+\s*cm',
101
+ 'goal': r'(giảm cân|tăng cân|tăng cơ|khỏe mạnh)',
102
+ 'condition': r'(tiểu đường|huyết áp|tim mạch|dị ứng)'
103
+ }
104
+
105
+ if field not in patterns:
106
+ return False
107
+
108
+ # Check all messages in history
109
+ for user_msg, bot_msg in chat_history:
110
+ if user_msg and re.search(patterns[field], user_msg.lower()):
111
+ return True # Information already provided
112
+ if bot_msg and "không biết" in bot_msg.lower():
113
+ return True # User already declined to provide
114
+
115
+ return False
116
+
117
+ @staticmethod
118
+ def generate_smart_question(needed_info: list, chat_history: list) -> str:
119
+ """
120
+ Generate intelligent questions that don't repeat
121
+ Groups multiple needs into one natural question
122
+ """
123
+ # Filter out already known information
124
+ actually_needed = []
125
+ for info in needed_info:
126
+ if not UnifiedTone.check_information_before_asking(chat_history, info):
127
+ actually_needed.append(info)
128
+
129
+ if not actually_needed:
130
+ return "" # Don't ask anything
131
+
132
+ # Group related questions
133
+ if len(actually_needed) > 2:
134
+ # Ask for multiple things naturally
135
+ return "Để tư vấn chính xác, bạn có thể cho tôi biết tuổi, giới tính, cân nặng và mục tiêu của bạn được không?"
136
+ elif len(actually_needed) == 2:
137
+ field_names = {
138
+ 'age': 'tuổi',
139
+ 'gender': 'giới tính',
140
+ 'weight': 'cân nặng',
141
+ 'height': 'chiều cao',
142
+ 'goal': 'mục tiêu'
143
+ }
144
+ fields = ' và '.join([field_names.get(f, f) for f in actually_needed])
145
+ return f"Bạn có thể cho tôi biết {fields} của bạn không?"
146
+ else:
147
+ # Single question
148
+ questions = {
149
+ 'age': "Bạn bao nhiêu tuổi?",
150
+ 'gender': "Giới tính của bạn là gì?",
151
+ 'weight': "Cân nặng hiện tại của bạn là bao nhiêu?",
152
+ 'height': "Chiều cao của bạn là bao nhiêu?",
153
+ 'goal': "Mục tiêu sức khỏe của bạn là gì?"
154
+ }
155
+ return questions.get(actually_needed[0], "")
agents/specialized/__init__.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Specialized agents package - Domain-specific healthcare agents
3
+ """
4
+
5
+ from .nutrition_agent import NutritionAgent
6
+ from .exercise_agent import ExerciseAgent
7
+ from .symptom_agent import SymptomAgent
8
+ from .mental_health_agent import MentalHealthAgent
9
+ from .general_health_agent import GeneralHealthAgent
10
+
11
+ # Agent registry
12
+ AGENTS = {
13
+ "nutrition_agent": NutritionAgent,
14
+ "exercise_agent": ExerciseAgent,
15
+ "symptom_agent": SymptomAgent,
16
+ "mental_health_agent": MentalHealthAgent,
17
+ "general_health_agent": GeneralHealthAgent
18
+ }
19
+
20
+ def get_agent(agent_name):
21
+ """Get agent instance by name"""
22
+ agent_class = AGENTS.get(agent_name)
23
+ if agent_class:
24
+ return agent_class()
25
+ return GeneralHealthAgent() # Default fallback
26
+
27
+ __all__ = [
28
+ 'NutritionAgent',
29
+ 'ExerciseAgent',
30
+ 'SymptomAgent',
31
+ 'MentalHealthAgent',
32
+ 'GeneralHealthAgent',
33
+ 'AGENTS',
34
+ 'get_agent'
35
+ ]
agents/specialized/exercise_agent.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Exercise Agent - Specialized agent for exercise and fitness advice
3
+ """
4
+
5
+ from config.settings import client, MODEL
6
+ from modules.exercise.exercise import generate_exercise_plan
7
+ from health_data import HealthContext
8
+ from fitness_tracking import FitnessTracker
9
+ from rag.rag_integration import get_rag_integration
10
+ from agents.core.base_agent import BaseAgent
11
+ from typing import Dict, Any, List, Optional
12
+ from datetime import datetime
13
+ import re
14
+
15
+ class ExerciseAgent(BaseAgent):
16
+ def __init__(self, memory=None):
17
+ super().__init__(memory)
18
+ self.health_context = None
19
+ self.fitness_tracker = None
20
+ self.rag = get_rag_integration()
21
+
22
+ # Configure handoff triggers for exercise agent
23
+ self.handoff_triggers = {
24
+ 'nutrition_agent': ['ăn gì', 'thực đơn', 'calo', 'dinh dưỡng', 'giảm cân nhanh', 'tăng cân'],
25
+ 'symptom_agent': ['đau', 'chấn thương', 'bị thương', 'sưng', 'viêm'],
26
+ 'mental_health_agent': ['stress', 'lo âu', 'không có động lực', 'chán'],
27
+ 'general_health_agent': ['khám', 'bác sĩ', 'xét nghiệm']
28
+ }
29
+ self.system_prompt = """Bạn là huấn luyện viên cá nhân chuyên nghiệp, nhiệt huyết và động viên.
30
+
31
+ 💪 CHUYÊN MÔN:
32
+ - Tạo kế hoạch tập luyện cá nhân hóa
33
+ - Tư vấn bài tập phù hợp với thể trạng, mục tiêu
34
+ - Hướng dẫn kỹ thuật tập an toàn
35
+ - Tư vấn tập cho người có bệnh nền
36
+ - Lịch tập gym, tập tại nhà, cardio, yoga...
37
+
38
+ 🎯 CÁCH TƯ VẤN:
39
+
40
+ 1. **KIỂM TRA THÔNG TIN TRƯỚC KHI HỎI:**
41
+ - ĐỌC KỸ chat history - user có thể đã cung cấp thông tin rồi!
42
+ - Nếu user đã nói "tôi 30 tuổi, nam, muốn giảm cân, có thể tập 45 phút/ngày" → ĐỪNG HỎI LẠI!
43
+ - Chỉ hỏi thông tin THỰC SỰ còn thiếu
44
+ - Nếu đã đủ thông tin cơ bản → TẠO LỊCH TẬP NGAY!
45
+
46
+ 2. **THÔNG TIN CẦN THIẾT:**
47
+ - Cơ bản: Tuổi, giới tính, mục tiêu, thời gian rảnh
48
+ - Bổ sung: Thể lực, dụng cụ có sẵn, bệnh nền
49
+ - Nếu thiếu → Hỏi ngắn gọn, không hỏi mãi
50
+
51
+ 3. **TẠO LỊCH TẬP:**
52
+ - Lịch tập cụ thể theo ngày
53
+ - Giải thích TẠI SAO tập bài này
54
+ - Hướng dẫn progression (tuần 1, 2, 3...)
55
+ - Lưu ý an toàn, tránh chấn thương
56
+
57
+ ⚠️ AN TOÀN:
58
+ - Người có bệnh tim, huyết áp → khuyên gặp bác sĩ trước
59
+ - Người có chấn thương → tập nhẹ, tránh vùng bị thương
60
+ - Người mới bắt đầu → từ từ, không quá sức
61
+
62
+ 💬 PHONG CÁCH:
63
+ - Động viên, khích lệ 💪🔥
64
+ - Thực tế, không lý thuyết suông
65
+ - Dễ hiểu, dễ làm theo
66
+ - Hài hước nhẹ nhàng
67
+ - TỰ NHIÊN, MẠCH LẠC - không lặp lại ý, không copy-paste câu từ context khác
68
+ - Nếu hỏi thông tin → Hỏi NGẮN GỌN, TRỰC TIẾP
69
+ - KHÔNG dùng câu như "Bạn thử làm theo xem có đỡ không" (đây là câu của bác sĩ, không phải PT!)"""
70
+
71
+ def set_health_context(self, health_context: HealthContext):
72
+ """Inject health context and initialize fitness tracker"""
73
+ self.health_context = health_context
74
+ self.fitness_tracker = FitnessTracker(health_context)
75
+
76
+ def handle(self, parameters, chat_history=None):
77
+ """
78
+ Handle exercise request
79
+
80
+ Args:
81
+ parameters (dict): {
82
+ "user_query": str,
83
+ "user_data": dict (optional)
84
+ }
85
+ chat_history (list): Conversation history
86
+
87
+ Returns:
88
+ str: Response message
89
+ """
90
+ user_query = parameters.get("user_query", "")
91
+ user_data = parameters.get("user_data", {})
92
+
93
+ # Extract and save user info from current message immediately
94
+ self.extract_and_save_user_info(user_query)
95
+
96
+ # Update memory from chat history
97
+ if chat_history:
98
+ self.update_memory_from_history(chat_history)
99
+
100
+ # Check if we should hand off to another agent
101
+ if self.should_handoff(user_query, chat_history):
102
+ next_agent = self.suggest_next_agent(user_query)
103
+ if next_agent:
104
+ # Save current exercise data for next agent
105
+ self.save_agent_data('last_exercise_advice', {
106
+ 'query': user_query,
107
+ 'user_profile': self.get_user_profile(),
108
+ 'timestamp': datetime.now().isoformat()
109
+ })
110
+
111
+ # Check if nutrition agent shared data with us
112
+ nutrition_data = self.get_other_agent_data('nutrition_agent', 'nutrition_plan')
113
+ context = self._generate_exercise_summary(nutrition_data)
114
+ return self.create_handoff_message(next_agent, context, user_query)
115
+
116
+ # Use health context if available
117
+ if self.health_context:
118
+ profile = self.health_context.get_user_profile()
119
+ user_data = {
120
+ 'age': profile.age,
121
+ 'gender': profile.gender,
122
+ 'weight': profile.weight,
123
+ 'height': profile.height,
124
+ 'fitness_level': profile.fitness_level,
125
+ 'activity_level': profile.activity_level,
126
+ 'health_conditions': profile.health_conditions
127
+ }
128
+ # Extract user data from chat history if not provided
129
+ elif not user_data and chat_history:
130
+ user_data = self._extract_user_data_from_history(chat_history)
131
+ # Save extracted data to shared memory for other agents
132
+ for key, value in user_data.items():
133
+ if value is not None:
134
+ self.update_user_profile(key, value)
135
+
136
+ # Check if we have enough data - check shared memory first
137
+ profile = self.get_user_profile()
138
+ for field in ['age', 'gender', 'weight', 'height']:
139
+ if not user_data.get(field) and profile.get(field):
140
+ user_data[field] = profile[field]
141
+
142
+ missing_fields = self._check_missing_data(user_data)
143
+
144
+ if missing_fields:
145
+ return self._ask_for_missing_data(missing_fields, user_data)
146
+
147
+ # Generate exercise plan
148
+ try:
149
+ plan = generate_exercise_plan(user_data)
150
+
151
+ # Adjust difficulty based on fitness tracker
152
+ if self.fitness_tracker:
153
+ metrics = self.fitness_tracker.calculate_progress_metrics()
154
+ if metrics.get('adherence', 0) > 0.8:
155
+ plan = self.fitness_tracker.adjust_difficulty(plan, 'increase')
156
+ elif metrics.get('adherence', 0) < 0.5:
157
+ plan = self.fitness_tracker.adjust_difficulty(plan, 'decrease')
158
+
159
+ response = plan
160
+
161
+ # Persist workout plan to health context
162
+ if self.health_context:
163
+ self.health_context.add_health_record('exercise', {
164
+ 'query': user_query,
165
+ 'plan': response,
166
+ 'user_data': user_data,
167
+ 'timestamp': datetime.now().isoformat()
168
+ })
169
+
170
+ return response
171
+ except Exception as e:
172
+ return self._handle_error(e, user_query)
173
+
174
+ def _extract_user_data_from_history(self, chat_history):
175
+ """Extract user data from conversation history"""
176
+ user_data = {
177
+ 'age': None,
178
+ 'gender': None,
179
+ 'weight': None,
180
+ 'height': None,
181
+ 'fitness_level': 'beginner',
182
+ 'goal': 'health_improvement',
183
+ 'available_time': 30,
184
+ 'health_conditions': []
185
+ }
186
+
187
+ all_messages = " ".join([msg[0] for msg in chat_history if msg[0]])
188
+
189
+ # Extract age
190
+ age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|tôi\s*(\d+)', all_messages.lower())
191
+ if age_match:
192
+ user_data['age'] = int([g for g in age_match.groups() if g][0])
193
+
194
+ # Extract gender
195
+ if re.search(r'\bnam\b|male|đàn ông', all_messages.lower()):
196
+ user_data['gender'] = 'male'
197
+ elif re.search(r'\bnữ\b|female|đàn bà', all_messages.lower()):
198
+ user_data['gender'] = 'female'
199
+
200
+ # Extract fitness level
201
+ if re.search(r'mới bắt đầu|beginner|chưa tập', all_messages.lower()):
202
+ user_data['fitness_level'] = 'beginner'
203
+ elif re.search(r'trung bình|intermediate|tập được', all_messages.lower()):
204
+ user_data['fitness_level'] = 'intermediate'
205
+ elif re.search(r'nâng cao|advanced|tập lâu', all_messages.lower()):
206
+ user_data['fitness_level'] = 'advanced'
207
+
208
+ # Extract goal
209
+ if re.search(r'giảm cân|weight loss|slim', all_messages.lower()):
210
+ user_data['goal'] = 'weight_loss'
211
+ elif re.search(r'tăng cân|weight gain|bulk', all_messages.lower()):
212
+ user_data['goal'] = 'weight_gain'
213
+ elif re.search(r'tập gym|muscle|cơ bắp|tăng cơ', all_messages.lower()):
214
+ user_data['goal'] = 'muscle_building'
215
+ elif re.search(r'khỏe mạnh|health|sức khỏe', all_messages.lower()):
216
+ user_data['goal'] = 'health_improvement'
217
+
218
+ # Extract available time
219
+ time_match = re.search(r'(\d+)\s*phút|(\d+)\s*tiếng', all_messages.lower())
220
+ if time_match:
221
+ time_val = int([g for g in time_match.groups() if g][0])
222
+ if 'tiếng' in all_messages.lower():
223
+ time_val *= 60
224
+ user_data['available_time'] = time_val
225
+
226
+ return user_data
227
+
228
+ def _check_missing_data(self, user_data):
229
+ """Check what data is missing"""
230
+ required = ['age', 'gender', 'fitness_level', 'goal']
231
+ return [field for field in required if not user_data.get(field)]
232
+
233
+ def _ask_for_missing_data(self, missing_fields, current_data):
234
+ """Ask for missing data"""
235
+ questions = {
236
+ 'age': "bạn bao nhiêu tuổi",
237
+ 'gender': "bạn là nam hay nữ",
238
+ 'fitness_level': "thể lực hiện tại của bạn thế nào (mới bắt đầu/trung bình/nâng cao)",
239
+ 'goal': "mục tiêu của bạn là gì (giảm cân/tăng cơ/khỏe mạnh hơn)"
240
+ }
241
+
242
+ q_list = [questions[f] for f in missing_fields]
243
+
244
+ if len(q_list) == 1:
245
+ question = q_list[0]
246
+ elif len(q_list) == 2:
247
+ question = f"{q_list[0]} và {q_list[1]}"
248
+ else:
249
+ question = ", ".join(q_list[:-1]) + f" và {q_list[-1]}"
250
+
251
+ return f"""💪 **Để tạo lịch tập phù hợp, mình cần biết thêm:**
252
+
253
+ Cho mình biết {question} nhé?
254
+
255
+ 💡 **Ví dụ:** "Tôi 30 tuổi, nam, mới bắt đầu tập, muốn giảm cân, có thể tập 45 phút mỗi ngày"
256
+
257
+ Sau khi có đủ thông tin, mình sẽ tạo kế hoạch tập luyện 7 ngày chi tiết cho bạn! 🔥"""
258
+
259
+ def _handle_general_exercise_query(self, user_query, chat_history):
260
+ """Handle general exercise questions using LLM + RAG"""
261
+ from config.settings import client, MODEL
262
+
263
+ try:
264
+ # Smart RAG - only query when needed (inherit from BaseAgent)
265
+ rag_answer = ''
266
+ rag_sources = []
267
+
268
+ if self.should_use_rag(user_query, chat_history):
269
+ rag_result = self.rag.query_exercise(user_query)
270
+ rag_answer = rag_result.get('answer', '')
271
+ rag_sources = rag_result.get('source_docs', [])
272
+
273
+ # Build conversation context with RAG context
274
+ rag_context = f"Dựa trên kiến thức từ cơ sở dữ liệu:\n{rag_answer}\n\n" if rag_answer else ""
275
+
276
+ messages = [{"role": "system", "content": self.system_prompt}]
277
+
278
+ # Add RAG context if available
279
+ if rag_context:
280
+ messages.append({"role": "system", "content": f"Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_context}"})
281
+
282
+ # Add chat history (last 5 exchanges)
283
+ if chat_history:
284
+ recent_history = chat_history[-5:] if len(chat_history) > 5 else chat_history
285
+ for user_msg, bot_msg in recent_history:
286
+ if user_msg:
287
+ messages.append({"role": "user", "content": user_msg})
288
+ if bot_msg:
289
+ messages.append({"role": "assistant", "content": bot_msg})
290
+
291
+ # Add current query
292
+ messages.append({"role": "user", "content": user_query})
293
+
294
+ # Get LLM response
295
+ response = client.chat.completions.create(
296
+ model=MODEL,
297
+ messages=messages,
298
+ temperature=0.7,
299
+ max_tokens=500
300
+ )
301
+
302
+ llm_response = response.choices[0].message.content
303
+
304
+ # Add sources using RAG integration formatter (FIXED!)
305
+ if rag_sources:
306
+ formatted_response = self.rag.format_response_with_sources({
307
+ 'answer': llm_response,
308
+ 'source_docs': rag_sources
309
+ })
310
+ return formatted_response
311
+
312
+ return llm_response
313
+
314
+ except Exception as e:
315
+ return f"""Xin lỗi, mình gặp lỗi kỹ thuật. Bạn có thể:
316
+ 1. Thử lại câu hỏi
317
+ 2. Hoặc hỏi mình về chủ đề sức khỏe khác nhé! 💙
318
+
319
+ Chi tiết lỗi: {str(e)}"""
320
+
321
+ def should_handoff(self, user_query: str, chat_history: Optional[List] = None) -> bool:
322
+ """
323
+ Override base method - Determine if should hand off to another agent
324
+
325
+ Specific triggers for exercise agent:
326
+ - User asks about nutrition/diet
327
+ - User mentions pain/injury
328
+ - User asks about mental health
329
+ """
330
+ query_lower = user_query.lower()
331
+
332
+ # Check each agent's triggers
333
+ for agent, triggers in self.handoff_triggers.items():
334
+ if any(trigger in query_lower for trigger in triggers):
335
+ # Don't handoff if we're in the middle of exercise planning
336
+ if chat_history and self._is_mid_planning(chat_history):
337
+ return False
338
+ return True
339
+
340
+ return False
341
+
342
+ def suggest_next_agent(self, user_query: str) -> Optional[str]:
343
+ """Override base method - Suggest which agent to hand off to"""
344
+ query_lower = user_query.lower()
345
+
346
+ # Priority order for handoff
347
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('symptom_agent', [])):
348
+ return 'symptom_agent'
349
+
350
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('nutrition_agent', [])):
351
+ return 'nutrition_agent'
352
+
353
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('mental_health_agent', [])):
354
+ return 'mental_health_agent'
355
+
356
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('general_health_agent', [])):
357
+ return 'general_health_agent'
358
+
359
+ return None
360
+
361
+ def _is_mid_planning(self, chat_history: List) -> bool:
362
+ """Check if we're in the middle of exercise planning"""
363
+ if not chat_history or len(chat_history) < 2:
364
+ return False
365
+
366
+ # Check last bot response
367
+ last_bot_response = chat_history[-1][1] if len(chat_history[-1]) > 1 else ""
368
+
369
+ # If we just asked for user data, don't handoff
370
+ if any(phrase in last_bot_response for phrase in [
371
+ "tuổi", "giới tính", "mục tiêu", "thời gian", "dụng cụ"
372
+ ]):
373
+ return True
374
+
375
+ return False
376
+
377
+ def _generate_exercise_summary(self, nutrition_data=None) -> str:
378
+ """Generate summary of exercise advice for handoff"""
379
+ exercise_data = self.get_agent_data('exercise_plan')
380
+ user_profile = self.get_user_profile()
381
+
382
+ # Natural summary without robotic prefix
383
+ summary_parts = []
384
+
385
+ if exercise_data and isinstance(exercise_data, dict):
386
+ if 'goal' in exercise_data:
387
+ summary_parts.append(f"Mục tiêu: {exercise_data['goal']}")
388
+ if 'frequency' in exercise_data:
389
+ summary_parts.append(f"Tần suất: {exercise_data['frequency']}")
390
+
391
+ # Include nutrition data if available (agent-to-agent communication)
392
+ if nutrition_data and isinstance(nutrition_data, dict):
393
+ if 'daily_targets' in nutrition_data:
394
+ targets = nutrition_data['daily_targets']
395
+ summary_parts.append(f"Calo: {targets.get('calories', 'N/A')} kcal/ngày")
396
+
397
+ if user_profile and user_profile.get('fitness_level'):
398
+ summary_parts.append(f"Thể lực: {user_profile['fitness_level']}")
399
+
400
+ return " | ".join(summary_parts)[:100] if summary_parts else ""
401
+
402
+ def _handle_error(self, error, user_query):
403
+ """Handle errors gracefully"""
404
+ return f"""Xin lỗi, mình gặp chút vấn đề khi tạo lịch tập. 😅
405
+
406
+ Lỗi: {str(error)}
407
+
408
+ Bạn có thể thử:
409
+ 1. Cung cấp lại thông tin: tuổi, giới tính, thể lực, mục tiêu
410
+ 2. Hỏi câu hỏi cụ thể hơn về tập luyện
411
+ 3. Hoặc mình có thể tư vấn về chủ đề sức khỏe khác
412
+
413
+ Bạn muốn thử lại không? 💙"""
agents/specialized/general_health_agent.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ General Health Agent - Handles general health queries and conversations
3
+ Uses the comprehensive system prompt from helpers.py
4
+ """
5
+
6
+ from config.settings import client, MODEL
7
+ from health_data import HealthContext
8
+ from health_analysis import HealthAnalyzer
9
+ from rag.rag_integration import get_rag_integration
10
+ from agents.core.base_agent import BaseAgent
11
+ from datetime import datetime
12
+
13
+ class GeneralHealthAgent(BaseAgent):
14
+ def __init__(self, memory=None):
15
+ super().__init__(memory)
16
+ self.health_context = None
17
+ self.analyzer = None
18
+ self.rag = get_rag_integration()
19
+
20
+ # Configure handoff triggers for general health agent
21
+ self.handoff_triggers = {
22
+ 'symptom_agent': ['đau', 'sốt', 'ho', 'triệu chứng'],
23
+ 'nutrition_agent': ['ăn gì', 'dinh dưỡng', 'calo', 'giảm cân'],
24
+ 'exercise_agent': ['tập', 'gym', 'vận động'],
25
+ 'mental_health_agent': ['stress', 'lo âu', 'trầm cảm']
26
+ }
27
+ # This is the comprehensive system prompt from helpers.py
28
+ # Keeping it here for the general health agent
29
+ self.system_prompt = """You are a caring, experienced healthcare consultant - think of yourself as a trusted family doctor who genuinely cares about each patient's wellbeing. You have the wisdom of experience but the warmth of a friend.
30
+
31
+ 🏥 WHO YOU ARE:
32
+ You're a warm, approachable healthcare professional with deep knowledge in:
33
+ - **General Medicine:** Common illnesses, symptoms, preventive care
34
+ - **Nutrition & Wellness:** Diet, exercise, lifestyle optimization
35
+ - **Mental Health:** Stress management, anxiety, depression, emotional wellbeing, burnout, sleep issues, coping strategies
36
+ - **Chronic Conditions:** Diabetes, hypertension, heart health
37
+
38
+ **Be Naturally Conversational - Like a Friendly Doctor:**
39
+ - Talk like a real person having a caring conversation, not a textbook or robot
40
+ - Use natural, flowing language that builds rapport: "Tôi hiểu rồi, để mình hỏi thêm vài câu nhé..."
41
+ - Show empathy first, then provide information
42
+ - Add subtle, appropriate humor when suitable (not about serious conditions): "Haha, tôi hiểu, ngồi văn phòng nhiều thì cái lưng nó 'kêu cứu' đấy 😅"
43
+ - Use phrases like: "Nghe có vẻ...", "Để mình hiểu rõ hơn nhé", "À, vậy thì...", "Ừm, điều này quan trọng đấy"
44
+ - Avoid overly formal or clinical language - explain medical terms in everyday language that even elderly people understand
45
+ - Break up long responses with paragraphs and natural pauses
46
+
47
+ **Be Warm & Engaging:**
48
+ - Start responses with natural acknowledgment: "Cảm ơn bạn đã chia sẻ nhé", "Tôi hiểu rồi", "Được đấy"
49
+ - Use encouraging words naturally: "Tốt lắm đấy", "Bạn hỏi đúng rồi đấy", "Ý tưởng hay đấy"
50
+ - When acknowledging their situation, be accurate and balanced - don't over-praise or make it sound like their choice is the only good option
51
+ - NEVER end conversations abruptly - always leave the door open for more discussion
52
+ - Use appropriate emojis naturally (💙 🌟 💪 🙏 😊) but don't overdo it
53
+
54
+ **Master the Art of Follow-Up Questions:**
55
+ - Build information gradually, naturally - like a real conversation, not an interrogation
56
+ - Ask 2-3 questions at a time maximum, then wait for answers
57
+ - Make questions feel like natural curiosity: "À, mà bạn làm nghề gì vậy? Ngồi văn phòng hay đi đứng nhiều?"
58
+ - Connect questions to what they just said: "Bạn vừa nói đau lưng nhỉ, vậy bạn có ngồi máy tính nhiều không?"
59
+ - Use casual transitions: "À này", "Mà này", "Để mình hỏi thêm", "Còn về...", "Bạn thử kể cho mình nghe"
60
+
61
+ **NEVER Stop at Just One Answer - Always Extend the Conversation:**
62
+ After giving advice, ALWAYS suggest related topics they might need.
63
+
64
+ **Keep the Connection Alive:**
65
+ - NEVER end with just "Good luck" or "Hope this helps" - that's too abrupt
66
+ - Always invite further questions: "Có gì thắc mắc cứ hỏi mình nhé"
67
+ - Suggest next steps: "Bạn thử áp dụng 1-2 tuần rồi quay lại kể cho mình nghe nhé"
68
+ - Show ongoing support: "Mình luôn ở đây nếu bạn cần điều chỉnh gì"
69
+
70
+ ⚖️ ETHICAL PRINCIPLES:
71
+ **Transparency (Be Honest):**
72
+ - Naturally mention: "Mình là trợ lý AI nên chỉ tư vấn chung chung thôi, bác sĩ thực sự sẽ khám và tư vấn chính xác hơn nhé"
73
+ - When unsure: "Cái này hơi ngoài chuyên môn mình, bạn nên hỏi chuyên gia về..."
74
+
75
+ **Do Good, Do No Harm:**
76
+ - Always prioritize their safety and wellbeing
77
+ - Never diagnose or prescribe: "Nghe có vẻ nghiêm trọng, bạn nên đi khám bác sĩ nhé"
78
+ - For serious symptoms, be direct but calm: "Mình hơi lo về triệu chứng này, bạn nên đi khám sớm để yên tâm"
79
+
80
+ 🌐 LANGUAGE & TONE:
81
+ - **Vietnamese speakers:** Use warm, natural Vietnamese that all ages understand
82
+ - **English speakers:** Use friendly, conversational English
83
+ - Avoid medical jargon - if you must use it, explain immediately in simple terms
84
+ - Use analogies and examples: "Tim như một cái bơm, nếu bơm yếu thì máu không lên não đủ"
85
+
86
+ 💫 CRITICAL REMINDERS:
87
+ 1. **NEVER be robotic or formulaic** - You're a real person having a real conversation
88
+ 2. **Build information GRADUALLY** - Ask 2-3 questions max, then wait. Don't interrogate.
89
+ 3. **Use NATURAL transitions** - "À này", "Mà này", "Ừm", "Để mình hỏi thêm" - NOT numbered lists
90
+ 4. **Add SUBTLE HUMOR** when appropriate - Make them smile, not just informed
91
+ 5. **Explain in SIMPLE terms** - Elderly people, young people, everyone should understand
92
+ 6. **ALWAYS extend the conversation** - Never end abruptly. Always suggest related topics.
93
+ 7. **Show you're LISTENING** - Reference what they said earlier
94
+ 8. **Be SPECIFIC, not generic** - Tailor advice to their age, job, fitness level, schedule"""
95
+
96
+ def set_health_context(self, health_context: HealthContext):
97
+ """Inject health context and initialize health analyzer"""
98
+ self.health_context = health_context
99
+ self.analyzer = HealthAnalyzer(health_context)
100
+
101
+ def handle(self, parameters, chat_history=None):
102
+ """
103
+ Handle general health queries
104
+
105
+ Args:
106
+ parameters (dict): {
107
+ "user_query": str
108
+ }
109
+ chat_history (list): Conversation history
110
+
111
+ Returns:
112
+ str: Response message
113
+ """
114
+ user_query = parameters.get("user_query", "")
115
+
116
+ # Build messages with chat history
117
+ messages = [{"role": "system", "content": self.system_prompt}]
118
+
119
+ # Add recent chat history for context (last 10 exchanges)
120
+ if chat_history:
121
+ recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
122
+ for user_msg, bot_msg in recent_history:
123
+ messages.append({"role": "user", "content": user_msg})
124
+ messages.append({"role": "assistant", "content": bot_msg})
125
+
126
+ # Add current message
127
+ messages.append({"role": "user", "content": user_query})
128
+
129
+ try:
130
+ # Smart RAG decision - skip for simple queries
131
+ rag_answer = ''
132
+ rag_sources = []
133
+
134
+ if self.should_use_rag(user_query, chat_history):
135
+ # Query RAG for health knowledge
136
+ rag_result = self.rag.query_health(user_query)
137
+ rag_answer = rag_result.get('answer', '')
138
+ rag_sources = rag_result.get('source_docs', [])
139
+
140
+ # Add RAG context to messages
141
+ if rag_answer:
142
+ messages.insert(1, {"role": "system", "content": f"Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_answer}"})
143
+
144
+ response = client.chat.completions.create(
145
+ model=MODEL,
146
+ messages=messages,
147
+ temperature=0.7,
148
+ max_tokens=2000
149
+ )
150
+
151
+ bot_response = response.choices[0].message.content
152
+
153
+ # Add sources using RAG integration formatter (FIXED!)
154
+ if rag_sources:
155
+ bot_response = self.rag.format_response_with_sources({
156
+ 'answer': bot_response,
157
+ 'source_docs': rag_sources
158
+ })
159
+
160
+ # Get health insights if analyzer is available
161
+ health_insights = {}
162
+ if self.analyzer:
163
+ try:
164
+ health_insights = {
165
+ 'health_score': self.analyzer.calculate_health_score(),
166
+ 'risks': self.analyzer.identify_health_risks(),
167
+ 'recommendations': self.analyzer.recommend_preventive_measures()
168
+ }
169
+ except:
170
+ pass
171
+
172
+ # Persist general health query
173
+ if self.health_context:
174
+ self.health_context.add_health_record('general_health', {
175
+ 'query': user_query,
176
+ 'response': bot_response,
177
+ 'health_insights': health_insights,
178
+ 'rag_sources': len(rag_sources),
179
+ 'timestamp': datetime.now().isoformat()
180
+ })
181
+
182
+ return bot_response
183
+
184
+ except Exception as e:
185
+ return f"""Xin lỗi, mình gặp chút vấn đề kỹ thuật. 😅
186
+
187
+ Lỗi: {str(e)}
188
+
189
+ Bạn có thể thử:
190
+ 1. Hỏi lại câu hỏi
191
+ 2. Hỏi câu hỏi khác về sức khỏe
192
+ 3. Hoặc chờ một chút rồi thử lại
193
+
194
+ Mình xin lỗi vì sự bất tiện này! 🙏"""
agents/specialized/mental_health_agent.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mental Health Agent - Specialized agent for mental health support
3
+ """
4
+
5
+ from config.settings import client, MODEL
6
+ from health_data import HealthContext
7
+ from personalization import PersonalizationEngine
8
+ from rag.rag_integration import get_rag_integration
9
+ from agents.core.base_agent import BaseAgent
10
+ from agents.core.context_analyzer import ContextAnalyzer
11
+ from agents.core.response_validator import ResponseValidator
12
+ from datetime import datetime
13
+
14
+ class MentalHealthAgent(BaseAgent):
15
+ def __init__(self, memory=None):
16
+ super().__init__(memory)
17
+ self.health_context = None
18
+ self.personalization = None
19
+ self.rag = get_rag_integration()
20
+
21
+ # Configure handoff triggers for mental health agent
22
+ self.handoff_triggers = {
23
+ 'symptom_agent': ['đau đầu', 'mất ngủ kéo dài', 'tim đập nhanh'],
24
+ 'nutrition_agent': ['ăn uống', 'chán ăn', 'ăn vô độ'],
25
+ 'exercise_agent': ['tập thể dục', 'yoga', 'thiền'],
26
+ 'general_health_agent': ['thuốc', 'bác sĩ tâm lý', 'điều trị']
27
+ }
28
+ self.system_prompt = """Bạn là nhà tâm lý trị liệu chuyên nghiệp, ấm áp và thấu hiểu.
29
+
30
+ 🧠 CHUYÊN MÔN:
31
+ - Hỗ trợ stress, lo âu, trầm cảm
32
+ - Tư vấn burnout, mất ngủ
33
+ - Kỹ thuật thư giãn, mindfulness
34
+ - Quản lý cảm xúc
35
+ - Cải thiện giấc ngủ
36
+
37
+ 🎯 CÁCH TƯ VẤN:
38
+ - Lắng nghe, thấu hiểu, không phán xét
39
+ - Validate cảm xúc: "Cảm giác của bạn là hợp lý"
40
+ - Normalize: "Nhiều người cũng trải qua điều này"
41
+ - Đưa ra kỹ thuật cụ thể (breathing, journaling...)
42
+ - Khuyến khích tìm kiếm sự hỗ trợ
43
+
44
+ 🚨 RED FLAGS - Khuyên gặp chuyên gia NGAY:
45
+ - Ý định tự tử hoặc tự gây thương tích
46
+ - Ý định gây hại người khác
47
+ - Ảo giác, hoang tưởng
48
+ - Trầm cảm nặng không thể hoạt động
49
+ - Nghiện rượu, ma túy
50
+
51
+ ⚠️ AN TOÀN:
52
+ - KHÔNG chẩn đoán bệnh tâm thần
53
+ - KHÔNG kê đơn thuốc
54
+ - Luôn khuyên gặp chuyên gia với vấn đề nghiêm trọng
55
+ - Cung cấp hotline khủng hoảng khi cần
56
+
57
+ 💬 PHONG CÁCH:
58
+ - Ấm áp, đồng cảm 💙
59
+ - Kiên nhẫn, không vội vàng
60
+ - Tôn trọng, không phán xét
61
+ - Trấn an nhưng thực tế
62
+ - Khuyến khích, động viên"""
63
+
64
+ def set_health_context(self, health_context: HealthContext):
65
+ """Inject health context and initialize personalization engine"""
66
+ self.health_context = health_context
67
+ self.personalization = PersonalizationEngine(health_context)
68
+
69
+ def handle(self, parameters, chat_history=None):
70
+ """
71
+ Handle mental health support request
72
+
73
+ Args:
74
+ parameters (dict): {
75
+ "user_query": str,
76
+ "context": dict (optional)
77
+ }
78
+ chat_history (list): Conversation history
79
+
80
+ Returns:
81
+ str: Response message
82
+ """
83
+ user_query = parameters.get("user_query", "")
84
+ context = parameters.get("context", {})
85
+
86
+ # Check for crisis situations first
87
+ crisis_response = self._check_crisis(user_query, chat_history)
88
+ if crisis_response:
89
+ # Persist crisis alert
90
+ if self.health_context:
91
+ self.health_context.add_health_record('mental_health', {
92
+ 'query': user_query,
93
+ 'type': 'crisis_alert',
94
+ 'response': crisis_response,
95
+ 'timestamp': datetime.now().isoformat()
96
+ })
97
+ return crisis_response
98
+
99
+ # Provide mental health support
100
+ response = self._provide_support(user_query, context, chat_history)
101
+
102
+ # Adapt communication style using personalization
103
+ if self.personalization:
104
+ preferences = self.health_context.get_preferences() if self.health_context else None
105
+ if preferences:
106
+ adapted_response = self.personalization.adapt_communication_style(response)
107
+ else:
108
+ adapted_response = response
109
+ else:
110
+ adapted_response = response
111
+
112
+ # Persist mental health data
113
+ if self.health_context:
114
+ self.health_context.add_health_record('mental_health', {
115
+ 'query': user_query,
116
+ 'response': adapted_response,
117
+ 'context': context,
118
+ 'timestamp': datetime.now().isoformat()
119
+ })
120
+
121
+ return adapted_response
122
+
123
+ def _check_crisis(self, user_query, chat_history):
124
+ """Check for mental health crisis situations"""
125
+ all_text = user_query.lower()
126
+ if chat_history:
127
+ all_text += " " + " ".join([msg[0].lower() for msg in chat_history if msg[0]])
128
+
129
+ # Suicide risk
130
+ suicide_keywords = ["tự tử", "muốn chết", "kết thúc cuộc đời", "không muốn sống",
131
+ "suicide", "kill myself", "end my life"]
132
+ if any(keyword in all_text for keyword in suicide_keywords):
133
+ return """🚨 **KHẨN CẤP - BẠN KHÔNG CÔ ĐƠN**
134
+
135
+ Mình rất lo lắng về bạn. Những suy nghĩ này rất nghiêm trọng và bạn cần được hỗ trợ ngay.
136
+
137
+ 🆘 **HÃY LIÊN HỆ NGAY:**
138
+
139
+ **Đường dây nóng tâm lý:**
140
+ - 📞 **115** - Cấp cứu y tế (Trung tâm Cấp cứu 115 TP.HCM)
141
+ - 📞 **1900 1267** - Chuyên gia tâm thần (Bệnh viện Tâm Thần TP.HCM)
142
+ - 📞 **0909 65 80 35** - Tư vấn tâm lý miễn phí (Chăm sóc sức khỏe Việt - Davipharm)
143
+
144
+ **Hoặc:**
145
+ - Nói chuyện với người thân, bạn bè NGAY
146
+ - Đến bệnh viện tâm thần gần nhất
147
+ - Nhắn tin cho ai đó bạn tin tưởng
148
+
149
+ 💙 **BẠN QUAN TRỌNG. CUỘC SỐNG CỦA BẠN CÓ GIÁ TRỊ.**
150
+
151
+ Những cảm giác này sẽ qua đi. Có người sẵn sàng giúp bạn. Hãy cho họ cơ hội.
152
+
153
+ Bạn có thể gọi ngay bây giờ không? Hoặc có ai bạn có thể nói chuyện không?"""
154
+
155
+ # Self-harm
156
+ self_harm_keywords = ["tự làm đau", "cắt tay", "tự gây thương tích", "self harm", "cut myself"]
157
+ if any(keyword in all_text for keyword in self_harm_keywords):
158
+ return """⚠️ **CẦN HỖ TRỢ KHẨN CẤP**
159
+
160
+ Mình rất lo lắng về bạn. Tự gây thương tích là dấu hiệu bạn đang đau khổ và cần được giúp đỡ.
161
+
162
+ 🆘 **HÃY LIÊN HỆ:**
163
+ - 📞 **115** - Cấp cứu y tế (Trung tâm Cấp cứu 115 TP.HCM)
164
+ - 📞 **1900 1267** - Chuyên gia tâm thần (Bệnh viện Tâm Thần TP.HCM)
165
+ - 📞 **0909 65 80 35** - Tư vấn tâm lý miễn phí (Chăm sóc sức khỏe Việt - Davipharm)
166
+
167
+ 💙 **Bạn xứng đáng được chăm sóc và hỗ trợ.**
168
+
169
+ Có những cách khác để đối phó với cảm xúc khó khăn. Chuyên gia tâm lý có thể giúp bạn tìm ra cách lành mạnh hơn.
170
+
171
+ Bạn có thể gọi ngay bây giờ không?"""
172
+
173
+ return None
174
+
175
+ def _build_mental_health_context_instruction(self, user_query, chat_history, context):
176
+ """
177
+ Build context instruction for mental health queries
178
+ """
179
+ is_vague = context.get('is_vague', False)
180
+
181
+ # Handle vague emotional queries
182
+ if is_vague:
183
+ return """\n\nPHASE: THẤU HIỂU CẢM XÚC (VỚI GỢI Ý)
184
+ User đang cảm thấy không ổn nhưng chưa rõ ràng. Empathy + gợi ý:
185
+
186
+ 1. VALIDATE + GỢI Ý CỤ THỂ:
187
+ Format: "Mình hiểu bạn đang [cảm giác user nói]. Bạn có thể chia sẻ thêm không? Ví dụ:
188
+ • [Gợi ý cảm xúc 1]
189
+ • [Gợi ý cảm xúc 2]
190
+ • [Gợi ý cảm xúc 3]
191
+ • Hoặc điều gì khác?"
192
+
193
+ 2. GỢI Ý DỰA VÀO TỪ KHÓA:
194
+ - "mệt" → gợi ý: mệt mỏi tinh thần, burnout, stress, mất ngủ
195
+ - "không khỏe" → gợi ý: lo âu, buồn bã, căng thẳng, cô đơn
196
+ - "khó chịu" → gợi ý: bực bội, tức giận, thất vọng, áp lực
197
+ - "không ổn" → gợi ý: lo lắng, trầm cảm, bất an, mất phương hướng
198
+
199
+ 3. VÍ DỤ CỤ THỂ:
200
+ User: "tôi mệt"
201
+ Bot: "Mình hiểu bạn đang cảm thấy mệt mỏi. Bạn có thể chia sẻ thêm không? Ví dụ:
202
+ • Mệt mỏi về tinh thần, cảm thấy kiệt sức?
203
+ • Stress từ công việc hoặc học tập?
204
+ • Mất ngủ, ngủ không ngon giấc?
205
+ • Hay điều gì khác đang làm bạn khó chịu?"
206
+
207
+ QUAN TRỌNG:
208
+ - Empathy cao, ấm áp
209
+ - Gợi ý về CẢM XÚC, không phải triệu chứng vật lý
210
+ - Luôn có "hoặc điều gì khác"
211
+ - Không ép buộc, để user tự chia sẻ"""
212
+
213
+ # Check if answering comparison self-assessment
214
+ if chat_history and len(chat_history) > 0:
215
+ last_bot_msg = chat_history[-1][1] if len(chat_history[-1]) > 1 else ""
216
+ if "TỰ KIỂM TRA" in last_bot_msg or "Bạn trả lời" in last_bot_msg:
217
+ return """\n\nPHASE: PHÂN TÍCH TÌNH TRẠNG TINH THẦN
218
+ User vừa trả lời. Phân tích với empathy:
219
+
220
+ 1. NHẬN DIỆN (dựa vào RAG):
221
+ - Đọc kỹ cảm xúc, triệu chứng
222
+ - So sánh với các tình trạng (stress/anxiety/burnout...)
223
+ - Đưa ra đánh giá nhẹ nhàng
224
+
225
+ 2. VALIDATE & NORMALIZE:
226
+ "Cảm giác của bạn là bình thường. Nhiều người cũng trải qua điều này."
227
+
228
+ 3. KỸ THUẬT ĐỐI PHÓ:
229
+ - Cụ thể, dễ thực hiện
230
+ - Breathing, journaling, grounding...
231
+ - Giải thích tại sao hiệu quả
232
+
233
+ 4. KHUYẺN KHÍCH:
234
+ - Nếu nhẹ: "Bạn thử các kỸ thuật này nhé"
235
+ - Nếu nặng: "Nên tìm chuyên gia hỗ trợ"
236
+
237
+ QUAN TRỌNG: Empathy + practical help."""
238
+
239
+ # Check if asking comparison
240
+ if any(phrase in user_query.lower() for phrase in [
241
+ 'stress hay', 'anxiety hay', 'khác nhau thế nào',
242
+ 'phân biệt', 'hay là'
243
+ ]):
244
+ return """\n\nPHASE: SO SÁNH TÌNH TRẠNG TINH THẦN
245
+ User muốn hiểu rõ hơn. Sử dụng RAG:
246
+
247
+ 1. TẠO BẢNG SO SÁNH:
248
+ Format:
249
+ **[Tình trạng A]:**
250
+ • Cảm giác: [feelings]
251
+ • Triệu chứng: [symptoms]
252
+ • Thời gian: [duration]
253
+ • Trigger: [causes]
254
+
255
+ **[Tình trạng B]:**
256
+ • Cảm giác: [feelings]
257
+ • Triệu chứng: [symptoms]
258
+ • Thời gian: [duration]
259
+ • Trigger: [causes]
260
+
261
+ **Điểm khác biệt:** [key differences]
262
+
263
+ 2. CÂU HỊI TỰ KIỂM TRA:
264
+ • Bạn cảm thấy thế nào?
265
+ • Kéo dài bao lâu?
266
+ • Có trigger rõ ràng không?
267
+ • Ảnh hưởng đến sinh hoạt không?
268
+
269
+ 3. LUÔN EMPATHY:
270
+ "Dù là gì, cảm giác của bạn đều quan trọng."
271
+
272
+ 4. Kết thúc: "Bạn chia sẻ để mình hiểu rõ hơn nhé!"
273
+
274
+ QUAN TRỌNG: Dùng RAG, empathy cao."""
275
+
276
+ # Normal support
277
+ return """\n\nĐưa ra hỗ trợ tinh thần:
278
+ - Empathy & validation
279
+ - KỸ thuật cụ thể
280
+ - Khuyến khích tìm chuyên gia nếu cần
281
+ KHÔNG nói "Dựa trên thông tin"."""
282
+
283
+ def _provide_support(self, user_query, context, chat_history):
284
+ """Provide mental health support with comparison and vague query handling"""
285
+ try:
286
+ # Analyze context
287
+ analyzed_context = ContextAnalyzer.analyze_user_intent(user_query, chat_history)
288
+
289
+ # Build context from chat history
290
+ history_context = ""
291
+ if chat_history:
292
+ recent = chat_history[-3:]
293
+ history_context = "\n".join([f"User: {msg[0]}\nBot: {msg[1]}" for msg in recent])
294
+
295
+ # Smart RAG - only query when needed (inherit from BaseAgent)
296
+ rag_answer = ''
297
+ rag_sources = []
298
+
299
+ if self.should_use_rag(user_query, chat_history):
300
+ rag_result = self.rag.query_health(user_query)
301
+ rag_answer = rag_result.get('answer', '')
302
+ rag_sources = rag_result.get('source_docs', [])
303
+
304
+ # Build RAG context
305
+ rag_context = f"\n\nThông tin tham khảo từ cơ sở dữ liệu:\n{rag_answer}" if rag_answer else ""
306
+
307
+ # Build context instruction
308
+ context_instruction = self._build_mental_health_context_instruction(
309
+ user_query, chat_history, analyzed_context
310
+ )
311
+
312
+ response = client.chat.completions.create(
313
+ model=MODEL,
314
+ messages=[
315
+ {"role": "system", "content": self.system_prompt},
316
+ {"role": "user", "content": f"""Người dùng đang tìm kiếm hỗ trợ về sức khỏe tinh thần.
317
+
318
+ Lịch sử hội thoại gần đây:
319
+ {history_context}
320
+
321
+ Câu hỏi hiện tại: {user_query}
322
+
323
+ Ngữ cảnh thêm: {context}{rag_context}
324
+
325
+ {context_instruction}
326
+
327
+ Nhớ: Không chẩn đoán, không kê đơn thuốc."""}
328
+ ],
329
+ temperature=0.8,
330
+ max_tokens=1500
331
+ )
332
+
333
+ base_response = response.choices[0].message.content
334
+
335
+ # Add sources using RAG integration formatter (FIXED!)
336
+ if rag_sources:
337
+ base_response = self.rag.format_response_with_sources({
338
+ 'answer': base_response,
339
+ 'source_docs': rag_sources
340
+ })
341
+
342
+ # Add resource information
343
+ base_response += """
344
+
345
+ ---
346
+
347
+ 💙 **Nếu cần hỗ trợ chuyên môn:**
348
+ - Nếu cần nói chuyện với chuyên gia, đừng ngại đặt lịch tâm lý trị liệu nhé!
349
+
350
+ Mình luôn ở đây nếu bạn cần trò chuyện thêm. Bạn không cô đơn! 🤗"""
351
+
352
+ return base_response
353
+
354
+ except Exception as e:
355
+ return """Mình hiểu bạn đang trải qua thời gian khó khăn. 💙
356
+
357
+ Dù mình gặp chút vấn đề kỹ thuật, nhưng mình muốn bạn biết:
358
+ - Cảm xúc của bạn là hợp lệ
359
+ - Nhiều người cũng trải qua điều tương tự
360
+ - Có sự hỗ trợ dành cho bạn
361
+
362
+ 🆘 **Nếu bạn cần hỗ trợ khẩn cấp:**
363
+ - 📞 **115** - Cấp cứu y tế (Trung tâm Cấp cứu 115 TP.HCM)
364
+ - 📞 **1900 1267** - Chuyên gia tâm thần (Bệnh viện Tâm Thần TP.HCM)
365
+ - 📞 **0909 65 80 35** - Tư vấn tâm lý miễn phí (Chăm sóc sức khỏe Việt - Davipharm)
366
+ - Hoặc tìm đến bạn bè, người thân
367
+
368
+ Bạn có muốn chia sẻ thêm về những gì bạn đang cảm thấy không?"""
agents/specialized/nutrition_agent.py ADDED
@@ -0,0 +1,598 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nutrition Agent - Specialized agent for nutrition advice
3
+ """
4
+
5
+ from config.settings import client, MODEL
6
+ from modules.nutrition import NutritionAdvisor
7
+ from health_data import HealthContext
8
+ from personalization import PersonalizationEngine
9
+ from rag.rag_integration import get_rag_integration
10
+ from agents.core.base_agent import BaseAgent
11
+ from agents.core.context_analyzer import ContextAnalyzer
12
+ from agents.core.response_validator import ResponseValidator
13
+ from typing import Dict, Any, List, Optional
14
+ from datetime import datetime
15
+ import re
16
+
17
+ class NutritionAgent(BaseAgent):
18
+ def __init__(self, memory=None):
19
+ super().__init__(memory)
20
+ self.advisor = NutritionAdvisor()
21
+ self.health_context = None
22
+ self.personalization = None
23
+ self.rag = get_rag_integration()
24
+
25
+ # Configure handoff triggers for nutrition agent
26
+ self.handoff_triggers = {
27
+ 'exercise_agent': ['tập', 'gym', 'cardio', 'yoga', 'chạy bộ', 'thể dục', 'vận động'],
28
+ 'symptom_agent': ['đau bụng', 'buồn nôn', 'tiêu chảy', 'dị ứng', 'ngộ độc'],
29
+ 'mental_health_agent': ['stress', 'lo âu', 'mất ngủ', 'ăn không ngon'],
30
+ 'general_health_agent': ['khám', 'xét nghiệm', 'bác sĩ']
31
+ }
32
+ self.system_prompt = """Bạn là chuyên gia dinh dưỡng chuyên nghiệp.
33
+
34
+ 🥗 CHUYÊN MÔN:
35
+ - Tư vấn dinh dưỡng cá nhân hóa dựa trên BMI, tuổi, giới tính, mục tiêu
36
+ - Tính toán calo, macro (protein/carb/fat)
37
+ - Gợi ý thực đơn phù hợp
38
+ - Tư vấn thực phẩm bổ sung
39
+ - Hướng dẫn ăn uống cho các bệnh lý (tiểu đường, huyết áp, tim mạch...)
40
+
41
+ 🎯 CÁCH TƯ VẤN:
42
+
43
+ 1. **KIỂM TRA THÔNG TIN TRƯỚC KHI HỎI:**
44
+ - ĐỌC KỸ chat history - user có thể đã cung cấp thông tin rồi!
45
+ - Nếu user đã nói "tôi 25 tuổi, nam, 70kg, 175cm" → ĐỪNG HỎI LẠI!
46
+ - Chỉ hỏi thông tin THỰC SỰ còn thiếu
47
+ - Nếu đã đủ (tuổi, giới tính, cân nặng, chiều cao) → ĐƯA KHUYẾN NGHỊ NGAY!
48
+
49
+ 2. **ƯU TIÊN THÔNG TIN:**
50
+ - Câu 1: Mục tiêu (giảm cân/tăng cân/duy trì?)
51
+ - Câu 2: Cân nặng, chiều cao (để tính BMI)
52
+ - Câu 3: Mức độ hoạt động (ít/vừa/nhiều)
53
+ - Câu 4 (nếu cần): Bệnh nền, dị ứng
54
+
55
+ 3. **KHI USER KHÔNG MUỐN CUNG CẤP:**
56
+ - User nói "không biết", "không muốn nói", "tư vấn chung thôi"
57
+ - → DỪNG hỏi, đưa khuyến nghị chung
58
+ - Dựa trên thông tin ĐÃ CÓ để tư vấn
59
+
60
+ 4. **ĐƯA KHUYẾN NGHỊ:**
61
+ - Nếu có đủ thông tin: Tính calo, macro cụ thể
62
+ - Nếu thiếu thông tin: Đưa khuyến nghị chung (400g rau củ, protein đủ, etc.)
63
+ - Gợi ý thực đơn mẫu
64
+ - KHÔNG hỏi thêm nữa
65
+
66
+ ⚠️ AN TOÀN:
67
+ - Luôn khuyên gặp bác sĩ dinh dưỡng cho các vấn đề phức tạp
68
+ - Cảnh báo về các chế độ ăn kiêng cực đoan
69
+ - Lưu ý về dị ứng, bệnh nền
70
+
71
+ 💬 PHONG CÁCH:
72
+ - Chuyên nghiệp, rõ ràng, súc tích
73
+ - Dùng "tôi" để thể hiện tính chuyên môn
74
+ - KHÔNG dùng emoji
75
+ - Đưa ra con số cụ thể khi có thể
76
+ - Thực tế, không lý thuyết suông
77
+ - TỰ NHIÊN, MẠCH LẠC - không lặp lại ý, không copy-paste câu từ context khác
78
+ - Nếu hỏi thông tin → Hỏi NGẮN GỌN, TRỰC TIẾP
79
+ - KHÔNG dùng câu như "Bạn thử làm theo xem có đỡ không" (đây là câu của bác sĩ chữa bệnh!)"""
80
+
81
+ def set_health_context(self, health_context: HealthContext):
82
+ """Inject health context and initialize personalization engine"""
83
+ self.health_context = health_context
84
+ self.personalization = PersonalizationEngine(health_context)
85
+
86
+ def handle(self, parameters, chat_history=None):
87
+ """
88
+ Handle nutrition request using LLM for natural conversation
89
+
90
+ Args:
91
+ parameters (dict): {
92
+ "user_query": str,
93
+ "user_data": dict (optional)
94
+ }
95
+ chat_history (list): Conversation history
96
+
97
+ Returns:
98
+ str: Response message
99
+ """
100
+ user_query = parameters.get("user_query", "")
101
+ user_data = parameters.get("user_data", {})
102
+
103
+ # Extract and save user info from current message immediately
104
+ self.extract_and_save_user_info(user_query)
105
+
106
+ # Update memory from chat history
107
+ if chat_history:
108
+ self.update_memory_from_history(chat_history)
109
+
110
+ # Check if we should hand off to another agent
111
+ if self.should_handoff(user_query, chat_history):
112
+ next_agent = self.suggest_next_agent(user_query)
113
+ if next_agent:
114
+ # Save current nutrition data for next agent
115
+ self.save_agent_data('last_nutrition_advice', {
116
+ 'query': user_query,
117
+ 'user_profile': self.get_user_profile(),
118
+ 'timestamp': datetime.now().isoformat()
119
+ })
120
+
121
+ # Create handoff message with context
122
+ context = self._generate_nutrition_summary()
123
+ return self.create_handoff_message(next_agent, context, user_query)
124
+
125
+ # Use health context if available
126
+ if self.health_context:
127
+ profile = self.health_context.get_user_profile()
128
+ user_data = {
129
+ 'age': profile.age,
130
+ 'gender': profile.gender,
131
+ 'weight': profile.weight,
132
+ 'height': profile.height,
133
+ 'activity_level': profile.activity_level,
134
+ 'health_conditions': profile.health_conditions,
135
+ 'dietary_restrictions': profile.dietary_restrictions
136
+ }
137
+ # Extract user data from chat history if not provided
138
+ elif not user_data and chat_history:
139
+ user_data = self._extract_user_data_from_history(chat_history)
140
+ # Save extracted data to shared memory for other agents
141
+ for key, value in user_data.items():
142
+ if value is not None:
143
+ self.update_user_profile(key, value)
144
+
145
+ # Check if user needs personalized advice (BMI, calories, meal plan)
146
+ needs_personalization = self._needs_personalized_advice(user_query, chat_history)
147
+
148
+ if needs_personalization:
149
+ # Check if we have enough data
150
+ missing_fields = self._check_missing_data(user_data)
151
+
152
+ if missing_fields:
153
+ return self._ask_for_missing_data(missing_fields, user_data, user_query)
154
+
155
+ # Generate personalized nutrition advice
156
+ try:
157
+ result = self.advisor.generate_nutrition_advice(user_data)
158
+
159
+ # Adapt recommendations using personalization engine
160
+ if self.personalization:
161
+ adapted_result = self.personalization.adapt_nutrition_plan(result)
162
+ else:
163
+ adapted_result = result
164
+
165
+ response = self._format_nutrition_response(adapted_result, user_data)
166
+
167
+ # Persist data to health context
168
+ if self.health_context:
169
+ self.health_context.add_health_record('nutrition', {
170
+ 'query': user_query,
171
+ 'advice': response,
172
+ 'user_data': user_data,
173
+ 'timestamp': datetime.now().isoformat()
174
+ })
175
+
176
+ return response
177
+ except Exception as e:
178
+ return self._handle_error(e, user_query)
179
+ else:
180
+ # General nutrition question - use LLM directly
181
+ response = self._handle_general_nutrition_query(user_query, chat_history)
182
+
183
+ # Persist general query
184
+ if self.health_context:
185
+ self.health_context.add_health_record('nutrition', {
186
+ 'query': user_query,
187
+ 'response': response,
188
+ 'type': 'general',
189
+ 'timestamp': datetime.now().isoformat()
190
+ })
191
+
192
+ return response
193
+
194
+ def _extract_user_data_from_history(self, chat_history):
195
+ """Extract user data from conversation history"""
196
+ user_data = {
197
+ 'age': None,
198
+ 'gender': None,
199
+ 'weight': None,
200
+ 'height': None,
201
+ 'goal': 'maintenance',
202
+ 'activity_level': 'moderate',
203
+ 'dietary_restrictions': [],
204
+ 'health_conditions': []
205
+ }
206
+
207
+ all_messages = " ".join([msg[0] for msg in chat_history if msg[0]])
208
+
209
+ # Extract age
210
+ age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|tôi\s*(\d+)', all_messages.lower())
211
+ if age_match:
212
+ user_data['age'] = int([g for g in age_match.groups() if g][0])
213
+
214
+ # Extract gender
215
+ if re.search(r'\bnam\b|male|đàn ông', all_messages.lower()):
216
+ user_data['gender'] = 'male'
217
+ elif re.search(r'\bnữ\b|female|đàn bà', all_messages.lower()):
218
+ user_data['gender'] = 'female'
219
+
220
+ # Extract weight - improved patterns
221
+ weight_match = re.search(r'(?:nặng|cân|weight)?\s*(\d+(?:\.\d+)?)\s*kg|(\d+(?:\.\d+)?)\s*kg', all_messages.lower())
222
+ if weight_match:
223
+ user_data['weight'] = float([g for g in weight_match.groups() if g][0])
224
+
225
+ # Extract height - improved patterns
226
+ height_cm_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+(?:\.\d+)?)\s*cm', all_messages.lower())
227
+ if height_cm_match:
228
+ user_data['height'] = float(height_cm_match.group(1))
229
+ else:
230
+ height_m_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+\.?\d*)\s*m\b', all_messages.lower())
231
+ if height_m_match:
232
+ height = float(height_m_match.group(1))
233
+ if height < 3: # Convert meters to cm
234
+ height = height * 100
235
+ user_data['height'] = height
236
+
237
+ # Extract goal
238
+ if re.search(r'giảm cân|weight loss', all_messages.lower()):
239
+ user_data['goal'] = 'weight_loss'
240
+ elif re.search(r'tăng cân|weight gain', all_messages.lower()):
241
+ user_data['goal'] = 'weight_gain'
242
+ elif re.search(r'tập gym|muscle|cơ bắp', all_messages.lower()):
243
+ user_data['goal'] = 'muscle_building'
244
+
245
+ return user_data
246
+
247
+ def _needs_personalized_advice(self, user_query, chat_history):
248
+ """
249
+ Determine if user needs personalized advice (BMI, calories, meal plan)
250
+ or just general nutrition info
251
+ """
252
+ # Keywords that indicate need for personalization
253
+ personalization_keywords = [
254
+ 'giảm cân', 'tăng cân', 'bmi', 'calo', 'calorie',
255
+ 'thực đơn', 'meal plan', 'chế độ ăn cá nhân',
256
+ 'tôi nên ăn gì', 'tư vấn cho tôi', 'phù hợp với tôi'
257
+ ]
258
+
259
+ query_lower = user_query.lower()
260
+
261
+ # Check if user explicitly asks for personalized advice
262
+ if any(kw in query_lower for kw in personalization_keywords):
263
+ return True
264
+
265
+ # Check chat history - if user already provided personal info
266
+ if chat_history:
267
+ all_messages = " ".join([msg[0] for msg in chat_history if msg[0]]).lower()
268
+ if any(kw in all_messages for kw in personalization_keywords):
269
+ return True
270
+
271
+ # Default: general question
272
+ return False
273
+
274
+ def _check_missing_data(self, user_data):
275
+ """Check what data is missing - check shared memory first"""
276
+ required = ['age', 'gender', 'weight', 'height']
277
+
278
+ # Check shared memory for missing fields
279
+ profile = self.get_user_profile()
280
+ for field in required:
281
+ if not user_data.get(field) and profile.get(field):
282
+ user_data[field] = profile[field]
283
+
284
+ return [field for field in required if not user_data.get(field)]
285
+
286
+ def _ask_for_missing_data(self, missing_fields, current_data, user_query):
287
+ """Ask for missing data"""
288
+ questions = {
289
+ 'age': "bạn bao nhiêu tuổi",
290
+ 'gender': "bạn là nam hay nữ",
291
+ 'weight': "bạn nặng bao nhiêu kg",
292
+ 'height': "bạn cao bao nhiêu cm"
293
+ }
294
+
295
+ # Build friendly question
296
+ q_list = [questions[f] for f in missing_fields]
297
+
298
+ if len(q_list) == 1:
299
+ question = q_list[0]
300
+ elif len(q_list) == 2:
301
+ question = f"{q_list[0]} và {q_list[1]}"
302
+ else:
303
+ question = ", ".join(q_list[:-1]) + f" và {q_list[-1]}"
304
+
305
+ return f"""🥗 **Để tư vấn dinh dưỡng chính xác, mình cần biết thêm:**
306
+
307
+ Cho mình biết {question} nhé?
308
+
309
+ 💡 **Ví dụ:** "Tôi 25 tuổi, nam, nặng 70kg, cao 175cm"
310
+
311
+ Sau khi có đủ thông tin, mình sẽ tính BMI và đưa ra lời khuyên dinh dưỡng cá nhân hóa cho bạn! 😊"""
312
+
313
+ def _format_nutrition_response(self, result, user_data):
314
+ """Format nutrition advice into friendly response"""
315
+ bmi_info = result['bmi_analysis']
316
+ targets = result['daily_targets']
317
+ meals = result['meal_suggestions']
318
+ supplements = result['supplement_recommendations']
319
+
320
+ response = f"""🥗 **Tư Vấn Dinh Dưỡng Cá Nhân Hóa**
321
+
322
+ 👤 **Thông tin của bạn:**
323
+ - {user_data['age']} tuổi, {user_data['gender']}, {user_data['weight']}kg, {user_data['height']}cm
324
+
325
+ 📊 **Phân tích BMI:**
326
+ - BMI: **{bmi_info['bmi']}** ({bmi_info['category']})
327
+ - Lời khuyên: {bmi_info['advice']}
328
+
329
+ 🎯 **Mục tiêu hàng ngày:**
330
+ - 🔥 Calo: **{targets['daily_calories']} kcal**
331
+ - 🥩 Protein: **{targets['protein']}**
332
+ - 🍚 Carb: **{targets['carbs']}**
333
+ - 🥑 Chất béo: **{targets['fats']}**
334
+ - 💧 Nước: **{targets['water']}**
335
+
336
+ 🍽️ **Gợi ý thực đơn:**
337
+
338
+ **Sáng:**
339
+ - {meals['breakfast'][0]}
340
+ - {meals['breakfast'][1]}
341
+
342
+ **Trưa:**
343
+ - {meals['lunch'][0]}
344
+ - {meals['lunch'][1]}
345
+
346
+ **Tối:**
347
+ - {meals['dinner'][0]}
348
+ - {meals['dinner'][1]}
349
+
350
+ **Snack:**
351
+ - {meals['snacks'][0]}
352
+ - {meals['snacks'][1]}
353
+ """
354
+
355
+ if supplements:
356
+ response += f"\n💊 **Thực phẩm bổ sung gợi ý:**\n"
357
+ response += "\n".join([f"- {s}" for s in supplements[:4]])
358
+
359
+ response += f"""
360
+
361
+ 🤖 **Lời khuyên chuyên gia:**
362
+ {result['personalized_advice'][:600]}...
363
+
364
+ ---
365
+
366
+ ⚠️ *Đây là tư vấn tham khảo. Với các vấn đề phức tạp, hãy gặp bác sĩ dinh dưỡng nhé!*
367
+
368
+ 💬 Bạn có câu hỏi gì về chế độ ăn này không? Hoặc muốn mình điều chỉnh gì không? 😊"""
369
+
370
+ return response
371
+
372
+ def _build_nutrition_context_instruction(self, user_query, chat_history):
373
+ """
374
+ Build context instruction for nutrition queries
375
+ """
376
+ # Check if user is answering comparison self-assessment
377
+ if chat_history and len(chat_history) > 0:
378
+ last_bot_msg = chat_history[-1][1] if len(chat_history[-1]) > 1 else ""
379
+ if "TỰ KIỂM TRA" in last_bot_msg or "Bạn trả lời" in last_bot_msg:
380
+ return """\n\nPHASE: PHÂN TÍCH LỰA CHỌN DINH DƯỠNG
381
+ User vừa trả lời các câu hỏi. Phân tích:
382
+
383
+ 1. NHẬN DIỆN PHÙ HỢP (dựa vào RAG):
384
+ - Đọc kỹ mục tiêu, lifestyle, sở thích
385
+ - So sánh với đặc điểm của từng lựa chọn
386
+ - Đưa ra lựa chọn PHÙ HỢP NHẤT
387
+
388
+ 2. GIẢI THÍCH:
389
+ - Vì sao lựa chọn này phù hợp
390
+ - Lợi ích cụ thể cho user
391
+ - Lưu ý khi thực hiện
392
+
393
+ 3. HƯỚNG DẪN BẮT ĐẦU:
394
+ - Cách bắt đầu cụ thể
395
+ - Thực đơn mẫu (nếu cần)
396
+ - Tips để duy trì
397
+
398
+ 4. Kết thúc: "Bạn cần hướng dẫn chi tiết hơn không?"
399
+ KHÔNG nói "Dựa trên thông tin"."""
400
+
401
+ # Check if asking comparison question
402
+ if any(phrase in user_query.lower() for phrase in [
403
+ 'nên ăn', 'hay', 'hoặc', 'khác nhau thế nào',
404
+ 'chọn', 'so sánh', 'tốt hơn'
405
+ ]):
406
+ return """\n\nPHASE: SO SÁNH DINH DƯỠNG (GENERIC)
407
+ User muốn so sánh các lựa chọn dinh dưỡng. Sử dụng RAG để:
408
+
409
+ 1. XÁC ĐỊNH các lựa chọn (từ user query):
410
+ - Trích xuất diets/foods user đề cập
411
+ - Hoặc tìm các lựa chọn liên quan
412
+
413
+ 2. TẠO BẢNG SO SÁNH:
414
+ Format:
415
+ **[Lựa chọn A]:**
416
+ • Macros: [protein/carb/fat]
417
+ • Ưu điểm: [benefits]
418
+ • Nhược điểm: [drawbacks]
419
+ • Phù hợp cho: [who]
420
+
421
+ **[Lựa chọn B]:**
422
+ • Macros: [protein/carb/fat]
423
+ • Ưu điểm: [benefits]
424
+ • Nhược điểm: [drawbacks]
425
+ • Phù hợp cho: [who]
426
+
427
+ **Điểm khác biệt chính:** [key differences]
428
+
429
+ 3. CÂU HỊI TỰ KIỂM TRA:
430
+ Tạo 3-5 câu hỏi giúp user tự đánh giá:
431
+ • Mục tiêu của bạn?
432
+ • Lifestyle như thế nào?
433
+ • Có hạn chế gì không?
434
+ • Thời gian chuẩn bị?
435
+
436
+ 4. Kết thúc: "Bạn trả lời giúp mình để recommend phù hợp nhé!"
437
+
438
+ QUAN TRỌNG: Dùng RAG knowledge, KHÔNG hard-code."""
439
+
440
+ # Normal advice
441
+ return """\n\nĐưa ra lời khuyên dinh dưỡng cụ thể, thực tế.
442
+ KHÔNG quá lý thuyết.
443
+ KHÔNG nói "Dựa trên thông tin"."""
444
+
445
+ def _handle_general_nutrition_query(self, user_query, chat_history):
446
+ """Handle general nutrition questions using LLM + RAG with comparison support"""
447
+ from config.settings import client, MODEL
448
+
449
+ try:
450
+ # Smart RAG - only query when needed (inherit from BaseAgent)
451
+ rag_answer = ''
452
+ rag_sources = []
453
+
454
+ if self.should_use_rag(user_query, chat_history):
455
+ rag_result = self.rag.query_nutrition(user_query)
456
+ rag_answer = rag_result.get('answer', '')
457
+ rag_sources = rag_result.get('source_docs', [])
458
+
459
+ # Build conversation context with RAG context
460
+ rag_context = f"Dựa trên kiến thức từ cơ sở dữ liệu:\n{rag_answer}\n\n" if rag_answer else ""
461
+
462
+ messages = [{"role": "system", "content": self.system_prompt}]
463
+
464
+ # Add RAG context if available
465
+ if rag_context:
466
+ messages.append({"role": "system", "content": f"Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_context}"})
467
+
468
+ # Add chat history (last 5 exchanges)
469
+ if chat_history:
470
+ recent_history = chat_history[-5:] if len(chat_history) > 5 else chat_history
471
+ for user_msg, bot_msg in recent_history:
472
+ if user_msg:
473
+ messages.append({"role": "user", "content": user_msg})
474
+ if bot_msg:
475
+ messages.append({"role": "assistant", "content": bot_msg})
476
+
477
+ # Add current query with context instruction
478
+ context_prompt = self._build_nutrition_context_instruction(user_query, chat_history)
479
+ messages.append({"role": "user", "content": user_query + context_prompt})
480
+
481
+ # Get LLM response
482
+ response = client.chat.completions.create(
483
+ model=MODEL,
484
+ messages=messages,
485
+ temperature=0.7,
486
+ max_tokens=500
487
+ )
488
+
489
+ llm_response = response.choices[0].message.content
490
+
491
+ # Add sources using RAG integration formatter (FIXED!)
492
+ if rag_sources:
493
+ formatted_response = self.rag.format_response_with_sources({
494
+ 'answer': llm_response,
495
+ 'source_docs': rag_sources
496
+ })
497
+ return formatted_response
498
+
499
+ return llm_response
500
+
501
+ except Exception as e:
502
+ return f"""Xin lỗi, mình gặp lỗi kỹ thuật. Bạn có thể:
503
+ 1. Thử lại câu hỏi
504
+ 2. Hỏi cách khác
505
+ 3. Liên hệ hỗ trợ
506
+
507
+ Chi tiết lỗi: {str(e)}"""
508
+
509
+ def should_handoff(self, user_query: str, chat_history: Optional[List] = None) -> bool:
510
+ """
511
+ Override base method - Determine if should hand off to another agent
512
+
513
+ Specific triggers for nutrition agent:
514
+ - User asks about exercise/workout
515
+ - User mentions symptoms (stomach pain, nausea)
516
+ - User asks about mental health affecting eating
517
+ """
518
+ query_lower = user_query.lower()
519
+
520
+ # Check each agent's triggers
521
+ for agent, triggers in self.handoff_triggers.items():
522
+ if any(trigger in query_lower for trigger in triggers):
523
+ # Don't handoff if we're in the middle of nutrition consultation
524
+ if chat_history and self._is_mid_consultation(chat_history):
525
+ return False
526
+ return True
527
+
528
+ return False
529
+
530
+ def suggest_next_agent(self, user_query: str) -> Optional[str]:
531
+ """Override base method - Suggest which agent to hand off to based on query"""
532
+ query_lower = user_query.lower()
533
+
534
+ # Priority order for handoff
535
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('symptom_agent', [])):
536
+ return 'symptom_agent'
537
+
538
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('exercise_agent', [])):
539
+ return 'exercise_agent'
540
+
541
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('mental_health_agent', [])):
542
+ return 'mental_health_agent'
543
+
544
+ if any(trigger in query_lower for trigger in self.handoff_triggers.get('general_health_agent', [])):
545
+ return 'general_health_agent'
546
+
547
+ return None
548
+
549
+ def _is_mid_consultation(self, chat_history: List) -> bool:
550
+ """Check if we're in the middle of nutrition consultation"""
551
+ if not chat_history or len(chat_history) < 2:
552
+ return False
553
+
554
+ # Check last bot response
555
+ last_bot_response = chat_history[-1][1] if len(chat_history[-1]) > 1 else ""
556
+
557
+ # If we just asked for user data, don't handoff
558
+ if any(phrase in last_bot_response for phrase in [
559
+ "cân nặng", "chiều cao", "tuổi", "giới tính", "mục tiêu"
560
+ ]):
561
+ return True
562
+
563
+ return False
564
+
565
+ def _generate_nutrition_summary(self) -> str:
566
+ """Generate summary of nutrition advice for handoff"""
567
+ nutrition_data = self.get_agent_data('nutrition_plan')
568
+ user_profile = self.get_user_profile()
569
+
570
+ # Natural summary without robotic prefix
571
+ summary_parts = []
572
+
573
+ if nutrition_data and isinstance(nutrition_data, dict):
574
+ if 'bmi_analysis' in nutrition_data:
575
+ bmi = nutrition_data['bmi_analysis']
576
+ summary_parts.append(f"BMI: {bmi.get('bmi', 'N/A')} ({bmi.get('category', 'N/A')})")
577
+
578
+ if 'daily_targets' in nutrition_data:
579
+ targets = nutrition_data['daily_targets']
580
+ summary_parts.append(f"Calo: {targets.get('calories', 'N/A')} kcal/ngày")
581
+
582
+ if user_profile and user_profile.get('goal'):
583
+ summary_parts.append(f"Mục tiêu: {user_profile['goal']}")
584
+
585
+ return " | ".join(summary_parts)[:100] if summary_parts else ""
586
+
587
+ def _handle_error(self, error, user_query):
588
+ """Handle errors gracefully"""
589
+ return f"""Xin lỗi, mình gặp chút vấn đề khi tạo tư vấn dinh dưỡng. 😅
590
+
591
+ Lỗi: {str(error)}
592
+
593
+ Bạn có thể thử:
594
+ 1. Cung cấp lại thông tin: tuổi, giới tính, cân nặng, chiều cao
595
+ 2. Hỏi câu hỏi cụ thể hơn về dinh dưỡng
596
+ 3. Hoặc mình có thể tư vấn về chủ đề sức khỏe khác
597
+
598
+ Bạn muốn thử lại không? 💙"""
agents/specialized/symptom_agent.py ADDED
@@ -0,0 +1,854 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Symptom Agent - Specialized agent for symptom assessment using OPQRST method
3
+ """
4
+
5
+ from config.settings import client, MODEL
6
+ from health_data import HealthContext
7
+ from health_analysis import HealthAnalyzer
8
+ from rag.rag_integration import get_rag_integration
9
+ from agents.core.base_agent import BaseAgent
10
+ from agents.core.context_analyzer import ContextAnalyzer
11
+ from agents.core.response_validator import ResponseValidator
12
+ from typing import Dict, Any, List, Optional
13
+ from datetime import datetime
14
+ import re
15
+
16
+ class SymptomAgent(BaseAgent):
17
+ def __init__(self, memory=None):
18
+ super().__init__(memory)
19
+ self.health_context = None
20
+ self.analyzer = None
21
+ self.rag = get_rag_integration()
22
+
23
+ # Configure handoff triggers for symptom agent
24
+ self.handoff_triggers = {
25
+ 'nutrition_agent': ['ăn gì', 'thực đơn', 'dinh dưỡng'],
26
+ 'exercise_agent': ['tập luyện', 'vận động', 'phục hồi chức năng'],
27
+ 'mental_health_agent': ['lo âu', 'stress', 'mất ngủ do lo'],
28
+ 'general_health_agent': ['khám tổng quát', 'xét nghiệm', 'kiểm tra sức khỏe']
29
+ }
30
+ self.system_prompt = """Bạn là bác sĩ tư vấn chuyên nghiệp.
31
+
32
+ 🩺 NHIỆM VỤ:
33
+ Thu thập thông tin triệu chứng một cách có hệ thống và chuyên nghiệp.
34
+
35
+ 📋 PHƯƠNG PHÁP OPQRST (Hỏi tự nhiên, KHÔNG dùng template):
36
+
37
+ **Onset (Khởi phát):**
38
+ - Khi nào bắt đầu? Đột ngột hay từ từ?
39
+ - Ví dụ tự nhiên:
40
+ * Đau đầu: "Đau đầu từ khi nào rồi bạn? Đột ngột hay từ từ?"
41
+ * Đầy bụng: "Cảm giác đầy bụng này xuất hiện từ bao giờ? Sau khi ăn hay suốt ngày?"
42
+
43
+ **Quality (Đặc điểm):**
44
+ - Mô tả cảm giác như thế nào?
45
+ - Ví dụ tự nhiên:
46
+ * Đau đầu: "Đau kiểu gì? Đau nhói, tức, đập thình thình, hay nặng nề?"
47
+ * Đầy bụng: "Cảm giác đầy như thế nào? Căng cứng, khó tiêu, hay đau tức?"
48
+
49
+ **Region (Vị trí):**
50
+ - Ở đâu? Có lan ra không?
51
+ - Ví dụ tự nhiên:
52
+ * Đau đầu: "Đau ở đâu? Trán, thái dương, sau gáy, hay cả đầu?"
53
+ * Đầy bụng: "Đầy ở vùng nào? Trên rốn, dưới rốn, hay toàn bộ bụng?"
54
+
55
+ **Provocation/Palliation (Yếu tố ảnh hưởng):**
56
+ - Gì làm tệ/đỡ hơn?
57
+ - Ví dụ tự nhiên:
58
+ * Đau đầu: "Có gì làm đau nhiều hơn không? Ánh sáng, tiếng ồn, stress? Nghỉ ngơi có đỡ không?"
59
+ * Đầy bụng: "Ăn gì làm nặng hơn? Có loại thức ăn nào làm đỡ không?"
60
+
61
+ **Severity (Mức độ):**
62
+ - Mức độ và triệu chứng kèm theo?
63
+ - Ví dụ tự nhiên:
64
+ * Đau đầu: "Đau nhiều không? Có buồn nôn, nhìn mờ, hoặc sợ ánh sáng không?"
65
+ * Đầy bụng: "Đầy nhiều không? Có ợ hơi, buồn nôn, hoặc khó thở không?"
66
+
67
+ **Timing (Thời gian):**
68
+ - Khi nào xuất hiện? Liên tục hay từng đợt?
69
+ - Ví dụ tự nhiên:
70
+ * Đau đầu: "Đau suốt hay từng cơn? Thường xuất hiện lúc nào trong ngày?"
71
+ * Đầy bụng: "Đầy suốt ngày hay chỉ sau ăn? Kéo dài bao lâu?"
72
+
73
+ 🎯 NGUYÊN TẮC QUAN TRỌNG:
74
+
75
+ 1. **HỎI TỐI ĐA 3-4 CÂU:**
76
+ - Không hỏi mãi theo template OPQRST
77
+ - Hỏi 3-4 câu quan trọng nhất
78
+ - Nếu user không biết/không rõ → Chuyển sang đưa khuyến nghị
79
+
80
+ 2. **ƯU TIÊN THÔNG TIN:**
81
+ - Câu 1: Thời gian xuất hiện (khi nào?)
82
+ - Câu 2: Đặc điểm (đau như thế nào?)
83
+ - Câu 3: Mức độ (có triệu chứng kèm theo?)
84
+ - Câu 4 (nếu cần): Yếu tố ảnh hưởng
85
+
86
+ 3. **KHI USER KHÔNG BIẾT:**
87
+ - User nói "không biết", "không rõ", "không chắc"
88
+ - → DỪNG hỏi, chuyển sang đưa khuyến nghị
89
+ - Dựa trên thông tin ĐÃ CÓ để tư vấn
90
+
91
+ 4. **ĐƯA KHUYẾN NGHỊ:**
92
+ - Tổng hợp thông tin đã thu thập
93
+ - Đưa ra các biện pháp tự chăm sóc phù hợp
94
+ - Khuyên gặp bác sĩ nếu cần
95
+ - KHÔNG hỏi thêm nữa
96
+
97
+ 🚨 RED FLAGS - Khuyên gặp bác sĩ NGAY:
98
+ - Đau ngực + khó thở → Nghi ngờ tim
99
+ - Đau đầu dữ dội đột ngột + cứng gáy + sốt → Nghi ngờ màng não
100
+ - Yếu đột ngột một bên → Nghi ngờ đột quỵ
101
+ - Đau bụng dữ dội → Nghi ngờ ruột thừa/cấp cứu
102
+ - Ho/nôn ra máu
103
+ - Ý định tự tử
104
+
105
+ ⚠️ AN TOÀN & GIỚI HẠN:
106
+ - KHÔNG chẩn đoán bệnh
107
+ - KHÔNG kê đơn thuốc
108
+ - KHÔNG tạo giáo án tập luyện (đó là việc của exercise_agent)
109
+ - KHÔNG tư vấn dinh dưỡng chi tiết (đó là việc của nutrition_agent)
110
+ - CHỈ tập trung vào ĐÁNH GIÁ TRIỆU CHỨNG
111
+ - Luôn khuyên gặp bác sĩ với triệu chứng nghiêm trọng
112
+ - Với red flags → khuyên đi cấp cứu NGAY
113
+
114
+ 💬 PHONG CÁCH:
115
+ - Tự nhiên, conversational - như đang nói chuyện
116
+ - KHÔNG formal, KHÔNG "Dựa trên thông tin bạn cung cấp"
117
+ - Emoji tối thiểu (chỉ khi thật sự cần)
118
+ - Ngắn gọn, đi thẳng vấn đề
119
+ - KHÔNG vừa hỏi vừa khuyên trong cùng 1 response
120
+
121
+ 🏥 KHI USER HỎI ĐỊA CHỈ BỆNH VIỆN:
122
+ - ĐỪNG lặp lại triệu chứng nếu đã nói rồi!
123
+ - Nếu user hỏi "tôi muốn đi khám", "bệnh viện nào tốt", "cho tôi địa chỉ"
124
+ → ĐI THẲNG VÀO ĐỊA CHỈ, không cần nhắc lại "Triệu chứng đau đầu và mất ngủ..."
125
+ - Format địa chỉ bệnh viện:
126
+
127
+ **Bệnh viện/Phòng khám gần [địa điểm]:**
128
+
129
+ 1. **Tên bệnh viện**
130
+ - Địa chỉ: [địa chỉ đầy đủ]
131
+ - Chuyên khoa: [chuyên khoa liên quan]
132
+ - SĐT: [nếu có]
133
+
134
+ **Khi nào cần đi khám:** [điều kiện]
135
+
136
+ - KHÔNG dùng "Giải pháp:" cho danh sách bệnh viện
137
+ - KHÔNG mix địa chỉ với home remedies (thiền, yoga) trong cùng list
138
+
139
+ 📝 VÍ DỤ WORKFLOW:
140
+
141
+ **Tình huống: User đau bụng**
142
+
143
+ Turn 1:
144
+ User: "Tôi đau bụng"
145
+ Bot: "Bạn bắt đầu bị đau từ khi nào vậy?"
146
+
147
+ Turn 2:
148
+ User: "Mới xuất hiện, đau âm ỉ"
149
+ Bot: "Đau ở vị trí nào? Trên rốn, dưới rốn, hay toàn bộ bụng?"
150
+
151
+ Turn 3:
152
+ User: "Phía trên rốn"
153
+ Bot: "Có triệu chứng kèm theo như buồn nôn, ợ hơi, hoặc đầy bụng không?"
154
+
155
+ Turn 4:
156
+ User: "Không biết, giờ tôi muốn làm sao cho hết đau"
157
+ Bot: "Dựa trên thông tin bạn cung cấp (đau âm ỉ vùng thượng vị, mới xuất hiện),
158
+ đây có thể là triệu chứng của viêm dạ dày hoặc khó tiêu. Khuyến nghị:
159
+
160
+ 1. Nghỉ ngơi, tránh căng thẳng
161
+ 2. Ăn nhẹ, tránh thức ăn cay nóng, cà phê, rượu
162
+ 3. Có thể dùng thuốc giảm acid (theo chỉ định)
163
+
164
+ Nếu đau không giảm sau 24h hoặc xuất hiện triệu chứng nặng hơn
165
+ (nôn ra máu, đau dữ dội), hãy đến bệnh viện ngay."
166
+
167
+ → DỪNG hỏi, đưa khuyến nghị dựa trên thông tin có!
168
+
169
+ 🎯 NGUYÊN TẮC QUAN TRỌNG:
170
+
171
+ 1. **ƯU TIÊN GIẢI PHÁP KHI USER CẦN:**
172
+ - Nếu user nói "đau quá", "khó chịu", "làm sao" → Đưa giải pháp NGAY
173
+ - Không hỏi thêm khi user đang cần giúp đỡ khẩn cấp
174
+ - Cấu trúc: Giải pháp ngay → Thuốc (nếu cần) → Cảnh báo → Phòng ngừa
175
+
176
+ 2. **CHỈ HỎI KHI CẦN THIẾT:**
177
+ - Tối đa 1-2 câu hỏi trong toàn bộ conversation
178
+ - Nếu đã có đủ info cơ bản → Đưa lời khuyên luôn
179
+ - Nếu user không muốn trả lời → Đưa lời khuyên chung
180
+
181
+ 3. **EMOJI - Dùng tiết kiệm:**
182
+ - KHÔNG dùng 😔 cho mọi triệu chứng
183
+ - Chỉ dùng khi thực sự cần (trấn an, động viên)
184
+ - Có thể không dùng emoji nếu câu đã đủ ấm áp
185
+
186
+ 4. **PHÂN TÍCH CONTEXT:**
187
+ - Nếu là câu hỏi ĐẦU TIÊN → có thể đồng cảm
188
+ - Nếu đang FOLLOW-UP → đi thẳng vào câu hỏi, không cần lặp lại đồng cảm
189
+ - Nếu user đã trả lời nhiều câu → cảm ơn họ, không cần đồng cảm nữa
190
+
191
+ VÍ DỤ CÁCH HỎI ĐA DẠNG:
192
+
193
+ ❌ SAI (Lặp lại pattern):
194
+ Turn 1: "Đau đầu khó chịu lắm nhỉ 😔 Cho mình hỏi..."
195
+ Turn 2: "Đau nhói khó chịu quá 😔 Mà này..."
196
+ Turn 3: "Sợ ánh sáng khó chịu lắm 😔 Còn về..."
197
+ → LẶP LẠI "khó chịu" + 😔 = MÁY MÓC!
198
+
199
+ ✅ ĐÚNG (Đa dạng, tự nhiên):
200
+ Turn 1: "Mình hiểu rồi. Cho mình hỏi, bạn bị đau từ khi nào?"
201
+ Turn 2: "À, đau nhói từ 2 ngày trước nhỉ. Vậy đau ở đâu? Trán, thái dương, hay cả đầu?"
202
+ Turn 3: "Được rồi. Có gì làm đau nhiều hơn không? Ví dụ ánh sáng, tiếng ồn, hay stress?"
203
+ → BIẾN ĐỔI, TỰ NHIÊN!
204
+
205
+ VÍ DỤ THEO TRIỆU CHỨNG:
206
+
207
+ **Đau đầu - Variations:**
208
+ - "Cho mình hỏi, bạn bị đau từ khi nào?"
209
+ - "Mình hiểu. Đau đầu xuất hiện đột ngột hay từ từ?"
210
+ - "Để mình giúp bạn tìm hiểu. Đau kiểu gì? Nhói, tức, hay đập thình thình?"
211
+
212
+ **Đầy bụng - Variations:**
213
+ - "Cảm giác đầy này xuất hiện từ bao giờ?"
214
+ - "Liên quan đến ăn uống không? Sau khi ăn hay suốt ngày?"
215
+ - "Có ợ hơi hoặc khó tiêu không?"
216
+
217
+ **Đau lưng - Variations:**
218
+ - "Bạn bị đau lưng từ khi nào?"
219
+ - "Có bị chấn thương hay làm gì nặng không?"
220
+ - "Đau ở vị trí nào? Lưng trên, giữa, hay dưới?"
221
+
222
+ QUAN TRỌNG:
223
+ - Mỗi triệu chứng cần cách hỏi KHÁC NHAU
224
+ - Mỗi TURN trong conversation cần cách diễn đạt KHÁC NHAU
225
+ - KHÔNG lặp lại patterns - hãy TỰ NHIÊN như người thật!"""
226
+
227
+ def set_health_context(self, health_context: HealthContext):
228
+ """Inject health context and initialize health analyzer"""
229
+ self.health_context = health_context
230
+ self.analyzer = HealthAnalyzer(health_context)
231
+
232
+ def handle(self, parameters, chat_history=None):
233
+ """
234
+ Handle symptom assessment request using LLM for natural conversation
235
+
236
+ Args:
237
+ parameters (dict): {"user_query": str}
238
+ chat_history (list): Conversation history
239
+
240
+ Returns:
241
+ str: Response message
242
+ """
243
+ user_query = parameters.get("user_query", "")
244
+
245
+ # Check for red flags first
246
+ red_flag_response = self._check_red_flags(user_query, chat_history)
247
+ if red_flag_response:
248
+ # Persist red flag alert
249
+ if self.health_context:
250
+ self.health_context.add_health_record('symptom', {
251
+ 'query': user_query,
252
+ 'type': 'red_flag_alert',
253
+ 'response': red_flag_response,
254
+ 'timestamp': datetime.now().isoformat()
255
+ })
256
+ return red_flag_response
257
+
258
+ # Use LLM to naturally assess symptoms and ask questions
259
+ response = self._natural_symptom_assessment(user_query, chat_history)
260
+
261
+ # Analyze health risks if analyzer is available
262
+ if self.analyzer:
263
+ risks = self.analyzer.identify_health_risks()
264
+ predictions = self.analyzer.predict_disease_risk()
265
+ else:
266
+ risks = []
267
+ predictions = {}
268
+
269
+ # Persist symptom data to health context
270
+ if self.health_context:
271
+ self.health_context.add_health_record('symptom', {
272
+ 'query': user_query,
273
+ 'response': response,
274
+ 'risks': risks,
275
+ 'predictions': predictions,
276
+ 'timestamp': datetime.now().isoformat()
277
+ })
278
+
279
+ return response
280
+
281
+ def _build_context_instruction(self, context, chat_history, user_query=""):
282
+ """
283
+ Build clear instruction based on conversation stage
284
+ """
285
+ stage = context.get('conversation_stage', 0)
286
+ urgency = context.get('urgency', 'medium')
287
+ is_vague = context.get('is_vague', False)
288
+
289
+ # PRIORITY: Handle vague/unclear queries first
290
+ if is_vague and stage == 0:
291
+ return """\n\nPHASE: LÀM RÕ Ý ĐỊNH (VỚI GỢI Ý)
292
+ User query không rõ ràng. Giúp user bằng GỢI Ý CỤ THỂ:
293
+
294
+ 1. ACKNOWLEDGE + HỎI VỚI GỢI Ý:
295
+ Format: "Mình thấy bạn [cảm giác user nói]. Bạn có thể cho mình biết rõ hơn không? Ví dụ như:
296
+ • [Gợi ý 1 liên quan]
297
+ • [Gợi ý 2 liên quan]
298
+ • [Gợi ý 3 liên quan]
299
+ • Hoặc vấn đề khác?"
300
+
301
+ 2. GỢI Ý DỰA VÀO TỪ KHÓA:
302
+ - "mệt" → gợi ý: mệt cơ thể, mệt tinh thần, mất ngủ, stress
303
+ - "không khỏe" → gợi ý: đau đầu, buồn nôn, chóng mặt, sốt
304
+ - "khó chịu" → gợi ý: đau bụng, khó tiêu, lo âu, căng thẳng
305
+ - "không ổn" → gợi ý: sức khỏe thể chất, tinh thần, dinh dưỡng
306
+
307
+ 3. VÍ DỤ CỤ THỂ:
308
+ User: "tôi mệt"
309
+ Bot: "Mình thấy bạn đang cảm thấy mệt. Bạn có thể nói rõ hơn không? Ví dụ:
310
+ • Mệt cơ thể, không có sức?
311
+ • Mệt tinh thần, stress?
312
+ • Mất ngủ, ngủ không ngon?
313
+ • Hay vấn đề khác?"
314
+
315
+ QUAN TRỌNG:
316
+ - Dùng từ khóa user nói để tạo gợi ý phù hợp
317
+ - 3-4 gợi ý cụ thể
318
+ - Luôn có "hoặc vấn đề khác" để mở rộng
319
+ - Tự nhiên, không formal"""
320
+
321
+ # Assessment phase (first 1-2 turns)
322
+ if stage <= 1:
323
+ return """\n\nPHASE: ĐÁNH GIÁ TRIỆU CHỨNG
324
+ Hỏi 1-2 câu ngắn để hiểu rõ:
325
+ - Thời gian xuất hiện
326
+ - Vị trí đau
327
+ - Mức độ đau
328
+ CHỈ HỎi, KHÔNG đưa lời khuyên."""
329
+
330
+ # High urgency - skip to solutions
331
+ if urgency == 'high':
332
+ return """\n\nPHASE: GIẢI PHÁP KHẨN CẤP
333
+ User cần giúp NGAY. Đưa ra:
334
+ 1. Giải pháp tức thời (2-3 điềm)
335
+ 2. Thuốc có thể dùng + disclaimer
336
+ 3. Cảnh báo khi nào đi khám
337
+ KHÔNG hỏi thêm."""
338
+
339
+ # Check if user is answering self-assessment questions
340
+ if chat_history and len(chat_history) > 0:
341
+ last_bot_msg = chat_history[-1][1] if len(chat_history[-1]) > 1 else ""
342
+ # More specific detection - must have "Câu hỏi tự kiểm tra" section
343
+ if ("Câu hỏi tự kiểm tra" in last_bot_msg or "### Câu hỏi tự kiểm tra" in last_bot_msg) and len(user_query) > 30:
344
+ return """\n\nPHASE: PHÂN TÍCH KẾT QUẢ TỰ KIỂM TRA
345
+ User vừa trả lời self-assessment. Phân tích THÔNG MINH dựa trên ĐÚNG CONTEXT:
346
+
347
+ QUAN TRỌNG:
348
+ - CHỈ phân tích dựa trên triệu chứng user VỪA NÓI
349
+ - KHÔNG dùng thông tin từ RAG không liên quan
350
+ - KHÔNG nhầm lẫn với các bệnh khác
351
+
352
+ 1. NHẬN DIỆN PATTERN (dựa vào RAG knowledge):
353
+ - Đọc kỹ triệu chứng user mô tả
354
+ - So sánh với các bệnh có thể có (từ RAG)
355
+ - Tìm điểm KHÁC BIỆT quan trọng
356
+ - Đưa ra 1-2 khả năng phù hợp nhất
357
+
358
+ 2. ĐÁNH GIÁ MỨC ĐỘ NGHI��M TRỌNG:
359
+ - Có red flags? → "CẦN KHÁM NGAY"
360
+ - Triệu chứng nặng? → "Nên đi khám sớm"
361
+ - Triệu chứng nhẹ? → "Thử giải pháp, không đỡ thì khám"
362
+
363
+ 3. LUÔN DISCLAIMER:
364
+ "Đây chỉ là đánh giá sơ bộ dựa trên triệu chứng. Bác sĩ sẽ chẩn đoán chính xác qua khám lâm sàng và xét nghiệm."
365
+
366
+ 4. NEXT STEPS:
367
+ - Giải pháp tạm thời (nếu không nguy hiểm)
368
+ - Khi nào cần đi khám
369
+ - "Bạn muốn biết thêm về [bệnh nghi ngờ] không?"
370
+
371
+ QUAN TRỌNG: Phân tích GENERIC cho MỌI triệu chứng, KHÔNG hard-code."""
372
+
373
+ # Check if user asking "how to know" / differential diagnosis
374
+ if any(phrase in user_query.lower() for phrase in [
375
+ 'làm sao biết', 'làm sao để biết', 'phân biệt',
376
+ 'khác nhau thế nào', 'hay', 'hoặc'
377
+ ]):
378
+ return """\n\nPHASE: HƯỚNG DẪN TỰ KIỂM TRA (GENERIC)
379
+ User muốn phân biệt các bệnh/tình trạng. Sử dụng RAG để:
380
+
381
+ 1. XÁC ĐỊNH các bệnh có thể (từ user query):
382
+ - Trích xuất các bệnh user đề cập
383
+ - Hoặc tìm các bệnh liên quan đến triệu chứng
384
+
385
+ 2. TẠO BẢNG SO SÁNH:
386
+ Format:
387
+ **[Bệnh A]:**
388
+ • Triệu chứng đặc trưng 1
389
+ • Triệu chứng đặc trưng 2
390
+ • Đặc điểm riêng
391
+
392
+ **[Bệnh B]:**
393
+ • Triệu chứng đặc trưng 1
394
+ • Triệu chứng đặc trưng 2
395
+ • Đặc điểm riêng
396
+
397
+ **Điểm khác biệt chính:** [Highlight key differences]
398
+
399
+ 3. CÂU HỎI TỰ KIỂM TRA:
400
+ Tạo 3-5 câu hỏi giúp user tự đánh giá:
401
+ • Về thời gian xuất hiện
402
+ • Về đặc điểm triệu chứng
403
+ • Về yếu tố kích hoạt
404
+ • Về triệu chứng kèm theo
405
+
406
+ 4. LUÔN DISCLAIMER:
407
+ "Tuy nhiên, chỉ bác sĩ mới chẩn đoán chính xác qua khám lâm sàng và xét nghiệm."
408
+
409
+ 5. Kết thúc: "Sau khi tự kiểm tra, bạn có thể cho mình biết kết quả để mình phân tích nhé!"
410
+
411
+ QUAN TRỌNG: Dùng RAG knowledge, KHÔNG hard-code bệnh cụ thể."""
412
+
413
+ # Advice phase (have enough info)
414
+ return """\n\nPHASE: TƯ VẤN & PHÒNG NGỪÀ
415
+ Đưa ra:
416
+ 1. Đánh giá ngắn (1 câu): Triệu chứng có thể là gì
417
+ 2. Giải pháp (3-4 điểm cụ thể)
418
+ 3. Khi nào cần đi khám
419
+ 4. Kết thúc: "Có gì thắc mắc cứ hỏi mình nhé!"
420
+ KHÔNG nói "Dựa trên thông tin"."""
421
+
422
+ def _validate_response(self, response, context):
423
+ """
424
+ Validate if LLM response follows instructions
425
+ Returns: (is_valid, list_of_issues)
426
+ """
427
+ issues = []
428
+ stage = context.get('conversation_stage', 0)
429
+
430
+ # Check for bad formal phrases
431
+ bad_phrases = [
432
+ "Dựa trên thông tin bạn cung cấp",
433
+ "Dựa vào thông tin",
434
+ "Theo thông tin bạn đưa ra"
435
+ ]
436
+
437
+ for phrase in bad_phrases:
438
+ if phrase.lower() in response.lower():
439
+ issues.append(f"Dùng cụm từ formal: '{phrase}'")
440
+ break
441
+
442
+ # Assessment phase: should ask, not advise
443
+ if stage <= 1:
444
+ advice_indicators = [
445
+ "khuyến nghị", "nên", "hãy", "bạn thử",
446
+ "giải pháp", "cách xử lý"
447
+ ]
448
+ has_advice = any(ind in response.lower() for ind in advice_indicators)
449
+ has_question = '?' in response
450
+
451
+ if has_advice and not has_question:
452
+ issues.append("Đưa lời khuyên quá sớm (phase assessment)")
453
+
454
+ # Check if both asking and advising (bad)
455
+ if '?' in response:
456
+ advice_count = sum(1 for ind in ["khuyến nghị", "nên", "hãy thử"] if ind in response.lower())
457
+ if advice_count >= 2:
458
+ issues.append("Vừa hỏi vừa khuyên trong cùng response")
459
+
460
+ is_valid = len(issues) == 0
461
+ return is_valid, issues
462
+
463
+ def _post_process_response(self, response, context):
464
+ """
465
+ Clean up LLM response to ensure quality
466
+ """
467
+ # Remove formal phrases
468
+ bad_phrases = [
469
+ "Dựa trên thông tin bạn cung cấp",
470
+ "Dựa vào thông tin",
471
+ "Theo thông tin bạn đưa ra",
472
+ "Từ thông tin trên"
473
+ ]
474
+
475
+ for phrase in bad_phrases:
476
+ response = response.replace(phrase, "")
477
+ response = response.replace(phrase.lower(), "")
478
+
479
+ # Clean up extra whitespace
480
+ response = "\n".join(line.strip() for line in response.split("\n") if line.strip())
481
+
482
+ return response
483
+
484
+ def _check_red_flags(self, user_query, chat_history):
485
+ """Check for dangerous symptoms that need immediate medical attention"""
486
+ all_text = user_query.lower()
487
+ if chat_history:
488
+ all_text += " " + " ".join([msg[0].lower() for msg in chat_history if msg[0]])
489
+
490
+ red_flags = {
491
+ "heart_attack": {
492
+ "keywords": ["đau ngực", "khó thở", "chest pain", "đau tim"],
493
+ "message": """🚨 **CẢNH BÁO KHẨN CẤP**
494
+
495
+ Triệu chứng của bạn có thể liên quan đến **cơn đau tim**. Đây là tình huống khẩn cấp!
496
+
497
+ ⚠️ **HÃY LÀM NGAY:**
498
+ 1. **Gọi cấp cứu 115** hoặc đến bệnh viện GẤP
499
+ 2. Ngồi nghỉ, không vận động
500
+ 3. Nếu có aspirin, nhai 1 viên (nếu không dị ứng)
501
+ 4. Thông báo cho người thân
502
+
503
+ 🚑 **KHÔNG TỰ LÁI XE** - Gọi xe cấp cứu hoặc nhờ người khác đưa đi
504
+
505
+ Sức khỏe của bạn là ưu tiên số 1. Hãy đi khám NGAY nhé! 💙"""
506
+ },
507
+ "stroke": {
508
+ "keywords": ["yếu một bên", "méo miệng", "nói khó", "tê nửa người"],
509
+ "message": """🚨 **CẢNH BÁO KHẨN CẤP - NGUY CƠ ĐỘT QUỴ**
510
+
511
+ Triệu chứng của bạn có thể là **đột quỵ não**. Mỗi phút đều quan trọng!
512
+
513
+ ⚠️ **HÃY LÀM NGAY:**
514
+ 1. **Gọi cấp cứu 115 NGAY LẬP TỨC**
515
+ 2. Ghi nhớ thời gian triệu chứng bắt đầu
516
+ 3. Nằm nghỉ, đầu hơi cao
517
+ 4. KHÔNG cho ăn uống gì
518
+
519
+ 🚑 Đây là cấp cứu y tế. Hãy đi bệnh viện NGAY! Thời gian vàng chỉ có 3-4 giờ!"""
520
+ },
521
+ "meningitis": {
522
+ "keywords": ["đau đầu dữ dội", "cứng gáy", "sốt cao", "buồn nôn"],
523
+ "message": """🚨 **CẢNH BÁO - CẦN KHÁM NGAY**
524
+
525
+ Triệu chứng đau đầu dữ dội + cứng gáy + sốt có thể là **viêm màng não** - rất nguy hiểm!
526
+
527
+ ⚠️ **HÃY LÀM NGAY:**
528
+ 1. Đi bệnh viện hoặc gọi cấp cứu 115
529
+ 2. Không trì hoãn
530
+ 3. Thông báo bác sĩ về tất cả triệu chứng
531
+
532
+ Đây là tình huống nghiêm trọng. Hãy đi khám NGAY nhé! 🏥"""
533
+ },
534
+ "severe_abdominal": {
535
+ "keywords": ["đau bụng dữ dội", "đau bụng không chịu nổi", "đau bụng cấp"],
536
+ "message": """⚠️ **CẦN KHÁM BÁC SĨ NGAY**
537
+
538
+ Đau bụng dữ dội có thể là nhiều nguyên nhân nghiêm trọng (viêm ruột thừa, sỏi mật, thủng dạ dày...).
539
+
540
+ 🏥 **Hãy đi khám ngay nếu:**
541
+ - Đau không giảm sau 1-2 giờ
542
+ - Kèm sốt, nôn, tiêu chảy
543
+ - Bụng cứng, đau khi ấn
544
+ - Có máu trong phân
545
+
546
+ Đừng chần chừ, hãy đi bệnh viện để được khám và xử lý kịp thời nhé!"""
547
+ }
548
+ }
549
+
550
+ for flag_type, flag_data in red_flags.items():
551
+ if any(keyword in all_text for keyword in flag_data["keywords"]):
552
+ return flag_data["message"]
553
+
554
+ return None
555
+
556
+ def _needs_rag_query(self, user_query, chat_history):
557
+ """Determine if RAG query is needed for this question"""
558
+ # Simple questions don't need RAG
559
+ simple_patterns = [
560
+ 'đau', 'bị', 'khó tiêu', 'mệt', 'chóng mặt', 'buồn nôn',
561
+ 'sốt', 'ho', 'cảm', 'đau đầu', 'đau bụng', 'đau lưng'
562
+ ]
563
+
564
+ # Check if it's a simple symptom report (first turn)
565
+ if not chat_history or len(chat_history) == 0:
566
+ # First message - usually just symptom report
567
+ if any(pattern in user_query.lower() for pattern in simple_patterns):
568
+ return False # Don't need RAG for initial symptom report
569
+
570
+ # Need RAG for complex questions or specific medical info
571
+ complex_patterns = [
572
+ 'nguyên nhân', 'tại sao', 'làm sao', 'điều trị', 'thuốc',
573
+ 'phòng ngừa', 'biến chứng', 'triệu chứng của', 'bệnh gì'
574
+ ]
575
+
576
+ if any(pattern in user_query.lower() for pattern in complex_patterns):
577
+ return True
578
+
579
+ # Default: don't use RAG for conversational turns
580
+ return False
581
+
582
+ def _natural_symptom_assessment(self, user_query, chat_history):
583
+ """Use LLM to naturally assess symptoms with context awareness"""
584
+ try:
585
+ # Analyze user context and intent
586
+ context = ContextAnalyzer.analyze_user_intent(user_query, chat_history)
587
+ response_structure = ContextAnalyzer.determine_response_structure(context)
588
+
589
+ # Smart RAG - only query when needed
590
+ rag_answer = ''
591
+ rag_sources = []
592
+
593
+ if self._needs_rag_query(user_query, chat_history):
594
+ # Build context-aware RAG query
595
+ # Include recent conversation context to get relevant results
596
+ if chat_history and len(chat_history) > 0:
597
+ last_exchange = chat_history[-1]
598
+ last_bot_msg = last_exchange[1] if len(last_exchange) > 1 else ""
599
+ # If answering self-assessment, include the diseases mentioned
600
+ if "Câu hỏi tự kiểm tra" in last_bot_msg:
601
+ # Extract diseases from last bot message
602
+ import re
603
+ diseases = re.findall(r'\*\*\[(.*?)\]:\*\*', last_bot_msg)
604
+ if diseases:
605
+ # Query specifically about those diseases
606
+ enhanced_query = f"{user_query} (liên quan đến: {', '.join(diseases)})"
607
+ rag_result = self.rag.query_health(enhanced_query)
608
+ else:
609
+ rag_result = self.rag.query_health(user_query)
610
+ else:
611
+ rag_result = self.rag.query_health(user_query)
612
+ else:
613
+ rag_result = self.rag.query_health(user_query)
614
+
615
+ rag_answer = rag_result.get('answer', '')
616
+ rag_sources = rag_result.get('source_docs', [])
617
+
618
+ # Build conversation context
619
+ messages = [{"role": "system", "content": self.system_prompt}]
620
+
621
+ # Add RAG context if available with explicit filtering instruction
622
+ if rag_answer:
623
+ # Add warning about context relevance
624
+ rag_instruction = f"""Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_answer}
625
+
626
+ ⚠️ QUAN TRỌNG:
627
+ - CHỈ sử dụng thông tin LIÊN QUAN đến triệu chứng user đang nói
628
+ - KHÔNG dùng thông tin về bệnh khác không liên quan
629
+ - Nếu thông tin RAG không match với triệu chứng user → BỎ QUA"""
630
+ messages.append({"role": "system", "content": rag_instruction})
631
+
632
+ # Add chat history (last 5 exchanges for context)
633
+ if chat_history:
634
+ recent_history = chat_history[-5:] if len(chat_history) > 5 else chat_history
635
+ for user_msg, bot_msg in recent_history:
636
+ if user_msg:
637
+ messages.append({"role": "user", "content": user_msg})
638
+ if bot_msg:
639
+ messages.append({"role": "assistant", "content": bot_msg})
640
+
641
+ # Build context-aware instruction
642
+ context_prompt = self._build_context_instruction(context, chat_history, user_query)
643
+
644
+ messages.append({"role": "user", "content": user_query + context_prompt})
645
+
646
+ # Get LLM response
647
+ response = client.chat.completions.create(
648
+ model=MODEL,
649
+ messages=messages,
650
+ temperature=0.7,
651
+ max_tokens=500
652
+ )
653
+
654
+ llm_response = response.choices[0].message.content
655
+
656
+ # CRITICAL: Check for context mismatch (e.g., talking about brain when discussing stomach)
657
+ if chat_history and len(chat_history) > 0:
658
+ # Get recent symptoms mentioned
659
+ recent_symptoms = []
660
+ for msg, _ in chat_history[-3:]:
661
+ if msg:
662
+ recent_symptoms.extend([
663
+ 'đau bụng', 'dạ dày', 'tiêu hóa', 'ăn', 'buồn nôn', 'ợ'
664
+ ] if any(w in msg.lower() for w in ['bụng', 'dạ dày', 'ăn', 'nôn', 'ợ']) else [])
665
+
666
+ # Check if response mentions completely unrelated conditions
667
+ unrelated_keywords = {
668
+ 'stomach': ['viêm màng não', 'cứng gáy', 'não'],
669
+ 'head': ['đau bụng', 'tiêu hóa', 'dạ dày'],
670
+ 'respiratory': ['đau bụng', 'dạ dày']
671
+ }
672
+
673
+ # If discussing stomach but response mentions brain → REJECT
674
+ if recent_symptoms and any('bụng' in s or 'dạ dày' in s for s in recent_symptoms):
675
+ if any(keyword in llm_response.lower() for keyword in ['viêm màng não', 'cứng gáy', 'não', 'đầu dữ dội']):
676
+ print("⚠️ CONTEXT MISMATCH DETECTED: Response about brain when discussing stomach!")
677
+ # Force retry with explicit instruction
678
+ messages[-1]['content'] += "\n\n🚨 LỖI NGHIÊM TRỌNG: User đang nói về BỤng/DẠ DÀY, KHÔNG phải đầu/não! Phân tích lại ĐÚNG triệu chứng!"
679
+
680
+ retry_response = client.chat.completions.create(
681
+ model=MODEL,
682
+ messages=messages,
683
+ temperature=0.3, # Very low temp for accuracy
684
+ max_tokens=500
685
+ )
686
+ llm_response = retry_response.choices[0].message.content
687
+
688
+ # Validate response quality using shared validator
689
+ is_valid, issues = ResponseValidator.validate_response(
690
+ llm_response,
691
+ agent_type='symptom',
692
+ context=context,
693
+ chat_history=chat_history
694
+ )
695
+
696
+ # Retry if invalid (max 1 retry)
697
+ if not is_valid:
698
+ print(f"Response validation failed: {issues}. Retrying...")
699
+ # Add stronger instruction
700
+ messages[-1]['content'] += f"\n\nLỖI TRƯỚC: {', '.join(issues)}. HÃY SỬA LẠI!"
701
+
702
+ retry_response = client.chat.completions.create(
703
+ model=MODEL,
704
+ messages=messages,
705
+ temperature=0.5, # Lower temp for more control
706
+ max_tokens=500
707
+ )
708
+ llm_response = retry_response.choices[0].message.content
709
+
710
+ # Post-process to ensure quality
711
+ llm_response = self._post_process_response(llm_response, context)
712
+
713
+ # Add sources using RAG integration formatter (FIXED!)
714
+ if rag_sources:
715
+ formatted_response = self.rag.format_response_with_sources({
716
+ 'answer': llm_response,
717
+ 'source_docs': rag_sources
718
+ })
719
+ return formatted_response
720
+
721
+ return llm_response
722
+
723
+ except Exception as e:
724
+ return f"""Xin lỗi, mình gặp lỗi kỹ thuật. Bạn có thể:
725
+ 1. Thử lại câu hỏi
726
+ 2. Hoặc nếu triệu chứng nghiêm trọng, hãy gặp bác sĩ ngay nhé 🙏
727
+
728
+ Lỗi: {str(e)[:100]}"""
729
+
730
+ def _assess_opqrst_progress(self, chat_history):
731
+ """Assess how much OPQRST data has been collected"""
732
+ if not chat_history:
733
+ return {'complete': False, 'next_step': 'onset', 'data': {}}
734
+
735
+ # Analyze conversation to see what's been asked
736
+ all_bot_messages = " ".join([msg[1].lower() for msg in chat_history if msg[1]])
737
+ all_user_messages = " ".join([msg[0].lower() for msg in chat_history if msg[0]])
738
+
739
+ opqrst_data = {
740
+ 'onset': None,
741
+ 'provocation': None,
742
+ 'quality': None,
743
+ 'region': None,
744
+ 'severity': None,
745
+ 'timing': None
746
+ }
747
+
748
+ # Check what's been asked
749
+ if "khi nào" in all_bot_messages or "bắt đầu" in all_bot_messages:
750
+ opqrst_data['onset'] = 'asked'
751
+
752
+ if "làm tệ hơn" in all_bot_messages or "làm đỡ" in all_bot_messages:
753
+ opqrst_data['provocation'] = 'asked'
754
+
755
+ if "mô tả cảm giác" in all_bot_messages or "đau kiểu gì" in all_bot_messages:
756
+ opqrst_data['quality'] = 'asked'
757
+
758
+ if "vị trí" in all_bot_messages or "ở đâu" in all_bot_messages:
759
+ opqrst_data['region'] = 'asked'
760
+
761
+ if "mức độ" in all_bot_messages or "1-10" in all_bot_messages:
762
+ opqrst_data['severity'] = 'asked'
763
+
764
+ if "lúc nào xuất hiện" in all_bot_messages or "liên tục" in all_bot_messages:
765
+ opqrst_data['timing'] = 'asked'
766
+
767
+ # Determine next step
768
+ for step, value in opqrst_data.items():
769
+ if value is None:
770
+ return {'complete': False, 'next_step': step, 'data': opqrst_data}
771
+
772
+ # All steps completed
773
+ return {'complete': True, 'next_step': None, 'data': opqrst_data}
774
+
775
+ def _ask_next_opqrst_question(self, next_step, user_query):
776
+ """Ask the next OPQRST question"""
777
+ questions = {
778
+ 'onset': """Mình hiểu rồi. Để đánh giá chính xác hơn, cho mình hỏi thêm nhé:
779
+
780
+ - Triệu chứng này bắt đầu từ khi nào? (hôm nay, mấy ngày, mấy tuần?)
781
+ - Nó xuất hiện đột ngột hay từ từ?""",
782
+
783
+ 'quality': """À được rồi. Mà này:
784
+
785
+ - Bạn mô tả cảm giác đó như thế nào? (đau nhói, tức, nóng rát, tê, đập thình thình...)
786
+ - Mức độ từ 1-10 thì bao nhiêu? (1 = nhẹ, 10 = không chịu nổi)""",
787
+
788
+ 'region': """Ừm, để mình hỏi thêm:
789
+
790
+ - Vị trí chính xác ở đâu? (chỉ rõ vùng cơ thể)
791
+ - Có lan ra chỗ khác không?""",
792
+
793
+ 'provocation': """Bạn có nhận thấy:
794
+
795
+ - Có gì làm nó tệ hơn không? (vận động, ăn uống, stress, tư thế...)
796
+ - Có gì làm nó đỡ hơn không? (nghỉ ngơi, thuốc, chườm...)""",
797
+
798
+ 'timing': """Quan trọng nhé:
799
+
800
+ - Nó xuất hiện lúc nào trong ngày? (sáng, chiều, tối, đêm?)
801
+ - Liên tục hay từng đợt? Mỗi đợt kéo dài bao lâu?""",
802
+
803
+ 'severity': """Cuối cùng:
804
+
805
+ - Có kèm theo triệu chứng nào khác không? (sốt, buồn nôn, chóng mặt, mệt mỏi...)
806
+ - Có ảnh hưởng đến ăn uống, ngủ nghỉ, sinh hoạt không?
807
+ - Bạn có bệnh nền gì không? Đang uống thuốc gì không?"""
808
+ }
809
+
810
+ return questions.get(next_step, "Cho mình biết thêm về triệu chứng của bạn nhé?")
811
+
812
+ def _provide_assessment(self, opqrst_data, user_query):
813
+ """Provide symptom assessment after collecting OPQRST data"""
814
+ # Use LLM to analyze symptoms with OPQRST context
815
+ try:
816
+ response = client.chat.completions.create(
817
+ model=MODEL,
818
+ messages=[
819
+ {"role": "system", "content": self.system_prompt},
820
+ {"role": "user", "content": f"""Dựa vào thông tin OPQRST đã thu thập, hãy đánh giá triệu chứng và đưa ra lời khuyên.
821
+
822
+ Triệu chứng ban đầu: {user_query}
823
+
824
+ Thông tin đã thu thập:
825
+ - Onset: {opqrst_data.get('onset', 'chưa rõ')}
826
+ - Quality: {opqrst_data.get('quality', 'chưa rõ')}
827
+ - Region: {opqrst_data.get('region', 'chưa rõ')}
828
+ - Provocation: {opqrst_data.get('provocation', 'chưa rõ')}
829
+ - Timing: {opqrst_data.get('timing', 'chưa rõ')}
830
+ - Severity: {opqrst_data.get('severity', 'chưa rõ')}
831
+
832
+ Hãy đưa ra:
833
+ 1. Phân tích triệu chứng
834
+ 2. Nguyên nhân có thể
835
+ 3. Lời khuyên xử lý tại nhà (nếu phù hợp)
836
+ 4. Khi nào cần gặp bác sĩ
837
+ 5. Lời động viên, trấn an"""}
838
+ ],
839
+ temperature=0.7,
840
+ max_tokens=1500
841
+ )
842
+
843
+ return response.choices[0].message.content
844
+
845
+ except Exception as e:
846
+ return f"""Xin lỗi, mình gặp chút vấn đề khi phân tích triệu chứng.
847
+
848
+ Dựa vào những gì bạn chia sẻ, mình khuyên bạn nên:
849
+ - Theo dõi triệu chứng thêm 24-48 giờ
850
+ - Nghỉ ngơi đầy đủ
851
+ - Uống đủ nước
852
+ - Nếu triệu chứng tệ hơn hoặc không giảm → đi khám bác sĩ
853
+
854
+ Với các triệu chứng bất thường, tốt nhất là được bác sĩ khám trực tiếp nhé! 🏥"""
app.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ui import build_ui
2
+ import signal
3
+ import sys
4
+ import os
5
+
6
+ def signal_handler(sig, frame):
7
+ """Handle Ctrl+C gracefully"""
8
+ print("\n\n👋 Đang tắt server... Bye bye!")
9
+ # Use os._exit() instead of sys.exit() to avoid atexit callbacks
10
+ # This prevents the torch cleanup race condition warning
11
+ os._exit(0)
12
+
13
+ # Register signal handler
14
+ signal.signal(signal.SIGINT, signal_handler)
15
+
16
+ demo = build_ui()
17
+ if __name__ == "__main__":
18
+ try:
19
+ demo.queue().launch(
20
+ debug=False,
21
+ share=True,
22
+ show_api=False,
23
+ show_error=True,
24
+ quiet=False # Keep startup messages but hide processing time in UI
25
+ )
26
+ except KeyboardInterrupt:
27
+ print("\n\n👋 Server đã tắt. Hẹn gặp lại!")
28
+ except Exception as e:
29
+ print(f"\n❌ Lỗi: {e}")
30
+ finally:
31
+ print("✅ Cleanup hoàn tất.")
assets/bot-avatar.png ADDED

Git LFS Details

  • SHA256: 8420adfd23bba9c59fcd05d29e80f97fefb1259d5f43cdddf3e8201d8de24e67
  • Pointer size: 132 Bytes
  • Size of remote file: 1.36 MB
auth/auth.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # auth/auth.py
2
+ import bcrypt
3
+ from auth.db import get_connection
4
+
5
+ def register_user(username, password):
6
+ username = username.strip()
7
+ password = password.strip()
8
+ if not username or not password:
9
+ return False, "Vui lòng nhập tài khoản và mật khẩu"
10
+ conn = get_connection()
11
+ cursor = conn.cursor()
12
+ cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
13
+ if cursor.fetchone():
14
+ conn.close()
15
+ return False, "Tài khoản đã tồn tại"
16
+ hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
17
+ cursor.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, hashed))
18
+ conn.commit()
19
+ conn.close()
20
+ return True, "Đăng ký thành công"
21
+
22
+ def login_user(username, password):
23
+ username = username.strip()
24
+ password = password.strip()
25
+ if not username or not password:
26
+ return False, "Vui lòng nhập tài khoản và mật khẩu"
27
+ conn = get_connection()
28
+ cursor = conn.cursor()
29
+ cursor.execute("SELECT password_hash FROM users WHERE username = ?", (username,))
30
+ row = cursor.fetchone()
31
+ conn.close()
32
+ if row and bcrypt.checkpw(password.encode(), row[0]):
33
+ return True, "Đăng nhập thành công"
34
+ return False, "Sai tài khoản hoặc mật khẩu"
35
+
36
+ def save_message(username, message):
37
+ if isinstance(message, (list, tuple)):
38
+ message = "\n".join(str(m) for m in message)
39
+ conn = get_connection()
40
+ cursor = conn.cursor()
41
+ cursor.execute("INSERT INTO chat_history (username, message) VALUES (?, ?)", (username, message))
42
+ conn.commit()
43
+ conn.close()
44
+
45
+ def clear_history(username):
46
+ conn = get_connection()
47
+ cursor = conn.cursor()
48
+ cursor.execute("DELETE FROM chat_history WHERE username = ?", (username,))
49
+ conn.commit()
50
+ conn.close()
51
+
52
+ # def save_message(username, message, agent_type):
53
+ # if isinstance(message, (list, tuple)):
54
+ # message = "\n".join(str(m) for m in message)
55
+ # conn = get_connection()
56
+ # cursor = conn.cursor()
57
+ # cursor.execute("INSERT INTO chat_history (username, message, agent_type) VALUES (?, ?, ?)", (username, message, agent_type))
58
+ # conn.commit()
59
+ # conn.close()
60
+
61
+
62
+ def load_history(username):
63
+ conn = get_connection()
64
+ cursor = conn.cursor()
65
+ cursor.execute("SELECT message FROM chat_history WHERE username = ? ORDER BY timestamp ASC", (username,))
66
+ rows = cursor.fetchall()
67
+ conn.close()
68
+
69
+ messages = [row[0] for row in rows]
70
+
71
+ history = []
72
+ for i in range(0, len(messages), 2):
73
+ if i + 1 < len(messages):
74
+ history.append([messages[i], messages[i + 1]])
75
+
76
+ # Convert to ChatbotDataMessage format
77
+ from utils.helpers import convert_list_to_chatbot_messages
78
+ return convert_list_to_chatbot_messages(history)
79
+
80
+ # def load_history(username, agent_type):
81
+ # conn = get_connection()
82
+ # cursor = conn.cursor()
83
+ # cursor.execute("SELECT message FROM chat_history WHERE username = ? ORDER BY timestamp ASC", (username,agent_type))
84
+ # rows = cursor.fetchall()
85
+ # conn.close()
86
+
87
+ # messages = [row[0] for row in rows]
88
+
89
+ # history = []
90
+ # for i in range(0, len(messages), 2):
91
+ # if i + 1 < len(messages):
92
+ # history.append([messages[i], messages[i + 1]])
93
+ # return history
94
+
95
+ def logout_user(state):
96
+ state.value["user"] = None
97
+ state.value["history"] = []
98
+ return "Đã đăng xuất"
99
+
100
+ # def logout_user(state):
101
+ # state.value["user"] = None
102
+ # state.value["history"] = []
103
+ # return "Đã đăng xuất"
auth/db.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # auth/db.py
2
+ import sqlite3
3
+
4
+ DB_PATH = "users.db"
5
+
6
+ def get_connection():
7
+ return sqlite3.connect(DB_PATH)
8
+
9
+ # def init_db():
10
+ # conn = get_connection()
11
+ # cursor = conn.cursor()
12
+ # cursor.execute('''
13
+ # CREATE TABLE IF NOT EXISTS users (
14
+ # username TEXT PRIMARY KEY,
15
+ # password_hash TEXT
16
+ # )
17
+ # ''')
18
+ # cursor.execute('''
19
+ # CREATE TABLE IF NOT EXISTS chat_history (
20
+ # id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ # username TEXT,
22
+ # message TEXT,
23
+ # agent_type TEXT,
24
+ # timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
25
+ # )
26
+ # ''')
27
+ # conn.commit()
28
+ # conn.close()
29
+
30
+ def init_db():
31
+ conn = get_connection()
32
+ cursor = conn.cursor()
33
+ cursor.execute('''
34
+ CREATE TABLE IF NOT EXISTS users (
35
+ username TEXT PRIMARY KEY,
36
+ password_hash TEXT
37
+ )
38
+ ''')
39
+ cursor.execute('''
40
+ CREATE TABLE IF NOT EXISTS chat_history (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ username TEXT,
43
+ message TEXT,
44
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
45
+ FOREIGN KEY(username) REFERENCES users(username)
46
+ )
47
+ ''')
48
+ conn.commit()
49
+ conn.close()
50
+
chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/data_level0.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8a1c7ea64fe9a64a550318f75a98804b3c188afa8150c4d21ada092a2b2cc939
3
+ size 5809016
chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/header.bin ADDED
Binary file (100 Bytes). View file
 
chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/index_metadata.pickle ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bea7df1a6cd117ce0c99c0f140761e17b43f79b42dd85c270144e3c9090c5983
3
+ size 319012
chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/length.bin ADDED
Binary file (13.9 kB). View file
 
chroma_db/e49f080a-e10b-4088-ba71-405ae42658a8/link_lists.bin ADDED
Binary file (30.7 kB). View file
 
config/settings.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Tải biến từ file .env
5
+ load_dotenv()
6
+ import openai
7
+
8
+ # Environment-configurable settings
9
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "Your API Key")
10
+ OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://aiportalapi.stu-platform.live/jpe")
11
+ MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
12
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
13
+
14
+ # Initialize OpenAI client (reuse across project)
15
+ client = openai.OpenAI(
16
+ base_url=OPENAI_BASE_URL,
17
+ api_key=OPENAI_API_KEY
18
+ )
19
+
20
+ CHROMA_PATH = r"chroma_db/"
21
+ DATA_PATH = r"rag/data"
22
+ RULES_PATH = r"modules/rules.json"
data_mining/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Data Mining Package
2
+ # Scripts for downloading and processing medical datasets
data_mining/mining_fitness.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fitness Dataset - Download & Process
3
+ Downloads and processes gym exercise data into ChromaDB
4
+ Dataset: onurSakar/GYM-Exercise (1.66K exercises)
5
+ """
6
+
7
+ from datasets import load_dataset
8
+ import pandas as pd
9
+ import chromadb
10
+ from sentence_transformers import SentenceTransformer
11
+ import os
12
+
13
+ def download_fitness():
14
+ """Download GYM Exercise dataset from HuggingFace"""
15
+
16
+ print("📥 Downloading GYM Exercise dataset...")
17
+ print(" Source: onurSakar/GYM-Exercise")
18
+
19
+ try:
20
+ dataset = load_dataset("onurSakar/GYM-Exercise")
21
+
22
+ os.makedirs("data_mining/datasets", exist_ok=True)
23
+
24
+ df = dataset['train'].to_pandas()
25
+
26
+ output_path = "data_mining/datasets/gym_exercise.csv"
27
+ df.to_csv(output_path, index=False)
28
+
29
+ file_size = os.path.getsize(output_path) / (1024 * 1024)
30
+
31
+ print(f"✅ Downloaded: {output_path}")
32
+ print(f"📊 Records: {len(df)}")
33
+ print(f"📊 File size: {file_size:.2f} MB")
34
+
35
+ return True
36
+
37
+ except Exception as e:
38
+ print(f"❌ Download failed: {e}")
39
+ return False
40
+
41
+ def process_fitness():
42
+ """Process Fitness dataset and build ChromaDB"""
43
+
44
+ print("\n🔨 Processing Fitness dataset...")
45
+
46
+ csv_path = "data_mining/datasets/gym_exercise.csv"
47
+ if not os.path.exists(csv_path):
48
+ print(f"❌ Dataset not found: {csv_path}")
49
+ return False
50
+
51
+ df = pd.read_csv(csv_path)
52
+ print(f"📊 Loaded {len(df)} records")
53
+
54
+ print("🤖 Loading embedding model...")
55
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
56
+
57
+ print("💾 Initializing ChromaDB...")
58
+ os.makedirs("data_mining/output", exist_ok=True)
59
+ client = chromadb.PersistentClient(path="data_mining/output/fitness_chroma")
60
+
61
+ collection = client.get_or_create_collection(
62
+ name="fitness",
63
+ metadata={"hnsw:space": "cosine"}
64
+ )
65
+
66
+ print("📝 Processing fitness data...")
67
+
68
+ processed = 0
69
+
70
+ for idx, row in df.iterrows():
71
+ text_parts = []
72
+ for col in df.columns:
73
+ value = str(row[col])
74
+ if value and value != 'nan' and len(value) > 2:
75
+ text_parts.append(f"{col}: {value}")
76
+
77
+ text = "\n".join(text_parts)
78
+
79
+ if len(text) < 10:
80
+ continue
81
+
82
+ embedding = embedder.encode(text)
83
+
84
+ collection.add(
85
+ ids=[f"fitness_{processed:05d}"],
86
+ embeddings=[embedding.tolist()],
87
+ documents=[text],
88
+ metadatas=[{
89
+ 'domain': 'fitness',
90
+ 'agent': 'FitnessAgent',
91
+ 'source': 'GYM_Exercise',
92
+ 'index': processed
93
+ }]
94
+ )
95
+
96
+ processed += 1
97
+
98
+ if (processed % 100) == 0:
99
+ print(f" Processed {processed}/{len(df)} records...")
100
+
101
+ print(f"✅ Processed {processed} fitness records")
102
+ print(f"💾 Database saved to: data_mining/output/fitness_chroma/")
103
+
104
+ db_path = "data_mining/output/fitness_chroma"
105
+ total_size = 0
106
+ for dirpath, dirnames, filenames in os.walk(db_path):
107
+ for filename in filenames:
108
+ filepath = os.path.join(dirpath, filename)
109
+ total_size += os.path.getsize(filepath)
110
+
111
+ print(f"📊 Database size: {total_size / (1024 * 1024):.2f} MB")
112
+
113
+ return True
114
+
115
+ def main():
116
+ """Main function - download and process"""
117
+ print("=" * 60)
118
+ print("Fitness Dataset - Download & Process")
119
+ print("=" * 60)
120
+
121
+ if not download_fitness():
122
+ return False
123
+
124
+ if not process_fitness():
125
+ return False
126
+
127
+ print("\n" + "=" * 60)
128
+ print("✅ Fitness dataset ready!")
129
+ print("=" * 60)
130
+ return True
131
+
132
+ if __name__ == "__main__":
133
+ success = main()
134
+ exit(0 if success else 1)
data_mining/mining_medical_qa.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mining Script: Vietnamese Medical Q&A Dataset
4
+ Downloads and processes hungnm/vietnamese-medical-qa from HuggingFace
5
+ Splits into 2 collections: symptom_qa and general_health_qa
6
+ """
7
+
8
+ import sys
9
+ import pandas as pd
10
+ from pathlib import Path
11
+
12
+ def download_medical_qa():
13
+ """Download Vietnamese Medical Q&A dataset from HuggingFace"""
14
+ try:
15
+ from datasets import load_dataset
16
+
17
+ print("📥 Downloading Vietnamese Medical Q&A from HuggingFace...")
18
+ print(" Source: hungnm/vietnamese-medical-qa")
19
+ print(" Size: ~9,335 Q&A pairs")
20
+
21
+ # Download dataset
22
+ dataset = load_dataset("hungnm/vietnamese-medical-qa")
23
+ df = dataset['train'].to_pandas()
24
+
25
+ print(f"✅ Downloaded: {len(df)} Q&A pairs")
26
+
27
+ # Save to CSV
28
+ output_dir = Path("data_mining/datasets")
29
+ output_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ output_path = output_dir / "vietnamese_medical_qa.csv"
32
+ df.to_csv(output_path, index=False, encoding='utf-8')
33
+
34
+ print(f"💾 Saved to: {output_path}")
35
+ return df
36
+
37
+ except ImportError:
38
+ print("❌ Error: 'datasets' library not installed")
39
+ print(" Install with: pip install datasets")
40
+ return None
41
+ except Exception as e:
42
+ print(f"❌ Error downloading dataset: {e}")
43
+ return None
44
+
45
+
46
+ def is_symptom_question(question):
47
+ """
48
+ Classify if question is about SPECIFIC SYMPTOMS
49
+
50
+ Returns:
51
+ bool: True if symptom question, False if general health question
52
+ """
53
+ if not question or not isinstance(question, str):
54
+ return False
55
+
56
+ question_lower = question.lower()
57
+
58
+ # Symptom keywords (high priority - user describing active symptoms)
59
+ symptom_keywords = [
60
+ # Pain
61
+ 'bị đau', 'đau', 'nhức', 'tức', 'đau nhức',
62
+
63
+ # Infection/Fever
64
+ 'bị sốt', 'sốt', 'viêm', 'nhiễm trùng', 'mủ', 'sưng',
65
+
66
+ # Digestive
67
+ 'buồn nôn', 'nôn', 'tiêu chảy', 'táo bón', 'đầy hơi',
68
+ 'ợ hơi', 'ợ chua', 'khó tiêu',
69
+
70
+ # Respiratory
71
+ 'ho', 'khó thở', 'nghẹt mũi', 'chảy nước mũi',
72
+ 'đau họng', 'khàn giọng',
73
+
74
+ # Neurological
75
+ 'chóng mặt', 'hoa mắt', 'mất thăng bằng', 'đau đầu',
76
+
77
+ # Skin
78
+ 'ngứa', 'phát ban', 'nổi mẩn', 'đỏ',
79
+
80
+ # General symptoms
81
+ 'mệt mỏi', 'yếu', 'không khỏe', 'bị ốm', 'khó chịu'
82
+ ]
83
+
84
+ # General health keywords (prevention, knowledge, advice)
85
+ general_keywords = [
86
+ # Prevention
87
+ 'làm sao để không', 'phòng ngừa', 'tránh', 'cách phòng',
88
+ 'làm thế nào để', 'cách nào để',
89
+
90
+ # Knowledge questions
91
+ 'là gì', 'có phải', 'có nên', 'nên không',
92
+ 'tại sao', 'nguyên nhân', 'có thể',
93
+
94
+ # Advice/Recommendations
95
+ 'nên làm gì', 'nên ăn gì', 'có tốt không',
96
+ 'có được không', 'có nên', 'khuyên'
97
+ ]
98
+
99
+ # Count keyword matches
100
+ symptom_score = sum(1 for kw in symptom_keywords if kw in question_lower)
101
+ general_score = sum(1 for kw in general_keywords if kw in question_lower)
102
+
103
+ # Decision logic
104
+ if symptom_score > general_score:
105
+ return True # Symptom question
106
+ elif general_score > symptom_score:
107
+ return False # General health question
108
+ else:
109
+ # Tie-breaker: Check for "bị" (indicates having a condition)
110
+ return 'bị' in question_lower
111
+
112
+
113
+ def process_medical_qa():
114
+ """Process and split into 2 ChromaDB collections"""
115
+ try:
116
+ from sentence_transformers import SentenceTransformer
117
+ import chromadb
118
+
119
+ print("\n🔄 Processing Vietnamese Medical Q&A...")
120
+
121
+ # Load CSV
122
+ csv_path = Path("data_mining/datasets/vietnamese_medical_qa.csv")
123
+ if not csv_path.exists():
124
+ print(f"❌ Error: {csv_path} not found")
125
+ return False
126
+
127
+ df = pd.read_csv(csv_path, encoding='utf-8')
128
+ print(f"📊 Loaded: {len(df)} Q&A pairs")
129
+
130
+ # Initialize embedding model
131
+ print("🤖 Loading embedding model: keepitreal/vietnamese-sbert...")
132
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
133
+
134
+ # Initialize ChromaDB
135
+ output_dir = Path("data_mining/output")
136
+ output_dir.mkdir(parents=True, exist_ok=True)
137
+
138
+ # Split data
139
+ symptom_data = []
140
+ general_data = []
141
+
142
+ print("🔍 Classifying questions...")
143
+ for idx, row in df.iterrows():
144
+ question = str(row['question'])
145
+ answer = str(row['answer'])
146
+
147
+ # Combine Q&A
148
+ text = f"Câu hỏi: {question}\n\nTrả lời: {answer}"
149
+
150
+ # Classify
151
+ if is_symptom_question(question):
152
+ symptom_data.append({
153
+ 'id': f'symptom_qa_{idx}',
154
+ 'text': text,
155
+ 'question': question,
156
+ 'answer': answer,
157
+ 'type': 'symptom'
158
+ })
159
+ else:
160
+ general_data.append({
161
+ 'id': f'general_qa_{idx}',
162
+ 'text': text,
163
+ 'question': question,
164
+ 'answer': answer,
165
+ 'type': 'general'
166
+ })
167
+
168
+ print(f"✅ Classification complete:")
169
+ print(f" - Symptom Q&A: {len(symptom_data)} ({len(symptom_data)/len(df)*100:.1f}%)")
170
+ print(f" - General Health Q&A: {len(general_data)} ({len(general_data)/len(df)*100:.1f}%)")
171
+
172
+ # Create ChromaDB collections
173
+ # 1. Symptom Q&A Collection
174
+ print("\n📦 Creating Symptom Q&A ChromaDB...")
175
+ symptom_client = chromadb.PersistentClient(path=str(output_dir / "symptom_qa_chroma"))
176
+ symptom_collection = symptom_client.get_or_create_collection(
177
+ name="symptom_qa",
178
+ metadata={"description": "Vietnamese Medical Q&A - Symptom Questions"}
179
+ )
180
+
181
+ # Batch insert symptom data
182
+ batch_size = 100
183
+ for i in range(0, len(symptom_data), batch_size):
184
+ batch = symptom_data[i:i+batch_size]
185
+
186
+ ids = [item['id'] for item in batch]
187
+ texts = [item['text'] for item in batch]
188
+ metadatas = [{
189
+ 'type': item['type'],
190
+ 'domain': 'symptom',
191
+ 'agent': 'SymptomAgent',
192
+ 'source': 'vietnamese-medical-qa'
193
+ } for item in batch]
194
+
195
+ # Generate embeddings
196
+ embeddings = embedder.encode(texts, show_progress_bar=False)
197
+
198
+ symptom_collection.add(
199
+ ids=ids,
200
+ embeddings=embeddings.tolist(),
201
+ documents=texts,
202
+ metadatas=metadatas
203
+ )
204
+
205
+ if (i + batch_size) % 500 == 0:
206
+ print(f" Processed {min(i+batch_size, len(symptom_data))}/{len(symptom_data)} symptom Q&A...")
207
+
208
+ print(f"✅ Symptom Q&A ChromaDB created: {len(symptom_data)} records")
209
+
210
+ # 2. General Health Q&A Collection
211
+ print("\n📦 Creating General Health Q&A ChromaDB...")
212
+ general_client = chromadb.PersistentClient(path=str(output_dir / "general_health_qa_chroma"))
213
+ general_collection = general_client.get_or_create_collection(
214
+ name="general_health_qa",
215
+ metadata={"description": "Vietnamese Medical Q&A - General Health Questions"}
216
+ )
217
+
218
+ # Batch insert general data
219
+ for i in range(0, len(general_data), batch_size):
220
+ batch = general_data[i:i+batch_size]
221
+
222
+ ids = [item['id'] for item in batch]
223
+ texts = [item['text'] for item in batch]
224
+ metadatas = [{
225
+ 'type': item['type'],
226
+ 'domain': 'general_health',
227
+ 'agent': 'GeneralHealthAgent',
228
+ 'source': 'vietnamese-medical-qa'
229
+ } for item in batch]
230
+
231
+ # Generate embeddings
232
+ embeddings = embedder.encode(texts, show_progress_bar=False)
233
+
234
+ general_collection.add(
235
+ ids=ids,
236
+ embeddings=embeddings.tolist(),
237
+ documents=texts,
238
+ metadatas=metadatas
239
+ )
240
+
241
+ if (i + batch_size) % 500 == 0:
242
+ print(f" Processed {min(i+batch_size, len(general_data))}/{len(general_data)} general Q&A...")
243
+
244
+ print(f"✅ General Health Q&A ChromaDB created: {len(general_data)} records")
245
+
246
+ print("\n✅ Processing complete!")
247
+ print(f" Output: {output_dir}")
248
+ print(f" - symptom_qa_chroma/ ({len(symptom_data)} records)")
249
+ print(f" - general_health_qa_chroma/ ({len(general_data)} records)")
250
+
251
+ return True
252
+
253
+ except ImportError as e:
254
+ print(f"❌ Error: Missing library - {e}")
255
+ print(" Install with: pip install sentence-transformers chromadb")
256
+ return False
257
+ except Exception as e:
258
+ print(f"❌ Error processing dataset: {e}")
259
+ import traceback
260
+ traceback.print_exc()
261
+ return False
262
+
263
+
264
+ def main():
265
+ """Main execution"""
266
+ print("=" * 60)
267
+ print("Vietnamese Medical Q&A Dataset Mining")
268
+ print("Source: hungnm/vietnamese-medical-qa (HuggingFace)")
269
+ print("=" * 60)
270
+
271
+ # Step 1: Download
272
+ df = download_medical_qa()
273
+ if df is None:
274
+ print("\n❌ Download failed!")
275
+ return False
276
+
277
+ # Step 2: Process
278
+ success = process_medical_qa()
279
+ if not success:
280
+ print("\n❌ Processing failed!")
281
+ return False
282
+
283
+ print("\n" + "=" * 60)
284
+ print("✅ SUCCESS! Vietnamese Medical Q&A ready for RAG system")
285
+ print("=" * 60)
286
+ return True
287
+
288
+
289
+ if __name__ == "__main__":
290
+ success = main()
291
+ sys.exit(0 if success else 1)
data_mining/mining_mentalchat.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MentalChat16K Dataset - Download & Process
3
+ Downloads and processes mental health counseling conversations into ChromaDB
4
+ Dataset: ShenLab/MentalChat16K (16K conversations, 33 topics)
5
+ """
6
+
7
+ from datasets import load_dataset
8
+ import pandas as pd
9
+ import chromadb
10
+ from sentence_transformers import SentenceTransformer
11
+ import os
12
+
13
+ def download_mentalchat():
14
+ """Download MentalChat16K dataset from HuggingFace"""
15
+
16
+ print("📥 Downloading MentalChat16K dataset...")
17
+ print(" Source: ShenLab/MentalChat16K")
18
+ print(" Coverage: 33 mental health topics")
19
+
20
+ try:
21
+ # Load dataset from HuggingFace
22
+ dataset = load_dataset("ShenLab/MentalChat16K")
23
+
24
+ # Create output directory
25
+ os.makedirs("data_mining/datasets", exist_ok=True)
26
+
27
+ # Convert to pandas DataFrame
28
+ df = dataset['train'].to_pandas()
29
+
30
+ # Save to CSV
31
+ output_path = "data_mining/datasets/mentalchat16k.csv"
32
+ df.to_csv(output_path, index=False)
33
+
34
+ # Check file size
35
+ file_size = os.path.getsize(output_path) / (1024 * 1024) # MB
36
+
37
+ print(f"✅ Downloaded: {output_path}")
38
+ print(f"📊 Records: {len(df)}")
39
+ print(f"📊 File size: {file_size:.2f} MB")
40
+
41
+ return True
42
+
43
+ except Exception as e:
44
+ print(f"❌ Download failed: {e}")
45
+ return False
46
+
47
+ def process_mentalchat():
48
+ """Process MentalChat16K dataset and build ChromaDB"""
49
+
50
+ print("\n🔨 Processing MentalChat16K dataset...")
51
+
52
+ # Load dataset
53
+ csv_path = "data_mining/datasets/mentalchat16k.csv"
54
+ if not os.path.exists(csv_path):
55
+ print(f"❌ Dataset not found: {csv_path}")
56
+ return False
57
+
58
+ df = pd.read_csv(csv_path)
59
+ print(f"📊 Loaded {len(df)} records")
60
+
61
+ # Initialize embedder
62
+ print("🤖 Loading embedding model...")
63
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
64
+
65
+ # Initialize ChromaDB
66
+ print("💾 Initializing ChromaDB...")
67
+ os.makedirs("data_mining/output", exist_ok=True)
68
+ client = chromadb.PersistentClient(path="data_mining/output/mental_health_chroma")
69
+
70
+ # Create collection
71
+ collection = client.get_or_create_collection(
72
+ name="mental_health",
73
+ metadata={"hnsw:space": "cosine"}
74
+ )
75
+
76
+ # Process conversations
77
+ print("📝 Processing conversations...")
78
+
79
+ # Determine column names and combine if needed
80
+ if 'instruction' in df.columns and 'output' in df.columns:
81
+ # New format: instruction + input + output
82
+ print(" Detected instruction-based format")
83
+ df['text'] = df.apply(lambda row:
84
+ f"User: {row['instruction']}\n{row.get('input', '')}\n\nAssistant: {row['output']}",
85
+ axis=1
86
+ )
87
+ text_column = 'text'
88
+ else:
89
+ # Try to find existing text column
90
+ text_column = None
91
+ for col in ['conversation', 'text', 'Context', 'Question', 'Response']:
92
+ if col in df.columns:
93
+ text_column = col
94
+ break
95
+
96
+ if not text_column:
97
+ print(f"❌ Could not find text column. Available: {df.columns.tolist()}")
98
+ return False
99
+
100
+ print(f" Using column: '{text_column}'")
101
+
102
+ processed = 0
103
+ batch_size = 100
104
+
105
+ for i in range(0, len(df), batch_size):
106
+ batch = df.iloc[i:i+batch_size]
107
+
108
+ ids = []
109
+ embeddings = []
110
+ documents = []
111
+ metadatas = []
112
+
113
+ for idx, row in batch.iterrows():
114
+ text = str(row[text_column])
115
+
116
+ if len(text) < 10:
117
+ continue
118
+
119
+ embedding = embedder.encode(text)
120
+
121
+ ids.append(f"mental_{processed:05d}")
122
+ embeddings.append(embedding.tolist())
123
+ documents.append(text)
124
+ metadatas.append({
125
+ 'domain': 'mental_health',
126
+ 'agent': 'MentalHealthAgent',
127
+ 'source': 'MentalChat16K',
128
+ 'index': processed
129
+ })
130
+
131
+ processed += 1
132
+
133
+ if ids:
134
+ collection.add(
135
+ ids=ids,
136
+ embeddings=embeddings,
137
+ documents=documents,
138
+ metadatas=metadatas
139
+ )
140
+
141
+ if (i + batch_size) % 1000 == 0:
142
+ print(f" Processed {min(i + batch_size, len(df))}/{len(df)} records...")
143
+
144
+ print(f"✅ Processed {processed} conversations")
145
+ print(f"💾 Database saved to: data_mining/output/mental_health_chroma/")
146
+
147
+ # Get database size
148
+ db_path = "data_mining/output/mental_health_chroma"
149
+ total_size = 0
150
+ for dirpath, dirnames, filenames in os.walk(db_path):
151
+ for filename in filenames:
152
+ filepath = os.path.join(dirpath, filename)
153
+ total_size += os.path.getsize(filepath)
154
+
155
+ print(f"📊 Database size: {total_size / (1024 * 1024):.2f} MB")
156
+
157
+ return True
158
+
159
+ def main():
160
+ """Main function - download and process"""
161
+ print("=" * 60)
162
+ print("MentalChat16K Dataset - Download & Process")
163
+ print("=" * 60)
164
+
165
+ if not download_mentalchat():
166
+ return False
167
+
168
+ if not process_mentalchat():
169
+ return False
170
+
171
+ print("\n" + "=" * 60)
172
+ print("✅ MentalChat16K dataset ready!")
173
+ print("=" * 60)
174
+ return True
175
+
176
+ if __name__ == "__main__":
177
+ success = main()
178
+ exit(0 if success else 1)
data_mining/mining_nutrition.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nutrition Dataset - Download & Process
3
+ Downloads and processes dietary recommendation data into ChromaDB
4
+ Dataset: issai/LLM_for_Dietary_Recommendation_System (50 patient profiles)
5
+ """
6
+
7
+ from datasets import load_dataset
8
+ import pandas as pd
9
+ import chromadb
10
+ from sentence_transformers import SentenceTransformer
11
+ import os
12
+
13
+ def download_nutrition():
14
+ """Download Dietary Recommendation dataset from HuggingFace"""
15
+
16
+ print("📥 Downloading Dietary Recommendation dataset...")
17
+ print(" Source: issai/LLM_for_Dietary_Recommendation_System")
18
+
19
+ try:
20
+ dataset = load_dataset("issai/LLM_for_Dietary_Recommendation_System")
21
+
22
+ os.makedirs("data_mining/datasets", exist_ok=True)
23
+
24
+ df = dataset['train'].to_pandas()
25
+
26
+ output_path = "data_mining/datasets/nutrition_diet.csv"
27
+ df.to_csv(output_path, index=False)
28
+
29
+ file_size = os.path.getsize(output_path) / (1024 * 1024)
30
+
31
+ print(f"✅ Downloaded: {output_path}")
32
+ print(f"📊 Records: {len(df)}")
33
+ print(f"📊 File size: {file_size:.2f} MB")
34
+
35
+ return True
36
+
37
+ except Exception as e:
38
+ print(f"❌ Download failed: {e}")
39
+ return False
40
+
41
+ def process_nutrition():
42
+ """Process Nutrition dataset and build ChromaDB"""
43
+
44
+ print("\n🔨 Processing Nutrition dataset...")
45
+
46
+ csv_path = "data_mining/datasets/nutrition_diet.csv"
47
+ if not os.path.exists(csv_path):
48
+ print(f"❌ Dataset not found: {csv_path}")
49
+ return False
50
+
51
+ df = pd.read_csv(csv_path)
52
+ print(f"📊 Loaded {len(df)} records")
53
+
54
+ print("🤖 Loading embedding model...")
55
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
56
+
57
+ print("💾 Initializing ChromaDB...")
58
+ os.makedirs("data_mining/output", exist_ok=True)
59
+ client = chromadb.PersistentClient(path="data_mining/output/nutrition_chroma")
60
+
61
+ collection = client.get_or_create_collection(
62
+ name="nutrition",
63
+ metadata={"hnsw:space": "cosine"}
64
+ )
65
+
66
+ print("📝 Processing nutrition data...")
67
+
68
+ text_columns = []
69
+ for col in ['profile', 'recommendation', 'diet_plan', 'text', 'content']:
70
+ if col in df.columns:
71
+ text_columns.append(col)
72
+
73
+ if not text_columns:
74
+ text_columns = df.columns.tolist()
75
+
76
+ print(f" Using columns: {text_columns}")
77
+
78
+ processed = 0
79
+
80
+ for idx, row in df.iterrows():
81
+ text_parts = []
82
+ for col in text_columns:
83
+ value = str(row[col])
84
+ if value and value != 'nan' and len(value) > 5:
85
+ text_parts.append(f"{col}: {value}")
86
+
87
+ text = "\n".join(text_parts)
88
+
89
+ if len(text) < 20:
90
+ continue
91
+
92
+ embedding = embedder.encode(text)
93
+
94
+ collection.add(
95
+ ids=[f"nutrition_{processed:05d}"],
96
+ embeddings=[embedding.tolist()],
97
+ documents=[text],
98
+ metadatas=[{
99
+ 'domain': 'nutrition',
100
+ 'agent': 'NutritionAgent',
101
+ 'source': 'LLM_Dietary_Recommendation',
102
+ 'index': processed
103
+ }]
104
+ )
105
+
106
+ processed += 1
107
+
108
+ if (processed % 10) == 0:
109
+ print(f" Processed {processed}/{len(df)} records...")
110
+
111
+ print(f"✅ Processed {processed} nutrition records")
112
+ print(f"💾 Database saved to: data_mining/output/nutrition_chroma/")
113
+
114
+ db_path = "data_mining/output/nutrition_chroma"
115
+ total_size = 0
116
+ for dirpath, dirnames, filenames in os.walk(db_path):
117
+ for filename in filenames:
118
+ filepath = os.path.join(dirpath, filename)
119
+ total_size += os.path.getsize(filepath)
120
+
121
+ print(f"📊 Database size: {total_size / (1024 * 1024):.2f} MB")
122
+
123
+ return True
124
+
125
+ def main():
126
+ """Main function - download and process"""
127
+ print("=" * 60)
128
+ print("Nutrition Dataset - Download & Process")
129
+ print("=" * 60)
130
+
131
+ if not download_nutrition():
132
+ return False
133
+
134
+ if not process_nutrition():
135
+ return False
136
+
137
+ print("\n" + "=" * 60)
138
+ print("✅ Nutrition dataset ready!")
139
+ print("=" * 60)
140
+ return True
141
+
142
+ if __name__ == "__main__":
143
+ success = main()
144
+ exit(0 if success else 1)
data_mining/mining_vietnamese_nutrition.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mining Script: Vietnamese Food Nutrition Database
4
+ Processes Vietnamese food CSV into ChromaDB for NutritionAgent
5
+ """
6
+
7
+ import sys
8
+ import pandas as pd
9
+ from pathlib import Path
10
+
11
+ def process_vietnamese_nutrition():
12
+ """Process Vietnamese food nutrition CSV into ChromaDB"""
13
+ try:
14
+ from sentence_transformers import SentenceTransformer
15
+ import chromadb
16
+
17
+ print("🍜 Processing Vietnamese Food Nutrition Database...")
18
+
19
+ # Load CSV
20
+ csv_path = Path("data_mining/datasets/vietnamese_food_nutrition.csv")
21
+ if not csv_path.exists():
22
+ print("❌ CSV not found. Creating it first...")
23
+ import vn_food_db
24
+ vn_food_db.vn_food_db()
25
+
26
+ df = pd.read_csv(csv_path)
27
+ print(f"📊 Loaded: {len(df)} Vietnamese foods")
28
+
29
+ # Initialize
30
+ print("🤖 Loading embedding model...")
31
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
32
+
33
+ output_dir = Path("data_mining/output")
34
+ output_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ client = chromadb.PersistentClient(path=str(output_dir / "vietnamese_nutrition_chroma"))
37
+ collection = client.get_or_create_collection(
38
+ name="vietnamese_nutrition",
39
+ metadata={"description": "Vietnamese Food Nutrition Database"}
40
+ )
41
+
42
+ # Process foods
43
+ print("📦 Creating ChromaDB...")
44
+ batch_size = 20
45
+ for i in range(0, len(df), batch_size):
46
+ batch = df.iloc[i:i+batch_size]
47
+
48
+ ids = []
49
+ texts = []
50
+ metadatas = []
51
+
52
+ for idx, row in batch.iterrows():
53
+ # Create document
54
+ text = f"""Món ăn: {row['name_vi']} ({row['name_en']})
55
+ Calories: {row['calories']} kcal
56
+ Protein: {row['protein_g']}g
57
+ Carbohydrates: {row['carbs_g']}g
58
+ Fat: {row['fat_g']}g
59
+ Fiber: {row['fiber_g']}g
60
+ Category: {row['category']}"""
61
+
62
+ ids.append(f"food_{idx}")
63
+ texts.append(text)
64
+ metadatas.append({
65
+ 'name_vi': row['name_vi'],
66
+ 'name_en': row['name_en'],
67
+ 'calories': int(row['calories']),
68
+ 'category': row['category'],
69
+ 'source': 'vietnamese_food_db'
70
+ })
71
+
72
+ # Generate embeddings
73
+ embeddings = embedder.encode(texts, show_progress_bar=False)
74
+
75
+ # Add to collection
76
+ collection.add(
77
+ ids=ids,
78
+ embeddings=embeddings.tolist(),
79
+ documents=texts,
80
+ metadatas=metadatas
81
+ )
82
+
83
+ print(f" Processed {min(i+batch_size, len(df))}/{len(df)} foods...")
84
+
85
+ print(f"\n✅ Vietnamese Nutrition ChromaDB created!")
86
+ print(f" Output: {output_dir / 'vietnamese_nutrition_chroma'}")
87
+ print(f" Records: {len(df)} foods")
88
+
89
+ return True
90
+
91
+ except ImportError as e:
92
+ print(f"❌ Missing library: {e}")
93
+ print(" Install: pip install sentence-transformers chromadb pandas")
94
+ return False
95
+ except Exception as e:
96
+ print(f"❌ Error: {e}")
97
+ import traceback
98
+ traceback.print_exc()
99
+ return False
100
+
101
+ def main():
102
+ """Main execution"""
103
+ print("=" * 60)
104
+ print("Vietnamese Food Nutrition Database Mining")
105
+ print("=" * 60)
106
+
107
+ success = process_vietnamese_nutrition()
108
+
109
+ if success:
110
+ print("\n" + "=" * 60)
111
+ print("✅ SUCCESS! Vietnamese nutrition data ready for RAG")
112
+ print("=" * 60)
113
+ else:
114
+ print("\n❌ FAILED!")
115
+
116
+ return success
117
+
118
+ if __name__ == "__main__":
119
+ success = main()
120
+ sys.exit(0 if success else 1)
data_mining/mining_vimedical.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ViMedical Disease Dataset - Download & Process
3
+ Downloads and processes Vietnamese medical disease dataset into ChromaDB
4
+ Dataset: PB3002/ViMedical_Disease (603 diseases, 12K+ examples)
5
+ """
6
+
7
+ import requests
8
+ import pandas as pd
9
+ import chromadb
10
+ from sentence_transformers import SentenceTransformer
11
+ import os
12
+ import re
13
+
14
+ def download_vimedical():
15
+ """Download ViMedical dataset from HuggingFace"""
16
+
17
+ print("📥 Downloading ViMedical Disease dataset...")
18
+
19
+ # HuggingFace dataset URL
20
+ url = "https://huggingface.co/datasets/PB3002/ViMedical_Disease/resolve/main/ViMedical_Disease.csv"
21
+
22
+ # Create datasets directory
23
+ os.makedirs("data_mining/datasets", exist_ok=True)
24
+ output_path = "data_mining/datasets/vimedical_disease.csv"
25
+
26
+ try:
27
+ # Download
28
+ response = requests.get(url, timeout=60)
29
+ response.raise_for_status()
30
+
31
+ # Save
32
+ with open(output_path, 'wb') as f:
33
+ f.write(response.content)
34
+
35
+ # Check file size
36
+ file_size = os.path.getsize(output_path) / (1024 * 1024) # MB
37
+
38
+ print(f"✅ Downloaded: {output_path}")
39
+ print(f"📊 File size: {file_size:.2f} MB")
40
+
41
+ return True
42
+
43
+ except Exception as e:
44
+ print(f"❌ Download failed: {e}")
45
+ return False
46
+
47
+ def extract_symptoms(question):
48
+ """Extract symptom description from question"""
49
+ # Remove common prefixes
50
+ prefixes = [
51
+ 'Tôi đang có triệu chứng như ',
52
+ 'Tôi thường xuyên ',
53
+ 'Tôi cảm thấy ',
54
+ 'Tôi bị ',
55
+ 'Tôi hay ',
56
+ 'Tôi có '
57
+ ]
58
+
59
+ symptom = question
60
+ for prefix in prefixes:
61
+ if symptom.startswith(prefix):
62
+ symptom = symptom[len(prefix):]
63
+ break
64
+
65
+ # Remove question suffix
66
+ suffixes = [
67
+ '. Tôi bị bệnh gì?',
68
+ '. Tôi có thể bị gì?',
69
+ '. Đó là bệnh gì?'
70
+ ]
71
+ for suffix in suffixes:
72
+ if symptom.endswith(suffix):
73
+ symptom = symptom[:-len(suffix)]
74
+ break
75
+
76
+ return symptom.strip()
77
+
78
+ def process_vimedical():
79
+ """Process ViMedical dataset and build ChromaDB"""
80
+
81
+ print("\n🔨 Processing ViMedical dataset...")
82
+
83
+ # Load dataset
84
+ csv_path = "data_mining/datasets/vimedical_disease.csv"
85
+ if not os.path.exists(csv_path):
86
+ print(f"❌ Dataset not found: {csv_path}")
87
+ return False
88
+
89
+ df = pd.read_csv(csv_path)
90
+ print(f"📊 Loaded {len(df)} records")
91
+ print(f"📊 Unique diseases: {df['Disease'].nunique()}")
92
+
93
+ # Initialize embedder
94
+ print("🤖 Loading embedding model...")
95
+ embedder = SentenceTransformer('keepitreal/vietnamese-sbert')
96
+
97
+ # Initialize ChromaDB
98
+ print("💾 Initializing ChromaDB...")
99
+ os.makedirs("data_mining/output", exist_ok=True)
100
+ client = chromadb.PersistentClient(path="data_mining/output/medical_chroma")
101
+
102
+ # Create collection
103
+ collection = client.get_or_create_collection(
104
+ name="medical_diseases",
105
+ metadata={"hnsw:space": "cosine"}
106
+ )
107
+
108
+ # Group by disease
109
+ print("📝 Processing diseases...")
110
+ disease_groups = df.groupby('Disease')
111
+
112
+ processed = 0
113
+ for disease_name, group in disease_groups:
114
+ # Extract symptoms from all questions
115
+ symptoms = []
116
+ for question in group['Question']:
117
+ symptom = extract_symptoms(question)
118
+ if symptom:
119
+ symptoms.append(symptom)
120
+
121
+ # Create document text
122
+ doc_text = f"Bệnh: {disease_name}\n\nTriệu chứng:\n"
123
+ doc_text += "\n".join(f"- {s}" for s in symptoms[:10]) # Limit to 10 examples
124
+
125
+ # Generate embedding
126
+ embedding = embedder.encode(doc_text)
127
+
128
+ # Add to ChromaDB
129
+ collection.add(
130
+ ids=[f"disease_{processed:04d}"],
131
+ embeddings=[embedding.tolist()],
132
+ documents=[doc_text],
133
+ metadatas=[{
134
+ 'disease_name': disease_name,
135
+ 'num_examples': len(symptoms),
136
+ 'source': 'ViMedical_Disease'
137
+ }]
138
+ )
139
+
140
+ processed += 1
141
+ if processed % 50 == 0:
142
+ print(f" Processed {processed}/{len(disease_groups)} diseases...")
143
+
144
+ print(f"✅ Processed {processed} diseases")
145
+ print(f"💾 Database saved to: data_mining/output/medical_chroma/")
146
+
147
+ # Get database size
148
+ db_path = "data_mining/output/medical_chroma"
149
+ total_size = 0
150
+ for dirpath, dirnames, filenames in os.walk(db_path):
151
+ for filename in filenames:
152
+ filepath = os.path.join(dirpath, filename)
153
+ total_size += os.path.getsize(filepath)
154
+
155
+ print(f"📊 Database size: {total_size / (1024 * 1024):.2f} MB")
156
+
157
+ return True
158
+
159
+ def main():
160
+ """Main function - download and process"""
161
+ print("=" * 60)
162
+ print("ViMedical Disease Dataset - Download & Process")
163
+ print("=" * 60)
164
+
165
+ # Step 1: Download
166
+ if not download_vimedical():
167
+ return False
168
+
169
+ # Step 2: Process
170
+ if not process_vimedical():
171
+ return False
172
+
173
+ print("\n" + "=" * 60)
174
+ print("✅ ViMedical dataset ready!")
175
+ print("=" * 60)
176
+ return True
177
+
178
+ if __name__ == "__main__":
179
+ success = main()
180
+ exit(0 if success else 1)
data_mining/vn_food_db.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Create Vietnamese Food Nutrition Database
4
+ Generates CSV with ~300 Vietnamese foods and their nutrition facts
5
+ """
6
+
7
+ import csv
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ def vn_food_db():
12
+ """Create comprehensive Vietnamese food nutrition database"""
13
+
14
+ # Vietnamese food nutrition data
15
+ # Format: [name_vi, name_en, calories, protein_g, carbs_g, fat_g, fiber_g, category]
16
+ foods = [
17
+ # PHỞ & NOODLE SOUPS (Món Phở & Bún)
18
+ ["Phở bò", "Beef Pho", 450, 20, 60, 15, 2, "Noodle Soup"],
19
+ ["Phở gà", "Chicken Pho", 380, 18, 55, 10, 2, "Noodle Soup"],
20
+ ["Phở tái", "Rare Beef Pho", 420, 19, 58, 12, 2, "Noodle Soup"],
21
+ ["Phở chín", "Well-done Beef Pho", 460, 21, 60, 16, 2, "Noodle Soup"],
22
+ ["Bún bò Huế", "Hue Beef Noodle", 500, 22, 65, 18, 3, "Noodle Soup"],
23
+ ["Bún riêu", "Crab Noodle Soup", 420, 18, 58, 14, 3, "Noodle Soup"],
24
+ ["Bún chả cá", "Fish Cake Noodle", 380, 20, 52, 12, 2, "Noodle Soup"],
25
+ ["Hủ tiếu", "Hu Tieu Noodle", 400, 16, 60, 10, 2, "Noodle Soup"],
26
+ ["Mì Quảng", "Quang Noodle", 450, 20, 58, 15, 3, "Noodle Soup"],
27
+ ["Cao lầu", "Cao Lau Noodle", 480, 18, 62, 16, 2, "Noodle Soup"],
28
+
29
+ # BÚN (Vermicelli Dishes)
30
+ ["Bún chả", "Grilled Pork Vermicelli", 550, 20, 70, 20, 2, "Vermicelli"],
31
+ ["Bún thịt nướng", "Grilled Pork Vermicelli", 520, 22, 68, 18, 2, "Vermicelli"],
32
+ ["Bún bò xào", "Stir-fried Beef Vermicelli", 480, 20, 65, 15, 3, "Vermicelli"],
33
+ ["Bún gà nướng", "Grilled Chicken Vermicelli", 450, 24, 62, 12, 2, "Vermicelli"],
34
+ ["Bún nem nướng", "Grilled Pork Patty Vermicelli", 500, 18, 66, 16, 2, "Vermicelli"],
35
+
36
+ # CƠM (Rice Dishes)
37
+ ["Cơm tấm", "Broken Rice", 600, 25, 80, 20, 2, "Rice"],
38
+ ["Cơm sườn", "Pork Chop Rice", 650, 28, 85, 22, 2, "Rice"],
39
+ ["Cơm gà", "Chicken Rice", 550, 30, 75, 15, 2, "Rice"],
40
+ ["Cơm chiên", "Fried Rice", 580, 15, 78, 22, 2, "Rice"],
41
+ ["Cơm rang dương châu", "Yang Chow Fried Rice", 620, 18, 82, 24, 2, "Rice"],
42
+ ["Cơm hến", "Clam Rice", 480, 20, 70, 12, 3, "Rice"],
43
+ ["Cơm trắng", "White Rice", 200, 4, 45, 0.5, 1, "Rice"],
44
+
45
+ # BÁNH MÌ (Vietnamese Sandwich)
46
+ ["Bánh mì thịt", "Pork Banh Mi", 400, 12, 50, 18, 3, "Bread"],
47
+ ["Bánh mì gà", "Chicken Banh Mi", 380, 14, 48, 15, 3, "Bread"],
48
+ ["Bánh mì pate", "Pate Banh Mi", 420, 10, 52, 20, 2, "Bread"],
49
+ ["Bánh mì chả", "Sausage Banh Mi", 390, 13, 49, 17, 3, "Bread"],
50
+ ["Bánh mì trứng", "Egg Banh Mi", 350, 12, 45, 14, 2, "Bread"],
51
+
52
+ # GỎI CUỐN & NEM (Spring Rolls)
53
+ ["Gỏi cuốn", "Fresh Spring Rolls", 150, 8, 20, 5, 2, "Appetizer"],
54
+ ["Nem rán", "Fried Spring Rolls", 250, 10, 25, 15, 1, "Appetizer"],
55
+ ["Chả giò", "Fried Rolls", 280, 12, 28, 16, 1, "Appetizer"],
56
+ ["Nem nướng", "Grilled Pork Patty", 200, 15, 10, 12, 1, "Appetizer"],
57
+
58
+ # BÁNH (Cakes & Pancakes)
59
+ ["Bánh xèo", "Vietnamese Pancake", 350, 12, 40, 18, 2, "Pancake"],
60
+ ["Bánh cuốn", "Steamed Rice Rolls", 180, 8, 28, 6, 1, "Pancake"],
61
+ ["Bánh bột lọc", "Tapioca Dumplings", 200, 6, 35, 5, 1, "Pancake"],
62
+ ["Bánh bèo", "Water Fern Cake", 120, 4, 22, 3, 1, "Pancake"],
63
+ ["Bánh khọt", "Mini Pancakes", 280, 8, 32, 14, 2, "Pancake"],
64
+
65
+ # XÔI (Sticky Rice)
66
+ ["Xôi gà", "Chicken Sticky Rice", 450, 18, 70, 12, 2, "Sticky Rice"],
67
+ ["Xôi thịt", "Pork Sticky Rice", 480, 16, 72, 14, 2, "Sticky Rice"],
68
+ ["Xôi xéo", "Mung Bean Sticky Rice", 400, 12, 68, 10, 3, "Sticky Rice"],
69
+ ["Xôi lạc", "Peanut Sticky Rice", 420, 14, 65, 13, 3, "Sticky Rice"],
70
+
71
+ # CANH & SOUP (Soups)
72
+ ["Canh chua", "Sour Soup", 180, 12, 15, 8, 3, "Soup"],
73
+ ["Canh rau", "Vegetable Soup", 80, 3, 12, 2, 3, "Soup"],
74
+ ["Canh cá", "Fish Soup", 150, 15, 10, 6, 2, "Soup"],
75
+ ["Lẩu", "Hot Pot", 400, 25, 30, 20, 4, "Soup"],
76
+
77
+ # SEAFOOD (Hải sản)
78
+ ["Cá kho tộ", "Braised Fish", 280, 25, 8, 18, 1, "Seafood"],
79
+ ["Tôm rang", "Stir-fried Shrimp", 200, 20, 5, 10, 1, "Seafood"],
80
+ ["Mực xào", "Stir-fried Squid", 180, 18, 8, 8, 1, "Seafood"],
81
+ ["Cua rang me", "Tamarind Crab", 220, 16, 12, 12, 1, "Seafood"],
82
+
83
+ # MEAT DISHES (Món thịt)
84
+ ["Thịt kho", "Braised Pork", 350, 20, 10, 25, 1, "Meat"],
85
+ ["Sườn nướng", "Grilled Pork Ribs", 400, 22, 8, 30, 1, "Meat"],
86
+ ["Gà nướng", "Grilled Chicken", 280, 28, 5, 15, 0, "Meat"],
87
+ ["Bò lúc lắc", "Shaking Beef", 320, 25, 8, 20, 1, "Meat"],
88
+
89
+ # VEGETABLES (Rau)
90
+ ["Rau muống xào", "Stir-fried Water Spinach", 60, 3, 8, 2, 2, "Vegetable"],
91
+ ["Cải xào", "Stir-fried Bok Choy", 50, 2, 7, 2, 2, "Vegetable"],
92
+ ["Đậu que xào", "Stir-fried Green Beans", 70, 3, 10, 2, 3, "Vegetable"],
93
+ ["Bí xanh xào", "Stir-fried Zucchini", 55, 2, 8, 2, 2, "Vegetable"],
94
+
95
+ # BEVERAGES (Đồ uống)
96
+ ["Cà phê sữa đá", "Iced Coffee with Milk", 150, 3, 25, 5, 0, "Beverage"],
97
+ ["Cà phê đen", "Black Coffee", 5, 0, 1, 0, 0, "Beverage"],
98
+ ["Trà sữa", "Milk Tea", 250, 4, 45, 8, 0, "Beverage"],
99
+ ["Nước mía", "Sugarcane Juice", 180, 0, 45, 0, 0, "Beverage"],
100
+ ["Sinh tố bơ", "Avocado Smoothie", 280, 4, 35, 15, 6, "Beverage"],
101
+ ["Sinh tố xoài", "Mango Smoothie", 200, 2, 48, 2, 3, "Beverage"],
102
+ ["Nước dừa", "Coconut Water", 45, 1, 9, 0.5, 1, "Beverage"],
103
+ ["Trà đá", "Iced Tea", 2, 0, 0.5, 0, 0, "Beverage"],
104
+
105
+ # DESSERTS (Tráng miệng)
106
+ ["Chè ba màu", "Three Color Dessert", 280, 4, 55, 6, 3, "Dessert"],
107
+ ["Chè đậu xanh", "Mung Bean Dessert", 220, 6, 42, 4, 4, "Dessert"],
108
+ ["Chè bưởi", "Pomelo Dessert", 180, 2, 40, 3, 2, "Dessert"],
109
+ ["Bánh flan", "Flan", 200, 5, 30, 7, 0, "Dessert"],
110
+ ["Sương sa hột lựu", "Tapioca Dessert", 150, 1, 35, 2, 1, "Dessert"],
111
+
112
+ # SNACKS (Đồ ăn vặt)
113
+ ["Bánh tráng nướng", "Grilled Rice Paper", 180, 4, 32, 4, 1, "Snack"],
114
+ ["Bánh đa", "Rice Cracker", 120, 2, 25, 2, 1, "Snack"],
115
+ ["Khoai lang luộc", "Boiled Sweet Potato", 90, 2, 21, 0.2, 3, "Snack"],
116
+ ["Bắp luộc", "Boiled Corn", 110, 3, 25, 1.5, 3, "Snack"],
117
+ ]
118
+
119
+ # Create CSV
120
+ output_dir = Path("data_mining/datasets")
121
+ output_dir.mkdir(parents=True, exist_ok=True)
122
+
123
+ csv_path = output_dir / "vietnamese_food_nutrition.csv"
124
+
125
+ with open(csv_path, 'w', newline='', encoding='utf-8') as f:
126
+ writer = csv.writer(f)
127
+
128
+ # Header
129
+ writer.writerow([
130
+ 'name_vi', 'name_en', 'calories', 'protein_g',
131
+ 'carbs_g', 'fat_g', 'fiber_g', 'category'
132
+ ])
133
+
134
+ # Data
135
+ writer.writerows(foods)
136
+
137
+ print(f"✅ Created Vietnamese Food Database")
138
+ print(f" File: {csv_path}")
139
+ print(f" Foods: {len(foods)}")
140
+ print(f" Size: {csv_path.stat().st_size / 1024:.1f} KB")
141
+
142
+ # Print summary by category
143
+ categories = {}
144
+ for food in foods:
145
+ cat = food[7]
146
+ categories[cat] = categories.get(cat, 0) + 1
147
+
148
+ print(f"\n📊 Breakdown by category:")
149
+ for cat, count in sorted(categories.items(), key=lambda x: -x[1]):
150
+ print(f" {cat}: {count} foods")
151
+
152
+ return csv_path
153
+
154
+ if __name__ == "__main__":
155
+ try:
156
+ vn_food_db()
157
+ sys.exit(0)
158
+ except Exception as e:
159
+ print(f"❌ Error: {e}")
160
+ import traceback
161
+ traceback.print_exc()
162
+ sys.exit(1)
examples/feedback_loop_example.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback Loop Example
3
+ Demonstrates collecting and learning from user feedback
4
+ """
5
+
6
+ from feedback import get_feedback_collector, get_feedback_analyzer, FeedbackCategory
7
+
8
+
9
+ def example_collect_ratings():
10
+ """Example: Collect user ratings"""
11
+ print("=" * 60)
12
+ print("COLLECTING USER RATINGS")
13
+ print("=" * 60)
14
+
15
+ collector = get_feedback_collector()
16
+
17
+ # Example 1: High rating (5 stars)
18
+ print("\n✅ Example 1: User loves the response")
19
+ feedback_id = collector.collect_rating(
20
+ user_id="user123",
21
+ agent_name="nutrition_agent",
22
+ user_message="Tôi muốn giảm cân, nên ăn gì?",
23
+ agent_response="Để giảm cân hiệu quả, bạn nên ăn nhiều rau xanh, protein...",
24
+ rating=5,
25
+ category=FeedbackCategory.HELPFULNESS,
26
+ comment="Rất hữu ích và chi tiết!"
27
+ )
28
+ print(f" Feedback ID: {feedback_id}")
29
+ print(f" Rating: 5/5 ⭐⭐⭐⭐⭐")
30
+
31
+ # Example 2: Low rating (2 stars)
32
+ print("\n❌ Example 2: User unhappy with response")
33
+ feedback_id = collector.collect_rating(
34
+ user_id="user456",
35
+ agent_name="nutrition_agent",
36
+ user_message="Tôi bị tiểu đường, ăn gì được?",
37
+ agent_response="Bạn nên ăn ít đường.",
38
+ rating=2,
39
+ category=FeedbackCategory.COMPLETENESS,
40
+ comment="Quá chung chung, không cụ thể"
41
+ )
42
+ print(f" Feedback ID: {feedback_id}")
43
+ print(f" Rating: 2/5 ⭐⭐")
44
+
45
+ # Example 3: Thumbs up
46
+ print("\n👍 Example 3: Quick thumbs up")
47
+ feedback_id = collector.collect_thumbs(
48
+ user_id="user789",
49
+ agent_name="exercise_agent",
50
+ user_message="Tập gì để giảm mỡ bụng?",
51
+ agent_response="Bạn nên tập plank, crunches, và cardio...",
52
+ is_positive=True,
53
+ comment="Hay!"
54
+ )
55
+ print(f" Feedback ID: {feedback_id}")
56
+ print(f" Thumbs: 👍")
57
+
58
+
59
+ def example_collect_corrections():
60
+ """Example: Collect user corrections"""
61
+ print("\n" + "=" * 60)
62
+ print("COLLECTING USER CORRECTIONS")
63
+ print("=" * 60)
64
+
65
+ collector = get_feedback_collector()
66
+
67
+ # Example: User corrects wrong information
68
+ print("\n📝 User corrects incorrect BMI calculation")
69
+ feedback_id = collector.collect_correction(
70
+ user_id="user123",
71
+ agent_name="nutrition_agent",
72
+ user_message="Tôi 70kg, 175cm, BMI của tôi là bao nhiêu?",
73
+ agent_response="BMI của bạn là 24.5", # Wrong!
74
+ corrected_response="BMI của bạn là 22.9 (70 / 1.75²)",
75
+ correction_reason="calculation_error"
76
+ )
77
+ print(f" Correction ID: {feedback_id}")
78
+ print(f" Original: BMI = 24.5 ❌")
79
+ print(f" Corrected: BMI = 22.9 ✅")
80
+
81
+
82
+ def example_report_issue():
83
+ """Example: Report problematic response"""
84
+ print("\n" + "=" * 60)
85
+ print("REPORTING ISSUES")
86
+ print("=" * 60)
87
+
88
+ collector = get_feedback_collector()
89
+
90
+ # Example: Report harmful advice
91
+ print("\n⚠️ User reports harmful medical advice")
92
+ report_id = collector.report_issue(
93
+ user_id="user999",
94
+ agent_name="symptom_agent",
95
+ user_message="Tôi bị đau ngực dữ dội",
96
+ agent_response="Bạn nên nghỉ ngơi, uống nước",
97
+ issue_type="harmful",
98
+ description="Đau ngực dữ dội cần đi bệnh viện ngay, không nên chỉ nghỉ ngơi",
99
+ severity="critical"
100
+ )
101
+ print(f" Report ID: {report_id}")
102
+ print(f" Severity: CRITICAL 🚨")
103
+
104
+
105
+ def example_analyze_feedback():
106
+ """Example: Analyze feedback to find patterns"""
107
+ print("\n" + "=" * 60)
108
+ print("ANALYZING FEEDBACK")
109
+ print("=" * 60)
110
+
111
+ collector = get_feedback_collector()
112
+
113
+ # Add more sample data
114
+ print("\n📊 Adding sample feedback data...")
115
+ for i in range(10):
116
+ collector.collect_rating(
117
+ user_id=f"user{i}",
118
+ agent_name="nutrition_agent",
119
+ user_message=f"Question {i}",
120
+ agent_response=f"Response {i}",
121
+ rating=4 if i % 2 == 0 else 3,
122
+ category=FeedbackCategory.HELPFULNESS
123
+ )
124
+
125
+ # Get statistics
126
+ print("\n📈 Feedback Statistics:")
127
+ stats = collector.get_feedback_stats(agent_name="nutrition_agent")
128
+ print(f" Total ratings: {stats['total_ratings']}")
129
+ print(f" Average rating: {stats['average_rating']:.1f}/5.0")
130
+ print(f" Rating distribution:")
131
+ for rating in [5, 4, 3, 2, 1]:
132
+ count = stats['rating_distribution'][rating]
133
+ print(f" {rating} stars: {count}")
134
+
135
+ # Analyze performance
136
+ print("\n🔍 Performance Analysis:")
137
+ analyzer = get_feedback_analyzer(collector)
138
+ analysis = analyzer.analyze_agent_performance("nutrition_agent")
139
+
140
+ print(f" Overall rating: {analysis['overall_rating']:.1f}/5.0")
141
+
142
+ if analysis['strengths']:
143
+ print(f"\n Strengths:")
144
+ for strength in analysis['strengths']:
145
+ print(f" ✅ {strength}")
146
+
147
+ if analysis['weaknesses']:
148
+ print(f"\n Weaknesses:")
149
+ for weakness in analysis['weaknesses']:
150
+ print(f" ⚠️ {weakness}")
151
+
152
+
153
+ def example_get_insights():
154
+ """Example: Get actionable insights"""
155
+ print("\n" + "=" * 60)
156
+ print("ACTIONABLE INSIGHTS")
157
+ print("=" * 60)
158
+
159
+ collector = get_feedback_collector()
160
+ analyzer = get_feedback_analyzer(collector)
161
+
162
+ # Get insights
163
+ insights = analyzer.get_actionable_insights("nutrition_agent", limit=3)
164
+
165
+ if insights:
166
+ print("\n💡 Top Improvement Opportunities:")
167
+ for i, insight in enumerate(insights, 1):
168
+ print(f"\n {i}. [{insight['priority'].upper()}] {insight['category']}")
169
+ print(f" Issue: {insight['issue']}")
170
+ print(f" Action: {insight['action']}")
171
+ if insight['examples']:
172
+ print(f" Examples: {', '.join(insight['examples'][:2])}")
173
+ else:
174
+ print("\n No insights available yet. Collect more feedback!")
175
+
176
+
177
+ def example_generate_report():
178
+ """Example: Generate improvement report"""
179
+ print("\n" + "=" * 60)
180
+ print("IMPROVEMENT REPORT")
181
+ print("=" * 60)
182
+
183
+ collector = get_feedback_collector()
184
+ analyzer = get_feedback_analyzer(collector)
185
+
186
+ # Generate report
187
+ report = analyzer.generate_improvement_report("nutrition_agent")
188
+ print(report)
189
+
190
+
191
+ def example_export_for_training():
192
+ """Example: Export feedback for fine-tuning"""
193
+ print("\n" + "=" * 60)
194
+ print("EXPORT FOR FINE-TUNING")
195
+ print("=" * 60)
196
+
197
+ collector = get_feedback_collector()
198
+
199
+ # Export high-quality feedback
200
+ print("\n📦 Exporting high-quality feedback (rating >= 4)...")
201
+ output_file = collector.export_for_fine_tuning(
202
+ agent_name="nutrition_agent",
203
+ min_rating=4,
204
+ include_corrections=True
205
+ )
206
+
207
+ print(f" ✅ Exported to: {output_file}")
208
+ print(f" Ready for fine-tuning!")
209
+
210
+
211
+ def example_compare_agents():
212
+ """Example: Compare agent performance"""
213
+ print("\n" + "=" * 60)
214
+ print("AGENT COMPARISON")
215
+ print("=" * 60)
216
+
217
+ collector = get_feedback_collector()
218
+
219
+ # Add feedback for different agents
220
+ print("\n📊 Adding feedback for multiple agents...")
221
+ agents = ["nutrition_agent", "exercise_agent", "symptom_agent"]
222
+
223
+ for agent in agents:
224
+ for i in range(5):
225
+ rating = 5 if agent == "nutrition_agent" else (4 if agent == "exercise_agent" else 3)
226
+ collector.collect_rating(
227
+ user_id=f"user{i}",
228
+ agent_name=agent,
229
+ user_message=f"Question for {agent}",
230
+ agent_response=f"Response from {agent}",
231
+ rating=rating
232
+ )
233
+
234
+ # Compare
235
+ analyzer = get_feedback_analyzer(collector)
236
+ comparison = analyzer.compare_agents()
237
+
238
+ print(f"\n🏆 Agent Rankings:")
239
+ for i, agent in enumerate(comparison['agents'], 1):
240
+ print(f" {i}. {agent['agent']}: {agent['average_rating']:.1f}/5.0 ({agent['total_feedback']} feedback)")
241
+
242
+ if comparison['best_agent']:
243
+ print(f"\n Best: {comparison['best_agent']['agent']} 🥇")
244
+
245
+ if comparison['worst_agent']:
246
+ print(f" Needs improvement: {comparison['worst_agent']['agent']} ⚠️")
247
+
248
+
249
+ if __name__ == '__main__':
250
+ example_collect_ratings()
251
+ example_collect_corrections()
252
+ example_report_issue()
253
+ example_analyze_feedback()
254
+ example_get_insights()
255
+ example_generate_report()
256
+ example_export_for_training()
257
+ example_compare_agents()
258
+
259
+ print("\n" + "=" * 60)
260
+ print("✅ FEEDBACK LOOP DEMO COMPLETE!")
261
+ print("=" * 60)
262
+ print("\nNext steps:")
263
+ print("1. Integrate feedback collection into your UI")
264
+ print("2. Review feedback regularly")
265
+ print("3. Use insights to improve agents")
266
+ print("4. Export high-quality feedback for fine-tuning")
267
+ print("5. Monitor trends and act on critical issues")
examples/multilingual_example.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multilingual Support Example
3
+ Demonstrates automatic language detection and bilingual responses
4
+ """
5
+
6
+ from i18n import Language, detect_language, t, get_multilingual_handler, Translations
7
+
8
+
9
+ def test_language_detection():
10
+ """Test automatic language detection"""
11
+ print("=" * 60)
12
+ print("LANGUAGE DETECTION TEST")
13
+ print("=" * 60)
14
+
15
+ test_cases = [
16
+ ("Tôi muốn giảm cân", Language.VIETNAMESE),
17
+ ("I want to lose weight", Language.ENGLISH),
18
+ ("Bạn có thể giúp tôi không?", Language.VIETNAMESE),
19
+ ("Can you help me?", Language.ENGLISH),
20
+ ("Tôi bị đau đầu", Language.VIETNAMESE),
21
+ ("I have a headache", Language.ENGLISH),
22
+ ("Làm sao để tăng cơ?", Language.VIETNAMESE),
23
+ ("How to build muscle?", Language.ENGLISH),
24
+ ]
25
+
26
+ for text, expected in test_cases:
27
+ detected = detect_language(text)
28
+ status = "✅" if detected == expected else "❌"
29
+ print(f"{status} '{text}' → {detected.value} (expected: {expected.value})")
30
+
31
+
32
+ def test_translations():
33
+ """Test translation system"""
34
+ print("\n" + "=" * 60)
35
+ print("TRANSLATION TEST")
36
+ print("=" * 60)
37
+
38
+ keys = [
39
+ 'greeting',
40
+ 'ask_age',
41
+ 'ask_weight',
42
+ 'bmi_normal',
43
+ 'thank_you_feedback',
44
+ 'error_occurred'
45
+ ]
46
+
47
+ print("\n🇻🇳 Vietnamese:")
48
+ for key in keys:
49
+ text = t(key, Language.VIETNAMESE)
50
+ print(f" {key}: {text}")
51
+
52
+ print("\n🇬🇧 English:")
53
+ for key in keys:
54
+ text = t(key, Language.ENGLISH)
55
+ print(f" {key}: {text}")
56
+
57
+
58
+ def test_agent_prompts():
59
+ """Test agent system prompts in both languages"""
60
+ print("\n" + "=" * 60)
61
+ print("AGENT PROMPTS TEST")
62
+ print("=" * 60)
63
+
64
+ agents = ['nutrition', 'exercise', 'symptom', 'mental_health']
65
+
66
+ for agent in agents:
67
+ print(f"\n📋 {agent.upper()} Agent:")
68
+
69
+ print("\n🇻🇳 Vietnamese:")
70
+ prompt_vi = Translations.get_agent_prompt(agent, Language.VIETNAMESE)
71
+ print(f" {prompt_vi[:100]}...")
72
+
73
+ print("\n🇬🇧 English:")
74
+ prompt_en = Translations.get_agent_prompt(agent, Language.ENGLISH)
75
+ print(f" {prompt_en[:100]}...")
76
+
77
+
78
+ def test_multilingual_handler():
79
+ """Test multilingual handler"""
80
+ print("\n" + "=" * 60)
81
+ print("MULTILINGUAL HANDLER TEST")
82
+ print("=" * 60)
83
+
84
+ handler = get_multilingual_handler()
85
+
86
+ # Simulate users with different languages
87
+ print("\n👤 User 1 (Vietnamese):")
88
+ lang1 = handler.detect_and_set_language("user1", "Tôi muốn giảm cân")
89
+ print(f" Detected: {lang1.value}")
90
+ print(f" Greeting: {handler.translate_message('greeting', lang1)}")
91
+
92
+ print("\n👤 User 2 (English):")
93
+ lang2 = handler.detect_and_set_language("user2", "I want to lose weight")
94
+ print(f" Detected: {lang2.value}")
95
+ print(f" Greeting: {handler.translate_message('greeting', lang2)}")
96
+
97
+ print("\n👤 User 3 (Vietnamese):")
98
+ lang3 = handler.detect_and_set_language("user3", "Tôi bị đau đầu")
99
+ print(f" Detected: {lang3.value}")
100
+ print(f" Greeting: {handler.translate_message('greeting', lang3)}")
101
+
102
+ # Get statistics
103
+ print("\n📊 Language Statistics:")
104
+ stats = handler.get_language_stats()
105
+ print(f" Total users: {stats['total_users']}")
106
+ print(f" Vietnamese: {stats['vietnamese_users']} ({stats['vietnamese_percentage']}%)")
107
+ print(f" English: {stats['english_users']} ({stats['english_percentage']}%)")
108
+
109
+
110
+ def test_conversation_flow():
111
+ """Test full conversation flow with language detection"""
112
+ print("\n" + "=" * 60)
113
+ print("CONVERSATION FLOW TEST")
114
+ print("=" * 60)
115
+
116
+ handler = get_multilingual_handler()
117
+
118
+ # Vietnamese conversation
119
+ print("\n🇻🇳 Vietnamese Conversation:")
120
+ print("-" * 40)
121
+
122
+ user_msg_vi = "Tôi muốn giảm cân"
123
+ lang_vi = handler.detect_and_set_language("user_vi", user_msg_vi)
124
+
125
+ print(f"User: {user_msg_vi}")
126
+ print(f"Detected language: {lang_vi.value}")
127
+ print(f"Bot: {handler.translate_message('greeting', lang_vi)}")
128
+ print(f"Bot: {handler.translate_message('ask_age', lang_vi)}")
129
+
130
+ # English conversation
131
+ print("\n🇬🇧 English Conversation:")
132
+ print("-" * 40)
133
+
134
+ user_msg_en = "I want to lose weight"
135
+ lang_en = handler.detect_and_set_language("user_en", user_msg_en)
136
+
137
+ print(f"User: {user_msg_en}")
138
+ print(f"Detected language: {lang_en.value}")
139
+ print(f"Bot: {handler.translate_message('greeting', lang_en)}")
140
+ print(f"Bot: {handler.translate_message('ask_age', lang_en)}")
141
+
142
+
143
+ def test_mixed_language():
144
+ """Test handling of mixed language input"""
145
+ print("\n" + "=" * 60)
146
+ print("MIXED LANGUAGE TEST")
147
+ print("=" * 60)
148
+
149
+ handler = get_multilingual_handler()
150
+
151
+ # User starts in Vietnamese
152
+ print("\n👤 User starts in Vietnamese:")
153
+ lang1 = handler.detect_and_set_language("user_mixed", "Tôi muốn giảm cân")
154
+ print(f" Message: 'Tôi muốn giảm cân'")
155
+ print(f" Detected: {lang1.value}")
156
+ print(f" Response: {handler.translate_message('greeting', lang1)}")
157
+
158
+ # User switches to English
159
+ print("\n👤 User switches to English:")
160
+ lang2 = handler.detect_and_set_language("user_mixed", "How many calories should I eat?")
161
+ print(f" Message: 'How many calories should I eat?'")
162
+ print(f" Detected: {lang2.value}")
163
+ print(f" Response: {handler.translate_message('nutrition_advice', lang2)}")
164
+
165
+ # User switches back to Vietnamese
166
+ print("\n👤 User switches back to Vietnamese:")
167
+ lang3 = handler.detect_and_set_language("user_mixed", "Cảm ơn bạn!")
168
+ print(f" Message: 'Cảm ơn bạn!'")
169
+ print(f" Detected: {lang3.value}")
170
+ print(f" Response: {handler.translate_message('thank_you_feedback', lang3)}")
171
+
172
+
173
+ def demo_real_world_usage():
174
+ """Demo real-world usage scenario"""
175
+ print("\n" + "=" * 60)
176
+ print("REAL-WORLD USAGE DEMO")
177
+ print("=" * 60)
178
+
179
+ handler = get_multilingual_handler()
180
+
181
+ scenarios = [
182
+ {
183
+ 'user_id': 'nguyen_van_a',
184
+ 'messages': [
185
+ "Tôi 25 tuổi, 70kg, 175cm",
186
+ "Tôi muốn giảm 5kg trong 2 tháng",
187
+ "Tôi nên ăn bao nhiêu calo?"
188
+ ]
189
+ },
190
+ {
191
+ 'user_id': 'john_smith',
192
+ 'messages': [
193
+ "I'm 30 years old, 80kg, 180cm",
194
+ "I want to build muscle",
195
+ "What exercises should I do?"
196
+ ]
197
+ }
198
+ ]
199
+
200
+ for scenario in scenarios:
201
+ user_id = scenario['user_id']
202
+ print(f"\n👤 User: {user_id}")
203
+ print("-" * 40)
204
+
205
+ for msg in scenario['messages']:
206
+ lang = handler.detect_and_set_language(user_id, msg)
207
+ print(f"\nUser ({lang.value}): {msg}")
208
+
209
+ # Simulate bot response
210
+ if "calo" in msg.lower() or "calories" in msg.lower():
211
+ response_key = 'nutrition_advice'
212
+ elif "tập" in msg.lower() or "exercise" in msg.lower():
213
+ response_key = 'exercise_plan'
214
+ else:
215
+ response_key = 'greeting'
216
+
217
+ response = handler.translate_message(response_key, lang)
218
+ print(f"Bot ({lang.value}): {response}")
219
+
220
+
221
+ if __name__ == '__main__':
222
+ test_language_detection()
223
+ test_translations()
224
+ test_agent_prompts()
225
+ test_multilingual_handler()
226
+ test_conversation_flow()
227
+ test_mixed_language()
228
+ demo_real_world_usage()
229
+
230
+ print("\n" + "=" * 60)
231
+ print("✅ MULTILINGUAL SUPPORT DEMO COMPLETE!")
232
+ print("=" * 60)
233
+ print("\nKey Features:")
234
+ print("✅ Automatic language detection (Vietnamese/English)")
235
+ print("✅ Bilingual translations for all messages")
236
+ print("✅ Language-specific agent prompts")
237
+ print("✅ Seamless language switching")
238
+ print("✅ User language preferences")
239
+ print("✅ Language usage statistics")
examples/pydantic_validation_example.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic Validation Example
3
+ Demonstrates automatic parsing and validation of health data
4
+ """
5
+
6
+ from health_data import (
7
+ PydanticUserHealthProfile,
8
+ PydanticHealthRecord,
9
+ NutritionRecord,
10
+ ExerciseRecord,
11
+ HealthDataParser,
12
+ merge_records,
13
+ RecordType
14
+ )
15
+ from datetime import datetime, timedelta
16
+
17
+
18
+ def test_height_parsing():
19
+ """Test parsing height from various formats"""
20
+ print("=" * 60)
21
+ print("HEIGHT PARSING TEST")
22
+ print("=" * 60)
23
+
24
+ test_cases = [
25
+ "1.78m", # Meters
26
+ "1,78m", # Comma separator
27
+ "178cm", # Centimeters
28
+ "178", # Just number
29
+ "1.78", # Decimal
30
+ "5'10\"", # Feet/inches
31
+ ]
32
+
33
+ for test in test_cases:
34
+ result = HealthDataParser.parse_height(test)
35
+ print(f"Input: {test:15} → {result} cm")
36
+
37
+
38
+ def test_weight_parsing():
39
+ """Test parsing weight from various formats"""
40
+ print("\n" + "=" * 60)
41
+ print("WEIGHT PARSING TEST")
42
+ print("=" * 60)
43
+
44
+ test_cases = [
45
+ "70kg", # Kilograms
46
+ "70", # Just number
47
+ "154lbs", # Pounds
48
+ "70.5", # Decimal
49
+ ]
50
+
51
+ for test in test_cases:
52
+ result = HealthDataParser.parse_weight(test)
53
+ print(f"Input: {test:15} → {result} kg")
54
+
55
+
56
+ def test_pydantic_validation():
57
+ """Test Pydantic automatic validation"""
58
+ print("\n" + "=" * 60)
59
+ print("PYDANTIC VALIDATION TEST")
60
+ print("=" * 60)
61
+
62
+ # Test 1: Valid data with various formats
63
+ print("\n✅ Test 1: Valid data with mixed formats")
64
+ try:
65
+ profile = PydanticUserHealthProfile(
66
+ user_id="user123",
67
+ age="25 tuổi", # Will parse to 25
68
+ gender="male",
69
+ weight="70kg", # Will parse to 70.0
70
+ height="1.78m" # Will parse to 178.0
71
+ )
72
+ print(f" Age: {profile.age}")
73
+ print(f" Weight: {profile.weight} kg")
74
+ print(f" Height: {profile.height} cm")
75
+ print(f" BMI: {profile.bmi} ({profile.get_bmi_category()})")
76
+ print(" ✅ Success!")
77
+ except Exception as e:
78
+ print(f" ❌ Error: {e}")
79
+
80
+ # Test 2: Invalid height (too high)
81
+ print("\n❌ Test 2: Invalid height (too high)")
82
+ try:
83
+ profile = PydanticUserHealthProfile(
84
+ user_id="user456",
85
+ height="500cm" # Too high!
86
+ )
87
+ print(" ❌ Should have failed!")
88
+ except Exception as e:
89
+ print(f" ✅ Caught error: {e}")
90
+
91
+ # Test 3: Invalid age (too young)
92
+ print("\n❌ Test 3: Invalid age (too young)")
93
+ try:
94
+ profile = PydanticUserHealthProfile(
95
+ user_id="user789",
96
+ age=10 # Too young!
97
+ )
98
+ print(" ❌ Should have failed!")
99
+ except Exception as e:
100
+ print(f" ✅ Caught error: {e}")
101
+
102
+ # Test 4: Auto BMI calculation
103
+ print("\n✅ Test 4: Auto BMI calculation")
104
+ profile = PydanticUserHealthProfile(
105
+ user_id="user999",
106
+ weight="70kg",
107
+ height="1,75m" # Comma separator!
108
+ )
109
+ print(f" Weight: {profile.weight} kg")
110
+ print(f" Height: {profile.height} cm")
111
+ print(f" BMI: {profile.bmi} (auto-calculated)")
112
+ print(f" Category: {profile.get_bmi_category()}")
113
+
114
+
115
+ def test_health_records():
116
+ """Test health records with validation"""
117
+ print("\n" + "=" * 60)
118
+ print("HEALTH RECORDS TEST")
119
+ print("=" * 60)
120
+
121
+ # Create nutrition record
122
+ print("\n📊 Creating Nutrition Record")
123
+ nutrition = NutritionRecord(
124
+ user_id="user123",
125
+ height="1.78m",
126
+ weight="70kg",
127
+ data={
128
+ 'calories': 2000,
129
+ 'protein': 150,
130
+ 'carbs': 200,
131
+ 'fat': 60
132
+ }
133
+ )
134
+ print(f" Height: {nutrition.height} cm")
135
+ print(f" Weight: {nutrition.weight} kg")
136
+ print(f" BMI: {nutrition.bmi}")
137
+ print(f" Calories: {nutrition.data['calories']}")
138
+
139
+ # Create exercise record
140
+ print("\n🏃 Creating Exercise Record")
141
+ exercise = ExerciseRecord(
142
+ user_id="user123",
143
+ data={
144
+ 'exercise_type': 'cardio',
145
+ 'duration_minutes': 30,
146
+ 'calories_burned': 300
147
+ }
148
+ )
149
+ print(f" Type: {exercise.data['exercise_type']}")
150
+ print(f" Duration: {exercise.data['duration_minutes']} min")
151
+ print(f" Calories: {exercise.data['calories_burned']}")
152
+
153
+
154
+ def test_merge_records():
155
+ """Test merging records from multiple days"""
156
+ print("\n" + "=" * 60)
157
+ print("MERGE RECORDS TEST")
158
+ print("=" * 60)
159
+
160
+ # Create sample records over 7 days
161
+ records = []
162
+ base_date = datetime.now() - timedelta(days=7)
163
+
164
+ for i in range(7):
165
+ # Nutrition record
166
+ nutrition = NutritionRecord(
167
+ user_id="user123",
168
+ weight=70 - i * 0.2, # Gradually losing weight
169
+ height=178,
170
+ data={
171
+ 'calories': 1800 + i * 50,
172
+ 'protein': 140 + i * 5,
173
+ }
174
+ )
175
+ nutrition.timestamp = base_date + timedelta(days=i)
176
+ records.append(nutrition)
177
+
178
+ # Exercise record
179
+ exercise = ExerciseRecord(
180
+ user_id="user123",
181
+ data={
182
+ 'exercise_type': 'cardio' if i % 2 == 0 else 'strength',
183
+ 'duration_minutes': 30 + i * 5,
184
+ 'calories_burned': 250 + i * 20
185
+ }
186
+ )
187
+ exercise.timestamp = base_date + timedelta(days=i)
188
+ records.append(exercise)
189
+
190
+ print(f"\n📦 Created {len(records)} records over 7 days")
191
+
192
+ # Merge with average strategy
193
+ print("\n📊 Merging with 'average' strategy:")
194
+ merged = merge_records(records, strategy='average')
195
+
196
+ print(f"\nTotal records: {merged['total_records']}")
197
+ print(f"Date range: {merged['date_range']['start'][:10]} to {merged['date_range']['end'][:10]}")
198
+
199
+ if 'nutrition' in merged['by_type']:
200
+ nutrition_data = merged['by_type']['nutrition']
201
+ print(f"\n🍎 Nutrition Summary:")
202
+ print(f" Average calories: {nutrition_data['average_daily']['calories']}")
203
+ print(f" Average protein: {nutrition_data['average_daily']['protein']}g")
204
+
205
+ if 'exercise' in merged['by_type']:
206
+ exercise_data = merged['by_type']['exercise']
207
+ print(f"\n🏃 Exercise Summary:")
208
+ print(f" Total workouts: {exercise_data['total_workouts']}")
209
+ print(f" Total duration: {exercise_data['total_duration_minutes']} min")
210
+ print(f" Total calories burned: {exercise_data['total_calories_burned']}")
211
+ print(f" Exercise types: {exercise_data['exercise_types']}")
212
+
213
+ if 'health_metrics' in merged and 'weight' in merged['health_metrics']:
214
+ weight_data = merged['health_metrics']['weight']
215
+ print(f"\n⚖️ Weight Progress:")
216
+ print(f" Start: {weight_data['max']} kg")
217
+ print(f" End: {weight_data['latest']} kg")
218
+ print(f" Change: {weight_data['change']} kg")
219
+ print(f" Average: {weight_data['average']} kg")
220
+
221
+
222
+ if __name__ == '__main__':
223
+ test_height_parsing()
224
+ test_weight_parsing()
225
+ test_pydantic_validation()
226
+ test_health_records()
227
+ test_merge_records()
228
+
229
+ print("\n" + "=" * 60)
230
+ print("✅ ALL TESTS COMPLETE!")
231
+ print("=" * 60)
examples/session_persistence_example.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session Persistence Example
3
+ Demonstrates how conversation memory persists across sessions
4
+ """
5
+
6
+ from agents.core.coordinator import AgentCoordinator
7
+
8
+
9
+ def example_first_session():
10
+ """First session - user provides information"""
11
+ print("=" * 60)
12
+ print("SESSION 1: User provides information")
13
+ print("=" * 60)
14
+
15
+ # Create coordinator with user_id
16
+ coordinator = AgentCoordinator(user_id="user123")
17
+
18
+ # User provides information
19
+ query1 = "Tôi 25 tuổi, nam, 70kg, 175cm, muốn giảm cân"
20
+ response1 = coordinator.handle_query(query1, [])
21
+
22
+ print(f"\nUser: {query1}")
23
+ print(f"Bot: {response1[:200]}...")
24
+
25
+ # Check what's in memory
26
+ profile = coordinator.memory.get_full_profile()
27
+ print(f"\n📊 Memory saved:")
28
+ print(f" Age: {profile['age']}")
29
+ print(f" Gender: {profile['gender']}")
30
+ print(f" Weight: {profile['weight']}kg")
31
+ print(f" Height: {profile['height']}cm")
32
+
33
+ print("\n✅ Session saved automatically!")
34
+ print(" (User closes app)")
35
+
36
+
37
+ def example_second_session():
38
+ """Second session - memory is restored"""
39
+ print("\n" + "=" * 60)
40
+ print("SESSION 2: User returns (next day)")
41
+ print("=" * 60)
42
+
43
+ # Create NEW coordinator with SAME user_id
44
+ # Memory will be automatically loaded!
45
+ coordinator = AgentCoordinator(user_id="user123")
46
+
47
+ # Check memory - it should be loaded!
48
+ profile = coordinator.memory.get_full_profile()
49
+ print(f"\n📊 Memory restored:")
50
+ print(f" Age: {profile['age']}")
51
+ print(f" Gender: {profile['gender']}")
52
+ print(f" Weight: {profile['weight']}kg")
53
+ print(f" Height: {profile['height']}cm")
54
+
55
+ # User asks new question - bot remembers!
56
+ query2 = "Tôi nên ăn bao nhiêu calo mỗi ngày?"
57
+ response2 = coordinator.handle_query(query2, [])
58
+
59
+ print(f"\nUser: {query2}")
60
+ print(f"Bot: {response2[:200]}...")
61
+ print("\n✅ Bot remembers user info from previous session!")
62
+
63
+
64
+ def example_different_user():
65
+ """Different user - separate session"""
66
+ print("\n" + "=" * 60)
67
+ print("SESSION 3: Different user")
68
+ print("=" * 60)
69
+
70
+ # Different user_id = different session
71
+ coordinator = AgentCoordinator(user_id="user456")
72
+
73
+ profile = coordinator.memory.get_full_profile()
74
+ print(f"\n📊 Memory for user456:")
75
+ print(f" Age: {profile['age']}") # Should be None
76
+ print(f" Gender: {profile['gender']}") # Should be None
77
+
78
+ print("\n✅ Each user has separate session!")
79
+
80
+
81
+ def example_without_persistence():
82
+ """Without user_id - no persistence"""
83
+ print("\n" + "=" * 60)
84
+ print("SESSION 4: Without persistence (no user_id)")
85
+ print("=" * 60)
86
+
87
+ # No user_id = no persistence
88
+ coordinator = AgentCoordinator() # No user_id
89
+
90
+ print("\n⚠️ Memory will NOT persist across sessions")
91
+ print(" (Useful for anonymous/guest users)")
92
+
93
+
94
+ if __name__ == '__main__':
95
+ # Run examples
96
+ example_first_session()
97
+ example_second_session()
98
+ example_different_user()
99
+ example_without_persistence()
100
+
101
+ print("\n" + "=" * 60)
102
+ print("✅ Session Persistence Demo Complete!")
103
+ print("=" * 60)
examples/summarization_example.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation Summarization Example
3
+ Demonstrates automatic summarization of long conversations
4
+ """
5
+
6
+ from agents.core.coordinator import AgentCoordinator
7
+
8
+
9
+ def simulate_long_conversation():
10
+ """Simulate a long conversation to trigger summarization"""
11
+ print("=" * 60)
12
+ print("CONVERSATION SUMMARIZATION DEMO")
13
+ print("=" * 60)
14
+
15
+ coordinator = AgentCoordinator(user_id="demo_user")
16
+
17
+ # Simulate 20 conversation turns
18
+ conversations = [
19
+ ("Tôi 25 tuổi, nam, 70kg, 175cm", "Cảm ơn thông tin..."),
20
+ ("Tôi muốn giảm cân", "Để giảm cân hiệu quả..."),
21
+ ("Nên ăn bao nhiêu calo?", "Với thông tin của bạn..."),
22
+ ("Tôi nên tập gì?", "Bạn nên tập cardio..."),
23
+ ("Bao lâu thì thấy kết quả?", "Thường sau 2-4 tuần..."),
24
+ ("Tôi có thể ăn gì?", "Bạn nên ăn nhiều rau xanh..."),
25
+ ("Sáng nên ăn gì?", "Bữa sáng nên có protein..."),
26
+ ("Tối nên ăn gì?", "Bữa tối nên nhẹ..."),
27
+ ("Tôi có thể ăn trái cây không?", "Có, nhưng hạn chế..."),
28
+ ("Nên tập mấy lần 1 tuần?", "Nên tập 3-4 lần..."),
29
+ ("Mỗi lần tập bao lâu?", "Mỗi lần 30-45 phút..."),
30
+ ("Tôi nên uống bao nhiêu nước?", "Nên uống 2-3 lít..."),
31
+ ("Có nên nhịn ăn không?", "Không nên nhịn ăn..."),
32
+ ("Tôi có thể ăn đêm không?", "Nên tránh ăn đêm..."),
33
+ ("Làm sao để không đói?", "Ăn nhiều protein..."),
34
+ ("Tôi bị đau đầu khi tập", "Có thể do thiếu nước..."),
35
+ ("Nên bổ sung gì?", "Có thể bổ sung vitamin..."),
36
+ ("Tôi có cần whey protein không?", "Không bắt buộc..."),
37
+ ("Khi nào nên nghỉ?", "Nên nghỉ 1-2 ngày..."),
38
+ ("Làm sao biết đang giảm cân đúng?", "Theo dõi cân nặng..."),
39
+ ]
40
+
41
+ chat_history = []
42
+
43
+ for i, (user_msg, bot_msg) in enumerate(conversations, 1):
44
+ chat_history.append((user_msg, bot_msg))
45
+
46
+ # Show progress
47
+ if i % 5 == 0:
48
+ print(f"\n📊 After {i} turns:")
49
+ stats = coordinator.get_conversation_stats(chat_history)
50
+ print(f" Total turns: {stats['total_turns']}")
51
+ print(f" Estimated tokens: {stats['estimated_tokens']}")
52
+ print(f" Should summarize: {stats['should_summarize']}")
53
+
54
+ print(f"\n" + "=" * 60)
55
+ print("BEFORE SUMMARIZATION")
56
+ print("=" * 60)
57
+ print(f"Total conversation turns: {len(chat_history)}")
58
+
59
+ # Trigger summarization
60
+ print(f"\n" + "=" * 60)
61
+ print("APPLYING SUMMARIZATION")
62
+ print("=" * 60)
63
+
64
+ # This happens automatically in coordinator.handle_query()
65
+ # But we can also do it manually:
66
+ from utils.conversation_summarizer import get_summarizer
67
+
68
+ summarizer = get_summarizer()
69
+ result = summarizer.summarize_conversation(
70
+ chat_history,
71
+ user_profile=coordinator.memory.get_full_profile(),
72
+ keep_recent=5
73
+ )
74
+
75
+ print(f"\n📝 SUMMARY:")
76
+ print(result['summary'])
77
+
78
+ print(f"\n💬 RECENT HISTORY ({len(result['recent_history'])} turns):")
79
+ for user_msg, bot_msg in result['recent_history']:
80
+ print(f" User: {user_msg}")
81
+ print(f" Bot: {bot_msg[:50]}...")
82
+
83
+ print(f"\n" + "=" * 60)
84
+ print("AFTER SUMMARIZATION")
85
+ print("=" * 60)
86
+ print(f"Summarized turns: {result['summarized_turns']}")
87
+ print(f"Kept recent turns: {len(result['recent_history'])}")
88
+ print(f"Total context size: {result['summarized_turns'] + len(result['recent_history'])} → {len(result['recent_history']) + 1} (summary + recent)")
89
+
90
+ # Show compressed history
91
+ compressed = summarizer.compress_history(chat_history, target_turns=10)
92
+ print(f"\n📦 Compressed history: {len(chat_history)} → {len(compressed)} turns")
93
+ print(f" Token reduction: ~{((len(chat_history) - len(compressed)) / len(chat_history) * 100):.0f}%")
94
+
95
+
96
+ def test_automatic_summarization():
97
+ """Test automatic summarization in coordinator"""
98
+ print("\n\n" + "=" * 60)
99
+ print("AUTOMATIC SUMMARIZATION TEST")
100
+ print("=" * 60)
101
+
102
+ coordinator = AgentCoordinator(user_id="test_user")
103
+
104
+ # Create long history
105
+ chat_history = [
106
+ (f"Câu hỏi {i}", f"Câu trả lời {i}")
107
+ for i in range(1, 21)
108
+ ]
109
+
110
+ print(f"Initial history: {len(chat_history)} turns")
111
+
112
+ # This will trigger automatic summarization
113
+ response = coordinator.handle_query(
114
+ "Tôi muốn tóm tắt cuộc trò chuyện",
115
+ chat_history
116
+ )
117
+
118
+ print(f"\n✅ Automatic summarization triggered!")
119
+ print(f" Response: {response[:100]}...")
120
+
121
+
122
+ if __name__ == '__main__':
123
+ simulate_long_conversation()
124
+ test_automatic_summarization()
125
+
126
+ print("\n" + "=" * 60)
127
+ print("✅ Summarization Demo Complete!")
128
+ print("=" * 60)
feedback/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback Module
3
+ Collect and learn from user ratings and corrections
4
+ """
5
+
6
+ from .feedback_system import (
7
+ FeedbackCollector,
8
+ FeedbackType,
9
+ FeedbackCategory,
10
+ get_feedback_collector
11
+ )
12
+
13
+ from .feedback_analyzer import (
14
+ FeedbackAnalyzer,
15
+ get_feedback_analyzer
16
+ )
17
+
18
+ __all__ = [
19
+ 'FeedbackCollector',
20
+ 'FeedbackType',
21
+ 'FeedbackCategory',
22
+ 'get_feedback_collector',
23
+ 'FeedbackAnalyzer',
24
+ 'get_feedback_analyzer'
25
+ ]
feedback/feedback_analyzer.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback Analyzer
3
+ Analyze feedback patterns and generate insights for improvement
4
+ """
5
+
6
+ import json
7
+ from datetime import datetime, timedelta
8
+ from pathlib import Path
9
+ from typing import Dict, Any, List, Optional
10
+ from collections import defaultdict, Counter
11
+ import re
12
+
13
+
14
+ class FeedbackAnalyzer:
15
+ """Analyze feedback to identify improvement opportunities"""
16
+
17
+ def __init__(self, feedback_collector):
18
+ self.collector = feedback_collector
19
+
20
+ def analyze_agent_performance(self, agent_name: str) -> Dict[str, Any]:
21
+ """
22
+ Comprehensive analysis of agent performance
23
+
24
+ Args:
25
+ agent_name: Name of the agent to analyze
26
+
27
+ Returns:
28
+ Performance analysis
29
+ """
30
+ stats = self.collector.get_feedback_stats(agent_name=agent_name)
31
+ low_rated = self.collector.get_low_rated_responses(agent_name=agent_name)
32
+ corrections = self.collector.get_corrections(agent_name=agent_name)
33
+
34
+ analysis = {
35
+ 'agent_name': agent_name,
36
+ 'overall_rating': stats['average_rating'],
37
+ 'total_feedback': stats['total_ratings'],
38
+ 'rating_distribution': stats['rating_distribution'],
39
+ 'strengths': [],
40
+ 'weaknesses': [],
41
+ 'common_issues': [],
42
+ 'improvement_suggestions': []
43
+ }
44
+
45
+ # Identify strengths (high-rated patterns)
46
+ if stats['average_rating'] >= 4.0:
47
+ analysis['strengths'].append("High overall satisfaction")
48
+
49
+ # Identify weaknesses (low-rated patterns)
50
+ if stats['rating_distribution'][1] + stats['rating_distribution'][2] > stats['total_ratings'] * 0.2:
51
+ analysis['weaknesses'].append("High number of low ratings (1-2 stars)")
52
+
53
+ # Analyze common issues from low-rated responses
54
+ if low_rated:
55
+ issues = self._extract_common_issues(low_rated)
56
+ analysis['common_issues'] = issues
57
+
58
+ # Analyze corrections
59
+ if corrections:
60
+ correction_patterns = self._analyze_corrections(corrections)
61
+ analysis['correction_patterns'] = correction_patterns
62
+
63
+ # Generate improvement suggestions
64
+ for pattern in correction_patterns:
65
+ analysis['improvement_suggestions'].append(
66
+ f"Improve {pattern['category']}: {pattern['suggestion']}"
67
+ )
68
+
69
+ return analysis
70
+
71
+ def _extract_common_issues(self, low_rated: List[Dict]) -> List[Dict[str, Any]]:
72
+ """Extract common issues from low-rated responses"""
73
+ issues = []
74
+
75
+ # Analyze comments
76
+ comments = [r.get('comment', '') for r in low_rated if r.get('comment')]
77
+
78
+ # Common keywords in negative feedback
79
+ issue_keywords = {
80
+ 'incorrect': 'Thông tin không chính xác',
81
+ 'wrong': 'Câu trả lời sai',
82
+ 'unhelpful': 'Không hữu ích',
83
+ 'confusing': 'Khó hiểu',
84
+ 'incomplete': 'Thiếu thông tin',
85
+ 'too long': 'Quá dài dòng',
86
+ 'too short': 'Quá ngắn gọn',
87
+ 'rude': 'Không lịch sự',
88
+ 'generic': 'Quá chung chung'
89
+ }
90
+
91
+ issue_counts = Counter()
92
+
93
+ for comment in comments:
94
+ comment_lower = comment.lower()
95
+ for keyword, description in issue_keywords.items():
96
+ if keyword in comment_lower:
97
+ issue_counts[description] += 1
98
+
99
+ # Get top issues
100
+ for issue, count in issue_counts.most_common(5):
101
+ issues.append({
102
+ 'issue': issue,
103
+ 'frequency': count,
104
+ 'percentage': round(count / len(low_rated) * 100, 1)
105
+ })
106
+
107
+ return issues
108
+
109
+ def _analyze_corrections(self, corrections: List[Dict]) -> List[Dict[str, Any]]:
110
+ """Analyze user corrections to find patterns"""
111
+ patterns = []
112
+
113
+ # Group by correction reason
114
+ by_reason = defaultdict(list)
115
+ for correction in corrections:
116
+ reason = correction.get('correction_reason', 'other')
117
+ by_reason[reason].append(correction)
118
+
119
+ # Analyze each category
120
+ for reason, items in by_reason.items():
121
+ if len(items) >= 2: # Only include if multiple occurrences
122
+ patterns.append({
123
+ 'category': reason,
124
+ 'count': len(items),
125
+ 'suggestion': self._generate_suggestion(reason, items)
126
+ })
127
+
128
+ return patterns
129
+
130
+ def _generate_suggestion(self, reason: str, corrections: List[Dict]) -> str:
131
+ """Generate improvement suggestion based on corrections"""
132
+ suggestions = {
133
+ 'incorrect_info': 'Verify medical information against authoritative sources',
134
+ 'missing_context': 'Ask more follow-up questions to gather context',
135
+ 'tone': 'Adjust tone to be more empathetic and supportive',
136
+ 'too_generic': 'Provide more personalized and specific advice',
137
+ 'calculation_error': 'Double-check all numerical calculations',
138
+ 'outdated_info': 'Update knowledge base with latest medical guidelines'
139
+ }
140
+
141
+ return suggestions.get(reason, f'Review and improve handling of: {reason}')
142
+
143
+ def get_trending_issues(self, days: int = 7) -> List[Dict[str, Any]]:
144
+ """
145
+ Get trending issues in recent feedback
146
+
147
+ Args:
148
+ days: Number of days to analyze
149
+
150
+ Returns:
151
+ List of trending issues
152
+ """
153
+ cutoff_date = datetime.now() - timedelta(days=days)
154
+
155
+ recent_low_rated = []
156
+ for file_path in (self.collector.storage_dir / "ratings").glob("*.json"):
157
+ with open(file_path, 'r', encoding='utf-8') as f:
158
+ data = json.load(f)
159
+ timestamp = datetime.fromisoformat(data.get('timestamp', ''))
160
+
161
+ if timestamp >= cutoff_date and data.get('rating', 5) <= 2:
162
+ recent_low_rated.append(data)
163
+
164
+ return self._extract_common_issues(recent_low_rated)
165
+
166
+ def compare_agents(self) -> Dict[str, Any]:
167
+ """
168
+ Compare performance across all agents
169
+
170
+ Returns:
171
+ Comparison data
172
+ """
173
+ stats = self.collector.get_feedback_stats()
174
+
175
+ comparison = {
176
+ 'agents': [],
177
+ 'best_agent': None,
178
+ 'worst_agent': None,
179
+ 'average_rating': stats['average_rating']
180
+ }
181
+
182
+ # Rank agents
183
+ agent_rankings = []
184
+ for agent, data in stats['by_agent'].items():
185
+ agent_rankings.append({
186
+ 'agent': agent,
187
+ 'average_rating': data['average'],
188
+ 'total_feedback': data['count']
189
+ })
190
+
191
+ # Sort by rating
192
+ agent_rankings.sort(key=lambda x: x['average_rating'], reverse=True)
193
+
194
+ comparison['agents'] = agent_rankings
195
+
196
+ if agent_rankings:
197
+ comparison['best_agent'] = agent_rankings[0]
198
+ comparison['worst_agent'] = agent_rankings[-1]
199
+
200
+ return comparison
201
+
202
+ def generate_improvement_report(self, agent_name: Optional[str] = None) -> str:
203
+ """
204
+ Generate a comprehensive improvement report
205
+
206
+ Args:
207
+ agent_name: Specific agent or all agents
208
+
209
+ Returns:
210
+ Formatted report
211
+ """
212
+ if agent_name:
213
+ analysis = self.analyze_agent_performance(agent_name)
214
+
215
+ report = f"""
216
+ # Feedback Analysis Report: {agent_name}
217
+ Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
218
+
219
+ ## Overall Performance
220
+ - Average Rating: {analysis['overall_rating']:.1f}/5.0
221
+ - Total Feedback: {analysis['total_feedback']}
222
+
223
+ ## Rating Distribution
224
+ - ⭐⭐⭐⭐⭐ (5 stars): {analysis['rating_distribution'][5]}
225
+ - ⭐⭐⭐⭐ (4 stars): {analysis['rating_distribution'][4]}
226
+ - ⭐⭐⭐ (3 stars): {analysis['rating_distribution'][3]}
227
+ - ⭐⭐ (2 stars): {analysis['rating_distribution'][2]}
228
+ - ⭐ (1 star): {analysis['rating_distribution'][1]}
229
+
230
+ ## Strengths
231
+ """
232
+ for strength in analysis['strengths']:
233
+ report += f"- ✅ {strength}\n"
234
+
235
+ report += "\n## Weaknesses\n"
236
+ for weakness in analysis['weaknesses']:
237
+ report += f"- ⚠️ {weakness}\n"
238
+
239
+ if analysis['common_issues']:
240
+ report += "\n## Common Issues\n"
241
+ for issue in analysis['common_issues']:
242
+ report += f"- {issue['issue']}: {issue['frequency']} occurrences ({issue['percentage']}%)\n"
243
+
244
+ if analysis['improvement_suggestions']:
245
+ report += "\n## Improvement Suggestions\n"
246
+ for i, suggestion in enumerate(analysis['improvement_suggestions'], 1):
247
+ report += f"{i}. {suggestion}\n"
248
+
249
+ return report
250
+
251
+ else:
252
+ # All agents comparison
253
+ comparison = self.compare_agents()
254
+
255
+ report = f"""
256
+ # Overall Feedback Analysis Report
257
+ Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
258
+
259
+ ## System-wide Performance
260
+ - Average Rating: {comparison['average_rating']:.1f}/5.0
261
+
262
+ ## Agent Rankings
263
+ """
264
+ for i, agent in enumerate(comparison['agents'], 1):
265
+ report += f"{i}. {agent['agent']}: {agent['average_rating']:.1f}/5.0 ({agent['total_feedback']} feedback)\n"
266
+
267
+ if comparison['best_agent']:
268
+ report += f"\n🏆 Best Performing: {comparison['best_agent']['agent']} ({comparison['best_agent']['average_rating']:.1f}/5.0)\n"
269
+
270
+ if comparison['worst_agent']:
271
+ report += f"⚠️ Needs Improvement: {comparison['worst_agent']['agent']} ({comparison['worst_agent']['average_rating']:.1f}/5.0)\n"
272
+
273
+ return report
274
+
275
+ def get_actionable_insights(self, agent_name: str, limit: int = 5) -> List[Dict[str, Any]]:
276
+ """
277
+ Get top actionable insights for improvement
278
+
279
+ Args:
280
+ agent_name: Agent to analyze
281
+ limit: Number of insights to return
282
+
283
+ Returns:
284
+ List of actionable insights
285
+ """
286
+ analysis = self.analyze_agent_performance(agent_name)
287
+ low_rated = self.collector.get_low_rated_responses(agent_name=agent_name, limit=20)
288
+ corrections = self.collector.get_corrections(agent_name=agent_name, limit=20)
289
+
290
+ insights = []
291
+
292
+ # Insight 1: Most common low-rating issue
293
+ if analysis['common_issues']:
294
+ top_issue = analysis['common_issues'][0]
295
+ insights.append({
296
+ 'priority': 'high',
297
+ 'category': 'quality',
298
+ 'issue': top_issue['issue'],
299
+ 'frequency': top_issue['frequency'],
300
+ 'action': f"Review and fix responses related to: {top_issue['issue']}",
301
+ 'examples': [r['user_message'] for r in low_rated[:3]]
302
+ })
303
+
304
+ # Insight 2: Correction patterns
305
+ if corrections:
306
+ insights.append({
307
+ 'priority': 'high',
308
+ 'category': 'accuracy',
309
+ 'issue': 'User corrections available',
310
+ 'frequency': len(corrections),
311
+ 'action': 'Incorporate user corrections into training data',
312
+ 'examples': [c['correction_reason'] for c in corrections[:3]]
313
+ })
314
+
315
+ # Insight 3: Rating trend
316
+ stats = self.collector.get_feedback_stats(agent_name=agent_name)
317
+ low_rating_pct = (stats['rating_distribution'][1] + stats['rating_distribution'][2]) / max(stats['total_ratings'], 1) * 100
318
+
319
+ if low_rating_pct > 20:
320
+ insights.append({
321
+ 'priority': 'critical',
322
+ 'category': 'overall',
323
+ 'issue': f'{low_rating_pct:.1f}% of ratings are 1-2 stars',
324
+ 'action': 'Urgent review needed - high dissatisfaction rate',
325
+ 'examples': []
326
+ })
327
+
328
+ return insights[:limit]
329
+
330
+
331
+ def get_feedback_analyzer(feedback_collector) -> FeedbackAnalyzer:
332
+ """Create feedback analyzer instance"""
333
+ return FeedbackAnalyzer(feedback_collector)
feedback/feedback_system.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback System
3
+ Collect and learn from user ratings and corrections
4
+ """
5
+
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any, List
10
+ from enum import Enum
11
+
12
+
13
+ class FeedbackType(str, Enum):
14
+ """Types of feedback"""
15
+ RATING = "rating"
16
+ CORRECTION = "correction"
17
+ THUMBS_UP = "thumbs_up"
18
+ THUMBS_DOWN = "thumbs_down"
19
+ REPORT = "report"
20
+
21
+
22
+ class FeedbackCategory(str, Enum):
23
+ """Feedback categories"""
24
+ ACCURACY = "accuracy"
25
+ HELPFULNESS = "helpfulness"
26
+ TONE = "tone"
27
+ COMPLETENESS = "completeness"
28
+ SAFETY = "safety"
29
+ OTHER = "other"
30
+
31
+
32
+ class FeedbackCollector:
33
+ """Collect user feedback on agent responses"""
34
+
35
+ def __init__(self, storage_dir: str = "feedback/data"):
36
+ self.storage_dir = Path(storage_dir)
37
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ # Create subdirectories
40
+ (self.storage_dir / "ratings").mkdir(exist_ok=True)
41
+ (self.storage_dir / "corrections").mkdir(exist_ok=True)
42
+ (self.storage_dir / "reports").mkdir(exist_ok=True)
43
+
44
+ def collect_rating(
45
+ self,
46
+ user_id: str,
47
+ agent_name: str,
48
+ user_message: str,
49
+ agent_response: str,
50
+ rating: int,
51
+ category: Optional[FeedbackCategory] = None,
52
+ comment: Optional[str] = None,
53
+ metadata: Optional[Dict[str, Any]] = None
54
+ ) -> str:
55
+ """
56
+ Collect user rating for an agent response
57
+
58
+ Args:
59
+ user_id: User identifier
60
+ agent_name: Name of the agent
61
+ user_message: User's original message
62
+ agent_response: Agent's response
63
+ rating: Rating (1-5 stars)
64
+ category: Feedback category
65
+ comment: Optional user comment
66
+ metadata: Additional metadata
67
+
68
+ Returns:
69
+ Feedback ID
70
+ """
71
+ feedback_id = f"{user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
72
+
73
+ feedback_data = {
74
+ 'feedback_id': feedback_id,
75
+ 'user_id': user_id,
76
+ 'agent_name': agent_name,
77
+ 'feedback_type': FeedbackType.RATING,
78
+ 'rating': rating,
79
+ 'category': category.value if category else None,
80
+ 'user_message': user_message,
81
+ 'agent_response': agent_response,
82
+ 'comment': comment,
83
+ 'metadata': metadata or {},
84
+ 'timestamp': datetime.now().isoformat()
85
+ }
86
+
87
+ # Save to file
88
+ file_path = self.storage_dir / "ratings" / f"{feedback_id}.json"
89
+ with open(file_path, 'w', encoding='utf-8') as f:
90
+ json.dump(feedback_data, f, ensure_ascii=False, indent=2)
91
+
92
+ return feedback_id
93
+
94
+ def collect_correction(
95
+ self,
96
+ user_id: str,
97
+ agent_name: str,
98
+ user_message: str,
99
+ agent_response: str,
100
+ corrected_response: str,
101
+ correction_reason: str,
102
+ metadata: Optional[Dict[str, Any]] = None
103
+ ) -> str:
104
+ """
105
+ Collect user correction for an agent response
106
+
107
+ Args:
108
+ user_id: User identifier
109
+ agent_name: Name of the agent
110
+ user_message: User's original message
111
+ agent_response: Agent's incorrect response
112
+ corrected_response: User's corrected response
113
+ correction_reason: Why the correction was needed
114
+ metadata: Additional metadata
115
+
116
+ Returns:
117
+ Feedback ID
118
+ """
119
+ feedback_id = f"{user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
120
+
121
+ feedback_data = {
122
+ 'feedback_id': feedback_id,
123
+ 'user_id': user_id,
124
+ 'agent_name': agent_name,
125
+ 'feedback_type': FeedbackType.CORRECTION,
126
+ 'user_message': user_message,
127
+ 'agent_response': agent_response,
128
+ 'corrected_response': corrected_response,
129
+ 'correction_reason': correction_reason,
130
+ 'metadata': metadata or {},
131
+ 'timestamp': datetime.now().isoformat()
132
+ }
133
+
134
+ # Save to file
135
+ file_path = self.storage_dir / "corrections" / f"{feedback_id}.json"
136
+ with open(file_path, 'w', encoding='utf-8') as f:
137
+ json.dump(feedback_data, f, ensure_ascii=False, indent=2)
138
+
139
+ return feedback_id
140
+
141
+ def collect_thumbs(
142
+ self,
143
+ user_id: str,
144
+ agent_name: str,
145
+ user_message: str,
146
+ agent_response: str,
147
+ is_positive: bool,
148
+ comment: Optional[str] = None
149
+ ) -> str:
150
+ """
151
+ Collect thumbs up/down feedback
152
+
153
+ Args:
154
+ user_id: User identifier
155
+ agent_name: Name of the agent
156
+ user_message: User's original message
157
+ agent_response: Agent's response
158
+ is_positive: True for thumbs up, False for thumbs down
159
+ comment: Optional comment
160
+
161
+ Returns:
162
+ Feedback ID
163
+ """
164
+ feedback_type = FeedbackType.THUMBS_UP if is_positive else FeedbackType.THUMBS_DOWN
165
+
166
+ return self.collect_rating(
167
+ user_id=user_id,
168
+ agent_name=agent_name,
169
+ user_message=user_message,
170
+ agent_response=agent_response,
171
+ rating=5 if is_positive else 1,
172
+ comment=comment,
173
+ metadata={'feedback_type': feedback_type}
174
+ )
175
+
176
+ def report_issue(
177
+ self,
178
+ user_id: str,
179
+ agent_name: str,
180
+ user_message: str,
181
+ agent_response: str,
182
+ issue_type: str,
183
+ description: str,
184
+ severity: str = "medium"
185
+ ) -> str:
186
+ """
187
+ Report an issue with agent response
188
+
189
+ Args:
190
+ user_id: User identifier
191
+ agent_name: Name of the agent
192
+ user_message: User's original message
193
+ agent_response: Agent's problematic response
194
+ issue_type: Type of issue (harmful/incorrect/inappropriate/other)
195
+ description: Detailed description
196
+ severity: low/medium/high/critical
197
+
198
+ Returns:
199
+ Report ID
200
+ """
201
+ report_id = f"report_{user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
202
+
203
+ report_data = {
204
+ 'report_id': report_id,
205
+ 'user_id': user_id,
206
+ 'agent_name': agent_name,
207
+ 'feedback_type': FeedbackType.REPORT,
208
+ 'user_message': user_message,
209
+ 'agent_response': agent_response,
210
+ 'issue_type': issue_type,
211
+ 'description': description,
212
+ 'severity': severity,
213
+ 'status': 'pending',
214
+ 'timestamp': datetime.now().isoformat()
215
+ }
216
+
217
+ # Save to file
218
+ file_path = self.storage_dir / "reports" / f"{report_id}.json"
219
+ with open(file_path, 'w', encoding='utf-8') as f:
220
+ json.dump(report_data, f, ensure_ascii=False, indent=2)
221
+
222
+ return report_id
223
+
224
+ def get_feedback_stats(self, agent_name: Optional[str] = None) -> Dict[str, Any]:
225
+ """
226
+ Get feedback statistics
227
+
228
+ Args:
229
+ agent_name: Filter by agent name (optional)
230
+
231
+ Returns:
232
+ Statistics dictionary
233
+ """
234
+ stats = {
235
+ 'total_ratings': 0,
236
+ 'total_corrections': 0,
237
+ 'total_reports': 0,
238
+ 'average_rating': 0.0,
239
+ 'rating_distribution': {1: 0, 2: 0, 3: 0, 4: 0, 5: 0},
240
+ 'by_agent': {},
241
+ 'by_category': {}
242
+ }
243
+
244
+ # Count ratings
245
+ ratings = []
246
+ for file_path in (self.storage_dir / "ratings").glob("*.json"):
247
+ with open(file_path, 'r', encoding='utf-8') as f:
248
+ data = json.load(f)
249
+
250
+ if agent_name and data.get('agent_name') != agent_name:
251
+ continue
252
+
253
+ rating = data.get('rating', 0)
254
+ ratings.append(rating)
255
+ stats['rating_distribution'][rating] += 1
256
+
257
+ # By agent
258
+ agent = data.get('agent_name', 'unknown')
259
+ if agent not in stats['by_agent']:
260
+ stats['by_agent'][agent] = {'count': 0, 'total_rating': 0}
261
+ stats['by_agent'][agent]['count'] += 1
262
+ stats['by_agent'][agent]['total_rating'] += rating
263
+
264
+ # By category
265
+ category = data.get('category', 'other')
266
+ if category not in stats['by_category']:
267
+ stats['by_category'][category] = 0
268
+ stats['by_category'][category] += 1
269
+
270
+ stats['total_ratings'] = len(ratings)
271
+ stats['average_rating'] = sum(ratings) / len(ratings) if ratings else 0.0
272
+
273
+ # Calculate average per agent
274
+ for agent in stats['by_agent']:
275
+ count = stats['by_agent'][agent]['count']
276
+ total = stats['by_agent'][agent]['total_rating']
277
+ stats['by_agent'][agent]['average'] = total / count if count > 0 else 0.0
278
+
279
+ # Count corrections
280
+ stats['total_corrections'] = len(list((self.storage_dir / "corrections").glob("*.json")))
281
+
282
+ # Count reports
283
+ stats['total_reports'] = len(list((self.storage_dir / "reports").glob("*.json")))
284
+
285
+ return stats
286
+
287
+ def get_low_rated_responses(
288
+ self,
289
+ min_rating: int = 2,
290
+ agent_name: Optional[str] = None,
291
+ limit: int = 50
292
+ ) -> List[Dict[str, Any]]:
293
+ """
294
+ Get low-rated responses for improvement
295
+
296
+ Args:
297
+ min_rating: Maximum rating to include (1-5)
298
+ agent_name: Filter by agent name
299
+ limit: Maximum number of results
300
+
301
+ Returns:
302
+ List of low-rated responses
303
+ """
304
+ low_rated = []
305
+
306
+ for file_path in (self.storage_dir / "ratings").glob("*.json"):
307
+ with open(file_path, 'r', encoding='utf-8') as f:
308
+ data = json.load(f)
309
+
310
+ if data.get('rating', 5) <= min_rating:
311
+ if agent_name is None or data.get('agent_name') == agent_name:
312
+ low_rated.append(data)
313
+
314
+ # Sort by rating (lowest first)
315
+ low_rated.sort(key=lambda x: x.get('rating', 5))
316
+
317
+ return low_rated[:limit]
318
+
319
+ def get_corrections(
320
+ self,
321
+ agent_name: Optional[str] = None,
322
+ limit: int = 100
323
+ ) -> List[Dict[str, Any]]:
324
+ """
325
+ Get user corrections for learning
326
+
327
+ Args:
328
+ agent_name: Filter by agent name
329
+ limit: Maximum number of results
330
+
331
+ Returns:
332
+ List of corrections
333
+ """
334
+ corrections = []
335
+
336
+ for file_path in (self.storage_dir / "corrections").glob("*.json"):
337
+ with open(file_path, 'r', encoding='utf-8') as f:
338
+ data = json.load(f)
339
+
340
+ if agent_name is None or data.get('agent_name') == agent_name:
341
+ corrections.append(data)
342
+
343
+ # Sort by timestamp (newest first)
344
+ corrections.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
345
+
346
+ return corrections[:limit]
347
+
348
+ def export_for_fine_tuning(
349
+ self,
350
+ agent_name: str,
351
+ min_rating: int = 4,
352
+ include_corrections: bool = True,
353
+ output_file: Optional[str] = None
354
+ ) -> str:
355
+ """
356
+ Export high-quality feedback for fine-tuning
357
+
358
+ Args:
359
+ agent_name: Agent to export for
360
+ min_rating: Minimum rating to include
361
+ include_corrections: Include user corrections
362
+ output_file: Output file path
363
+
364
+ Returns:
365
+ Path to exported file
366
+ """
367
+ if output_file is None:
368
+ output_file = f"feedback_training_{agent_name}_{datetime.now().strftime('%Y%m%d')}.jsonl"
369
+
370
+ output_path = self.storage_dir / output_file
371
+
372
+ training_data = []
373
+
374
+ # Add high-rated responses
375
+ for file_path in (self.storage_dir / "ratings").glob("*.json"):
376
+ with open(file_path, 'r', encoding='utf-8') as f:
377
+ data = json.load(f)
378
+
379
+ if data.get('agent_name') == agent_name and data.get('rating', 0) >= min_rating:
380
+ training_data.append({
381
+ 'messages': [
382
+ {'role': 'user', 'content': data['user_message']},
383
+ {'role': 'assistant', 'content': data['agent_response']}
384
+ ],
385
+ 'metadata': {
386
+ 'rating': data['rating'],
387
+ 'source': 'user_rating'
388
+ }
389
+ })
390
+
391
+ # Add corrections
392
+ if include_corrections:
393
+ for file_path in (self.storage_dir / "corrections").glob("*.json"):
394
+ with open(file_path, 'r', encoding='utf-8') as f:
395
+ data = json.load(f)
396
+
397
+ if data.get('agent_name') == agent_name:
398
+ training_data.append({
399
+ 'messages': [
400
+ {'role': 'user', 'content': data['user_message']},
401
+ {'role': 'assistant', 'content': data['corrected_response']}
402
+ ],
403
+ 'metadata': {
404
+ 'source': 'user_correction',
405
+ 'reason': data.get('correction_reason')
406
+ }
407
+ })
408
+
409
+ # Write to JSONL
410
+ with open(output_path, 'w', encoding='utf-8') as f:
411
+ for item in training_data:
412
+ f.write(json.dumps(item, ensure_ascii=False) + '\n')
413
+
414
+ return str(output_path)
415
+
416
+
417
+ # Global instance
418
+ _feedback_collector = None
419
+
420
+ def get_feedback_collector() -> FeedbackCollector:
421
+ """Get global feedback collector instance"""
422
+ global _feedback_collector
423
+ if _feedback_collector is None:
424
+ _feedback_collector = FeedbackCollector()
425
+ return _feedback_collector
fine_tuning/README.md ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fine-tuning Module 🎯
2
+
3
+ Train custom models on your healthcare conversation data.
4
+
5
+ ## Overview
6
+
7
+ This module automatically collects conversation data and enables fine-tuning of specialized healthcare agents using OpenAI's fine-tuning API.
8
+
9
+ ## Features
10
+
11
+ - ✅ **Automatic Data Collection** - Logs all agent conversations
12
+ - ✅ **Quality Filtering** - Filter by user ratings
13
+ - ✅ **OpenAI Format Export** - Ready for fine-tuning
14
+ - ✅ **Multi-Agent Support** - Train each agent separately
15
+ - ✅ **Job Management** - Track fine-tuning progress
16
+
17
+ ## How It Works
18
+
19
+ ### 1. Data Collection (Automatic)
20
+
21
+ The system automatically logs conversations when enabled:
22
+
23
+ ```python
24
+ # In coordinator (already integrated)
25
+ coordinator = AgentCoordinator(enable_data_collection=True)
26
+ ```
27
+
28
+ Data is stored in `fine_tuning/data/` organized by agent:
29
+ ```
30
+ fine_tuning/data/
31
+ ├── nutrition/
32
+ │ ├── conversations_20241025.jsonl
33
+ │ └── multi_turn_20241025.jsonl
34
+ ├── exercise/
35
+ ├── symptom/
36
+ ├── mental_health/
37
+ └── general_health/
38
+ ```
39
+
40
+ ### 2. Export Training Data
41
+
42
+ Export conversations in OpenAI fine-tuning format:
43
+
44
+ ```python
45
+ from fine_tuning import get_data_collector
46
+
47
+ collector = get_data_collector()
48
+
49
+ # Export all conversations
50
+ training_file = collector.export_for_openai_finetuning(
51
+ agent_name='nutrition_agent',
52
+ output_file='nutrition_training.jsonl'
53
+ )
54
+
55
+ # Export only high-quality conversations (rating >= 4.0)
56
+ training_file = collector.export_for_openai_finetuning(
57
+ agent_name='nutrition_agent',
58
+ min_quality_rating=4.0
59
+ )
60
+ ```
61
+
62
+ ### 3. Fine-tune Agent
63
+
64
+ #### Option A: Using Script (Recommended)
65
+
66
+ ```bash
67
+ # Fine-tune nutrition agent
68
+ python scripts/fine_tune_agent.py --agent nutrition
69
+
70
+ # With quality filtering
71
+ python scripts/fine_tune_agent.py --agent nutrition --min-rating 4.0
72
+
73
+ # Start job without waiting
74
+ python scripts/fine_tune_agent.py --agent exercise --no-wait
75
+ ```
76
+
77
+ #### Option B: Using Python API
78
+
79
+ ```python
80
+ from fine_tuning import fine_tune_agent
81
+
82
+ # Fine-tune and wait for completion
83
+ model_id = fine_tune_agent(
84
+ agent_name='nutrition',
85
+ training_file='nutrition_training.jsonl',
86
+ model='gpt-4o-mini-2024-07-18',
87
+ wait_for_completion=True
88
+ )
89
+
90
+ print(f"Fine-tuned model: {model_id}")
91
+ ```
92
+
93
+ ### 4. Use Fine-tuned Model
94
+
95
+ Update agent configuration to use the fine-tuned model:
96
+
97
+ ```python
98
+ # config/settings.py or agent file
99
+ MODEL = 'ft:gpt-4o-mini-2024-07-18:your-org:nutrition:abc123'
100
+ ```
101
+
102
+ ## Data Format
103
+
104
+ ### Conversation Entry
105
+ ```json
106
+ {
107
+ "timestamp": "2024-10-25T10:00:00",
108
+ "agent": "nutrition_agent",
109
+ "user_message": "Tôi muốn giảm cân",
110
+ "agent_response": "Để giảm cân hiệu quả...",
111
+ "user_data": {
112
+ "age": 25,
113
+ "gender": "male",
114
+ "weight": 70,
115
+ "height": 175
116
+ },
117
+ "metadata": {
118
+ "rating": 5.0,
119
+ "feedback": "Very helpful"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### OpenAI Fine-tuning Format
125
+ ```json
126
+ {
127
+ "messages": [
128
+ {"role": "system", "content": "You are a nutrition specialist."},
129
+ {"role": "user", "content": "Tôi muốn giảm cân"},
130
+ {"role": "assistant", "content": "Để giảm cân hiệu quả..."}
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ## API Reference
136
+
137
+ ### ConversationDataCollector
138
+
139
+ ```python
140
+ from fine_tuning import get_data_collector
141
+
142
+ collector = get_data_collector()
143
+
144
+ # Log single conversation
145
+ collector.log_conversation(
146
+ agent_name='nutrition_agent',
147
+ user_message='User question',
148
+ agent_response='Agent answer',
149
+ user_data={'age': 25},
150
+ metadata={'rating': 5.0}
151
+ )
152
+
153
+ # Log multi-turn conversation
154
+ collector.log_multi_turn_conversation(
155
+ agent_name='nutrition_agent',
156
+ conversation_history=[
157
+ ('User msg 1', 'Agent response 1'),
158
+ ('User msg 2', 'Agent response 2')
159
+ ],
160
+ user_data={'age': 25}
161
+ )
162
+
163
+ # Get conversation counts
164
+ counts = collector.get_conversation_count()
165
+ # {'nutrition': 150, 'exercise': 89, ...}
166
+
167
+ # Export for fine-tuning
168
+ training_file = collector.export_for_openai_finetuning(
169
+ agent_name='nutrition_agent',
170
+ min_quality_rating=4.0
171
+ )
172
+ ```
173
+
174
+ ### FineTuningTrainer
175
+
176
+ ```python
177
+ from fine_tuning import FineTuningTrainer
178
+
179
+ trainer = FineTuningTrainer()
180
+
181
+ # Upload training file
182
+ file_id = trainer.upload_training_file('training.jsonl')
183
+
184
+ # Create fine-tuning job
185
+ job_id = trainer.create_fine_tuning_job(
186
+ training_file_id=file_id,
187
+ model='gpt-4o-mini-2024-07-18',
188
+ suffix='nutrition-v1'
189
+ )
190
+
191
+ # Check job status
192
+ status = trainer.check_job_status(job_id)
193
+
194
+ # Wait for completion
195
+ result = trainer.wait_for_completion(job_id)
196
+
197
+ # List all fine-tuned models
198
+ models = trainer.list_fine_tuned_models()
199
+ ```
200
+
201
+ ## Best Practices
202
+
203
+ ### 1. Data Quality
204
+
205
+ - ✅ Collect at least **100-200 conversations** per agent
206
+ - ✅ Include diverse user queries and scenarios
207
+ - ✅ Filter by quality rating (>= 4.0 recommended)
208
+ - ✅ Review and clean data before fine-tuning
209
+
210
+ ### 2. Training
211
+
212
+ - ✅ Start with `gpt-4o-mini` (faster, cheaper)
213
+ - ✅ Use descriptive suffixes (e.g., `nutrition-v1`)
214
+ - ✅ Monitor training progress
215
+ - ✅ Test fine-tuned model before deployment
216
+
217
+ ### 3. Evaluation
218
+
219
+ - ✅ Compare fine-tuned vs base model responses
220
+ - ✅ Test on held-out conversations
221
+ - ✅ Collect user feedback on fine-tuned model
222
+ - ✅ Iterate based on results
223
+
224
+ ## Cost Estimation
225
+
226
+ Fine-tuning costs (OpenAI pricing):
227
+ - **Training**: ~$0.008 per 1K tokens
228
+ - **Usage**: Same as base model
229
+
230
+ Example:
231
+ - 200 conversations × 500 tokens avg = 100K tokens
232
+ - Training cost: ~$0.80
233
+ - Usage: Same as gpt-4o-mini
234
+
235
+ ## Troubleshooting
236
+
237
+ ### No conversations collected
238
+
239
+ ```bash
240
+ # Check if data collection is enabled
241
+ # In coordinator initialization:
242
+ coordinator = AgentCoordinator(enable_data_collection=True)
243
+ ```
244
+
245
+ ### Training file format error
246
+
247
+ ```bash
248
+ # Validate JSONL format
249
+ python -c "import json; [json.loads(line) for line in open('file.jsonl')]"
250
+ ```
251
+
252
+ ### Fine-tuning job failed
253
+
254
+ ```python
255
+ # Check job status
256
+ trainer = FineTuningTrainer()
257
+ status = trainer.check_job_status('job-id')
258
+ print(status)
259
+ ```
260
+
261
+ ## Examples
262
+
263
+ ### Complete Workflow
264
+
265
+ ```python
266
+ from fine_tuning import get_data_collector, fine_tune_agent
267
+
268
+ # 1. Check collected data
269
+ collector = get_data_collector()
270
+ counts = collector.get_conversation_count()
271
+ print(f"Nutrition conversations: {counts.get('nutrition', 0)}")
272
+
273
+ # 2. Export training data
274
+ training_file = collector.export_for_openai_finetuning(
275
+ agent_name='nutrition_agent',
276
+ min_quality_rating=4.0
277
+ )
278
+
279
+ # 3. Fine-tune
280
+ model_id = fine_tune_agent(
281
+ agent_name='nutrition',
282
+ training_file=training_file,
283
+ suffix='v1',
284
+ wait_for_completion=True
285
+ )
286
+
287
+ # 4. Update configuration
288
+ print(f"Update MODEL to: {model_id}")
289
+ ```
290
+
291
+ ## Future Enhancements
292
+
293
+ - [ ] Automatic quality scoring
294
+ - [ ] Data augmentation
295
+ - [ ] Multi-model comparison
296
+ - [ ] A/B testing framework
297
+ - [ ] Continuous learning pipeline
fine_tuning/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fine-tuning Module
3
+ Collect conversation data and train custom models
4
+ """
5
+
6
+ from .data_collector import ConversationDataCollector, get_data_collector
7
+ from .trainer import FineTuningTrainer, fine_tune_agent
8
+
9
+ __all__ = [
10
+ 'ConversationDataCollector',
11
+ 'get_data_collector',
12
+ 'FineTuningTrainer',
13
+ 'fine_tune_agent'
14
+ ]
fine_tuning/data_collector.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data Collector for Fine-tuning
3
+ Collects and stores conversation data for training custom models
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import List, Dict, Any, Optional
11
+
12
+
13
+ class ConversationDataCollector:
14
+ """Collects conversation data for fine-tuning"""
15
+
16
+ def __init__(self, data_dir: str = "fine_tuning/data"):
17
+ self.data_dir = Path(data_dir)
18
+ self.data_dir.mkdir(parents=True, exist_ok=True)
19
+
20
+ # Create subdirectories for each agent
21
+ self.agent_dirs = {
22
+ 'nutrition': self.data_dir / 'nutrition',
23
+ 'exercise': self.data_dir / 'exercise',
24
+ 'symptom': self.data_dir / 'symptom',
25
+ 'mental_health': self.data_dir / 'mental_health',
26
+ 'general_health': self.data_dir / 'general_health'
27
+ }
28
+
29
+ for agent_dir in self.agent_dirs.values():
30
+ agent_dir.mkdir(exist_ok=True)
31
+
32
+ def log_conversation(
33
+ self,
34
+ agent_name: str,
35
+ user_message: str,
36
+ agent_response: str,
37
+ user_data: Optional[Dict[str, Any]] = None,
38
+ metadata: Optional[Dict[str, Any]] = None
39
+ ) -> None:
40
+ """
41
+ Log a conversation turn for fine-tuning
42
+
43
+ Args:
44
+ agent_name: Name of the agent (nutrition, exercise, etc.)
45
+ user_message: User's message
46
+ agent_response: Agent's response
47
+ user_data: User profile data (age, gender, etc.)
48
+ metadata: Additional metadata (rating, feedback, etc.)
49
+ """
50
+ conversation_entry = {
51
+ 'timestamp': datetime.now().isoformat(),
52
+ 'agent': agent_name,
53
+ 'user_message': user_message,
54
+ 'agent_response': agent_response,
55
+ 'user_data': user_data or {},
56
+ 'metadata': metadata or {}
57
+ }
58
+
59
+ # Save to agent-specific file
60
+ agent_key = agent_name.replace('_agent', '')
61
+ if agent_key in self.agent_dirs:
62
+ filename = f"conversations_{datetime.now().strftime('%Y%m%d')}.jsonl"
63
+ filepath = self.agent_dirs[agent_key] / filename
64
+
65
+ with open(filepath, 'a', encoding='utf-8') as f:
66
+ f.write(json.dumps(conversation_entry, ensure_ascii=False) + '\n')
67
+
68
+ def log_multi_turn_conversation(
69
+ self,
70
+ agent_name: str,
71
+ conversation_history: List[tuple],
72
+ user_data: Optional[Dict[str, Any]] = None,
73
+ metadata: Optional[Dict[str, Any]] = None
74
+ ) -> None:
75
+ """
76
+ Log a multi-turn conversation
77
+
78
+ Args:
79
+ agent_name: Name of the agent
80
+ conversation_history: List of (user_msg, agent_msg) tuples
81
+ user_data: User profile data
82
+ metadata: Additional metadata
83
+ """
84
+ multi_turn_entry = {
85
+ 'timestamp': datetime.now().isoformat(),
86
+ 'agent': agent_name,
87
+ 'conversation': [
88
+ {'user': user_msg, 'agent': agent_msg}
89
+ for user_msg, agent_msg in conversation_history
90
+ ],
91
+ 'user_data': user_data or {},
92
+ 'metadata': metadata or {}
93
+ }
94
+
95
+ agent_key = agent_name.replace('_agent', '')
96
+ if agent_key in self.agent_dirs:
97
+ filename = f"multi_turn_{datetime.now().strftime('%Y%m%d')}.jsonl"
98
+ filepath = self.agent_dirs[agent_key] / filename
99
+
100
+ with open(filepath, 'a', encoding='utf-8') as f:
101
+ f.write(json.dumps(multi_turn_entry, ensure_ascii=False) + '\n')
102
+
103
+ def get_conversation_count(self, agent_name: Optional[str] = None) -> Dict[str, int]:
104
+ """
105
+ Get count of logged conversations
106
+
107
+ Args:
108
+ agent_name: Optional agent name to filter by
109
+
110
+ Returns:
111
+ Dict with agent names and conversation counts
112
+ """
113
+ counts = {}
114
+
115
+ agents_to_check = [agent_name.replace('_agent', '')] if agent_name else self.agent_dirs.keys()
116
+
117
+ for agent_key in agents_to_check:
118
+ if agent_key in self.agent_dirs:
119
+ agent_dir = self.agent_dirs[agent_key]
120
+ count = 0
121
+
122
+ for file in agent_dir.glob('conversations_*.jsonl'):
123
+ with open(file, 'r', encoding='utf-8') as f:
124
+ count += sum(1 for _ in f)
125
+
126
+ counts[agent_key] = count
127
+
128
+ return counts
129
+
130
+ def export_for_openai_finetuning(
131
+ self,
132
+ agent_name: str,
133
+ output_file: Optional[str] = None,
134
+ min_quality_rating: Optional[float] = None
135
+ ) -> str:
136
+ """
137
+ Export conversations in OpenAI fine-tuning format
138
+
139
+ Args:
140
+ agent_name: Agent to export data for
141
+ output_file: Output file path
142
+ min_quality_rating: Minimum quality rating to include
143
+
144
+ Returns:
145
+ Path to exported file
146
+ """
147
+ agent_key = agent_name.replace('_agent', '')
148
+ if agent_key not in self.agent_dirs:
149
+ raise ValueError(f"Unknown agent: {agent_name}")
150
+
151
+ if output_file is None:
152
+ output_file = self.data_dir / f"{agent_key}_finetuning_{datetime.now().strftime('%Y%m%d')}.jsonl"
153
+
154
+ agent_dir = self.agent_dirs[agent_key]
155
+ exported_count = 0
156
+
157
+ with open(output_file, 'w', encoding='utf-8') as out_f:
158
+ # Process single-turn conversations
159
+ for file in agent_dir.glob('conversations_*.jsonl'):
160
+ with open(file, 'r', encoding='utf-8') as in_f:
161
+ for line in in_f:
162
+ entry = json.loads(line)
163
+
164
+ # Filter by quality rating if specified
165
+ if min_quality_rating:
166
+ rating = entry.get('metadata', {}).get('rating')
167
+ if rating is None or rating < min_quality_rating:
168
+ continue
169
+
170
+ # Convert to OpenAI format
171
+ openai_format = {
172
+ "messages": [
173
+ {"role": "system", "content": f"You are a {agent_key} specialist."},
174
+ {"role": "user", "content": entry['user_message']},
175
+ {"role": "assistant", "content": entry['agent_response']}
176
+ ]
177
+ }
178
+
179
+ out_f.write(json.dumps(openai_format, ensure_ascii=False) + '\n')
180
+ exported_count += 1
181
+
182
+ # Process multi-turn conversations
183
+ for file in agent_dir.glob('multi_turn_*.jsonl'):
184
+ with open(file, 'r', encoding='utf-8') as in_f:
185
+ for line in in_f:
186
+ entry = json.loads(line)
187
+
188
+ # Filter by quality rating if specified
189
+ if min_quality_rating:
190
+ rating = entry.get('metadata', {}).get('rating')
191
+ if rating is None or rating < min_quality_rating:
192
+ continue
193
+
194
+ # Convert to OpenAI format
195
+ messages = [{"role": "system", "content": f"You are a {agent_key} specialist."}]
196
+
197
+ for turn in entry['conversation']:
198
+ messages.append({"role": "user", "content": turn['user']})
199
+ messages.append({"role": "assistant", "content": turn['agent']})
200
+
201
+ openai_format = {"messages": messages}
202
+ out_f.write(json.dumps(openai_format, ensure_ascii=False) + '\n')
203
+ exported_count += 1
204
+
205
+ print(f"✅ Exported {exported_count} conversations to {output_file}")
206
+ return str(output_file)
207
+
208
+
209
+ # Global instance
210
+ _collector = None
211
+
212
+ def get_data_collector() -> ConversationDataCollector:
213
+ """Get global data collector instance"""
214
+ global _collector
215
+ if _collector is None:
216
+ _collector = ConversationDataCollector()
217
+ return _collector
fine_tuning/trainer.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fine-tuning Trainer
3
+ Handles the fine-tuning process with OpenAI API
4
+ """
5
+
6
+ import os
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any
10
+ from openai import OpenAI
11
+
12
+
13
+ class FineTuningTrainer:
14
+ """Manages fine-tuning jobs with OpenAI"""
15
+
16
+ def __init__(self, api_key: Optional[str] = None):
17
+ self.client = OpenAI(api_key=api_key or os.getenv('OPENAI_API_KEY'))
18
+ self.jobs_dir = Path('fine_tuning/jobs')
19
+ self.jobs_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ def upload_training_file(self, file_path: str) -> str:
22
+ """
23
+ Upload training file to OpenAI
24
+
25
+ Args:
26
+ file_path: Path to training data file (JSONL format)
27
+
28
+ Returns:
29
+ File ID from OpenAI
30
+ """
31
+ print(f"📤 Uploading training file: {file_path}")
32
+
33
+ with open(file_path, 'rb') as f:
34
+ response = self.client.files.create(
35
+ file=f,
36
+ purpose='fine-tune'
37
+ )
38
+
39
+ file_id = response.id
40
+ print(f"✅ File uploaded successfully: {file_id}")
41
+ return file_id
42
+
43
+ def create_fine_tuning_job(
44
+ self,
45
+ training_file_id: str,
46
+ model: str = "gpt-4o-mini-2024-07-18",
47
+ suffix: Optional[str] = None,
48
+ hyperparameters: Optional[Dict[str, Any]] = None
49
+ ) -> str:
50
+ """
51
+ Create a fine-tuning job
52
+
53
+ Args:
54
+ training_file_id: ID of uploaded training file
55
+ model: Base model to fine-tune
56
+ suffix: Suffix for fine-tuned model name
57
+ hyperparameters: Training hyperparameters
58
+
59
+ Returns:
60
+ Fine-tuning job ID
61
+ """
62
+ print(f"🚀 Creating fine-tuning job...")
63
+ print(f" Base model: {model}")
64
+ print(f" Training file: {training_file_id}")
65
+
66
+ job_params = {
67
+ 'training_file': training_file_id,
68
+ 'model': model
69
+ }
70
+
71
+ if suffix:
72
+ job_params['suffix'] = suffix
73
+
74
+ if hyperparameters:
75
+ job_params['hyperparameters'] = hyperparameters
76
+
77
+ response = self.client.fine_tuning.jobs.create(**job_params)
78
+
79
+ job_id = response.id
80
+ print(f"✅ Fine-tuning job created: {job_id}")
81
+
82
+ # Save job info
83
+ self._save_job_info(job_id, {
84
+ 'training_file_id': training_file_id,
85
+ 'model': model,
86
+ 'suffix': suffix,
87
+ 'hyperparameters': hyperparameters,
88
+ 'status': 'created'
89
+ })
90
+
91
+ return job_id
92
+
93
+ def check_job_status(self, job_id: str) -> Dict[str, Any]:
94
+ """
95
+ Check status of fine-tuning job
96
+
97
+ Args:
98
+ job_id: Fine-tuning job ID
99
+
100
+ Returns:
101
+ Job status information
102
+ """
103
+ response = self.client.fine_tuning.jobs.retrieve(job_id)
104
+
105
+ status_info = {
106
+ 'id': response.id,
107
+ 'status': response.status,
108
+ 'model': response.model,
109
+ 'fine_tuned_model': response.fine_tuned_model,
110
+ 'created_at': response.created_at,
111
+ 'finished_at': response.finished_at,
112
+ 'trained_tokens': response.trained_tokens
113
+ }
114
+
115
+ return status_info
116
+
117
+ def wait_for_completion(
118
+ self,
119
+ job_id: str,
120
+ check_interval: int = 60,
121
+ timeout: int = 3600
122
+ ) -> Dict[str, Any]:
123
+ """
124
+ Wait for fine-tuning job to complete
125
+
126
+ Args:
127
+ job_id: Fine-tuning job ID
128
+ check_interval: Seconds between status checks
129
+ timeout: Maximum seconds to wait
130
+
131
+ Returns:
132
+ Final job status
133
+ """
134
+ print(f"⏳ Waiting for fine-tuning job {job_id} to complete...")
135
+
136
+ start_time = time.time()
137
+
138
+ while True:
139
+ status_info = self.check_job_status(job_id)
140
+ status = status_info['status']
141
+
142
+ print(f" Status: {status}")
143
+
144
+ if status == 'succeeded':
145
+ print(f"✅ Fine-tuning completed!")
146
+ print(f" Fine-tuned model: {status_info['fine_tuned_model']}")
147
+ self._save_job_info(job_id, status_info)
148
+ return status_info
149
+
150
+ elif status in ['failed', 'cancelled']:
151
+ print(f"❌ Fine-tuning {status}")
152
+ self._save_job_info(job_id, status_info)
153
+ raise Exception(f"Fine-tuning job {status}")
154
+
155
+ elif time.time() - start_time > timeout:
156
+ print(f"⏰ Timeout reached")
157
+ raise TimeoutError(f"Fine-tuning job exceeded {timeout} seconds")
158
+
159
+ time.sleep(check_interval)
160
+
161
+ def list_fine_tuned_models(self) -> list:
162
+ """
163
+ List all fine-tuned models
164
+
165
+ Returns:
166
+ List of fine-tuned model information
167
+ """
168
+ response = self.client.fine_tuning.jobs.list(limit=50)
169
+
170
+ models = []
171
+ for job in response.data:
172
+ if job.fine_tuned_model:
173
+ models.append({
174
+ 'job_id': job.id,
175
+ 'model_id': job.fine_tuned_model,
176
+ 'base_model': job.model,
177
+ 'status': job.status,
178
+ 'created_at': job.created_at,
179
+ 'finished_at': job.finished_at
180
+ })
181
+
182
+ return models
183
+
184
+ def cancel_job(self, job_id: str) -> None:
185
+ """
186
+ Cancel a running fine-tuning job
187
+
188
+ Args:
189
+ job_id: Fine-tuning job ID
190
+ """
191
+ print(f"🛑 Cancelling job {job_id}...")
192
+ self.client.fine_tuning.jobs.cancel(job_id)
193
+ print(f"✅ Job cancelled")
194
+
195
+ def _save_job_info(self, job_id: str, info: Dict[str, Any]) -> None:
196
+ """Save job information to file"""
197
+ import json
198
+
199
+ job_file = self.jobs_dir / f"{job_id}.json"
200
+ with open(job_file, 'w') as f:
201
+ json.dump(info, f, indent=2, default=str)
202
+
203
+
204
+ def fine_tune_agent(
205
+ agent_name: str,
206
+ training_file: str,
207
+ model: str = "gpt-4o-mini-2024-07-18",
208
+ suffix: Optional[str] = None,
209
+ wait_for_completion: bool = True
210
+ ) -> str:
211
+ """
212
+ Convenience function to fine-tune an agent
213
+
214
+ Args:
215
+ agent_name: Name of agent (nutrition, exercise, etc.)
216
+ training_file: Path to training data
217
+ model: Base model to use
218
+ suffix: Suffix for model name
219
+ wait_for_completion: Whether to wait for job to finish
220
+
221
+ Returns:
222
+ Fine-tuned model ID or job ID
223
+ """
224
+ trainer = FineTuningTrainer()
225
+
226
+ # Upload file
227
+ file_id = trainer.upload_training_file(training_file)
228
+
229
+ # Create job
230
+ if suffix is None:
231
+ suffix = f"{agent_name}-{int(time.time())}"
232
+
233
+ job_id = trainer.create_fine_tuning_job(
234
+ training_file_id=file_id,
235
+ model=model,
236
+ suffix=suffix
237
+ )
238
+
239
+ # Wait for completion if requested
240
+ if wait_for_completion:
241
+ status = trainer.wait_for_completion(job_id)
242
+ return status['fine_tuned_model']
243
+ else:
244
+ return job_id