Cuong2004 commited on
Commit
e83f5e9
·
1 Parent(s): ac0f906

version 1.1

Browse files
.env.example CHANGED
@@ -23,4 +23,11 @@ WEBSOCKET_PATH=/notify
23
  # Application settings
24
  ENVIRONMENT=production
25
  DEBUG=false
26
- PORT=7860
 
 
 
 
 
 
 
 
23
  # Application settings
24
  ENVIRONMENT=production
25
  DEBUG=false
26
+ PORT=7860
27
+
28
+ # Cache Configuration
29
+ CACHE_TTL_SECONDS=300
30
+ CACHE_CLEANUP_INTERVAL=60
31
+ CACHE_MAX_SIZE=1000
32
+ HISTORY_QUEUE_SIZE=10
33
+ HISTORY_CACHE_TTL=3600
.gitattributes DELETED
@@ -1,29 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto eol=lf
3
-
4
- # Documents
5
- *.md text
6
- *.txt text
7
- *.ini text
8
- *.yaml text
9
- *.yml text
10
- *.json text
11
- *.py text
12
- *.env.example text
13
-
14
- # Binary files
15
- *.png binary
16
- *.jpg binary
17
- *.jpeg binary
18
- *.gif binary
19
- *.ico binary
20
- *.db binary
21
-
22
- # Git related files
23
- .gitignore text
24
- .gitattributes text
25
-
26
- # Docker related files
27
- Dockerfile text
28
- docker-compose.yml text
29
- .dockerignore text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -60,6 +60,8 @@ tests/
60
 
61
  Admin_bot/
62
 
 
 
63
  # Hugging Face Spaces
64
  .gitattributes
65
 
@@ -77,3 +79,5 @@ Thumbs.db
77
  *.log
78
  .env
79
  main.py
 
 
 
60
 
61
  Admin_bot/
62
 
63
+ Pix-Agent/
64
+
65
  # Hugging Face Spaces
66
  .gitattributes
67
 
 
79
  *.log
80
  .env
81
  main.py
82
+
83
+ test/
README.md CHANGED
@@ -358,4 +358,62 @@ You can customize the retrieval parameters when making API requests:
358
 
359
  ## Implementation Details
360
 
361
- The system is implemented as a custom retriever class `ThresholdRetriever` that integrates with LangChain's retrieval infrastructure while providing enhanced functionality.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
  ## Implementation Details
360
 
361
+ The system is implemented as a custom retriever class `ThresholdRetriever` that integrates with LangChain's retrieval infrastructure while providing enhanced functionality.
362
+
363
+ ## In-Memory Cache
364
+
365
+ Dự án bao gồm một hệ thống cache trong bộ nhớ để giảm thiểu truy cập đến cơ sở dữ liệu PostgreSQL và MongoDB.
366
+
367
+ ### Cấu hình Cache
368
+
369
+ Cache được cấu hình thông qua các biến môi trường:
370
+
371
+ ```
372
+ # Cache Configuration
373
+ CACHE_TTL_SECONDS=300 # Thời gian tồn tại của cache item (giây)
374
+ CACHE_CLEANUP_INTERVAL=60 # Chu kỳ xóa cache hết hạn (giây)
375
+ CACHE_MAX_SIZE=1000 # Số lượng item tối đa trong cache
376
+ HISTORY_QUEUE_SIZE=10 # Số lượng item tối đa trong queue lịch sử người dùng
377
+ HISTORY_CACHE_TTL=3600 # Thời gian tồn tại của lịch sử người dùng (giây)
378
+ ```
379
+
380
+ ### Cơ chế Cache
381
+
382
+ Hệ thống cache kết hợp hai cơ chế hết hạn:
383
+
384
+ 1. **Lazy Expiration**: Kiểm tra thời hạn khi truy cập cache item. Nếu item đã hết hạn, nó sẽ bị xóa và trả về kết quả là không tìm thấy.
385
+
386
+ 2. **Active Expiration**: Một background thread định kỳ quét và xóa các item đã hết hạn. Điều này giúp tránh tình trạng cache quá lớn với các item không còn được sử dụng.
387
+
388
+ ### Các loại dữ liệu được cache
389
+
390
+ - **Dữ liệu PostgreSQL**: Thông tin từ các bảng FAQ, Emergency Contacts, và Events.
391
+ - **Lịch sử người dùng từ MongoDB**: Lịch sử hội thoại người dùng được lưu trong queue với thời gian sống tính theo lần truy cập cuối cùng.
392
+
393
+ ### API Cache
394
+
395
+ Dự án cung cấp các API endpoints để quản lý cache:
396
+
397
+ - `GET /cache/stats`: Xem thống kê về cache (tổng số item, bộ nhớ sử dụng, v.v.)
398
+ - `DELETE /cache/clear`: Xóa toàn bộ cache
399
+ - `GET /debug/cache`: (Chỉ trong chế độ debug) Xem thông tin chi tiết về cache, bao gồm các keys và cấu hình
400
+
401
+ ### Cách hoạt động
402
+
403
+ 1. Khi một request đến, hệ thống sẽ kiểm tra dữ liệu trong cache trước.
404
+ 2. Nếu dữ liệu tồn tại và còn hạn, trả về từ cache.
405
+ 3. Nếu dữ liệu không tồn tại hoặc đã hết hạn, truy vấn từ database và lưu kết quả vào cache.
406
+ 4. Khi dữ liệu được cập nhật hoặc xóa, cache liên quan sẽ tự động được xóa.
407
+
408
+ ### Lịch sử người dùng
409
+
410
+ Lịch sử hội thoại người dùng được lưu trong queue riêng với cơ chế đặc biệt:
411
+
412
+ - Mỗi người dùng có một queue riêng với kích thước giới hạn (`HISTORY_QUEUE_SIZE`).
413
+ - Thời gian sống của queue được làm mới mỗi khi có tương tác mới.
414
+ - Khi queue đầy, các item cũ nhất sẽ bị loại bỏ.
415
+ - Queue tự động bị xóa sau một thời gian không hoạt động.
416
+
417
+ ## Tác giả
418
+
419
+ - **PIX Project Team**
api_documentation.txt DELETED
@@ -1,318 +0,0 @@
1
- # Frontend Integration Guide for PixAgent API
2
-
3
- This guide provides instructions for integrating with the optimized PostgreSQL-based API endpoints for Event, FAQ, and Emergency data.
4
-
5
- ## API Endpoints
6
-
7
- ### Events
8
-
9
- | Endpoint | Method | Description |
10
- |----------|--------|-------------|
11
- | /postgres/events/ | GET | Fetch all events (with optional filtering) |
12
- | /postgres/events/{event_id} | GET | Fetch a specific event by ID |
13
- | /postgres/events/featured | GET | Fetch featured events |
14
- | /postgres/events/ | POST | Create a new event |
15
- | /postgres/events/{event_id} | PUT | Update an existing event |
16
- | /postgres/events/{event_id} | DELETE | Delete an event |
17
-
18
- ### FAQs
19
-
20
- | Endpoint | Method | Description |
21
- |----------|--------|-------------|
22
- | /postgres/faqs/ | GET | Fetch all FAQs |
23
- | /postgres/faqs/{faq_id} | GET | Fetch a specific FAQ by ID |
24
- | /postgres/faqs/ | POST | Create a new FAQ |
25
- | /postgres/faqs/{faq_id} | PUT | Update an existing FAQ |
26
- | /postgres/faqs/{faq_id} | DELETE | Delete a FAQ |
27
-
28
- ### Emergency Contacts
29
-
30
- | Endpoint | Method | Description |
31
- |----------|--------|-------------|
32
- | /postgres/emergencies/ | GET | Fetch all emergency contacts |
33
- | /postgres/emergencies/{emergency_id} | GET | Fetch a specific emergency contact by ID |
34
- | /postgres/emergencies/ | POST | Create a new emergency contact |
35
- | /postgres/emergencies/{emergency_id} | PUT | Update an existing emergency contact |
36
- | /postgres/emergencies/{emergency_id} | DELETE | Delete an emergency contact |
37
-
38
- ## Response Models
39
-
40
- ### Event Response Model
41
-
42
- interface EventResponse {
43
- id: number;
44
- name: string;
45
- description: string;
46
- date_start: string; // ISO format date
47
- date_end: string; // ISO format date
48
- location: string;
49
- image_url: string;
50
- price: {
51
- currency: string;
52
- amount: string;
53
- };
54
- featured: boolean;
55
- is_active: boolean;
56
- created_at: string; // ISO format date
57
- updated_at: string; // ISO format date
58
- }
59
-
60
- ### FAQ Response Model
61
-
62
- interface FaqResponse {
63
- id: number;
64
- question: string;
65
- answer: string;
66
- is_active: boolean;
67
- created_at: string; // ISO format date
68
- updated_at: string; // ISO format date
69
- }
70
-
71
- ### Emergency Response Model
72
-
73
- interface EmergencyResponse {
74
- id: number;
75
- name: string;
76
- phone_number: string;
77
- description: string;
78
- address: string;
79
- priority: number;
80
- is_active: boolean;
81
- created_at: string; // ISO format date
82
- updated_at: string; // ISO format date
83
- }
84
-
85
- ## Example Usage (React)
86
-
87
- ### Fetching Events
88
-
89
- import { useState, useEffect } from 'react';
90
- import axios from 'axios';
91
-
92
- const API_BASE_URL = 'http://localhost:8000';
93
-
94
- function EventList() {
95
- const [events, setEvents] = useState([]);
96
- const [loading, setLoading] = useState(true);
97
- const [error, setError] = useState(null);
98
-
99
- useEffect(() => {
100
- const fetchEvents = async () => {
101
- try {
102
- setLoading(true);
103
- const response = await axios.get(`${API_BASE_URL}/postgres/events/`);
104
- setEvents(response.data);
105
- setLoading(false);
106
- } catch (err) {
107
- setError('Failed to fetch events');
108
- setLoading(false);
109
- console.error('Error fetching events:', err);
110
- }
111
- };
112
-
113
- fetchEvents();
114
- }, []);
115
-
116
- if (loading) return <p>Loading events...</p>;
117
- if (error) return <p>{error}</p>;
118
-
119
- return (
120
- <div>
121
- <h1>Events</h1>
122
- <div className="event-list">
123
- {events.map(event => (
124
- <div key={event.id} className="event-card">
125
- <h2>{event.name}</h2>
126
- <p>{event.description}</p>
127
- <p>
128
- <strong>When:</strong> {new Date(event.date_start).toLocaleDateString()} - {new Date(event.date_end).toLocaleDateString()}
129
- </p>
130
- <p><strong>Where:</strong> {event.location}</p>
131
- <p><strong>Price:</strong> {event.price.amount} {event.price.currency}</p>
132
- {event.featured && <span className="featured-badge">Featured</span>}
133
- </div>
134
- ))}
135
- </div>
136
- </div>
137
- );
138
- }
139
-
140
- ### Creating an Event
141
-
142
- import { useState } from 'react';
143
- import axios from 'axios';
144
-
145
- function CreateEvent() {
146
- const [eventData, setEventData] = useState({
147
- name: '',
148
- description: '',
149
- date_start: '',
150
- date_end: '',
151
- location: '',
152
- image_url: '',
153
- price: {
154
- currency: 'USD',
155
- amount: '0'
156
- },
157
- featured: false,
158
- is_active: true
159
- });
160
- const [loading, setLoading] = useState(false);
161
- const [error, setError] = useState(null);
162
- const [success, setSuccess] = useState(false);
163
-
164
- const handleChange = (e) => {
165
- const { name, value, type, checked } = e.target;
166
-
167
- if (name === 'price_amount') {
168
- setEventData(prev => ({
169
- ...prev,
170
- price: {
171
- ...prev.price,
172
- amount: value
173
- }
174
- }));
175
- } else if (name === 'price_currency') {
176
- setEventData(prev => ({
177
- ...prev,
178
- price: {
179
- ...prev.price,
180
- currency: value
181
- }
182
- }));
183
- } else {
184
- setEventData(prev => ({
185
- ...prev,
186
- [name]: type === 'checkbox' ? checked : value
187
- }));
188
- }
189
- };
190
-
191
- const handleSubmit = async (e) => {
192
- e.preventDefault();
193
- try {
194
- setLoading(true);
195
- setError(null);
196
- setSuccess(false);
197
-
198
- const response = await axios.post(`${API_BASE_URL}/postgres/events/`, eventData);
199
- setSuccess(true);
200
- setEventData({
201
- name: '',
202
- description: '',
203
- date_start: '',
204
- date_end: '',
205
- location: '',
206
- image_url: '',
207
- price: {
208
- currency: 'USD',
209
- amount: '0'
210
- },
211
- featured: false,
212
- is_active: true
213
- });
214
- setLoading(false);
215
- } catch (err) {
216
- setError('Failed to create event');
217
- setLoading(false);
218
- console.error('Error creating event:', err);
219
- }
220
- };
221
-
222
- return (
223
- <div>
224
- <h1>Create New Event</h1>
225
- {success && <div className="success-message">Event created successfully!</div>}
226
- {error && <div className="error-message">{error}</div>}
227
- <form onSubmit={handleSubmit}>
228
- {/* Form fields would go here */}
229
- <button type="submit" disabled={loading}>
230
- {loading ? 'Creating...' : 'Create Event'}
231
- </button>
232
- </form>
233
- </div>
234
- );
235
- }
236
-
237
- ## Performance Optimizations
238
-
239
- The API now includes several performance optimizations:
240
-
241
- ### Caching
242
-
243
- The server implements caching for read operations, which significantly improves response times for repeated requests. The average cache improvement is over 70%.
244
-
245
- Frontend considerations:
246
- No need to implement client-side caching for data that doesn't change frequently
247
- For real-time data, consider adding a refresh button in the UI
248
- If data might be updated by other users, consider adding a polling mechanism or websocket for updates
249
-
250
-
251
- ### Error Handling
252
-
253
- The API returns standardized error responses. Example:
254
-
255
- async function fetchData(url) {
256
- try {
257
- const response = await fetch(url);
258
- if (!response.ok) {
259
- const errorData = await response.json();
260
- throw new Error(errorData.detail || 'An error occurred');
261
- }
262
- return await response.json();
263
- } catch (error) {
264
- console.error('API request failed:', error);
265
- // Handle error in UI
266
- return null;
267
- }
268
- }
269
-
270
- ### Price Field Handling
271
-
272
- The price field of events is a JSON object with currency and amount properties. When creating or updating events, ensure this is properly formatted:
273
-
274
- // Correct format for price field
275
- const eventData = {
276
- // other fields...
277
- price: {
278
- currency: 'USD',
279
- amount: '10.99'
280
- }
281
- };
282
-
283
- // When displaying price
284
- function formatPrice(price) {
285
- if (!price) return 'Free';
286
- if (typeof price === 'string') {
287
- try {
288
- price = JSON.parse(price);
289
- } catch {
290
- return price;
291
- }
292
- }
293
- return `${price.amount} ${price.currency}`;
294
- }
295
-
296
- ## CORS Configuration
297
-
298
- The API has CORS enabled for frontend applications. If you're experiencing CORS issues, ensure your frontend domain is allowed in the server configuration.
299
-
300
- For local development, the following origins are typically allowed:
301
- - http://localhost:3000
302
- - http://localhost:5000
303
- - http://localhost:8080
304
-
305
- ## Status Codes
306
-
307
- | Status Code | Description |
308
- |-------------|-------------|
309
- | 200 | Success - The request was successful |
310
- | 201 | Created - A new resource was successfully created |
311
- | 400 | Bad Request - The request could not be understood or was missing required parameters |
312
- | 404 | Not Found - Resource not found |
313
- | 422 | Validation Error - Request data failed validation |
314
- | 500 | Internal Server Error - An error occurred on the server |
315
-
316
- ## Questions?
317
-
318
- For further inquiries about the API, please contact the development team.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -64,11 +64,9 @@ async def lifespan(app: FastAPI):
64
  # Startup: kiểm tra kết nối các database
65
  logger.info("Starting application...")
66
  db_status = check_database_connections()
67
- if all(db_status.values()):
68
- logger.info("All database connections are working")
69
 
70
  # Khởi tạo bảng trong cơ sở dữ liệu (nếu chưa tồn tại)
71
- if DEBUG: # Chỉ khởi tạo bảng trong chế độ debug
72
  from app.database.postgresql import create_tables
73
  if create_tables():
74
  logger.info("Database tables created or already exist")
@@ -84,6 +82,7 @@ try:
84
  from app.api.postgresql_routes import router as postgresql_router
85
  from app.api.rag_routes import router as rag_router
86
  from app.api.websocket_routes import router as websocket_router
 
87
 
88
  # Import middlewares
89
  from app.utils.middleware import RequestLoggingMiddleware, ErrorHandlingMiddleware, DatabaseCheckMiddleware
@@ -91,6 +90,9 @@ try:
91
  # Import debug utilities
92
  from app.utils.debug_utils import debug_view, DebugInfo, error_tracker, performance_monitor
93
 
 
 
 
94
  except ImportError as e:
95
  logger.error(f"Error importing routes or middlewares: {e}")
96
  raise
@@ -126,6 +128,7 @@ app.include_router(mongodb_router)
126
  app.include_router(postgresql_router)
127
  app.include_router(rag_router)
128
  app.include_router(websocket_router)
 
129
 
130
  # Root endpoint
131
  @app.get("/")
@@ -149,6 +152,25 @@ def health_check():
149
  "databases": db_status
150
  }
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  # Debug endpoints (chỉ có trong chế độ debug)
153
  if DEBUG:
154
  @app.get("/debug/config")
@@ -190,6 +212,29 @@ if DEBUG:
190
  def debug_full_report(request: Request):
191
  """Hiển thị báo cáo debug đầy đủ (chỉ trong chế độ debug)"""
192
  return debug_view(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
  # Run the app with uvicorn when executed directly
195
  if __name__ == "__main__":
 
64
  # Startup: kiểm tra kết nối các database
65
  logger.info("Starting application...")
66
  db_status = check_database_connections()
 
 
67
 
68
  # Khởi tạo bảng trong cơ sở dữ liệu (nếu chưa tồn tại)
69
+ if DEBUG and all(db_status.values()): # Chỉ khởi tạo bảng trong chế độ debug và khi tất cả kết nối DB thành công
70
  from app.database.postgresql import create_tables
71
  if create_tables():
72
  logger.info("Database tables created or already exist")
 
82
  from app.api.postgresql_routes import router as postgresql_router
83
  from app.api.rag_routes import router as rag_router
84
  from app.api.websocket_routes import router as websocket_router
85
+ from app.api.pdf_routes import router as pdf_router
86
 
87
  # Import middlewares
88
  from app.utils.middleware import RequestLoggingMiddleware, ErrorHandlingMiddleware, DatabaseCheckMiddleware
 
90
  # Import debug utilities
91
  from app.utils.debug_utils import debug_view, DebugInfo, error_tracker, performance_monitor
92
 
93
+ # Import cache
94
+ from app.utils.cache import get_cache
95
+
96
  except ImportError as e:
97
  logger.error(f"Error importing routes or middlewares: {e}")
98
  raise
 
128
  app.include_router(postgresql_router)
129
  app.include_router(rag_router)
130
  app.include_router(websocket_router)
131
+ app.include_router(pdf_router)
132
 
133
  # Root endpoint
134
  @app.get("/")
 
152
  "databases": db_status
153
  }
154
 
155
+ @app.get("/api/ping")
156
+ async def ping():
157
+ return {"status": "pong"}
158
+
159
+ # Cache stats endpoint
160
+ @app.get("/cache/stats")
161
+ def cache_stats():
162
+ """Trả về thống kê về cache"""
163
+ cache = get_cache()
164
+ return cache.stats()
165
+
166
+ # Cache clear endpoint
167
+ @app.delete("/cache/clear")
168
+ def cache_clear():
169
+ """Xóa tất cả dữ liệu trong cache"""
170
+ cache = get_cache()
171
+ cache.clear()
172
+ return {"message": "Cache cleared successfully"}
173
+
174
  # Debug endpoints (chỉ có trong chế độ debug)
175
  if DEBUG:
176
  @app.get("/debug/config")
 
212
  def debug_full_report(request: Request):
213
  """Hiển thị báo cáo debug đầy đủ (chỉ trong chế độ debug)"""
214
  return debug_view(request)
215
+
216
+ @app.get("/debug/cache")
217
+ def debug_cache():
218
+ """Hiển thị thông tin chi tiết về cache (chỉ trong chế độ debug)"""
219
+ cache = get_cache()
220
+ cache_stats = cache.stats()
221
+
222
+ # Thêm thông tin chi tiết về các key trong cache
223
+ cache_keys = list(cache.cache.keys())
224
+ history_users = list(cache.user_history_queues.keys())
225
+
226
+ return {
227
+ "stats": cache_stats,
228
+ "keys": cache_keys,
229
+ "history_users": history_users,
230
+ "config": {
231
+ "ttl": cache.ttl,
232
+ "cleanup_interval": cache.cleanup_interval,
233
+ "max_size": cache.max_size,
234
+ "history_queue_size": os.getenv("HISTORY_QUEUE_SIZE", "10"),
235
+ "history_cache_ttl": os.getenv("HISTORY_CACHE_TTL", "3600"),
236
+ }
237
+ }
238
 
239
  # Run the app with uvicorn when executed directly
240
  if __name__ == "__main__":
app/__init__.py CHANGED
@@ -11,7 +11,9 @@ import os
11
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
 
13
  try:
14
- from app.py import app
 
 
15
  except ImportError:
16
  # Thử cách khác nếu import trực tiếp không hoạt động
17
  import importlib.util
 
11
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
 
13
  try:
14
+ # Sửa lại cách import đúng - 'app.py' không phải là module hợp lệ
15
+ # 'app' là tên module, '.py' là phần mở rộng tệp
16
+ from app import app
17
  except ImportError:
18
  # Thử cách khác nếu import trực tiếp không hoạt động
19
  import importlib.util
app/api/mongodb_routes.py CHANGED
@@ -8,7 +8,7 @@ import asyncio
8
 
9
  from app.database.mongodb import (
10
  save_session,
11
- get_user_history,
12
  update_session_response,
13
  check_db_connection,
14
  session_collection
@@ -178,7 +178,7 @@ async def get_history(user_id: str, n: int = Query(3, ge=1, le=10)):
178
  )
179
 
180
  # Get user history from MongoDB
181
- history_data = get_user_history(user_id=user_id, n=n)
182
 
183
  # Convert to response model
184
  return HistoryResponse(history=history_data)
 
8
 
9
  from app.database.mongodb import (
10
  save_session,
11
+ get_chat_history,
12
  update_session_response,
13
  check_db_connection,
14
  session_collection
 
178
  )
179
 
180
  # Get user history from MongoDB
181
+ history_data = get_chat_history(user_id=user_id, n=n)
182
 
183
  # Convert to response model
184
  return HistoryResponse(history=history_data)
app/api/pdf_routes.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import uuid
4
+ from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks
5
+ from fastapi.responses import JSONResponse
6
+ from typing import Optional, List, Dict, Any
7
+
8
+ from app.utils.pdf_processor import PDFProcessor
9
+ from app.models.pdf_models import PDFResponse, DeleteDocumentRequest, DocumentsListResponse
10
+ from app.api.pdf_websocket import (
11
+ send_pdf_upload_started,
12
+ send_pdf_upload_progress,
13
+ send_pdf_upload_completed,
14
+ send_pdf_upload_failed,
15
+ send_pdf_delete_started,
16
+ send_pdf_delete_completed,
17
+ send_pdf_delete_failed
18
+ )
19
+
20
+ # Khởi tạo router
21
+ router = APIRouter(
22
+ prefix="/pdf",
23
+ tags=["PDF Processing"],
24
+ )
25
+
26
+ # Thư mục lưu file tạm - sử dụng /tmp để tránh lỗi quyền truy cập
27
+ TEMP_UPLOAD_DIR = "/tmp/uploads/temp"
28
+ STORAGE_DIR = "/tmp/uploads/pdfs"
29
+
30
+ # Đảm bảo thư mục upload tồn tại
31
+ os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
32
+ os.makedirs(STORAGE_DIR, exist_ok=True)
33
+
34
+ # Endpoint upload và xử lý PDF
35
+ @router.post("/upload", response_model=PDFResponse)
36
+ async def upload_pdf(
37
+ file: UploadFile = File(...),
38
+ namespace: str = Form("Default"),
39
+ index_name: str = Form("testbot768"),
40
+ title: Optional[str] = Form(None),
41
+ description: Optional[str] = Form(None),
42
+ user_id: Optional[str] = Form(None),
43
+ background_tasks: BackgroundTasks = None
44
+ ):
45
+ """
46
+ Upload và xử lý file PDF để tạo embeddings và lưu vào Pinecone
47
+
48
+ - **file**: File PDF cần xử lý
49
+ - **namespace**: Namespace trong Pinecone để lưu embeddings (mặc định: "Default")
50
+ - **index_name**: Tên index Pinecone (mặc định: "testbot768")
51
+ - **title**: Tiêu đề của tài liệu (tùy chọn)
52
+ - **description**: Mô tả về tài liệu (tùy chọn)
53
+ - **user_id**: ID của người dùng để cập nhật trạng thái qua WebSocket
54
+ """
55
+ try:
56
+ # Kiểm tra file có phải PDF không
57
+ if not file.filename.lower().endswith('.pdf'):
58
+ raise HTTPException(status_code=400, detail="Chỉ chấp nhận file PDF")
59
+
60
+ # Tạo file_id và lưu file tạm
61
+ file_id = str(uuid.uuid4())
62
+ temp_file_path = os.path.join(TEMP_UPLOAD_DIR, f"{file_id}.pdf")
63
+
64
+ # Gửi thông báo bắt đầu xử lý qua WebSocket nếu có user_id
65
+ if user_id:
66
+ await send_pdf_upload_started(user_id, file.filename, file_id)
67
+
68
+ # Lưu file
69
+ with open(temp_file_path, "wb") as buffer:
70
+ shutil.copyfileobj(file.file, buffer)
71
+
72
+ # Tạo metadata
73
+ metadata = {
74
+ "filename": file.filename,
75
+ "content_type": file.content_type
76
+ }
77
+
78
+ if title:
79
+ metadata["title"] = title
80
+ if description:
81
+ metadata["description"] = description
82
+
83
+ # Gửi thông báo tiến độ qua WebSocket
84
+ if user_id:
85
+ await send_pdf_upload_progress(
86
+ user_id,
87
+ file_id,
88
+ "file_preparation",
89
+ 0.2,
90
+ "File saved, preparing for processing"
91
+ )
92
+
93
+ # Khởi tạo PDF processor
94
+ processor = PDFProcessor(index_name=index_name, namespace=namespace)
95
+
96
+ # Gửi thông báo bắt đầu embedding qua WebSocket
97
+ if user_id:
98
+ await send_pdf_upload_progress(
99
+ user_id,
100
+ file_id,
101
+ "embedding_start",
102
+ 0.4,
103
+ "Starting to process PDF and create embeddings"
104
+ )
105
+
106
+ # Xử lý PDF và tạo embeddings
107
+ # Tạo callback function để xử lý cập nhật tiến độ
108
+ async def progress_callback_wrapper(step, progress, message):
109
+ if user_id:
110
+ await send_progress_update(user_id, file_id, step, progress, message)
111
+
112
+ # Xử lý PDF và tạo embeddings với callback đã được xử lý đúng cách
113
+ result = await processor.process_pdf(
114
+ file_path=temp_file_path,
115
+ document_id=file_id,
116
+ metadata=metadata,
117
+ progress_callback=progress_callback_wrapper
118
+ )
119
+
120
+ # Nếu thành công, chuyển file vào storage
121
+ if result.get('success'):
122
+ storage_path = os.path.join(STORAGE_DIR, f"{file_id}.pdf")
123
+ shutil.move(temp_file_path, storage_path)
124
+
125
+ # Gửi thông báo hoàn thành qua WebSocket
126
+ if user_id:
127
+ await send_pdf_upload_completed(
128
+ user_id,
129
+ file_id,
130
+ file.filename,
131
+ result.get('chunks_processed', 0)
132
+ )
133
+ else:
134
+ # Gửi thông báo lỗi qua WebSocket
135
+ if user_id:
136
+ await send_pdf_upload_failed(
137
+ user_id,
138
+ file_id,
139
+ file.filename,
140
+ result.get('error', 'Unknown error')
141
+ )
142
+
143
+ # Dọn dẹp: xóa file tạm nếu vẫn còn
144
+ if os.path.exists(temp_file_path):
145
+ os.remove(temp_file_path)
146
+
147
+ return result
148
+ except Exception as e:
149
+ # Dọn dẹp nếu có lỗi
150
+ if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
151
+ os.remove(temp_file_path)
152
+
153
+ # Gửi thông báo lỗi qua WebSocket
154
+ if 'user_id' in locals() and user_id and 'file_id' in locals():
155
+ await send_pdf_upload_failed(
156
+ user_id,
157
+ file_id,
158
+ file.filename,
159
+ str(e)
160
+ )
161
+
162
+ return PDFResponse(
163
+ success=False,
164
+ error=str(e)
165
+ )
166
+
167
+ # Function để gửi cập nhật tiến độ - được sử dụng trong callback
168
+ async def send_progress_update(user_id, document_id, step, progress, message):
169
+ if user_id:
170
+ await send_pdf_upload_progress(user_id, document_id, step, progress, message)
171
+
172
+ # Endpoint xóa tài liệu
173
+ @router.delete("/namespace", response_model=PDFResponse)
174
+ async def delete_namespace(
175
+ namespace: str = "Default",
176
+ index_name: str = "testbot768",
177
+ user_id: Optional[str] = None
178
+ ):
179
+ """
180
+ Xóa toàn bộ embeddings trong một namespace từ Pinecone (tương ứng xoá namespace)
181
+
182
+ - **namespace**: Namespace trong Pinecone (mặc định: "Default")
183
+ - **index_name**: Tên index Pinecone (mặc định: "testbot768")
184
+ - **user_id**: ID của người dùng để cập nhật trạng thái qua WebSocket
185
+ """
186
+ try:
187
+ # Gửi thông báo bắt đầu xóa qua WebSocket
188
+ if user_id:
189
+ await send_pdf_delete_started(user_id, namespace)
190
+
191
+ processor = PDFProcessor(index_name=index_name, namespace=namespace)
192
+ result = await processor.delete_namespace()
193
+
194
+ # Gửi thông báo kết quả qua WebSocket
195
+ if user_id:
196
+ if result.get('success'):
197
+ await send_pdf_delete_completed(user_id, namespace)
198
+ else:
199
+ await send_pdf_delete_failed(user_id, namespace, result.get('error', 'Unknown error'))
200
+
201
+ return result
202
+ except Exception as e:
203
+ # Gửi thông báo lỗi qua WebSocket
204
+ if user_id:
205
+ await send_pdf_delete_failed(user_id, namespace, str(e))
206
+
207
+ return PDFResponse(
208
+ success=False,
209
+ error=str(e)
210
+ )
211
+
212
+ # Endpoint lấy danh sách tài liệu
213
+ @router.get("/documents", response_model=DocumentsListResponse)
214
+ async def get_documents(namespace: str = "Default", index_name: str = "testbot768"):
215
+ """
216
+ Lấy thông tin về tất cả tài liệu đã được embed
217
+
218
+ - **namespace**: Namespace trong Pinecone (mặc định: "Default")
219
+ - **index_name**: Tên index Pinecone (mặc định: "testbot768")
220
+ """
221
+ try:
222
+ # Khởi tạo PDF processor
223
+ processor = PDFProcessor(index_name=index_name, namespace=namespace)
224
+
225
+ # Lấy danh sách documents
226
+ result = await processor.list_documents()
227
+
228
+ return result
229
+ except Exception as e:
230
+ return DocumentsListResponse(
231
+ success=False,
232
+ error=str(e)
233
+ )
app/api/pdf_websocket.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Dict, List, Optional, Any
3
+ from fastapi import WebSocket, WebSocketDisconnect, APIRouter
4
+ from pydantic import BaseModel
5
+ import json
6
+ import time
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Models cho Swagger documentation
12
+ class ConnectionStatus(BaseModel):
13
+ user_id: str
14
+ active: bool
15
+ connection_count: int
16
+ last_activity: Optional[float] = None
17
+
18
+ class UserConnection(BaseModel):
19
+ user_id: str
20
+ connection_count: int
21
+
22
+ class AllConnectionsStatus(BaseModel):
23
+ total_users: int
24
+ total_connections: int
25
+ users: List[UserConnection]
26
+
27
+ # Khởi tạo router
28
+ router = APIRouter(
29
+ prefix="/ws",
30
+ tags=["WebSockets"],
31
+ )
32
+
33
+ class ConnectionManager:
34
+ """Quản lý các kết nối WebSocket"""
35
+
36
+ def __init__(self):
37
+ # Lưu trữ các kết nối theo user_id
38
+ self.active_connections: Dict[str, List[WebSocket]] = {}
39
+
40
+ async def connect(self, websocket: WebSocket, user_id: str):
41
+ """Kết nối một WebSocket mới"""
42
+ await websocket.accept()
43
+ if user_id not in self.active_connections:
44
+ self.active_connections[user_id] = []
45
+ self.active_connections[user_id].append(websocket)
46
+ logger.info(f"New WebSocket connection for user {user_id}. Total connections: {len(self.active_connections[user_id])}")
47
+
48
+ def disconnect(self, websocket: WebSocket, user_id: str):
49
+ """Ngắt kết nối WebSocket"""
50
+ if user_id in self.active_connections:
51
+ if websocket in self.active_connections[user_id]:
52
+ self.active_connections[user_id].remove(websocket)
53
+ # Xóa user_id khỏi dict nếu không còn kết nối nào
54
+ if not self.active_connections[user_id]:
55
+ del self.active_connections[user_id]
56
+ logger.info(f"WebSocket disconnected for user {user_id}")
57
+
58
+ async def send_message(self, message: Dict[str, Any], user_id: str):
59
+ """Gửi tin nhắn tới tất cả kết nối của một user"""
60
+ if user_id in self.active_connections:
61
+ disconnected_websockets = []
62
+ for websocket in self.active_connections[user_id]:
63
+ try:
64
+ await websocket.send_text(json.dumps(message))
65
+ except Exception as e:
66
+ logger.error(f"Error sending message to WebSocket: {str(e)}")
67
+ disconnected_websockets.append(websocket)
68
+
69
+ # Xóa các kết nối bị ngắt
70
+ for websocket in disconnected_websockets:
71
+ self.disconnect(websocket, user_id)
72
+
73
+ def get_connection_status(self, user_id: str = None) -> Dict[str, Any]:
74
+ """Lấy thông tin về trạng thái kết nối WebSocket"""
75
+ if user_id:
76
+ # Trả về thông tin kết nối cho user cụ thể
77
+ if user_id in self.active_connections:
78
+ return {
79
+ "user_id": user_id,
80
+ "active": True,
81
+ "connection_count": len(self.active_connections[user_id]),
82
+ "last_activity": time.time()
83
+ }
84
+ else:
85
+ return {
86
+ "user_id": user_id,
87
+ "active": False,
88
+ "connection_count": 0,
89
+ "last_activity": None
90
+ }
91
+ else:
92
+ # Trả về thông tin tất cả kết nối
93
+ result = {
94
+ "total_users": len(self.active_connections),
95
+ "total_connections": sum(len(connections) for connections in self.active_connections.values()),
96
+ "users": []
97
+ }
98
+
99
+ for uid, connections in self.active_connections.items():
100
+ result["users"].append({
101
+ "user_id": uid,
102
+ "connection_count": len(connections)
103
+ })
104
+
105
+ return result
106
+
107
+
108
+ # Tạo instance của ConnectionManager
109
+ manager = ConnectionManager()
110
+
111
+ @router.websocket("/pdf/{user_id}")
112
+ async def websocket_endpoint(websocket: WebSocket, user_id: str):
113
+ """Endpoint WebSocket để cập nhật tiến trình xử lý PDF"""
114
+ await manager.connect(websocket, user_id)
115
+ try:
116
+ while True:
117
+ # Đợi tin nhắn từ client (chỉ để giữ kết nối)
118
+ await websocket.receive_text()
119
+ except WebSocketDisconnect:
120
+ manager.disconnect(websocket, user_id)
121
+ except Exception as e:
122
+ logger.error(f"WebSocket error: {str(e)}")
123
+ manager.disconnect(websocket, user_id)
124
+
125
+ # API endpoints để kiểm tra trạng thái WebSocket
126
+ @router.get("/status", response_model=AllConnectionsStatus, responses={
127
+ 200: {
128
+ "description": "Successful response",
129
+ "content": {
130
+ "application/json": {
131
+ "example": {
132
+ "total_users": 2,
133
+ "total_connections": 3,
134
+ "users": [
135
+ {"user_id": "user1", "connection_count": 2},
136
+ {"user_id": "user2", "connection_count": 1}
137
+ ]
138
+ }
139
+ }
140
+ }
141
+ }
142
+ })
143
+ async def get_all_websocket_connections():
144
+ """
145
+ Lấy thông tin về tất cả kết nối WebSocket hiện tại.
146
+
147
+ Endpoint này trả về:
148
+ - Tổng số người dùng đang kết nối
149
+ - Tổng số kết nối WebSocket
150
+ - Danh sách người dùng kèm theo số lượng kết nối của mỗi người
151
+ """
152
+ return manager.get_connection_status()
153
+
154
+ @router.get("/status/{user_id}", response_model=ConnectionStatus, responses={
155
+ 200: {
156
+ "description": "Successful response for active connection",
157
+ "content": {
158
+ "application/json": {
159
+ "examples": {
160
+ "active_connection": {
161
+ "summary": "Active connection",
162
+ "value": {
163
+ "user_id": "user123",
164
+ "active": True,
165
+ "connection_count": 2,
166
+ "last_activity": 1634567890.123
167
+ }
168
+ },
169
+ "no_connection": {
170
+ "summary": "No active connection",
171
+ "value": {
172
+ "user_id": "user456",
173
+ "active": False,
174
+ "connection_count": 0,
175
+ "last_activity": None
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ })
183
+ async def get_user_websocket_status(user_id: str):
184
+ """
185
+ Lấy thông tin về kết nối WebSocket của một người dùng cụ thể.
186
+
187
+ Parameters:
188
+ - **user_id**: ID của người dùng cần kiểm tra
189
+
190
+ Returns:
191
+ - Thông tin về trạng thái kết nối, bao gồm:
192
+ - active: Có đang kết nối hay không
193
+ - connection_count: Số lượng kết nối hiện tại
194
+ - last_activity: Thời gian hoạt động gần nhất
195
+ """
196
+ return manager.get_connection_status(user_id)
197
+
198
+ # Các hàm gửi thông báo cập nhật trạng thái
199
+
200
+ async def send_pdf_upload_started(user_id: str, filename: str, document_id: str):
201
+ """Gửi thông báo bắt đầu upload PDF"""
202
+ await manager.send_message({
203
+ "type": "pdf_upload_started",
204
+ "document_id": document_id,
205
+ "filename": filename,
206
+ "timestamp": int(time.time())
207
+ }, user_id)
208
+
209
+ async def send_pdf_upload_progress(user_id: str, document_id: str, step: str, progress: float, message: str):
210
+ """Gửi thông báo tiến độ upload PDF"""
211
+ await manager.send_message({
212
+ "type": "pdf_upload_progress",
213
+ "document_id": document_id,
214
+ "step": step,
215
+ "progress": progress,
216
+ "message": message,
217
+ "timestamp": int(time.time())
218
+ }, user_id)
219
+
220
+ async def send_pdf_upload_completed(user_id: str, document_id: str, filename: str, chunks: int):
221
+ """Gửi thông báo hoàn thành upload PDF"""
222
+ await manager.send_message({
223
+ "type": "pdf_upload_completed",
224
+ "document_id": document_id,
225
+ "filename": filename,
226
+ "chunks": chunks,
227
+ "timestamp": int(time.time())
228
+ }, user_id)
229
+
230
+ async def send_pdf_upload_failed(user_id: str, document_id: str, filename: str, error: str):
231
+ """Gửi thông báo lỗi upload PDF"""
232
+ await manager.send_message({
233
+ "type": "pdf_upload_failed",
234
+ "document_id": document_id,
235
+ "filename": filename,
236
+ "error": error,
237
+ "timestamp": int(time.time())
238
+ }, user_id)
239
+
240
+ async def send_pdf_delete_started(user_id: str, namespace: str):
241
+ """Gửi thông báo bắt đầu xóa PDF"""
242
+ await manager.send_message({
243
+ "type": "pdf_delete_started",
244
+ "namespace": namespace,
245
+ "timestamp": int(time.time())
246
+ }, user_id)
247
+
248
+ async def send_pdf_delete_completed(user_id: str, namespace: str):
249
+ """Gửi thông báo hoàn thành xóa PDF"""
250
+ await manager.send_message({
251
+ "type": "pdf_delete_completed",
252
+ "namespace": namespace,
253
+ "timestamp": int(time.time())
254
+ }, user_id)
255
+
256
+ async def send_pdf_delete_failed(user_id: str, namespace: str, error: str):
257
+ """Gửi thông báo lỗi xóa PDF"""
258
+ await manager.send_message({
259
+ "type": "pdf_delete_failed",
260
+ "namespace": namespace,
261
+ "error": error,
262
+ "timestamp": int(time.time())
263
+ }, user_id)
app/api/postgresql_routes.py CHANGED
The diff for this file is too large to render. See raw diff
 
app/api/rag_routes.py CHANGED
@@ -11,9 +11,9 @@ import google.generativeai as genai
11
  from datetime import datetime
12
  from langchain.prompts import PromptTemplate
13
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
14
- from app.utils.utils import cache, timer_decorator
15
 
16
- from app.database.mongodb import get_user_history, get_chat_history, get_request_history, save_session, session_collection
17
  from app.database.pinecone import (
18
  search_vectors,
19
  get_chain,
@@ -33,32 +33,6 @@ from app.models.rag_models import (
33
  UserMessageModel
34
  )
35
 
36
- # Sử dụng bộ nhớ đệm thay vì Redis
37
- class SimpleCache:
38
- def __init__(self):
39
- self.cache = {}
40
- self.expiration = {}
41
-
42
- async def get(self, key):
43
- if key in self.cache:
44
- # Kiểm tra xem cache đã hết hạn chưa
45
- if key in self.expiration and self.expiration[key] > time.time():
46
- return self.cache[key]
47
- else:
48
- # Xóa cache đã hết hạn
49
- if key in self.cache:
50
- del self.cache[key]
51
- if key in self.expiration:
52
- del self.expiration[key]
53
- return None
54
-
55
- async def set(self, key, value, ex=300): # Mặc định 5 phút
56
- self.cache[key] = value
57
- self.expiration[key] = time.time() + ex
58
-
59
- # Khởi tạo SimpleCache
60
- redis_client = SimpleCache()
61
-
62
  # Configure logging
63
  logger = logging.getLogger(__name__)
64
 
@@ -72,6 +46,29 @@ router = APIRouter(
72
  tags=["RAG"],
73
  )
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  # Create a prompt template with conversation history
76
  prompt = PromptTemplate(
77
  template = """Goal:
@@ -87,7 +84,7 @@ Warning:
87
  Let's support users like a real tour guide, not a bot. The information in core knowledge is your own knowledge.
88
  Your knowledge is provided in the Core Knowledge. All of information in Core Knowledge is about Da Nang, Vietnam.
89
  You just care about current time that user mention when user ask about Solana event.
90
- If you do not have enough information to answer user's question, please reply with "I don't know. I don't have information about that".
91
 
92
  Core knowledge:
93
  {context}
@@ -162,102 +159,18 @@ async def chat(request: ChatRequest, background_tasks: BackgroundTasks):
162
  """
163
  start_time = time.time()
164
  try:
165
- # Create cache key for request
166
- cache_key = f"rag_chat:{request.user_id}:{request.question}:{request.include_history}:{request.use_rag}:{request.similarity_top_k}:{request.limit_k}:{request.similarity_metric}:{request.similarity_threshold}"
167
-
168
- # Check cache using redis_client instead of cache
169
- cached_response = await redis_client.get(cache_key)
170
- if cached_response is not None:
171
- logger.info(f"Cache hit for RAG chat request from user {request.user_id}")
172
- try:
173
- # If cached_response is string (JSON), parse it
174
- if isinstance(cached_response, str):
175
- cached_data = json.loads(cached_response)
176
- return ChatResponse(
177
- answer=cached_data.get("answer", ""),
178
- processing_time=cached_data.get("processing_time", 0.0)
179
- )
180
- # If cached_response is object with sources, extract answer and processing_time
181
- elif hasattr(cached_response, 'sources'):
182
- return ChatResponse(
183
- answer=cached_response.answer,
184
- processing_time=cached_response.processing_time
185
- )
186
- # Otherwise, return cached response as is
187
- return cached_response
188
- except Exception as e:
189
- logger.error(f"Error parsing cached response: {e}")
190
- # Continue processing if cache parsing fails
191
-
192
  # Save user message first (so it's available for user history)
193
  session_id = request.session_id or f"{request.user_id}_{datetime.now().strftime('%Y-%m-%d_%H:%M:%S')}"
194
- logger.info(f"Processing chat request for user {request.user_id}, session {session_id}")
195
-
196
- # First, save the user's message so it's available for history lookups
197
- try:
198
- # Save user's question
199
- save_session(
200
- session_id=session_id,
201
- factor="user",
202
- action="asking_freely",
203
- first_name=getattr(request, 'first_name', "User"),
204
- last_name=getattr(request, 'last_name', ""),
205
- message=request.question,
206
- user_id=request.user_id,
207
- username=getattr(request, 'username', ""),
208
- response=None # No response yet
209
- )
210
- logger.info(f"User message saved for session {session_id}")
211
- except Exception as e:
212
- logger.error(f"Error saving user message to session: {e}")
213
- # Continue processing even if saving fails
214
-
215
- # Use the RAG pipeline
216
- if request.use_rag:
217
- # Get the retriever with custom parameters
218
- retriever = get_chain(
219
- top_k=request.similarity_top_k,
220
- limit_k=request.limit_k,
221
- similarity_metric=request.similarity_metric,
222
- similarity_threshold=request.similarity_threshold
223
- )
224
- if not retriever:
225
- raise HTTPException(status_code=500, detail="Failed to initialize retriever")
226
-
227
- # Get request history for context
228
- context_query = get_request_history(request.user_id) if request.include_history else request.question
229
- logger.info(f"Using context query for retrieval: {context_query[:100]}...")
230
-
231
- # Retrieve relevant documents
232
- retrieved_docs = retriever.invoke(context_query)
233
- context = "\n".join([doc.page_content for doc in retrieved_docs])
234
-
235
- # Prepare sources
236
- sources = []
237
- for doc in retrieved_docs:
238
- source = None
239
- metadata = {}
240
-
241
- if hasattr(doc, 'metadata'):
242
- source = doc.metadata.get('source', None)
243
- # Extract score information
244
- score = doc.metadata.get('score', None)
245
- normalized_score = doc.metadata.get('normalized_score', None)
246
- # Remove score info from metadata to avoid duplication
247
- metadata = {k: v for k, v in doc.metadata.items()
248
- if k not in ['text', 'source', 'score', 'normalized_score']}
249
-
250
- sources.append(SourceDocument(
251
- text=doc.page_content,
252
- source=source,
253
- score=score,
254
- normalized_score=normalized_score,
255
- metadata=metadata
256
- ))
257
- else:
258
- # No RAG
259
- context = ""
260
- sources = None
261
 
262
  # Get chat history
263
  chat_history = get_chat_history(request.user_id) if request.include_history else ""
@@ -295,11 +208,50 @@ async def chat(request: ChatRequest, background_tasks: BackgroundTasks):
295
  generation_config=generation_config,
296
  safety_settings=safety_settings
297
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  # Generate the prompt using template
300
  prompt_text = prompt.format(
301
  context=context,
302
- question=request.question,
303
  chat_history=chat_history
304
  )
305
  logger.info(f"Full prompt with history and context: {prompt_text}")
@@ -308,59 +260,11 @@ async def chat(request: ChatRequest, background_tasks: BackgroundTasks):
308
  response = model.generate_content(prompt_text)
309
  answer = response.text
310
 
311
- # Save the RAG response
312
- try:
313
- # Now save the RAG response with the same session_id
314
- save_session(
315
- session_id=session_id,
316
- factor="rag",
317
- action="RAG_response",
318
- first_name=getattr(request, 'first_name', "User"),
319
- last_name=getattr(request, 'last_name', ""),
320
- message=request.question,
321
- user_id=request.user_id,
322
- username=getattr(request, 'username', ""),
323
- response=answer
324
- )
325
- logger.info(f"RAG response saved for session {session_id}")
326
-
327
- # Check if the response starts with "I don't know" and trigger notification
328
- if answer.strip().lower().startswith("i don't know"):
329
- from app.api.websocket_routes import send_notification
330
- notification_data = {
331
- "session_id": session_id,
332
- "factor": "rag",
333
- "action": "RAG_response",
334
- "message": request.question,
335
- "user_id": request.user_id,
336
- "username": getattr(request, 'username', ""),
337
- "first_name": getattr(request, 'first_name', "User"),
338
- "last_name": getattr(request, 'last_name', ""),
339
- "response": answer,
340
- "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
341
- }
342
- background_tasks.add_task(send_notification, notification_data)
343
- logger.info(f"Notification queued for session {session_id} - response starts with 'I don't know'")
344
- except Exception as e:
345
- logger.error(f"Error saving RAG response to session: {e}")
346
- # Continue processing even if saving fails
347
-
348
  # Calculate processing time
349
  processing_time = time.time() - start_time
350
 
351
- # Create internal response object with sources for logging
352
- internal_response = ChatResponseInternal(
353
- answer=answer,
354
- sources=sources,
355
- processing_time=processing_time
356
- )
357
-
358
  # Log full response with sources
359
- logger.info(f"Generated response for user {request.user_id}: {answer}")
360
- if sources:
361
- logger.info(f"Sources used: {len(sources)} documents")
362
- for i, source in enumerate(sources):
363
- logger.info(f"Source {i+1}: {source.source or 'Unknown'} (score: {source.score})")
364
 
365
  # Create response object for API (without sources)
366
  chat_response = ChatResponse(
@@ -368,18 +272,6 @@ async def chat(request: ChatRequest, background_tasks: BackgroundTasks):
368
  processing_time=processing_time
369
  )
370
 
371
- # Cache result using redis_client instead of cache
372
- try:
373
- # Convert to JSON to ensure it can be cached
374
- cache_data = {
375
- "answer": answer,
376
- "processing_time": processing_time
377
- }
378
- await redis_client.set(cache_key, json.dumps(cache_data), ex=300)
379
- except Exception as e:
380
- logger.error(f"Error caching response: {e}")
381
- # Continue even if caching fails
382
-
383
  # Return response
384
  return chat_response
385
  except Exception as e:
@@ -443,96 +335,4 @@ async def health_check():
443
  "services": services,
444
  "retrieval_config": retrieval_config,
445
  "timestamp": datetime.now().isoformat()
446
- }
447
-
448
- @router.post("/rag")
449
- async def process_rag(request: Request, user_data: UserMessageModel, background_tasks: BackgroundTasks):
450
- """
451
- Process a user message through the RAG pipeline and return a response.
452
-
453
- Parameters:
454
- - **user_id**: User ID from the client application
455
- - **session_id**: Session ID for tracking the conversation
456
- - **message**: User's message/question
457
- - **similarity_top_k**: (Optional) Number of top similar documents to return after filtering
458
- - **limit_k**: (Optional) Maximum number of documents to retrieve from vector store
459
- - **similarity_metric**: (Optional) Similarity metric to use (cosine, dotproduct, euclidean)
460
- - **similarity_threshold**: (Optional) Threshold for vector similarity (0-1)
461
- """
462
- try:
463
- # Extract request data
464
- user_id = user_data.user_id
465
- session_id = user_data.session_id
466
- message = user_data.message
467
-
468
- # Extract retrieval parameters (use defaults if not provided)
469
- top_k = user_data.similarity_top_k or DEFAULT_TOP_K
470
- limit_k = user_data.limit_k or DEFAULT_LIMIT_K
471
- similarity_metric = user_data.similarity_metric or DEFAULT_SIMILARITY_METRIC
472
- similarity_threshold = user_data.similarity_threshold or DEFAULT_SIMILARITY_THRESHOLD
473
-
474
- logger.info(f"RAG request received for user_id={user_id}, session_id={session_id}")
475
- logger.info(f"Message: {message[:100]}..." if len(message) > 100 else f"Message: {message}")
476
- logger.info(f"Retrieval parameters: top_k={top_k}, limit_k={limit_k}, metric={similarity_metric}, threshold={similarity_threshold}")
477
-
478
- # Create a cache key for this request to avoid reprocessing identical questions
479
- cache_key = f"rag_{user_id}_{session_id}_{hashlib.md5(message.encode()).hexdigest()}_{top_k}_{limit_k}_{similarity_metric}_{similarity_threshold}"
480
-
481
- # Check if we have this response cached
482
- cached_result = await redis_client.get(cache_key)
483
- if cached_result:
484
- logger.info(f"Cache hit for key: {cache_key}")
485
- if isinstance(cached_result, str): # If stored as JSON string
486
- return json.loads(cached_result)
487
- return cached_result
488
-
489
- # Save user message to MongoDB
490
- try:
491
- # Save user's question
492
- save_session(
493
- session_id=session_id,
494
- factor="user",
495
- action="asking_freely",
496
- first_name="User", # You can update this with actual data if available
497
- last_name="",
498
- message=message,
499
- user_id=user_id,
500
- username="",
501
- response=None # No response yet
502
- )
503
- logger.info(f"User message saved to MongoDB with session_id: {session_id}")
504
- except Exception as e:
505
- logger.error(f"Error saving user message: {e}")
506
- # Continue anyway to try to get a response
507
-
508
- # Create a ChatRequest object to reuse the existing chat endpoint
509
- chat_request = ChatRequest(
510
- user_id=user_id,
511
- question=message,
512
- include_history=True,
513
- use_rag=True,
514
- similarity_top_k=top_k,
515
- limit_k=limit_k,
516
- similarity_metric=similarity_metric,
517
- similarity_threshold=similarity_threshold,
518
- session_id=session_id
519
- )
520
-
521
- # Process through the chat endpoint
522
- response = await chat(chat_request, background_tasks)
523
-
524
- # Cache the response
525
- try:
526
- await redis_client.set(cache_key, json.dumps({
527
- "answer": response.answer,
528
- "processing_time": response.processing_time
529
- }))
530
- logger.info(f"Cached response for key: {cache_key}")
531
- except Exception as e:
532
- logger.error(f"Failed to cache response: {e}")
533
-
534
- return response
535
- except Exception as e:
536
- logger.error(f"Error processing RAG request: {e}")
537
- logger.error(traceback.format_exc())
538
- raise HTTPException(status_code=500, detail=f"Error processing request: {str(e)}")
 
11
  from datetime import datetime
12
  from langchain.prompts import PromptTemplate
13
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
14
+ from app.utils.utils import timer_decorator
15
 
16
+ from app.database.mongodb import get_chat_history, get_request_history, session_collection
17
  from app.database.pinecone import (
18
  search_vectors,
19
  get_chain,
 
33
  UserMessageModel
34
  )
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  # Configure logging
37
  logger = logging.getLogger(__name__)
38
 
 
46
  tags=["RAG"],
47
  )
48
 
49
+ fix_request = PromptTemplate(
50
+ template = """Goal:
51
+ Your task is fixing user'srequest to get all information of history chat.
52
+ You will received a conversation history and current request of user.
53
+ Generate a new request that make sense if current request related to history conversation.
54
+
55
+ Return Format:
56
+ Only return the fully users' request with all the important keywords.
57
+ If the current message is NOT related to the conversation history or there is no chat history: Return user's current request.
58
+ If the current message IS related to the conversation history: Return new request based on information from the conversation history and the current request.
59
+
60
+ Warning:
61
+ Only use history chat if current request is truly relevant to the previous conversation.
62
+
63
+ Conversation History:
64
+ {chat_history}
65
+
66
+ User current message:
67
+ {question}
68
+ """,
69
+ input_variables = ["chat_history", "question"],
70
+ )
71
+
72
  # Create a prompt template with conversation history
73
  prompt = PromptTemplate(
74
  template = """Goal:
 
84
  Let's support users like a real tour guide, not a bot. The information in core knowledge is your own knowledge.
85
  Your knowledge is provided in the Core Knowledge. All of information in Core Knowledge is about Da Nang, Vietnam.
86
  You just care about current time that user mention when user ask about Solana event.
87
+ Only use core knowledge to answer. If you do not have enough information to answer user's question, please reply with "I'm sorry. I don't have information about that" and Give users some more options to ask.
88
 
89
  Core knowledge:
90
  {context}
 
159
  """
160
  start_time = time.time()
161
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  # Save user message first (so it's available for user history)
163
  session_id = request.session_id or f"{request.user_id}_{datetime.now().strftime('%Y-%m-%d_%H:%M:%S')}"
164
+ # logger.info(f"Processing chat request for user {request.user_id}, session {session_id}")
165
+
166
+ retriever = get_chain(
167
+ top_k=request.similarity_top_k,
168
+ limit_k=request.limit_k,
169
+ similarity_metric=request.similarity_metric,
170
+ similarity_threshold=request.similarity_threshold
171
+ )
172
+ if not retriever:
173
+ raise HTTPException(status_code=500, detail="Failed to initialize retriever")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  # Get chat history
176
  chat_history = get_chat_history(request.user_id) if request.include_history else ""
 
208
  generation_config=generation_config,
209
  safety_settings=safety_settings
210
  )
211
+
212
+ prompt_request = fix_request.format(
213
+ question=request.question,
214
+ chat_history=chat_history
215
+ )
216
+
217
+ # Log thời gian bắt đầu final_request
218
+ final_request_start_time = time.time()
219
+ final_request = model.generate_content(prompt_request)
220
+ # Log thời gian hoàn thành final_request
221
+ logger.info(f"Fixed Request: {final_request.text}")
222
+ logger.info(f"Final request generation time: {time.time() - final_request_start_time:.2f} seconds")
223
+ # print(final_request.text)
224
+
225
+ retrieved_docs = retriever.invoke(final_request.text)
226
+ logger.info(f"Retrieve: {retrieved_docs}")
227
+ context = "\n".join([doc.page_content for doc in retrieved_docs])
228
+
229
+ sources = []
230
+ for doc in retrieved_docs:
231
+ source = None
232
+ metadata = {}
233
+
234
+ if hasattr(doc, 'metadata'):
235
+ source = doc.metadata.get('source', None)
236
+ # Extract score information
237
+ score = doc.metadata.get('score', None)
238
+ normalized_score = doc.metadata.get('normalized_score', None)
239
+ # Remove score info from metadata to avoid duplication
240
+ metadata = {k: v for k, v in doc.metadata.items()
241
+ if k not in ['text', 'source', 'score', 'normalized_score']}
242
+
243
+ sources.append(SourceDocument(
244
+ text=doc.page_content,
245
+ source=source,
246
+ score=score,
247
+ normalized_score=normalized_score,
248
+ metadata=metadata
249
+ ))
250
 
251
  # Generate the prompt using template
252
  prompt_text = prompt.format(
253
  context=context,
254
+ question=final_request.text,
255
  chat_history=chat_history
256
  )
257
  logger.info(f"Full prompt with history and context: {prompt_text}")
 
260
  response = model.generate_content(prompt_text)
261
  answer = response.text
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  # Calculate processing time
264
  processing_time = time.time() - start_time
265
 
 
 
 
 
 
 
 
266
  # Log full response with sources
267
+ # logger.info(f"Generated response for user {request.user_id}: {answer}")
 
 
 
 
268
 
269
  # Create response object for API (without sources)
270
  chat_response = ChatResponse(
 
272
  processing_time=processing_time
273
  )
274
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  # Return response
276
  return chat_response
277
  except Exception as e:
 
335
  "services": services,
336
  "retrieval_config": retrieval_config,
337
  "timestamp": datetime.now().isoformat()
338
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/database/models.py CHANGED
@@ -25,6 +25,8 @@ class EmergencyItem(Base):
25
  location = Column(String, nullable=True) # Will be converted to/from PostGIS POINT type
26
  priority = Column(Integer, default=0)
27
  is_active = Column(Boolean, default=True)
 
 
28
  created_at = Column(DateTime, server_default=func.now())
29
  updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
30
 
@@ -39,11 +41,36 @@ class EventItem(Base):
39
  date_start = Column(DateTime, nullable=False)
40
  date_end = Column(DateTime, nullable=True)
41
  price = Column(JSON, nullable=True)
 
42
  is_active = Column(Boolean, default=True)
43
  featured = Column(Boolean, default=False)
44
  created_at = Column(DateTime, server_default=func.now())
45
  updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  class VectorDatabase(Base):
48
  __tablename__ = "vector_database"
49
 
 
25
  location = Column(String, nullable=True) # Will be converted to/from PostGIS POINT type
26
  priority = Column(Integer, default=0)
27
  is_active = Column(Boolean, default=True)
28
+ section = Column(String, nullable=True) # Section field (16.1, 16.2.1, 16.2.2, 16.3)
29
+ section_id = Column(Integer, nullable=True) # Numeric identifier for section
30
  created_at = Column(DateTime, server_default=func.now())
31
  updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
32
 
 
41
  date_start = Column(DateTime, nullable=False)
42
  date_end = Column(DateTime, nullable=True)
43
  price = Column(JSON, nullable=True)
44
+ url = Column(String, nullable=True)
45
  is_active = Column(Boolean, default=True)
46
  featured = Column(Boolean, default=False)
47
  created_at = Column(DateTime, server_default=func.now())
48
  updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
49
 
50
+ class AboutPixity(Base):
51
+ __tablename__ = "about_pixity"
52
+
53
+ id = Column(Integer, primary_key=True, index=True)
54
+ content = Column(Text, nullable=False)
55
+ created_at = Column(DateTime, server_default=func.now())
56
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
57
+
58
+ class SolanaSummit(Base):
59
+ __tablename__ = "solana_summit"
60
+
61
+ id = Column(Integer, primary_key=True, index=True)
62
+ content = Column(Text, nullable=False)
63
+ created_at = Column(DateTime, server_default=func.now())
64
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
65
+
66
+ class DaNangBucketList(Base):
67
+ __tablename__ = "danang_bucket_list"
68
+
69
+ id = Column(Integer, primary_key=True, index=True)
70
+ content = Column(Text, nullable=False)
71
+ created_at = Column(DateTime, server_default=func.now())
72
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
73
+
74
  class VectorDatabase(Base):
75
  __tablename__ = "vector_database"
76
 
app/database/mongodb.py CHANGED
@@ -20,6 +20,10 @@ COLLECTION_NAME = os.getenv("COLLECTION_NAME", "session_chat")
20
  # Set timeout for MongoDB connection
21
  MONGODB_TIMEOUT = int(os.getenv("MONGODB_TIMEOUT", "5000")) # 5 seconds by default
22
 
 
 
 
 
23
  # Create MongoDB connection with timeout
24
  try:
25
  client = MongoClient(MONGODB_URL, serverSelectionTimeoutMS=MONGODB_TIMEOUT)
@@ -82,6 +86,7 @@ def save_session(session_id, factor, action, first_name, last_name, message, use
82
  }
83
  result = session_collection.insert_one(session_data)
84
  logger.info(f"Session saved with ID: {result.inserted_id}")
 
85
  return {
86
  "acknowledged": result.acknowledged,
87
  "inserted_id": str(result.inserted_id),
@@ -94,15 +99,18 @@ def save_session(session_id, factor, action, first_name, last_name, message, use
94
  def update_session_response(session_id, response):
95
  """Update a session with response"""
96
  try:
 
 
 
 
 
 
 
97
  result = session_collection.update_one(
98
  {"session_id": session_id},
99
  {"$set": {"response": response}}
100
  )
101
 
102
- if result.matched_count == 0:
103
- logger.warning(f"No session found with ID: {session_id}")
104
- return False
105
-
106
  logger.info(f"Session {session_id} updated with response")
107
  return True
108
  except Exception as e:
@@ -112,80 +120,61 @@ def update_session_response(session_id, response):
112
  def get_recent_sessions(user_id, action, n=3):
113
  """Get n most recent sessions for a specific user and action"""
114
  try:
115
- return list(
 
116
  session_collection.find(
117
  {"user_id": user_id, "action": action},
118
  {"_id": 0, "message": 1, "response": 1}
119
  ).sort("created_at_datetime", -1).limit(n)
120
  )
121
- except Exception as e:
122
- logger.error(f"Error getting recent sessions: {e}")
123
- return []
124
-
125
- def get_user_history(user_id, n=3):
126
- """Get user history for a specific user"""
127
- try:
128
- # Find all messages of this user
129
- user_messages = list(
130
- session_collection.find(
131
- {
132
- "user_id": user_id,
133
- "message": {"$exists": True, "$ne": None},
134
- # Include all user messages regardless of action type
135
- }
136
- ).sort("created_at_datetime", -1).limit(n * 2) # Get more to ensure we have enough pairs
137
- )
138
-
139
- # Group messages by session_id to find pairs
140
- session_dict = {}
141
- for msg in user_messages:
142
- session_id = msg.get("session_id")
143
- if session_id not in session_dict:
144
- session_dict[session_id] = {}
145
-
146
- if msg.get("factor", "").lower() == "user":
147
- session_dict[session_id]["question"] = msg.get("message", "")
148
- session_dict[session_id]["timestamp"] = msg.get("created_at_datetime")
149
- elif msg.get("factor", "").lower() == "rag":
150
- session_dict[session_id]["answer"] = msg.get("response", "")
151
-
152
- # Build history from complete pairs only (with both question and answer)
153
- history = []
154
- for session_id, data in session_dict.items():
155
- if "question" in data and "answer" in data and data.get("answer"):
156
- history.append({
157
- "question": data["question"],
158
- "answer": data["answer"]
159
- })
160
-
161
- # Sort by timestamp and limit to n
162
- history = sorted(history, key=lambda x: x.get("timestamp", 0), reverse=True)[:n]
163
 
164
- logger.info(f"Retrieved {len(history)} history items for user {user_id}")
165
- return history
166
  except Exception as e:
167
- logger.error(f"Error getting user history: {e}")
168
  return []
169
 
170
- # Functions from chatbot.py
171
- def get_chat_history(user_id, n=5):
172
- """Get conversation history for a specific user from MongoDB in format suitable for LLM prompt"""
 
 
 
 
 
 
173
  try:
174
- history = get_user_history(user_id, n)
 
 
 
 
 
 
 
175
 
176
- # Format history for prompt context
177
- formatted_history = ""
178
- for item in history:
179
- formatted_history += f"User: {item['question']}\nAssistant: {item['answer']}\n\n"
 
 
 
180
 
181
- return formatted_history
 
 
 
 
 
182
  except Exception as e:
183
- logger.error(f"Error getting chat history for prompt: {e}")
184
  return ""
185
 
186
  def get_request_history(user_id, n=3):
187
  """Get the most recent user requests to use as context for retrieval"""
188
  try:
 
189
  history = get_user_history(user_id, n)
190
 
191
  # Just extract the questions for context
 
20
  # Set timeout for MongoDB connection
21
  MONGODB_TIMEOUT = int(os.getenv("MONGODB_TIMEOUT", "5000")) # 5 seconds by default
22
 
23
+ # Legacy cache settings - now only used for configuration purposes
24
+ HISTORY_CACHE_TTL = int(os.getenv("HISTORY_CACHE_TTL", "3600")) # 1 hour by default
25
+ HISTORY_QUEUE_SIZE = int(os.getenv("HISTORY_QUEUE_SIZE", "10")) # 10 items by default
26
+
27
  # Create MongoDB connection with timeout
28
  try:
29
  client = MongoClient(MONGODB_URL, serverSelectionTimeoutMS=MONGODB_TIMEOUT)
 
86
  }
87
  result = session_collection.insert_one(session_data)
88
  logger.info(f"Session saved with ID: {result.inserted_id}")
89
+
90
  return {
91
  "acknowledged": result.acknowledged,
92
  "inserted_id": str(result.inserted_id),
 
99
  def update_session_response(session_id, response):
100
  """Update a session with response"""
101
  try:
102
+ # Lấy session hiện có
103
+ existing_session = session_collection.find_one({"session_id": session_id})
104
+
105
+ if not existing_session:
106
+ logger.warning(f"No session found with ID: {session_id}")
107
+ return False
108
+
109
  result = session_collection.update_one(
110
  {"session_id": session_id},
111
  {"$set": {"response": response}}
112
  )
113
 
 
 
 
 
114
  logger.info(f"Session {session_id} updated with response")
115
  return True
116
  except Exception as e:
 
120
  def get_recent_sessions(user_id, action, n=3):
121
  """Get n most recent sessions for a specific user and action"""
122
  try:
123
+ # Truy vấn trực tiếp từ MongoDB
124
+ result = list(
125
  session_collection.find(
126
  {"user_id": user_id, "action": action},
127
  {"_id": 0, "message": 1, "response": 1}
128
  ).sort("created_at_datetime", -1).limit(n)
129
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ logger.debug(f"Retrieved {len(result)} recent sessions for user {user_id}, action {action}")
132
+ return result
133
  except Exception as e:
134
+ logger.error(f"Error getting recent sessions: {e}")
135
  return []
136
 
137
+ def get_chat_history(user_id, n = 5) -> str:
138
+ """
139
+ Lấy lịch sử chat cho user_id từ MongoDB ghép thành chuỗi theo định dạng:
140
+
141
+ User: ...
142
+ Bot: ...
143
+ User: ...
144
+ Bot: ...
145
+ """
146
  try:
147
+ # Truy vấn các document có user_id, sắp xếp theo created_at tăng dần
148
+ # Get the 4 most recent documents first, then sort them in ascending order
149
+ docs = list(session_collection.find({"user_id": str(user_id)}).sort("created_at", -1).limit(n))
150
+ # Reverse the list to get chronological order (oldest to newest)
151
+ docs.reverse()
152
+ if not docs:
153
+ logger.info(f"Không tìm thấy dữ liệu cho user_id: {user_id}")
154
+ return ""
155
 
156
+ conversation_lines = []
157
+ # Xử lý từng document theo cấu trúc mới
158
+ for doc in docs:
159
+ factor = doc.get("factor", "").lower()
160
+ action = doc.get("action", "").lower()
161
+ message = doc.get("message", "")
162
+ response = doc.get("response", "")
163
 
164
+ if factor == "user" and action == "asking_freely":
165
+ conversation_lines.append(f"User: {message}")
166
+ conversation_lines.append(f"Bot: {response}")
167
+
168
+ # Ghép các dòng thành chuỗi
169
+ return "\n".join(conversation_lines)
170
  except Exception as e:
171
+ logger.error(f"Lỗi khi lấy lịch sử chat cho user_id {user_id}: {e}")
172
  return ""
173
 
174
  def get_request_history(user_id, n=3):
175
  """Get the most recent user requests to use as context for retrieval"""
176
  try:
177
+ # Lấy lịch sử trực tiếp từ MongoDB (thông qua get_user_history đã sửa đổi)
178
  history = get_user_history(user_id, n)
179
 
180
  # Just extract the questions for context
app/database/pinecone.py CHANGED
@@ -6,7 +6,6 @@ from typing import Optional, List, Dict, Any, Union, Tuple
6
  import time
7
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
8
  import google.generativeai as genai
9
- from app.utils.utils import cache
10
  from langchain_core.retrievers import BaseRetriever
11
  from langchain.callbacks.manager import Callbacks
12
  from langchain_core.documents import Document
@@ -73,23 +72,39 @@ def init_pinecone():
73
  if pc is None:
74
  logger.info(f"Initializing Pinecone connection to index {PINECONE_INDEX_NAME}...")
75
 
 
 
 
 
 
 
 
 
 
76
  # Initialize Pinecone client using the new API
77
  pc = Pinecone(api_key=PINECONE_API_KEY)
78
 
79
- # Check if index exists
80
- index_list = pc.list_indexes()
81
-
82
- if not hasattr(index_list, 'names') or PINECONE_INDEX_NAME not in index_list.names():
83
- logger.error(f"Index {PINECONE_INDEX_NAME} does not exist in Pinecone")
 
 
 
 
 
 
 
 
84
  return None
85
 
86
- # Get existing index
87
- index = pc.Index(PINECONE_INDEX_NAME)
88
- logger.info(f"Pinecone connection established to index {PINECONE_INDEX_NAME}")
89
-
90
  return index
 
 
 
91
  except Exception as e:
92
- logger.error(f"Error initializing Pinecone: {e}")
93
  return None
94
 
95
  # Get Pinecone index singleton
@@ -184,7 +199,7 @@ async def search_vectors(
184
  limit_k: int = DEFAULT_LIMIT_K,
185
  similarity_metric: str = DEFAULT_SIMILARITY_METRIC,
186
  similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
187
- namespace: str = "",
188
  filter: Optional[Dict] = None
189
  ) -> Dict:
190
  """
@@ -211,23 +226,13 @@ async def search_vectors(
211
  if limit_k < top_k:
212
  logger.warning(f"limit_k ({limit_k}) must be greater than or equal to top_k ({top_k}). Setting limit_k to {top_k}")
213
  limit_k = top_k
214
-
215
- # Create cache key from parameters
216
- vector_hash = hash(str(query_vector))
217
- cache_key = f"pinecone_search:{vector_hash}:{limit_k}:{similarity_metric}:{similarity_threshold}:{namespace}:{filter}"
218
-
219
- # Check cache first
220
- cached_result = cache.get(cache_key)
221
- if cached_result is not None:
222
- logger.info("Returning cached Pinecone search results")
223
- return cached_result
224
 
225
- # If not in cache, perform search
226
  pinecone_index = get_pinecone_index()
227
  if pinecone_index is None:
228
  logger.error("Failed to get Pinecone index for search")
229
  return None
230
-
231
  # Query Pinecone with the provided metric and higher limit_k to allow for threshold filtering
232
  results = pinecone_index.query(
233
  vector=query_vector,
@@ -250,10 +255,7 @@ async def search_vectors(
250
 
251
  # Log search result metrics
252
  match_count = len(filtered_matches)
253
- logger.info(f"Pinecone search returned {match_count} matches after threshold filtering (metric: {similarity_metric}, threshold: {similarity_threshold})")
254
-
255
- # Store result in cache with 5 minute TTL
256
- cache.set(cache_key, results, ttl=300)
257
 
258
  return results
259
  except Exception as e:
@@ -261,7 +263,7 @@ async def search_vectors(
261
  return None
262
 
263
  # Upsert vectors to Pinecone
264
- async def upsert_vectors(vectors, namespace=""):
265
  """Upsert vectors to Pinecone index"""
266
  try:
267
  pinecone_index = get_pinecone_index()
@@ -284,7 +286,7 @@ async def upsert_vectors(vectors, namespace=""):
284
  return None
285
 
286
  # Delete vectors from Pinecone
287
- async def delete_vectors(ids, namespace=""):
288
  """Delete vectors from Pinecone index"""
289
  try:
290
  pinecone_index = get_pinecone_index()
@@ -304,7 +306,7 @@ async def delete_vectors(ids, namespace=""):
304
  return False
305
 
306
  # Fetch vector metadata from Pinecone
307
- async def fetch_metadata(ids, namespace=""):
308
  """Fetch metadata for specific vector IDs"""
309
  try:
310
  pinecone_index = get_pinecone_index()
@@ -336,7 +338,8 @@ class ThresholdRetriever(BaseRetriever):
336
  limit_k: int = Field(default=DEFAULT_LIMIT_K, description="Maximum number of results to retrieve from Pinecone")
337
  similarity_metric: str = Field(default=DEFAULT_SIMILARITY_METRIC, description="Similarity metric to use")
338
  similarity_threshold: float = Field(default=DEFAULT_SIMILARITY_THRESHOLD, description="Threshold for similarity")
339
-
 
340
  class Config:
341
  """Configuration for this pydantic object."""
342
  arbitrary_types_allowed = True
@@ -347,7 +350,7 @@ class ThresholdRetriever(BaseRetriever):
347
  limit_k: int = DEFAULT_LIMIT_K,
348
  similarity_metric: str = DEFAULT_SIMILARITY_METRIC,
349
  similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
350
- namespace: str = "",
351
  filter: Optional[Dict] = None
352
  ) -> Dict:
353
  """Synchronous wrapper for search_vectors"""
@@ -440,8 +443,8 @@ class ThresholdRetriever(BaseRetriever):
440
  limit_k=self.limit_k,
441
  similarity_metric=self.similarity_metric,
442
  similarity_threshold=self.similarity_threshold,
443
- namespace=getattr(self.vectorstore, "namespace", ""),
444
- filter=self.search_kwargs.get("filter", None)
445
  ))
446
 
447
  # Run the async function in a thread
@@ -455,8 +458,8 @@ class ThresholdRetriever(BaseRetriever):
455
  limit_k=self.limit_k,
456
  similarity_metric=self.similarity_metric,
457
  similarity_threshold=self.similarity_threshold,
458
- namespace=getattr(self.vectorstore, "namespace", ""),
459
- filter=self.search_kwargs.get("filter", None)
460
  ))
461
 
462
  # Convert to documents
@@ -517,14 +520,6 @@ def get_chain(
517
  if _retriever_instance is not None:
518
  return _retriever_instance
519
 
520
- # Check if chain has been cached
521
- cache_key = f"pinecone_retriever:{index_name}:{namespace}:{top_k}:{limit_k}:{similarity_metric}:{similarity_threshold}"
522
- cached_retriever = cache.get(cache_key)
523
- if cached_retriever is not None:
524
- _retriever_instance = cached_retriever
525
- logger.info("Retrieved cached Pinecone retriever")
526
- return _retriever_instance
527
-
528
  start_time = time.time()
529
  logger.info("Initializing new retriever chain with threshold-based filtering")
530
 
@@ -572,9 +567,6 @@ def get_chain(
572
 
573
  logger.info(f"Pinecone retriever initialized in {time.time() - start_time:.2f} seconds")
574
 
575
- # Cache the retriever with longer TTL (1 hour) since it rarely changes
576
- cache.set(cache_key, _retriever_instance, ttl=3600)
577
-
578
  return _retriever_instance
579
  except Exception as e:
580
  logger.error(f"Error creating retrieval chain: {e}")
 
6
  import time
7
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
8
  import google.generativeai as genai
 
9
  from langchain_core.retrievers import BaseRetriever
10
  from langchain.callbacks.manager import Callbacks
11
  from langchain_core.documents import Document
 
72
  if pc is None:
73
  logger.info(f"Initializing Pinecone connection to index {PINECONE_INDEX_NAME}...")
74
 
75
+ # Check if API key and index name are set
76
+ if not PINECONE_API_KEY:
77
+ logger.error("PINECONE_API_KEY is not set in environment variables")
78
+ return None
79
+
80
+ if not PINECONE_INDEX_NAME:
81
+ logger.error("PINECONE_INDEX_NAME is not set in environment variables")
82
+ return None
83
+
84
  # Initialize Pinecone client using the new API
85
  pc = Pinecone(api_key=PINECONE_API_KEY)
86
 
87
+ try:
88
+ # Check if index exists
89
+ index_list = pc.list_indexes()
90
+
91
+ if not hasattr(index_list, 'names') or PINECONE_INDEX_NAME not in index_list.names():
92
+ logger.error(f"Index {PINECONE_INDEX_NAME} does not exist in Pinecone")
93
+ return None
94
+
95
+ # Get existing index
96
+ index = pc.Index(PINECONE_INDEX_NAME)
97
+ logger.info(f"Pinecone connection established to index {PINECONE_INDEX_NAME}")
98
+ except Exception as connection_error:
99
+ logger.error(f"Error connecting to Pinecone index: {connection_error}")
100
  return None
101
 
 
 
 
 
102
  return index
103
+ except ImportError as e:
104
+ logger.error(f"Required package for Pinecone is missing: {e}")
105
+ return None
106
  except Exception as e:
107
+ logger.error(f"Unexpected error initializing Pinecone: {e}")
108
  return None
109
 
110
  # Get Pinecone index singleton
 
199
  limit_k: int = DEFAULT_LIMIT_K,
200
  similarity_metric: str = DEFAULT_SIMILARITY_METRIC,
201
  similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
202
+ namespace: str = "Default",
203
  filter: Optional[Dict] = None
204
  ) -> Dict:
205
  """
 
226
  if limit_k < top_k:
227
  logger.warning(f"limit_k ({limit_k}) must be greater than or equal to top_k ({top_k}). Setting limit_k to {top_k}")
228
  limit_k = top_k
 
 
 
 
 
 
 
 
 
 
229
 
230
+ # Perform search directly without cache
231
  pinecone_index = get_pinecone_index()
232
  if pinecone_index is None:
233
  logger.error("Failed to get Pinecone index for search")
234
  return None
235
+
236
  # Query Pinecone with the provided metric and higher limit_k to allow for threshold filtering
237
  results = pinecone_index.query(
238
  vector=query_vector,
 
255
 
256
  # Log search result metrics
257
  match_count = len(filtered_matches)
258
+ logger.info(f"Pinecone search returned {match_count} matches after threshold filtering (metric: {similarity_metric}, threshold: {similarity_threshold}, namespace: {namespace})")
 
 
 
259
 
260
  return results
261
  except Exception as e:
 
263
  return None
264
 
265
  # Upsert vectors to Pinecone
266
+ async def upsert_vectors(vectors, namespace="Default"):
267
  """Upsert vectors to Pinecone index"""
268
  try:
269
  pinecone_index = get_pinecone_index()
 
286
  return None
287
 
288
  # Delete vectors from Pinecone
289
+ async def delete_vectors(ids, namespace="Default"):
290
  """Delete vectors from Pinecone index"""
291
  try:
292
  pinecone_index = get_pinecone_index()
 
306
  return False
307
 
308
  # Fetch vector metadata from Pinecone
309
+ async def fetch_metadata(ids, namespace="Default"):
310
  """Fetch metadata for specific vector IDs"""
311
  try:
312
  pinecone_index = get_pinecone_index()
 
338
  limit_k: int = Field(default=DEFAULT_LIMIT_K, description="Maximum number of results to retrieve from Pinecone")
339
  similarity_metric: str = Field(default=DEFAULT_SIMILARITY_METRIC, description="Similarity metric to use")
340
  similarity_threshold: float = Field(default=DEFAULT_SIMILARITY_THRESHOLD, description="Threshold for similarity")
341
+ namespace: str = "Default"
342
+
343
  class Config:
344
  """Configuration for this pydantic object."""
345
  arbitrary_types_allowed = True
 
350
  limit_k: int = DEFAULT_LIMIT_K,
351
  similarity_metric: str = DEFAULT_SIMILARITY_METRIC,
352
  similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
353
+ namespace: str = "Default",
354
  filter: Optional[Dict] = None
355
  ) -> Dict:
356
  """Synchronous wrapper for search_vectors"""
 
443
  limit_k=self.limit_k,
444
  similarity_metric=self.similarity_metric,
445
  similarity_threshold=self.similarity_threshold,
446
+ namespace=self.namespace,
447
+ # filter=self.search_kwargs.get("filter", None)
448
  ))
449
 
450
  # Run the async function in a thread
 
458
  limit_k=self.limit_k,
459
  similarity_metric=self.similarity_metric,
460
  similarity_threshold=self.similarity_threshold,
461
+ namespace=self.namespace,
462
+ # filter=self.search_kwargs.get("filter", None)
463
  ))
464
 
465
  # Convert to documents
 
520
  if _retriever_instance is not None:
521
  return _retriever_instance
522
 
 
 
 
 
 
 
 
 
523
  start_time = time.time()
524
  logger.info("Initializing new retriever chain with threshold-based filtering")
525
 
 
567
 
568
  logger.info(f"Pinecone retriever initialized in {time.time() - start_time:.2f} seconds")
569
 
 
 
 
570
  return _retriever_instance
571
  except Exception as e:
572
  logger.error(f"Error creating retrieval chain: {e}")
app/database/postgresql.py CHANGED
@@ -6,7 +6,7 @@ from sqlalchemy.exc import SQLAlchemyError, OperationalError
6
  from dotenv import load_dotenv
7
  import logging
8
 
9
- # Cấu hình logging
10
  logger = logging.getLogger(__name__)
11
 
12
  # Load environment variables
@@ -24,66 +24,76 @@ else:
24
 
25
  if not DATABASE_URL:
26
  logger.error("No database URL configured. Please set AIVEN_DB_URL environment variable.")
27
- DATABASE_URL = "postgresql://localhost/test" # Fallback để không crash khi khởi động
28
 
29
- # Create SQLAlchemy engine
30
  try:
31
  engine = create_engine(
32
  DATABASE_URL,
33
- pool_pre_ping=True,
34
- pool_recycle=300, # Recycle connections every 5 minutes
35
- pool_size=10, # Tăng kích thước pool từ 5 lên 10
36
- max_overflow=20, # Tăng số lượng kết nối tối đa từ 10 lên 20
 
37
  connect_args={
38
- "connect_timeout": 3, # Giảm timeout từ 5 xuống 3 giây
39
- "keepalives": 1, # Bật keepalive
40
- "keepalives_idle": 30, # Thời gian idle trước khi gửi keepalive
41
- "keepalives_interval": 10, # Khoảng thời gian giữa các gói keepalive
42
- "keepalives_count": 5 # Số lần thử lại trước khi đóng kết nối
 
43
  },
44
- # Thêm các tùy chọn hiệu suất
45
- isolation_level="READ COMMITTED", # Mức lập thấp hơn READ COMMITTED
46
- echo=False, # Tắt echo SQL để giảm overhead logging
47
- echo_pool=False # Tắt echo pool để giảm overhead logging
 
 
 
 
 
 
48
  )
49
- logger.info("PostgreSQL engine initialized")
50
  except Exception as e:
51
  logger.error(f"Failed to initialize PostgreSQL engine: {e}")
52
- # Không raise exception để tránh crash khi khởi động, các xử lý lỗi sẽ được thực hiện ở các function
53
 
54
- # Create session factory with optimized settings
55
  SessionLocal = sessionmaker(
56
  autocommit=False,
57
  autoflush=False,
58
  bind=engine,
59
- expire_on_commit=False # Tránh truy vấn lại DB sau khi commit
60
  )
61
 
62
  # Base class for declarative models - use sqlalchemy.orm for SQLAlchemy 2.0 compatibility
63
  from sqlalchemy.orm import declarative_base
64
  Base = declarative_base()
65
 
66
- # Kiểm tra kết nối PostgreSQL
67
  def check_db_connection():
68
- """Kiểm tra kết nối PostgreSQL"""
69
  try:
70
- # Thực hiện một truy vấn đơn giản để kiểm tra kết nối
71
  with engine.connect() as connection:
72
- connection.execute(text("SELECT 1"))
73
- logger.info("PostgreSQL connection is working")
74
  return True
75
  except OperationalError as e:
76
  logger.error(f"PostgreSQL connection failed: {e}")
77
  return False
78
  except Exception as e:
79
- logger.error(f"Unknown error when checking PostgreSQL connection: {e}")
80
  return False
81
 
82
- # Dependency to get DB session
83
  def get_db():
84
  """Get database session dependency for FastAPI endpoints"""
85
  db = SessionLocal()
86
  try:
 
 
87
  yield db
88
  except SQLAlchemyError as e:
89
  logger.error(f"Database session error: {e}")
@@ -92,13 +102,92 @@ def get_db():
92
  finally:
93
  db.close()
94
 
95
- # Tạo các bảng trong sở dữ liệu nếu chưa tồn tại
96
  def create_tables():
97
- """Tạo các bảng trong cơ sở dữ liệu"""
98
  try:
99
  Base.metadata.create_all(bind=engine)
100
  logger.info("Database tables created or already exist")
101
  return True
102
  except SQLAlchemyError as e:
103
- logger.error(f"Failed to create database tables: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  return False
 
6
  from dotenv import load_dotenv
7
  import logging
8
 
9
+ # Configure logging
10
  logger = logging.getLogger(__name__)
11
 
12
  # Load environment variables
 
24
 
25
  if not DATABASE_URL:
26
  logger.error("No database URL configured. Please set AIVEN_DB_URL environment variable.")
27
+ DATABASE_URL = "postgresql://localhost/test" # Fallback to avoid crash on startup
28
 
29
+ # Create SQLAlchemy engine with optimized settings
30
  try:
31
  engine = create_engine(
32
  DATABASE_URL,
33
+ pool_pre_ping=True, # Enable connection health checks
34
+ pool_recycle=300, # Recycle connections every 5 minutes
35
+ pool_size=20, # Increase pool size for more concurrent connections
36
+ max_overflow=30, # Allow more overflow connections
37
+ pool_timeout=30, # Timeout for getting connection from pool
38
  connect_args={
39
+ "connect_timeout": 5, # Connection timeout in seconds
40
+ "keepalives": 1, # Enable TCP keepalives
41
+ "keepalives_idle": 30, # Time before sending keepalives
42
+ "keepalives_interval": 10, # Time between keepalives
43
+ "keepalives_count": 5, # Number of keepalive probes
44
+ "application_name": "pixagent_api" # Identify app in PostgreSQL logs
45
  },
46
+ # Performance optimizations
47
+ isolation_level="READ COMMITTED", # Lower isolation level for better performance
48
+ echo=False, # Disable SQL echo to reduce overhead
49
+ echo_pool=False, # Disable pool logging
50
+ future=True, # Use SQLAlchemy 2.0 features
51
+ # Execution options for common queries
52
+ execution_options={
53
+ "compiled_cache": {}, # Use an empty dict for compiled query caching
54
+ "logging_token": "SQL", # Tag for query logging
55
+ }
56
  )
57
+ logger.info("PostgreSQL engine initialized with optimized settings")
58
  except Exception as e:
59
  logger.error(f"Failed to initialize PostgreSQL engine: {e}")
60
+ # Don't raise exception to avoid crash on startup
61
 
62
+ # Create optimized session factory
63
  SessionLocal = sessionmaker(
64
  autocommit=False,
65
  autoflush=False,
66
  bind=engine,
67
+ expire_on_commit=False # Prevent automatic reloading after commit
68
  )
69
 
70
  # Base class for declarative models - use sqlalchemy.orm for SQLAlchemy 2.0 compatibility
71
  from sqlalchemy.orm import declarative_base
72
  Base = declarative_base()
73
 
74
+ # Check PostgreSQL connection
75
  def check_db_connection():
76
+ """Check PostgreSQL connection status"""
77
  try:
78
+ # Simple query to verify connection
79
  with engine.connect() as connection:
80
+ connection.execute(text("SELECT 1")).fetchone()
81
+ logger.info("PostgreSQL connection successful")
82
  return True
83
  except OperationalError as e:
84
  logger.error(f"PostgreSQL connection failed: {e}")
85
  return False
86
  except Exception as e:
87
+ logger.error(f"Unknown error checking PostgreSQL connection: {e}")
88
  return False
89
 
90
+ # Dependency to get DB session with improved error handling
91
  def get_db():
92
  """Get database session dependency for FastAPI endpoints"""
93
  db = SessionLocal()
94
  try:
95
+ # Test connection is valid before returning
96
+ db.execute(text("SELECT 1")).fetchone()
97
  yield db
98
  except SQLAlchemyError as e:
99
  logger.error(f"Database session error: {e}")
 
102
  finally:
103
  db.close()
104
 
105
+ # Create tables in database if they don't exist
106
  def create_tables():
107
+ """Create tables in database"""
108
  try:
109
  Base.metadata.create_all(bind=engine)
110
  logger.info("Database tables created or already exist")
111
  return True
112
  except SQLAlchemyError as e:
113
+ logger.error(f"Failed to create database tables (SQLAlchemy error): {e}")
114
+ return False
115
+ except Exception as e:
116
+ logger.error(f"Failed to create database tables (unexpected error): {e}")
117
+ return False
118
+
119
+ # Function to create indexes for better performance
120
+ def create_indexes():
121
+ """Create indexes for better query performance"""
122
+ try:
123
+ with engine.connect() as conn:
124
+ try:
125
+ # Index for featured events - use try-except to handle if index already exists
126
+ conn.execute(text("""
127
+ CREATE INDEX idx_event_featured
128
+ ON event_item(featured)
129
+ """))
130
+ except SQLAlchemyError:
131
+ logger.info("Index idx_event_featured already exists")
132
+
133
+ try:
134
+ # Index for active events
135
+ conn.execute(text("""
136
+ CREATE INDEX idx_event_active
137
+ ON event_item(is_active)
138
+ """))
139
+ except SQLAlchemyError:
140
+ logger.info("Index idx_event_active already exists")
141
+
142
+ try:
143
+ # Index for date filtering
144
+ conn.execute(text("""
145
+ CREATE INDEX idx_event_date_start
146
+ ON event_item(date_start)
147
+ """))
148
+ except SQLAlchemyError:
149
+ logger.info("Index idx_event_date_start already exists")
150
+
151
+ try:
152
+ # Composite index for combined filtering
153
+ conn.execute(text("""
154
+ CREATE INDEX idx_event_featured_active
155
+ ON event_item(featured, is_active)
156
+ """))
157
+ except SQLAlchemyError:
158
+ logger.info("Index idx_event_featured_active already exists")
159
+
160
+ # Indexes for FAQ and Emergency tables
161
+ try:
162
+ # FAQ active flag index
163
+ conn.execute(text("""
164
+ CREATE INDEX idx_faq_active
165
+ ON faq_item(is_active)
166
+ """))
167
+ except SQLAlchemyError:
168
+ logger.info("Index idx_faq_active already exists")
169
+
170
+ try:
171
+ # Emergency contact active flag and priority indexes
172
+ conn.execute(text("""
173
+ CREATE INDEX idx_emergency_active
174
+ ON emergency_item(is_active)
175
+ """))
176
+ except SQLAlchemyError:
177
+ logger.info("Index idx_emergency_active already exists")
178
+
179
+ try:
180
+ conn.execute(text("""
181
+ CREATE INDEX idx_emergency_priority
182
+ ON emergency_item(priority)
183
+ """))
184
+ except SQLAlchemyError:
185
+ logger.info("Index idx_emergency_priority already exists")
186
+
187
+ conn.commit()
188
+
189
+ logger.info("Database indexes created or verified")
190
+ return True
191
+ except SQLAlchemyError as e:
192
+ logger.error(f"Failed to create indexes: {e}")
193
  return False
app/models/pdf_models.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List, Dict, Any
3
+
4
+ class PDFUploadRequest(BaseModel):
5
+ """Request model cho upload PDF"""
6
+ namespace: Optional[str] = Field("Default", description="Namespace trong Pinecone")
7
+ index_name: Optional[str] = Field("testbot768", description="Tên index trong Pinecone")
8
+ title: Optional[str] = Field(None, description="Tiêu đề của tài liệu")
9
+ description: Optional[str] = Field(None, description="Mô tả về tài liệu")
10
+
11
+ class PDFResponse(BaseModel):
12
+ """Response model cho xử lý PDF"""
13
+ success: bool = Field(..., description="Trạng thái xử lý thành công hay không")
14
+ document_id: Optional[str] = Field(None, description="ID của tài liệu")
15
+ chunks_processed: Optional[int] = Field(None, description="Số lượng chunks đã xử lý")
16
+ total_text_length: Optional[int] = Field(None, description="Tổng độ dài văn bản")
17
+ error: Optional[str] = Field(None, description="Thông báo lỗi nếu có")
18
+
19
+ class Config:
20
+ schema_extra = {
21
+ "example": {
22
+ "success": True,
23
+ "document_id": "550e8400-e29b-41d4-a716-446655440000",
24
+ "chunks_processed": 25,
25
+ "total_text_length": 50000
26
+ }
27
+ }
28
+
29
+ class DeleteDocumentRequest(BaseModel):
30
+ """Request model cho xóa document"""
31
+ document_id: str = Field(..., description="ID của tài liệu cần xóa")
32
+ namespace: Optional[str] = Field("Default", description="Namespace trong Pinecone")
33
+ index_name: Optional[str] = Field("testbot768", description="Tên index trong Pinecone")
34
+
35
+ class DocumentsListResponse(BaseModel):
36
+ """Response model cho lấy danh sách tài liệu"""
37
+ success: bool = Field(..., description="Trạng thái xử lý thành công hay không")
38
+ total_vectors: Optional[int] = Field(None, description="Tổng số vectors trong index")
39
+ namespace: Optional[str] = Field(None, description="Namespace đang sử dụng")
40
+ index_name: Optional[str] = Field(None, description="Tên index đang sử dụng")
41
+ error: Optional[str] = Field(None, description="Thông báo lỗi nếu có")
42
+
43
+ class Config:
44
+ schema_extra = {
45
+ "example": {
46
+ "success": True,
47
+ "total_vectors": 5000,
48
+ "namespace": "Default",
49
+ "index_name": "testbot768"
50
+ }
51
+ }
app/utils/cache.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import threading
4
+ import logging
5
+ from typing import Dict, Any, Optional, Tuple, List, Callable, Generic, TypeVar, Union
6
+ from datetime import datetime
7
+ from dotenv import load_dotenv
8
+ import json
9
+
10
+ # Thiết lập logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Load biến môi trường
14
+ load_dotenv()
15
+
16
+ # Cấu hình cache từ biến môi trường
17
+ DEFAULT_CACHE_TTL = int(os.getenv("CACHE_TTL_SECONDS", "300")) # Mặc định 5 phút
18
+ DEFAULT_CACHE_CLEANUP_INTERVAL = int(os.getenv("CACHE_CLEANUP_INTERVAL", "60")) # Mặc định 1 phút
19
+ DEFAULT_CACHE_MAX_SIZE = int(os.getenv("CACHE_MAX_SIZE", "1000")) # Mặc định 1000 phần tử
20
+ DEFAULT_HISTORY_QUEUE_SIZE = int(os.getenv("HISTORY_QUEUE_SIZE", "10")) # Mặc định queue size là 10
21
+ DEFAULT_HISTORY_CACHE_TTL = int(os.getenv("HISTORY_CACHE_TTL", "3600")) # Mặc định 1 giờ
22
+
23
+ # Generic type để có thể sử dụng cho nhiều loại giá trị khác nhau
24
+ T = TypeVar('T')
25
+
26
+ # Cấu trúc cho một phần tử trong cache
27
+ class CacheItem(Generic[T]):
28
+ def __init__(self, value: T, ttl: int = DEFAULT_CACHE_TTL):
29
+ self.value = value
30
+ self.expire_at = time.time() + ttl
31
+ self.last_accessed = time.time()
32
+
33
+ def is_expired(self) -> bool:
34
+ """Kiểm tra xem item có hết hạn chưa"""
35
+ return time.time() > self.expire_at
36
+
37
+ def touch(self) -> None:
38
+ """Cập nhật thời gian truy cập lần cuối"""
39
+ self.last_accessed = time.time()
40
+
41
+ def extend(self, ttl: int = DEFAULT_CACHE_TTL) -> None:
42
+ """Gia hạn thời gian sống của item"""
43
+ self.expire_at = time.time() + ttl
44
+
45
+
46
+ # Lớp HistoryQueue để lưu trữ lịch sử người dùng
47
+ class HistoryQueue:
48
+ def __init__(self, max_size: int = DEFAULT_HISTORY_QUEUE_SIZE, ttl: int = DEFAULT_HISTORY_CACHE_TTL):
49
+ self.items: List[Dict[str, Any]] = []
50
+ self.max_size = max_size
51
+ self.ttl = ttl
52
+ self.expire_at = time.time() + ttl
53
+
54
+ def add(self, item: Dict[str, Any]) -> None:
55
+ """Thêm một item vào queue, nếu đã đầy thì loại bỏ item cũ nhất"""
56
+ if len(self.items) >= self.max_size:
57
+ self.items.pop(0)
58
+ self.items.append(item)
59
+ # Mỗi khi thêm item mới, cập nhật thời gian hết hạn
60
+ self.refresh_expiry()
61
+
62
+ def get_all(self) -> List[Dict[str, Any]]:
63
+ """Lấy tất cả items trong queue"""
64
+ return self.items
65
+
66
+ def is_expired(self) -> bool:
67
+ """Kiểm tra xem queue có hết hạn chưa"""
68
+ return time.time() > self.expire_at
69
+
70
+ def refresh_expiry(self) -> None:
71
+ """Làm mới thời gian hết hạn"""
72
+ self.expire_at = time.time() + self.ttl
73
+
74
+
75
+ # Lớp cache chính
76
+ class InMemoryCache:
77
+ def __init__(
78
+ self,
79
+ ttl: int = DEFAULT_CACHE_TTL,
80
+ cleanup_interval: int = DEFAULT_CACHE_CLEANUP_INTERVAL,
81
+ max_size: int = DEFAULT_CACHE_MAX_SIZE
82
+ ):
83
+ self.cache: Dict[str, CacheItem] = {}
84
+ self.ttl = ttl
85
+ self.cleanup_interval = cleanup_interval
86
+ self.max_size = max_size
87
+ self.user_history_queues: Dict[str, HistoryQueue] = {}
88
+ self.lock = threading.RLock() # Sử dụng RLock để tránh deadlock
89
+
90
+ # Khởi động thread dọn dẹp cache định kỳ (active expiration)
91
+ self.cleanup_thread = threading.Thread(target=self._cleanup_task, daemon=True)
92
+ self.cleanup_thread.start()
93
+
94
+ def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
95
+ """Lưu một giá trị vào cache"""
96
+ with self.lock:
97
+ ttl_value = ttl if ttl is not None else self.ttl
98
+
99
+ # Nếu cache đã đầy, xóa bớt các item ít được truy cập nhất
100
+ if len(self.cache) >= self.max_size and key not in self.cache:
101
+ self._evict_lru_items()
102
+
103
+ self.cache[key] = CacheItem(value, ttl_value)
104
+ logger.debug(f"Cache set: {key} (expires in {ttl_value}s)")
105
+
106
+ def get(self, key: str, default: Any = None) -> Any:
107
+ """
108
+ Lấy giá trị từ cache. Nếu key không tồn tại hoặc đã hết hạn, trả về giá trị mặc định.
109
+ Áp dụng lazy expiration: kiểm tra và xóa các item hết hạn khi truy cập.
110
+ """
111
+ with self.lock:
112
+ item = self.cache.get(key)
113
+
114
+ # Nếu không tìm thấy key hoặc item đã hết hạn
115
+ if item is None or item.is_expired():
116
+ # Nếu item tồn tại nhưng đã hết hạn, xóa nó (lazy expiration)
117
+ if item is not None:
118
+ logger.debug(f"Cache miss (expired): {key}")
119
+ del self.cache[key]
120
+ else:
121
+ logger.debug(f"Cache miss (not found): {key}")
122
+ return default
123
+
124
+ # Cập nhật thời gian truy cập
125
+ item.touch()
126
+ logger.debug(f"Cache hit: {key}")
127
+ return item.value
128
+
129
+ def delete(self, key: str) -> bool:
130
+ """Xóa một key khỏi cache"""
131
+ with self.lock:
132
+ if key in self.cache:
133
+ del self.cache[key]
134
+ logger.debug(f"Cache delete: {key}")
135
+ return True
136
+ return False
137
+
138
+ def clear(self) -> None:
139
+ """Xóa tất cả dữ liệu trong cache"""
140
+ with self.lock:
141
+ self.cache.clear()
142
+ logger.debug("Cache cleared")
143
+
144
+ def get_or_set(self, key: str, callback: Callable[[], T], ttl: Optional[int] = None) -> T:
145
+ """
146
+ Lấy giá trị từ cache nếu tồn tại, nếu không thì gọi callback để lấy giá trị
147
+ và lưu vào cache trước khi trả về.
148
+ """
149
+ with self.lock:
150
+ value = self.get(key)
151
+ if value is None:
152
+ value = callback()
153
+ self.set(key, value, ttl)
154
+ return value
155
+
156
+ def _cleanup_task(self) -> None:
157
+ """Thread để dọn dẹp các item đã hết hạn (active expiration)"""
158
+ while True:
159
+ time.sleep(self.cleanup_interval)
160
+ try:
161
+ self._remove_expired_items()
162
+ except Exception as e:
163
+ logger.error(f"Error in cache cleanup task: {e}")
164
+
165
+ def _remove_expired_items(self) -> None:
166
+ """Xóa tất cả các item đã hết hạn trong cache"""
167
+ with self.lock:
168
+ now = time.time()
169
+ expired_keys = [k for k, v in self.cache.items() if v.is_expired()]
170
+ for key in expired_keys:
171
+ del self.cache[key]
172
+
173
+ # Xóa các user history queue đã hết hạn
174
+ expired_user_ids = [uid for uid, queue in self.user_history_queues.items() if queue.is_expired()]
175
+ for user_id in expired_user_ids:
176
+ del self.user_history_queues[user_id]
177
+
178
+ if expired_keys or expired_user_ids:
179
+ logger.debug(f"Cleaned up {len(expired_keys)} expired cache items and {len(expired_user_ids)} expired history queues")
180
+
181
+ def _evict_lru_items(self, count: int = 1) -> None:
182
+ """Xóa bỏ các item ít được truy cập nhất khi cache đầy"""
183
+ items = sorted(self.cache.items(), key=lambda x: x[1].last_accessed)
184
+ for i in range(min(count, len(items))):
185
+ del self.cache[items[i][0]]
186
+ logger.debug(f"Evicted {min(count, len(items))} least recently used items from cache")
187
+
188
+ def stats(self) -> Dict[str, Any]:
189
+ """Trả về thống kê về cache"""
190
+ with self.lock:
191
+ now = time.time()
192
+ total_items = len(self.cache)
193
+ expired_items = sum(1 for item in self.cache.values() if item.is_expired())
194
+ memory_usage = self._estimate_memory_usage()
195
+ return {
196
+ "total_items": total_items,
197
+ "expired_items": expired_items,
198
+ "active_items": total_items - expired_items,
199
+ "memory_usage_bytes": memory_usage,
200
+ "memory_usage_mb": memory_usage / (1024 * 1024),
201
+ "max_size": self.max_size,
202
+ "history_queues": len(self.user_history_queues)
203
+ }
204
+
205
+ def _estimate_memory_usage(self) -> int:
206
+ """Ước tính dung lượng bộ nhớ của cache (gần đúng)"""
207
+ # Ước tính dựa trên kích thước của các key và giá trị
208
+ cache_size = sum(len(k) for k in self.cache.keys())
209
+ for item in self.cache.values():
210
+ try:
211
+ # Ước tính kích thước của value (gần đúng)
212
+ if isinstance(item.value, (str, bytes)):
213
+ cache_size += len(item.value)
214
+ elif isinstance(item.value, (dict, list)):
215
+ cache_size += len(json.dumps(item.value))
216
+ else:
217
+ # Giá trị mặc định cho các loại dữ liệu khác
218
+ cache_size += 100
219
+ except:
220
+ cache_size += 100
221
+
222
+ # Ước tính kích thước của user history queues
223
+ for queue in self.user_history_queues.values():
224
+ try:
225
+ cache_size += len(json.dumps(queue.items)) + 100 # 100 bytes cho metadata
226
+ except:
227
+ cache_size += 100
228
+
229
+ return cache_size
230
+
231
+ # Các phương thức chuyên biệt cho việc quản lý lịch sử người dùng
232
+ def add_user_history(self, user_id: str, item: Dict[str, Any], queue_size: Optional[int] = None, ttl: Optional[int] = None) -> None:
233
+ """Thêm một item vào history queue của người dùng"""
234
+ with self.lock:
235
+ # Tạo queue nếu chưa tồn tại
236
+ if user_id not in self.user_history_queues:
237
+ queue_size_value = queue_size if queue_size is not None else DEFAULT_HISTORY_QUEUE_SIZE
238
+ ttl_value = ttl if ttl is not None else DEFAULT_HISTORY_CACHE_TTL
239
+ self.user_history_queues[user_id] = HistoryQueue(max_size=queue_size_value, ttl=ttl_value)
240
+
241
+ # Thêm item vào queue
242
+ self.user_history_queues[user_id].add(item)
243
+ logger.debug(f"Added history item for user {user_id}")
244
+
245
+ def get_user_history(self, user_id: str, default: Any = None) -> List[Dict[str, Any]]:
246
+ """Lấy lịch sử của người dùng từ cache"""
247
+ with self.lock:
248
+ queue = self.user_history_queues.get(user_id)
249
+
250
+ # Nếu không tìm thấy queue hoặc queue đã hết hạn
251
+ if queue is None or queue.is_expired():
252
+ if queue is not None and queue.is_expired():
253
+ del self.user_history_queues[user_id]
254
+ logger.debug(f"User history queue expired: {user_id}")
255
+ return default if default is not None else []
256
+
257
+ # Làm mới thời gian hết hạn
258
+ queue.refresh_expiry()
259
+ logger.debug(f"Retrieved history for user {user_id}: {len(queue.items)} items")
260
+ return queue.get_all()
261
+
262
+
263
+ # Singleton instance
264
+ _cache_instance = None
265
+
266
+ def get_cache() -> InMemoryCache:
267
+ """Trả về instance singleton của InMemoryCache"""
268
+ global _cache_instance
269
+ if _cache_instance is None:
270
+ _cache_instance = InMemoryCache()
271
+ return _cache_instance
app/utils/pdf_processor.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
5
+ from langchain_community.document_loaders import PyPDFLoader
6
+ from langchain_google_genai import GoogleGenerativeAIEmbeddings
7
+ import logging
8
+
9
+ from app.database.pinecone import get_pinecone_index, init_pinecone
10
+
11
+ # Cấu hình logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Khởi tạo embeddings model
15
+ embeddings_model = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
16
+
17
+ class PDFProcessor:
18
+ """Lớp xử lý file PDF và tạo embeddings"""
19
+
20
+ def __init__(self, index_name="testbot768", namespace="Default"):
21
+ """Khởi tạo với tên index và namespace Pinecone mặc định"""
22
+ self.index_name = index_name
23
+ self.namespace = namespace
24
+ self.pinecone_index = None
25
+
26
+ def _init_pinecone_connection(self):
27
+ """Khởi tạo kết nối đến Pinecone"""
28
+ try:
29
+ # Sử dụng singleton pattern từ module database.pinecone
30
+ self.pinecone_index = get_pinecone_index()
31
+ if not self.pinecone_index:
32
+ logger.error("Không thể kết nối đến Pinecone")
33
+ return False
34
+ return True
35
+ except Exception as e:
36
+ logger.error(f"Lỗi khi kết nối Pinecone: {str(e)}")
37
+ return False
38
+
39
+ async def process_pdf(self, file_path, document_id=None, metadata=None, progress_callback=None):
40
+ """
41
+ Xử lý file PDF, chia thành chunks và tạo embeddings
42
+
43
+ Args:
44
+ file_path (str): Đường dẫn tới file PDF
45
+ document_id (str, optional): ID của tài liệu, nếu không cung cấp sẽ tạo ID mới
46
+ metadata (dict, optional): Metadata bổ sung cho tài liệu
47
+ progress_callback (callable, optional): Callback function để cập nhật tiến độ
48
+
49
+ Returns:
50
+ dict: Thông tin kết quả xử lý gồm document_id và số chunks đã xử lý
51
+ """
52
+ try:
53
+ # Khởi tạo kết nối Pinecone nếu chưa có
54
+ if not self.pinecone_index:
55
+ if not self._init_pinecone_connection():
56
+ return {"success": False, "error": "Không thể kết nối đến Pinecone"}
57
+
58
+ # Tạo document_id nếu không có
59
+ if not document_id:
60
+ document_id = str(uuid.uuid4())
61
+
62
+ # Đọc file PDF bằng PyPDFLoader
63
+ logger.info(f"Đang đọc file PDF: {file_path}")
64
+ if progress_callback:
65
+ await progress_callback("pdf_loading", 0.5, "Loading PDF file")
66
+
67
+ loader = PyPDFLoader(file_path)
68
+ pages = loader.load()
69
+
70
+ # Trích xuất và nối text từ tất cả các trang
71
+ all_text = ""
72
+ for page in pages:
73
+ all_text += page.page_content + "\n"
74
+
75
+ if progress_callback:
76
+ await progress_callback("text_extraction", 0.6, "Extracted text from PDF")
77
+
78
+ # Chia văn bản thành các chunk
79
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=300)
80
+ chunks = text_splitter.split_text(all_text)
81
+
82
+ logger.info(f"Đã chia file PDF thành {len(chunks)} chunks")
83
+ if progress_callback:
84
+ await progress_callback("chunking", 0.7, f"Split document into {len(chunks)} chunks")
85
+
86
+ # Xử lý embedding cho từng chunk và upsert lên Pinecone
87
+ vectors = []
88
+ for i, chunk in enumerate(chunks):
89
+ # Cập nhật tiến độ embedding
90
+ if progress_callback and i % 5 == 0: # Cập nhật sau mỗi 5 chunks để tránh quá nhiều thông báo
91
+ embedding_progress = 0.7 + (0.3 * (i / len(chunks)))
92
+ await progress_callback("embedding", embedding_progress, f"Processing chunk {i+1}/{len(chunks)}")
93
+
94
+ # Tạo vector embedding cho từng chunk
95
+ vector = embeddings_model.embed_query(chunk)
96
+
97
+ # Chuẩn bị metadata cho vector
98
+ vector_metadata = {
99
+ "document_id": document_id,
100
+ "chunk_index": i,
101
+ "text": chunk
102
+ }
103
+
104
+ # Thêm metadata bổ sung nếu có
105
+ if metadata:
106
+ for key, value in metadata.items():
107
+ if key not in vector_metadata:
108
+ vector_metadata[key] = value
109
+
110
+ # Thêm vector vào danh sách để upsert
111
+ vectors.append({
112
+ "id": f"{document_id}_{i}",
113
+ "values": vector,
114
+ "metadata": vector_metadata
115
+ })
116
+
117
+ # Upsert mỗi 100 vectors để tránh quá lớn
118
+ if len(vectors) >= 100:
119
+ await self._upsert_vectors(vectors)
120
+ vectors = []
121
+
122
+ # Upsert các vectors còn lại
123
+ if vectors:
124
+ await self._upsert_vectors(vectors)
125
+
126
+ logger.info(f"Đã embedding và lưu {len(chunks)} chunks từ PDF với document_id: {document_id}")
127
+
128
+ # Final progress update
129
+ if progress_callback:
130
+ await progress_callback("completed", 1.0, "PDF processing complete")
131
+
132
+ return {
133
+ "success": True,
134
+ "document_id": document_id,
135
+ "chunks_processed": len(chunks),
136
+ "total_text_length": len(all_text)
137
+ }
138
+
139
+ except Exception as e:
140
+ logger.error(f"Lỗi khi xử lý PDF: {str(e)}")
141
+ if progress_callback:
142
+ await progress_callback("error", 0, f"Error processing PDF: {str(e)}")
143
+ return {
144
+ "success": False,
145
+ "error": str(e)
146
+ }
147
+
148
+ async def _upsert_vectors(self, vectors):
149
+ """Upsert vectors vào Pinecone"""
150
+ try:
151
+ if not vectors:
152
+ return
153
+
154
+ result = self.pinecone_index.upsert(
155
+ vectors=vectors,
156
+ namespace=self.namespace
157
+ )
158
+
159
+ logger.info(f"Đã upsert {len(vectors)} vectors vào Pinecone")
160
+ return result
161
+ except Exception as e:
162
+ logger.error(f"Lỗi khi upsert vectors: {str(e)}")
163
+ raise
164
+
165
+ async def delete_namespace(self):
166
+ """
167
+ Xóa toàn bộ vectors trong namespace hiện tại (tương đương xoá namespace).
168
+ """
169
+ # Khởi tạo kết nối nếu cần
170
+ if not self.pinecone_index and not self._init_pinecone_connection():
171
+ return {"success": False, "error": "Không thể kết nối đến Pinecone"}
172
+
173
+ try:
174
+ # delete_all=True sẽ xóa toàn bộ vectors trong namespace
175
+ result = self.pinecone_index.delete(
176
+ delete_all=True,
177
+ namespace=self.namespace
178
+ )
179
+ logger.info(f"Đã xóa namespace '{self.namespace}' (tất cả vectors).")
180
+ return {"success": True, "detail": result}
181
+ except Exception as e:
182
+ logger.error(f"Lỗi khi xóa namespace '{self.namespace}': {e}")
183
+ return {"success": False, "error": str(e)}
184
+
185
+ async def list_documents(self):
186
+ """Lấy danh sách tất cả document_id từ Pinecone"""
187
+ try:
188
+ # Khởi tạo kết nối Pinecone nếu chưa có
189
+ if not self.pinecone_index:
190
+ if not self._init_pinecone_connection():
191
+ return {"success": False, "error": "Không thể kết nối đến Pinecone"}
192
+
193
+ # Lấy thông tin index
194
+ stats = self.pinecone_index.describe_index_stats()
195
+
196
+ # Thực hiện truy vấn để lấy danh sách tất cả document_id duy nhất
197
+ # Phương pháp này có thể không hiệu quả với dataset lớn, nhưng là cách đơn giản nhất
198
+ # Trong thực tế, nên lưu danh sách document_id trong một database riêng
199
+
200
+ return {
201
+ "success": True,
202
+ "total_vectors": stats.get('total_vector_count', 0),
203
+ "namespace": self.namespace,
204
+ "index_name": self.index_name
205
+ }
206
+ except Exception as e:
207
+ logger.error(f"Lỗi khi lấy danh sách documents: {str(e)}")
208
+ return {
209
+ "success": False,
210
+ "error": str(e)
211
+ }
app/utils/utils.py CHANGED
@@ -2,10 +2,13 @@ import logging
2
  import time
3
  import uuid
4
  import threading
 
5
  from functools import wraps
6
  from datetime import datetime, timedelta
7
  import pytz
8
- from typing import Callable, Any, Dict, Optional
 
 
9
 
10
  # Configure logging
11
  logging.basicConfig(
@@ -70,46 +73,395 @@ def truncate_text(text, max_length=100):
70
  return text
71
  return text[:max_length] + "..."
72
 
73
- # Simple in-memory cache implementation (replaces Redis dependency)
74
- class SimpleCache:
75
- def __init__(self):
76
- self._cache = {}
77
- self._expiry = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- def get(self, key: str) -> Optional[Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  """Get value from cache if it exists and hasn't expired"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  if key in self._cache:
82
- # Check if the key has expired
83
- if key in self._expiry and self._expiry[key] > datetime.now():
84
- return self._cache[key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  else:
86
- # Clean up expired keys
87
- if key in self._cache:
88
- del self._cache[key]
89
- if key in self._expiry:
90
- del self._expiry[key]
91
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  def set(self, key: str, value: Any, ttl: int = 300) -> None:
94
  """Set a value in the cache with TTL in seconds"""
95
- self._cache[key] = value
96
- # Set expiry time
97
- self._expiry[key] = datetime.now() + timedelta(seconds=ttl)
98
 
99
  def delete(self, key: str) -> None:
100
  """Delete a key from the cache"""
101
- if key in self._cache:
102
- del self._cache[key]
103
- if key in self._expiry:
104
- del self._expiry[key]
105
 
106
  def clear(self) -> None:
107
  """Clear the entire cache"""
108
- self._cache.clear()
109
- self._expiry.clear()
110
-
111
- # Initialize cache
112
- cache = SimpleCache()
113
 
114
  def get_host_url(request) -> str:
115
  """
 
2
  import time
3
  import uuid
4
  import threading
5
+ import os
6
  from functools import wraps
7
  from datetime import datetime, timedelta
8
  import pytz
9
+ from typing import Callable, Any, Dict, Optional, List, Tuple, Set
10
+ import gc
11
+ import heapq
12
 
13
  # Configure logging
14
  logging.basicConfig(
 
73
  return text
74
  return text[:max_length] + "..."
75
 
76
+ class CacheStrategy:
77
+ """Cache loading strategy enumeration"""
78
+ LAZY = "lazy" # Only load items into cache when requested
79
+ EAGER = "eager" # Preload items into cache at initialization
80
+ MIXED = "mixed" # Preload high-priority items, lazy load others
81
+
82
+ class CacheItem:
83
+ """Represents an item in the cache with metadata"""
84
+ def __init__(self, key: str, value: Any, ttl: int = 300, priority: int = 1):
85
+ self.key = key
86
+ self.value = value
87
+ self.expiry = datetime.now() + timedelta(seconds=ttl)
88
+ self.priority = priority # Higher number = higher priority
89
+ self.access_count = 0 # Track number of accesses
90
+ self.last_accessed = datetime.now()
91
+
92
+ def is_expired(self) -> bool:
93
+ """Check if the item is expired"""
94
+ return datetime.now() > self.expiry
95
 
96
+ def touch(self):
97
+ """Update last accessed time and access count"""
98
+ self.last_accessed = datetime.now()
99
+ self.access_count += 1
100
+
101
+ def __lt__(self, other):
102
+ """For heap comparisons - lower priority items are evicted first"""
103
+ # First compare priority
104
+ if self.priority != other.priority:
105
+ return self.priority < other.priority
106
+ # Then compare access frequency (less frequently accessed items are evicted first)
107
+ if self.access_count != other.access_count:
108
+ return self.access_count < other.access_count
109
+ # Finally compare last access time (oldest accessed first)
110
+ return self.last_accessed < other.last_accessed
111
+
112
+ def get_size(self) -> int:
113
+ """Approximate memory size of the cache item in bytes"""
114
+ try:
115
+ import sys
116
+ return sys.getsizeof(self.value) + sys.getsizeof(self.key) + 64 # Additional overhead
117
+ except:
118
+ # Default estimate if we can't get the size
119
+ return 1024
120
+
121
+ # Enhanced in-memory cache implementation
122
+ class EnhancedCache:
123
+ def __init__(self,
124
+ strategy: str = "lazy",
125
+ max_items: int = 10000,
126
+ max_size_mb: int = 100,
127
+ cleanup_interval: int = 60,
128
+ stats_enabled: bool = True):
129
+ """
130
+ Initialize enhanced cache with configurable strategy.
131
+
132
+ Args:
133
+ strategy: Cache loading strategy (lazy, eager, mixed)
134
+ max_items: Maximum number of items to store in cache
135
+ max_size_mb: Maximum size of cache in MB
136
+ cleanup_interval: Interval in seconds to run cleanup
137
+ stats_enabled: Whether to collect cache statistics
138
+ """
139
+ self._cache: Dict[str, CacheItem] = {}
140
+ self._namespace_cache: Dict[str, Set[str]] = {} # Tracking keys by namespace
141
+ self._strategy = strategy
142
+ self._max_items = max_items
143
+ self._max_size_bytes = max_size_mb * 1024 * 1024
144
+ self._current_size_bytes = 0
145
+ self._stats_enabled = stats_enabled
146
+
147
+ # Statistics
148
+ self._hits = 0
149
+ self._misses = 0
150
+ self._evictions = 0
151
+ self._total_get_time = 0
152
+ self._total_set_time = 0
153
+
154
+ # Setup cleanup thread
155
+ self._last_cleanup = datetime.now()
156
+ self._cleanup_interval = cleanup_interval
157
+ self._lock = threading.RLock()
158
+
159
+ if cleanup_interval > 0:
160
+ self._start_cleanup_thread(cleanup_interval)
161
+
162
+ logger.info(f"Enhanced cache initialized with strategy={strategy}, max_items={max_items}, max_size={max_size_mb}MB")
163
+
164
+ def _start_cleanup_thread(self, interval: int):
165
+ """Start background thread for periodic cleanup"""
166
+ def cleanup_worker():
167
+ while True:
168
+ time.sleep(interval)
169
+ try:
170
+ self.cleanup()
171
+ except Exception as e:
172
+ logger.error(f"Error in cache cleanup: {e}")
173
+
174
+ thread = threading.Thread(target=cleanup_worker, daemon=True)
175
+ thread.start()
176
+ logger.info(f"Cache cleanup thread started with interval {interval}s")
177
+
178
+ def get(self, key: str, namespace: str = None) -> Optional[Any]:
179
  """Get value from cache if it exists and hasn't expired"""
180
+ if self._stats_enabled:
181
+ start_time = time.time()
182
+
183
+ # Use namespaced key if namespace is provided
184
+ cache_key = f"{namespace}:{key}" if namespace else key
185
+
186
+ with self._lock:
187
+ cache_item = self._cache.get(cache_key)
188
+
189
+ if cache_item:
190
+ if cache_item.is_expired():
191
+ # Clean up expired key
192
+ self._remove_item(cache_key, namespace)
193
+ if self._stats_enabled:
194
+ self._misses += 1
195
+ value = None
196
+ else:
197
+ # Update access metadata
198
+ cache_item.touch()
199
+ if self._stats_enabled:
200
+ self._hits += 1
201
+ value = cache_item.value
202
+ else:
203
+ if self._stats_enabled:
204
+ self._misses += 1
205
+ value = None
206
+
207
+ if self._stats_enabled:
208
+ self._total_get_time += time.time() - start_time
209
+
210
+ return value
211
+
212
+ def set(self, key: str, value: Any, ttl: int = 300, priority: int = 1, namespace: str = None) -> None:
213
+ """Set a value in the cache with TTL in seconds"""
214
+ if self._stats_enabled:
215
+ start_time = time.time()
216
+
217
+ # Use namespaced key if namespace is provided
218
+ cache_key = f"{namespace}:{key}" if namespace else key
219
+
220
+ with self._lock:
221
+ # Create cache item
222
+ cache_item = CacheItem(cache_key, value, ttl, priority)
223
+ item_size = cache_item.get_size()
224
+
225
+ # Check if we need to make room
226
+ if (len(self._cache) >= self._max_items or
227
+ self._current_size_bytes + item_size > self._max_size_bytes):
228
+ self._evict_items(item_size)
229
+
230
+ # Update size tracking
231
+ if cache_key in self._cache:
232
+ # If replacing, subtract old size first
233
+ self._current_size_bytes -= self._cache[cache_key].get_size()
234
+ self._current_size_bytes += item_size
235
+
236
+ # Store the item
237
+ self._cache[cache_key] = cache_item
238
+
239
+ # Update namespace tracking
240
+ if namespace:
241
+ if namespace not in self._namespace_cache:
242
+ self._namespace_cache[namespace] = set()
243
+ self._namespace_cache[namespace].add(cache_key)
244
+
245
+ if self._stats_enabled:
246
+ self._total_set_time += time.time() - start_time
247
+
248
+ def delete(self, key: str, namespace: str = None) -> None:
249
+ """Delete a key from the cache"""
250
+ # Use namespaced key if namespace is provided
251
+ cache_key = f"{namespace}:{key}" if namespace else key
252
+
253
+ with self._lock:
254
+ self._remove_item(cache_key, namespace)
255
+
256
+ def _remove_item(self, key: str, namespace: str = None):
257
+ """Internal method to remove an item and update tracking"""
258
  if key in self._cache:
259
+ # Update size tracking
260
+ self._current_size_bytes -= self._cache[key].get_size()
261
+ # Remove from cache
262
+ del self._cache[key]
263
+
264
+ # Update namespace tracking
265
+ if namespace and namespace in self._namespace_cache:
266
+ if key in self._namespace_cache[namespace]:
267
+ self._namespace_cache[namespace].remove(key)
268
+ # Cleanup empty sets
269
+ if not self._namespace_cache[namespace]:
270
+ del self._namespace_cache[namespace]
271
+
272
+ def _evict_items(self, needed_space: int = 0) -> None:
273
+ """Evict items to make room in the cache"""
274
+ if not self._cache:
275
+ return
276
+
277
+ with self._lock:
278
+ # Convert cache items to a list for sorting
279
+ items = list(self._cache.values())
280
+
281
+ # Sort by priority, access count, and last accessed time
282
+ items.sort() # Uses the __lt__ method of CacheItem
283
+
284
+ # Evict items until we have enough space
285
+ space_freed = 0
286
+ evicted_count = 0
287
+
288
+ for item in items:
289
+ # Stop if we've made enough room
290
+ if (len(self._cache) - evicted_count <= self._max_items * 0.9 and
291
+ (space_freed >= needed_space or
292
+ self._current_size_bytes - space_freed <= self._max_size_bytes * 0.9)):
293
+ break
294
+
295
+ # Skip high priority items unless absolutely necessary
296
+ if item.priority > 9 and evicted_count < len(items) // 2:
297
+ continue
298
+
299
+ # Evict this item
300
+ item_size = item.get_size()
301
+ namespace = item.key.split(':', 1)[0] if ':' in item.key else None
302
+ self._remove_item(item.key, namespace)
303
+
304
+ space_freed += item_size
305
+ evicted_count += 1
306
+ if self._stats_enabled:
307
+ self._evictions += 1
308
+
309
+ logger.info(f"Cache eviction: removed {evicted_count} items, freed {space_freed / 1024:.2f}KB")
310
+
311
+ def clear(self, namespace: str = None) -> None:
312
+ """
313
+ Clear the cache or a specific namespace
314
+ """
315
+ with self._lock:
316
+ if namespace:
317
+ # Clear only keys in the specified namespace
318
+ if namespace in self._namespace_cache:
319
+ keys_to_remove = list(self._namespace_cache[namespace])
320
+ for key in keys_to_remove:
321
+ self._remove_item(key, namespace)
322
+ # The namespace should be auto-cleaned in _remove_item
323
  else:
324
+ # Clear the entire cache
325
+ self._cache.clear()
326
+ self._namespace_cache.clear()
327
+ self._current_size_bytes = 0
328
+
329
+ logger.info(f"Cache cleared{' for namespace ' + namespace if namespace else ''}")
330
+
331
+ def cleanup(self) -> None:
332
+ """Remove expired items and run garbage collection if needed"""
333
+ with self._lock:
334
+ now = datetime.now()
335
+ # Only run if it's been at least cleanup_interval since last cleanup
336
+ if (now - self._last_cleanup).total_seconds() < self._cleanup_interval:
337
+ return
338
+
339
+ # Find expired items
340
+ expired_keys = []
341
+ for key, item in self._cache.items():
342
+ if item.is_expired():
343
+ expired_keys.append((key, key.split(':', 1)[0] if ':' in key else None))
344
+
345
+ # Remove expired items
346
+ for key, namespace in expired_keys:
347
+ self._remove_item(key, namespace)
348
+
349
+ # Update last cleanup time
350
+ self._last_cleanup = now
351
+
352
+ # Run garbage collection if we removed several items
353
+ if len(expired_keys) > 100:
354
+ gc.collect()
355
+
356
+ logger.info(f"Cache cleanup: removed {len(expired_keys)} expired items")
357
+
358
+ def get_stats(self) -> Dict:
359
+ """Get cache statistics"""
360
+ with self._lock:
361
+ if not self._stats_enabled:
362
+ return {"stats_enabled": False}
363
+
364
+ # Calculate hit rate
365
+ total_requests = self._hits + self._misses
366
+ hit_rate = (self._hits / total_requests) * 100 if total_requests > 0 else 0
367
+
368
+ # Calculate average times
369
+ avg_get_time = (self._total_get_time / total_requests) * 1000 if total_requests > 0 else 0
370
+ avg_set_time = (self._total_set_time / self._evictions) * 1000 if self._evictions > 0 else 0
371
+
372
+ return {
373
+ "stats_enabled": True,
374
+ "item_count": len(self._cache),
375
+ "max_items": self._max_items,
376
+ "size_bytes": self._current_size_bytes,
377
+ "max_size_bytes": self._max_size_bytes,
378
+ "hits": self._hits,
379
+ "misses": self._misses,
380
+ "hit_rate_percent": round(hit_rate, 2),
381
+ "evictions": self._evictions,
382
+ "avg_get_time_ms": round(avg_get_time, 3),
383
+ "avg_set_time_ms": round(avg_set_time, 3),
384
+ "namespace_count": len(self._namespace_cache),
385
+ "namespaces": list(self._namespace_cache.keys())
386
+ }
387
+
388
+ def preload(self, items: List[Tuple[str, Any, int, int]], namespace: str = None) -> None:
389
+ """
390
+ Preload a list of items into the cache
391
+
392
+ Args:
393
+ items: List of (key, value, ttl, priority) tuples
394
+ namespace: Optional namespace for all items
395
+ """
396
+ for key, value, ttl, priority in items:
397
+ self.set(key, value, ttl, priority, namespace)
398
+
399
+ logger.info(f"Preloaded {len(items)} items into cache{' namespace ' + namespace if namespace else ''}")
400
+
401
+ def get_or_load(self, key: str, loader_func: Callable[[], Any],
402
+ ttl: int = 300, priority: int = 1, namespace: str = None) -> Any:
403
+ """
404
+ Get from cache or load using the provided function
405
+
406
+ Args:
407
+ key: Cache key
408
+ loader_func: Function to call if cache miss occurs
409
+ ttl: TTL in seconds
410
+ priority: Item priority
411
+ namespace: Optional namespace
412
+
413
+ Returns:
414
+ Cached or freshly loaded value
415
+ """
416
+ # Try to get from cache first
417
+ value = self.get(key, namespace)
418
+
419
+ # If not in cache, load it
420
+ if value is None:
421
+ value = loader_func()
422
+ # Only cache if we got a valid value
423
+ if value is not None:
424
+ self.set(key, value, ttl, priority, namespace)
425
+
426
+ return value
427
+
428
+ # Load cache configuration from environment variables
429
+ CACHE_STRATEGY = os.getenv("CACHE_STRATEGY", "mixed")
430
+ CACHE_MAX_ITEMS = int(os.getenv("CACHE_MAX_ITEMS", "10000"))
431
+ CACHE_MAX_SIZE_MB = int(os.getenv("CACHE_MAX_SIZE_MB", "100"))
432
+ CACHE_CLEANUP_INTERVAL = int(os.getenv("CACHE_CLEANUP_INTERVAL", "60"))
433
+ CACHE_STATS_ENABLED = os.getenv("CACHE_STATS_ENABLED", "true").lower() in ("true", "1", "yes")
434
+
435
+ # Initialize the enhanced cache
436
+ cache = EnhancedCache(
437
+ strategy=CACHE_STRATEGY,
438
+ max_items=CACHE_MAX_ITEMS,
439
+ max_size_mb=CACHE_MAX_SIZE_MB,
440
+ cleanup_interval=CACHE_CLEANUP_INTERVAL,
441
+ stats_enabled=CACHE_STATS_ENABLED
442
+ )
443
+
444
+ # Backward compatibility for SimpleCache - for a transition period
445
+ class SimpleCache:
446
+ def __init__(self):
447
+ """Legacy SimpleCache implementation that uses EnhancedCache underneath"""
448
+ logger.warning("SimpleCache is deprecated, please use EnhancedCache directly")
449
+
450
+ def get(self, key: str) -> Optional[Any]:
451
+ """Get value from cache if it exists and hasn't expired"""
452
+ return cache.get(key)
453
 
454
  def set(self, key: str, value: Any, ttl: int = 300) -> None:
455
  """Set a value in the cache with TTL in seconds"""
456
+ cache.set(key, value, ttl)
 
 
457
 
458
  def delete(self, key: str) -> None:
459
  """Delete a key from the cache"""
460
+ cache.delete(key)
 
 
 
461
 
462
  def clear(self) -> None:
463
  """Clear the entire cache"""
464
+ cache.clear()
 
 
 
 
465
 
466
  def get_host_url(request) -> str:
467
  """
docs/api_documentation.md ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Documentation
2
+
3
+ ## Frontend Setup
4
+
5
+ ```javascript
6
+ // Basic Axios setup
7
+ import axios from 'axios';
8
+
9
+ const api = axios.create({
10
+ baseURL: 'https://api.your-domain.com',
11
+ timeout: 10000,
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ 'Accept': 'application/json'
15
+ }
16
+ });
17
+
18
+ // Error handling
19
+ api.interceptors.response.use(
20
+ response => response.data,
21
+ error => {
22
+ const errorMessage = error.response?.data?.detail || 'An error occurred';
23
+ console.error('API Error:', errorMessage);
24
+ return Promise.reject(errorMessage);
25
+ }
26
+ );
27
+ ```
28
+
29
+ ## Caching System
30
+
31
+ - All GET endpoints support `use_cache=true` parameter (default)
32
+ - Cache TTL: 300 seconds (5 minutes)
33
+ - Cache is automatically invalidated on data changes
34
+
35
+ ## Authentication
36
+
37
+ Currently no authentication is required. If implemented in the future, use JWT Bearer tokens:
38
+
39
+ ```javascript
40
+ const api = axios.create({
41
+ // ...other config
42
+ headers: {
43
+ // ...other headers
44
+ 'Authorization': `Bearer ${token}`
45
+ }
46
+ });
47
+ ```
48
+
49
+ ## Error Codes
50
+
51
+ | Code | Description |
52
+ |------|-------------|
53
+ | 400 | Bad Request |
54
+ | 404 | Not Found |
55
+ | 500 | Internal Server Error |
56
+ | 503 | Service Unavailable |
57
+
58
+ ## PostgreSQL Endpoints
59
+
60
+ ### FAQ Endpoints
61
+
62
+ #### Get FAQs List
63
+ ```
64
+ GET /postgres/faq
65
+ ```
66
+
67
+ Parameters:
68
+ - `skip`: Number of items to skip (default: 0)
69
+ - `limit`: Maximum items to return (default: 100)
70
+ - `active_only`: Return only active items (default: false)
71
+ - `use_cache`: Use cached data if available (default: true)
72
+
73
+ Response:
74
+ ```json
75
+ [
76
+ {
77
+ "question": "How do I book a hotel?",
78
+ "answer": "You can book a hotel through our app or website.",
79
+ "is_active": true,
80
+ "id": 1,
81
+ "created_at": "2023-01-01T00:00:00",
82
+ "updated_at": "2023-01-01T00:00:00"
83
+ }
84
+ ]
85
+ ```
86
+
87
+ Example:
88
+ ```javascript
89
+ async function getFAQs() {
90
+ try {
91
+ const data = await api.get('/postgres/faq', {
92
+ params: { active_only: true, limit: 20 }
93
+ });
94
+ return data;
95
+ } catch (error) {
96
+ console.error('Error fetching FAQs:', error);
97
+ throw error;
98
+ }
99
+ }
100
+ ```
101
+
102
+ #### Create FAQ
103
+ ```
104
+ POST /postgres/faq
105
+ ```
106
+
107
+ Request Body:
108
+ ```json
109
+ {
110
+ "question": "How do I book a hotel?",
111
+ "answer": "You can book a hotel through our app or website.",
112
+ "is_active": true
113
+ }
114
+ ```
115
+
116
+ Response: Created FAQ object
117
+
118
+ #### Get FAQ Detail
119
+ ```
120
+ GET /postgres/faq/{faq_id}
121
+ ```
122
+
123
+ Parameters:
124
+ - `faq_id`: ID of FAQ (required)
125
+ - `use_cache`: Use cached data if available (default: true)
126
+
127
+ Response: FAQ object
128
+
129
+ #### Update FAQ
130
+ ```
131
+ PUT /postgres/faq/{faq_id}
132
+ ```
133
+
134
+ Parameters:
135
+ - `faq_id`: ID of FAQ to update (required)
136
+
137
+ Request Body: Partial or complete FAQ object
138
+ Response: Updated FAQ object
139
+
140
+ #### Delete FAQ
141
+ ```
142
+ DELETE /postgres/faq/{faq_id}
143
+ ```
144
+
145
+ Parameters:
146
+ - `faq_id`: ID of FAQ to delete (required)
147
+
148
+ Response:
149
+ ```json
150
+ {
151
+ "status": "success",
152
+ "message": "FAQ item 1 deleted"
153
+ }
154
+ ```
155
+
156
+ #### Batch Operations
157
+
158
+ Create multiple FAQs:
159
+ ```
160
+ POST /postgres/faqs/batch
161
+ ```
162
+
163
+ Update status of multiple FAQs:
164
+ ```
165
+ PUT /postgres/faqs/batch-update-status
166
+ ```
167
+
168
+ Delete multiple FAQs:
169
+ ```
170
+ DELETE /postgres/faqs/batch
171
+ ```
172
+
173
+ ### Emergency Contact Endpoints
174
+
175
+ #### Get Emergency Contacts
176
+ ```
177
+ GET /postgres/emergency
178
+ ```
179
+
180
+ Parameters:
181
+ - `skip`: Number of items to skip (default: 0)
182
+ - `limit`: Maximum items to return (default: 100)
183
+ - `active_only`: Return only active items (default: false)
184
+ - `use_cache`: Use cached data if available (default: true)
185
+
186
+ Response: Array of Emergency Contact objects
187
+
188
+ #### Create Emergency Contact
189
+ ```
190
+ POST /postgres/emergency
191
+ ```
192
+
193
+ Request Body:
194
+ ```json
195
+ {
196
+ "name": "Fire Department",
197
+ "phone_number": "114",
198
+ "description": "Fire rescue services",
199
+ "address": "Da Nang",
200
+ "location": "16.0544, 108.2022",
201
+ "priority": 1,
202
+ "is_active": true
203
+ }
204
+ ```
205
+
206
+ Response: Created Emergency Contact object
207
+
208
+ #### Get Emergency Contact
209
+ ```
210
+ GET /postgres/emergency/{emergency_id}
211
+ ```
212
+
213
+ #### Update Emergency Contact
214
+ ```
215
+ PUT /postgres/emergency/{emergency_id}
216
+ ```
217
+
218
+ #### Delete Emergency Contact
219
+ ```
220
+ DELETE /postgres/emergency/{emergency_id}
221
+ ```
222
+
223
+ #### Batch Operations
224
+
225
+ Create multiple Emergency Contacts:
226
+ ```
227
+ POST /postgres/emergency/batch
228
+ ```
229
+
230
+ Update status of multiple Emergency Contacts:
231
+ ```
232
+ PUT /postgres/emergency/batch-update-status
233
+ ```
234
+
235
+ Delete multiple Emergency Contacts:
236
+ ```
237
+ DELETE /postgres/emergency/batch
238
+ ```
239
+
240
+ ### Event Endpoints
241
+
242
+ #### Get Events
243
+ ```
244
+ GET /postgres/events
245
+ ```
246
+
247
+ Parameters:
248
+ - `skip`: Number of items to skip (default: 0)
249
+ - `limit`: Maximum items to return (default: 100)
250
+ - `active_only`: Return only active items (default: false)
251
+ - `featured_only`: Return only featured items (default: false)
252
+ - `use_cache`: Use cached data if available (default: true)
253
+
254
+ Response: Array of Event objects
255
+
256
+ #### Create Event
257
+ ```
258
+ POST /postgres/events
259
+ ```
260
+
261
+ Request Body:
262
+ ```json
263
+ {
264
+ "name": "Da Nang Fireworks Festival",
265
+ "description": "International Fireworks Festival Da Nang 2023",
266
+ "address": "Dragon Bridge, Da Nang",
267
+ "location": "16.0610, 108.2277",
268
+ "date_start": "2023-06-01T19:00:00",
269
+ "date_end": "2023-06-01T22:00:00",
270
+ "price": [
271
+ {"type": "VIP", "amount": 500000},
272
+ {"type": "Standard", "amount": 300000}
273
+ ],
274
+ "url": "https://danangfireworks.com",
275
+ "is_active": true,
276
+ "featured": true
277
+ }
278
+ ```
279
+
280
+ Response: Created Event object
281
+
282
+ #### Get Event
283
+ ```
284
+ GET /postgres/events/{event_id}
285
+ ```
286
+
287
+ #### Update Event
288
+ ```
289
+ PUT /postgres/events/{event_id}
290
+ ```
291
+
292
+ #### Delete Event
293
+ ```
294
+ DELETE /postgres/events/{event_id}
295
+ ```
296
+
297
+ #### Batch Operations
298
+
299
+ Create multiple Events:
300
+ ```
301
+ POST /postgres/events/batch
302
+ ```
303
+
304
+ Update status of multiple Events:
305
+ ```
306
+ PUT /postgres/events/batch-update-status
307
+ ```
308
+
309
+ Delete multiple Events:
310
+ ```
311
+ DELETE /postgres/events/batch
312
+ ```
313
+
314
+ ### About Pixity Endpoints
315
+
316
+ #### Get About Pixity
317
+ ```
318
+ GET /postgres/about-pixity
319
+ ```
320
+
321
+ Response:
322
+ ```json
323
+ {
324
+ "content": "PiXity is your smart, AI-powered local companion...",
325
+ "id": 1,
326
+ "created_at": "2023-01-01T00:00:00",
327
+ "updated_at": "2023-01-01T00:00:00"
328
+ }
329
+ ```
330
+
331
+ #### Update About Pixity
332
+ ```
333
+ PUT /postgres/about-pixity
334
+ ```
335
+
336
+ Request Body:
337
+ ```json
338
+ {
339
+ "content": "PiXity is your smart, AI-powered local companion..."
340
+ }
341
+ ```
342
+
343
+ Response: Updated About Pixity object
344
+
345
+ ### Da Nang Bucket List Endpoints
346
+
347
+ #### Get Da Nang Bucket List
348
+ ```
349
+ GET /postgres/danang-bucket-list
350
+ ```
351
+
352
+ Response: Bucket List object with JSON content string
353
+
354
+ #### Update Da Nang Bucket List
355
+ ```
356
+ PUT /postgres/danang-bucket-list
357
+ ```
358
+
359
+ ### Solana Summit Endpoints
360
+
361
+ #### Get Solana Summit
362
+ ```
363
+ GET /postgres/solana-summit
364
+ ```
365
+
366
+ Response: Solana Summit object with JSON content string
367
+
368
+ #### Update Solana Summit
369
+ ```
370
+ PUT /postgres/solana-summit
371
+ ```
372
+
373
+ ### Health Check
374
+ ```
375
+ GET /postgres/health
376
+ ```
377
+
378
+ Response:
379
+ ```json
380
+ {
381
+ "status": "healthy",
382
+ "message": "PostgreSQL connection is working",
383
+ "timestamp": "2023-01-01T00:00:00"
384
+ }
385
+ ```
386
+
387
+ ## MongoDB Endpoints
388
+
389
+ ### Session Endpoints
390
+
391
+ #### Create Session
392
+ ```
393
+ POST /session
394
+ ```
395
+
396
+ Request Body:
397
+ ```json
398
+ {
399
+ "user_id": "user123",
400
+ "query": "How do I book a room?",
401
+ "timestamp": "2023-01-01T00:00:00",
402
+ "metadata": {
403
+ "client_info": "web",
404
+ "location": "Da Nang"
405
+ }
406
+ }
407
+ ```
408
+
409
+ Response: Created Session object with session_id
410
+
411
+ #### Update Session with Response
412
+ ```
413
+ PUT /session/{session_id}/response
414
+ ```
415
+
416
+ Request Body:
417
+ ```json
418
+ {
419
+ "response": "You can book a room through our app or website.",
420
+ "response_timestamp": "2023-01-01T00:00:05",
421
+ "metadata": {
422
+ "response_time_ms": 234,
423
+ "model_version": "gpt-4"
424
+ }
425
+ }
426
+ ```
427
+
428
+ Response: Updated Session object
429
+
430
+ #### Get Session
431
+ ```
432
+ GET /session/{session_id}
433
+ ```
434
+
435
+ Response: Session object
436
+
437
+ #### Get User History
438
+ ```
439
+ GET /history
440
+ ```
441
+
442
+ Parameters:
443
+ - `user_id`: User ID (required)
444
+ - `limit`: Maximum sessions to return (default: 10)
445
+ - `skip`: Number of sessions to skip (default: 0)
446
+
447
+ Response:
448
+ ```json
449
+ {
450
+ "user_id": "user123",
451
+ "sessions": [
452
+ {
453
+ "session_id": "60f7a8b9c1d2e3f4a5b6c7d8",
454
+ "query": "How do I book a room?",
455
+ "timestamp": "2023-01-01T00:00:00",
456
+ "response": "You can book a room through our app or website.",
457
+ "response_timestamp": "2023-01-01T00:00:05"
458
+ }
459
+ ],
460
+ "total_count": 1
461
+ }
462
+ ```
463
+
464
+ #### Health Check
465
+ ```
466
+ GET /health
467
+ ```
468
+
469
+ ## RAG Endpoints
470
+
471
+ ### Create Embedding
472
+ ```
473
+ POST /embedding
474
+ ```
475
+
476
+ Request Body:
477
+ ```json
478
+ {
479
+ "text": "Text to embed"
480
+ }
481
+ ```
482
+
483
+ Response:
484
+ ```json
485
+ {
486
+ "embedding": [0.1, 0.2, 0.3, ...],
487
+ "dimensions": 1536
488
+ }
489
+ ```
490
+
491
+ ### Process Chat Request
492
+ ```
493
+ POST /chat
494
+ ```
495
+
496
+ Request Body:
497
+ ```json
498
+ {
499
+ "query": "Can you tell me about Pixity?",
500
+ "chat_history": [
501
+ {"role": "user", "content": "Hello"},
502
+ {"role": "assistant", "content": "Hello! How can I help you?"}
503
+ ]
504
+ }
505
+ ```
506
+
507
+ Response:
508
+ ```json
509
+ {
510
+ "answer": "Pixity is a platform...",
511
+ "sources": [
512
+ {
513
+ "document_id": "doc123",
514
+ "chunk_id": "chunk456",
515
+ "chunk_text": "Pixity was founded in...",
516
+ "relevance_score": 0.92
517
+ }
518
+ ]
519
+ }
520
+ ```
521
+
522
+ ### Direct RAG Query
523
+ ```
524
+ POST /rag
525
+ ```
526
+
527
+ Request Body:
528
+ ```json
529
+ {
530
+ "query": "Can you tell me about Pixity?",
531
+ "namespace": "about_pixity",
532
+ "top_k": 3
533
+ }
534
+ ```
535
+
536
+ Response: Query results with relevance scores
537
+
538
+ ### Health Check
539
+ ```
540
+ GET /health
541
+ ```
542
+
543
+ ## PDF Processing Endpoints
544
+
545
+ ### Upload and Process PDF
546
+ ```
547
+ POST /pdf/upload
548
+ ```
549
+
550
+ Form Data:
551
+ - `file`: PDF file (required)
552
+ - `namespace`: Vector database namespace (default: "Default")
553
+ - `index_name`: Vector database index name (default: "testbot768")
554
+ - `title`: Document title (optional)
555
+ - `description`: Document description (optional)
556
+ - `user_id`: User ID for WebSocket updates (optional)
557
+
558
+ Response: Processing results with document_id
559
+
560
+ ### Delete Documents in Namespace
561
+ ```
562
+ DELETE /pdf/namespace
563
+ ```
564
+
565
+ Parameters:
566
+ - `namespace`: Vector database namespace (default: "Default")
567
+ - `index_name`: Vector database index name (default: "testbot768")
568
+ - `user_id`: User ID for WebSocket updates (optional)
569
+
570
+ Response: Deletion results
571
+
572
+ ### Get Documents List
573
+ ```
574
+ GET /pdf/documents
575
+ ```
576
+
577
+ Parameters:
578
+ - `namespace`: Vector database namespace (default: "Default")
579
+ - `index_name`: Vector database index name (default: "testbot768")
580
+
581
+ Response: List of documents in the namespace
requirements.txt CHANGED
@@ -40,4 +40,8 @@ watchfiles==0.21.0
40
 
41
  # Core dependencies
42
  starlette==0.27.0
43
- psutil==5.9.6
 
 
 
 
 
40
 
41
  # Core dependencies
42
  starlette==0.27.0
43
+ psutil==5.9.6
44
+
45
+ # Upload PDF
46
+ pypdf==3.17.4
47
+