minhvtt commited on
Commit
85b0b5e
·
verified ·
1 Parent(s): 85708eb

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +185 -1292
main.py CHANGED
@@ -1,30 +1,19 @@
1
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from typing import Optional, List, Dict
6
- from PIL import Image
7
- import io
8
- import numpy as np
9
  import os
 
 
 
10
  from datetime import datetime
 
 
 
11
  from pymongo import MongoClient
12
- from huggingface_hub import InferenceClient
13
-
14
- from embedding_service import JinaClipEmbeddingService
15
- from qdrant_service import QdrantVectorService
16
- from advanced_rag import AdvancedRAG
17
- from pdf_parser import PDFIndexer
18
- from multimodal_pdf_parser import MultimodalPDFIndexer
19
 
20
- # Initialize FastAPI app
21
- app = FastAPI(
22
- title="Event Social Media Embeddings & ChatbotRAG API",
23
- description="API để embeddings, search và ChatbotRAG với Jina CLIP v2 + Qdrant + MongoDB + LLM",
24
- version="2.0.0"
25
- )
26
 
27
- # CORS middleware
28
  app.add_middleware(
29
  CORSMiddleware,
30
  allow_origins=["*"],
@@ -33,1286 +22,190 @@ app.add_middleware(
33
  allow_headers=["*"],
34
  )
35
 
36
- # Initialize services
37
- print("Initializing services...")
38
- embedding_service = JinaClipEmbeddingService(model_path="jinaai/jina-clip-v2")
39
-
40
- collection_name = os.getenv("COLLECTION_NAME", "event_social_media")
41
- qdrant_service = QdrantVectorService(
42
- collection_name=collection_name,
43
- vector_size=embedding_service.get_embedding_dimension()
44
- )
45
- print(f"✓ Qdrant collection: {collection_name}")
46
-
47
- # MongoDB connection
48
- mongodb_uri = os.getenv("MONGODB_URI", "mongodb+srv://truongtn7122003:7KaI9OT5KTUxWjVI@truongtn7122003.xogin4q.mongodb.net/")
49
- mongo_client = MongoClient(mongodb_uri)
50
- db = mongo_client[os.getenv("MONGODB_DB_NAME", "chatbot_rag")]
51
- documents_collection = db["documents"]
52
- chat_history_collection = db["chat_history"]
53
- print(" MongoDB connected")
54
-
55
- # Hugging Face token
56
- hf_token = os.getenv("HUGGINGFACE_TOKEN")
57
- if hf_token:
58
- print(" Hugging Face token configured")
59
-
60
- # Initialize Advanced RAG
61
- advanced_rag = AdvancedRAG(
62
- embedding_service=embedding_service,
63
- qdrant_service=qdrant_service
64
- )
65
- print("✓ Advanced RAG pipeline initialized")
66
-
67
- # Initialize PDF Indexer
68
- pdf_indexer = PDFIndexer(
69
- embedding_service=embedding_service,
70
- qdrant_service=qdrant_service,
71
- documents_collection=documents_collection
72
- )
73
- print("✓ PDF Indexer initialized")
74
-
75
- # Initialize Multimodal PDF Indexer (for PDFs with images)
76
- multimodal_pdf_indexer = MultimodalPDFIndexer(
77
- embedding_service=embedding_service,
78
- qdrant_service=qdrant_service,
79
- documents_collection=documents_collection
80
- )
81
- print("✓ Multimodal PDF Indexer initialized")
82
-
83
- print("✓ Services initialized successfully")
84
-
85
-
86
- # Pydantic models for embeddings
87
- class SearchRequest(BaseModel):
88
- text: Optional[str] = None
89
- limit: int = 10
90
- score_threshold: Optional[float] = None
91
- text_weight: float = 0.5
92
- image_weight: float = 0.5
93
-
94
-
95
- class SearchResponse(BaseModel):
96
- id: str
97
- confidence: float
98
- metadata: dict
99
-
100
-
101
- class IndexResponse(BaseModel):
102
- success: bool
103
- id: str
104
- message: str
105
-
106
-
107
- # Pydantic models for ChatbotRAG
108
- class ChatRequest(BaseModel):
109
- message: str
110
- use_rag: bool = True
111
- top_k: int = 3
112
- system_message: Optional[str] = "You are a helpful AI assistant."
113
- max_tokens: int = 512
114
- temperature: float = 0.7
115
- top_p: float = 0.95
116
- hf_token: Optional[str] = None
117
- # Advanced RAG options
118
- use_advanced_rag: bool = True
119
- use_query_expansion: bool = True
120
- use_reranking: bool = True
121
- use_compression: bool = True
122
- score_threshold: float = 0.5
123
-
124
-
125
- class ChatResponse(BaseModel):
126
- response: str
127
- context_used: List[Dict]
128
- timestamp: str
129
- rag_stats: Optional[Dict] = None # Stats from advanced RAG pipeline
130
-
131
-
132
- class AddDocumentRequest(BaseModel):
133
- text: str
134
- metadata: Optional[Dict] = None
135
-
136
-
137
- class AddDocumentResponse(BaseModel):
138
- success: bool
139
- doc_id: str
140
- message: str
141
-
142
-
143
- class UploadPDFResponse(BaseModel):
144
- success: bool
145
- document_id: str
146
- filename: str
147
- chunks_indexed: int
148
- message: str
149
-
150
-
151
- @app.get("/")
152
- async def root():
153
- """Health check endpoint with comprehensive API documentation"""
154
- return {
155
- "status": "running",
156
- "service": "ChatbotRAG API - Advanced RAG with Multimodal Support",
157
- "version": "3.0.0",
158
- "vector_db": "Qdrant",
159
- "document_db": "MongoDB",
160
- "features": {
161
- "multiple_inputs": "Index up to 10 texts + 10 images per request",
162
- "advanced_rag": "Query expansion, reranking, contextual compression",
163
- "pdf_support": "Upload PDFs and chat about their content",
164
- "multimodal_pdf": "PDFs with text and image URLs - perfect for user guides",
165
- "chat_history": "Track conversation history",
166
- "hybrid_search": "Text + image search with Jina CLIP v2"
167
- },
168
- "endpoints": {
169
- "indexing": {
170
- "POST /index": {
171
- "description": "Index multiple texts and images (NEW: up to 10 each)",
172
- "content_type": "multipart/form-data",
173
- "body": {
174
- "id": "string (required) - Document ID (primary)",
175
- "texts": "List[string] (optional) - Up to 10 texts",
176
- "images": "List[UploadFile] (optional) - Up to 10 images",
177
- "id_use": "string (optional) - ID của SocialMedia hoặc EventCode",
178
- "id_user": "string (optional) - ID của User"
179
- },
180
- "example": "curl -X POST '/index' -F 'id=doc1' -F 'id_use=social_123' -F 'id_user=user_789' -F 'texts=Text 1' -F 'images=@img1.jpg'",
181
- "response": {
182
- "success": True,
183
- "id": "doc1",
184
- "message": "Indexed successfully with 2 texts and 1 images"
185
- },
186
- "use_cases": {
187
- "social_media_post": {
188
- "id": "post_uuid_123",
189
- "id_use": "social_media_456",
190
- "id_user": "user_789",
191
- "description": "Link post to social media account and user"
192
- },
193
- "event_post": {
194
- "id": "post_uuid_789",
195
- "id_use": "event_code_ABC123",
196
- "id_user": "user_101",
197
- "description": "Link post to event and user"
198
- }
199
- }
200
- },
201
- "POST /documents": {
202
- "description": "Add text document to knowledge base",
203
- "content_type": "application/json",
204
- "body": {
205
- "text": "string (required) - Document content",
206
- "metadata": "object (optional) - Additional metadata"
207
- },
208
- "example": {
209
- "text": "How to create event: Click 'Create Event' button...",
210
- "metadata": {"category": "tutorial", "source": "user_guide"}
211
- }
212
- },
213
- "POST /upload-pdf": {
214
- "description": "Upload PDF file (text only)",
215
- "content_type": "multipart/form-data",
216
- "body": {
217
- "file": "UploadFile (required) - PDF file",
218
- "title": "string (optional) - Document title",
219
- "category": "string (optional) - Category",
220
- "description": "string (optional) - Description"
221
- },
222
- "example": "curl -X POST '/upload-pdf' -F 'file=@guide.pdf' -F 'title=User Guide'"
223
- },
224
- "POST /upload-pdf-multimodal": {
225
- "description": "Upload PDF with text and image URLs (RECOMMENDED for user guides)",
226
- "content_type": "multipart/form-data",
227
- "features": [
228
- "Extracts text from PDF",
229
- "Detects image URLs (http://, https://)",
230
- "Supports markdown: ![alt](url)",
231
- "Supports HTML: <img src='url'>",
232
- "Links images to text chunks",
233
- "Returns images with context in chat"
234
- ],
235
- "body": {
236
- "file": "UploadFile (required) - PDF file with image URLs",
237
- "title": "string (optional) - Document title",
238
- "category": "string (optional) - e.g. 'user_guide', 'tutorial'",
239
- "description": "string (optional)"
240
- },
241
- "example": "curl -X POST '/upload-pdf-multimodal' -F 'file=@guide_with_images.pdf' -F 'category=user_guide'",
242
- "response": {
243
- "success": True,
244
- "document_id": "pdf_multimodal_20251029_150000",
245
- "chunks_indexed": 25,
246
- "message": "PDF indexed with 25 chunks and 15 images"
247
- },
248
- "use_case": "Perfect for user guides with screenshots, tutorials with diagrams"
249
- }
250
- },
251
- "search": {
252
- "POST /search": {
253
- "description": "Hybrid search with text and/or image",
254
- "body": {
255
- "text": "string (optional) - Query text",
256
- "image": "UploadFile (optional) - Query image",
257
- "limit": "int (default: 10)",
258
- "score_threshold": "float (optional, 0-1)",
259
- "text_weight": "float (default: 0.5)",
260
- "image_weight": "float (default: 0.5)"
261
- }
262
- },
263
- "POST /search/text": {
264
- "description": "Text-only search",
265
- "body": {"text": "string", "limit": "int", "score_threshold": "float"}
266
- },
267
- "POST /search/image": {
268
- "description": "Image-only search",
269
- "body": {"image": "UploadFile", "limit": "int", "score_threshold": "float"}
270
- },
271
- "POST /rag/search": {
272
- "description": "Search in RAG knowledge base",
273
- "body": {"query": "string", "top_k": "int (default: 5)", "score_threshold": "float (default: 0.5)"}
274
- }
275
- },
276
- "chat": {
277
- "POST /chat": {
278
- "description": "Chat với Advanced RAG (Query expansion + Reranking + Compression)",
279
- "content_type": "application/json",
280
- "body": {
281
- "message": "string (required) - User question",
282
- "use_rag": "bool (default: true) - Enable RAG retrieval",
283
- "use_advanced_rag": "bool (default: true) - Use advanced RAG pipeline (RECOMMENDED)",
284
- "use_query_expansion": "bool (default: true) - Expand query with variations",
285
- "use_reranking": "bool (default: true) - Rerank results for accuracy",
286
- "use_compression": "bool (default: true) - Compress context to relevant parts",
287
- "top_k": "int (default: 3) - Number of documents to retrieve",
288
- "score_threshold": "float (default: 0.5) - Min relevance score (0-1)",
289
- "max_tokens": "int (default: 512) - Max response tokens",
290
- "temperature": "float (default: 0.7) - Creativity (0-1)",
291
- "hf_token": "string (optional) - Hugging Face token"
292
- },
293
- "response": {
294
- "response": "string - AI answer",
295
- "context_used": "array - Retrieved documents with metadata",
296
- "timestamp": "string",
297
- "rag_stats": "object - RAG pipeline statistics (query variants, retrieval counts)"
298
- },
299
- "example_advanced": {
300
- "message": "Làm sao để upload PDF có hình ảnh?",
301
- "use_advanced_rag": True,
302
- "use_reranking": True,
303
- "top_k": 5,
304
- "score_threshold": 0.5
305
- },
306
- "example_response_with_images": {
307
- "response": "Để upload PDF có hình ảnh, sử dụng endpoint /upload-pdf-multimodal...",
308
- "context_used": [
309
- {
310
- "id": "pdf_multimodal_...._p2_c1",
311
- "confidence": 0.89,
312
- "metadata": {
313
- "text": "Bước 1: Chuẩn bị PDF với image URLs...",
314
- "has_images": True,
315
- "image_urls": [
316
- "https://example.com/screenshot1.png",
317
- "https://example.com/diagram.jpg"
318
- ],
319
- "num_images": 2,
320
- "page": 2
321
- }
322
- }
323
- ],
324
- "rag_stats": {
325
- "original_query": "Làm sao để upload PDF có hình ảnh?",
326
- "expanded_queries": ["upload PDF hình ảnh", "PDF có ảnh"],
327
- "initial_results": 10,
328
- "after_rerank": 5,
329
- "after_compression": 5
330
- }
331
- },
332
- "notes": [
333
- "Advanced RAG significantly improves answer quality",
334
- "When multimodal PDF is used, images are returned in metadata",
335
- "Requires HUGGINGFACE_TOKEN for actual LLM generation"
336
- ]
337
- },
338
- "GET /history": {
339
- "description": "Get chat history",
340
- "query_params": {"limit": "int (default: 10)", "skip": "int (default: 0)"},
341
- "response": {"history": "array", "total": "int"}
342
- }
343
- },
344
- "management": {
345
- "GET /documents/pdf": {
346
- "description": "List all PDF documents",
347
- "response": {"documents": "array", "total": "int"}
348
- },
349
- "DELETE /documents/pdf/{document_id}": {
350
- "description": "Delete PDF and all its chunks",
351
- "response": {"success": "bool", "message": "string"}
352
- },
353
- "GET /document/{doc_id}": {
354
- "description": "Get document by ID",
355
- "response": {"success": "bool", "data": "object"}
356
- },
357
- "DELETE /delete/{doc_id}": {
358
- "description": "Delete document by ID",
359
- "response": {"success": "bool", "message": "string"}
360
- },
361
- "GET /stats": {
362
- "description": "Get Qdrant collection statistics",
363
- "response": {"vectors_count": "int", "segments": "int", "indexed_vectors_count": "int"}
364
- }
365
- }
366
- },
367
- "quick_start": {
368
- "1_upload_multimodal_pdf": "curl -X POST '/upload-pdf-multimodal' -F 'file=@user_guide.pdf' -F 'title=Guide'",
369
- "2_verify_upload": "curl '/documents/pdf'",
370
- "3_chat_with_rag": "curl -X POST '/chat' -H 'Content-Type: application/json' -d '{\"message\": \"How to...?\", \"use_advanced_rag\": true}'",
371
- "4_see_images_in_context": "response['context_used'][0]['metadata']['image_urls']"
372
- },
373
- "use_cases": {
374
- "user_guide_with_screenshots": {
375
- "endpoint": "/upload-pdf-multimodal",
376
- "description": "PDFs with text instructions + image URLs for visual guidance",
377
- "benefits": ["Images linked to text chunks", "Chatbot returns relevant screenshots", "Perfect for step-by-step guides"]
378
- },
379
- "simple_text_docs": {
380
- "endpoint": "/upload-pdf",
381
- "description": "Simple PDFs with text only (FAQ, policies, etc.)"
382
- },
383
- "social_media_posts": {
384
- "endpoint": "/index",
385
- "description": "Index multiple posts with texts (up to 10) and images (up to 10)"
386
- },
387
- "complex_queries": {
388
- "endpoint": "/chat",
389
- "description": "Use advanced RAG for better accuracy on complex questions",
390
- "settings": {"use_advanced_rag": True, "use_reranking": True, "use_compression": True}
391
- }
392
- },
393
- "best_practices": {
394
- "pdf_format": [
395
- "Include image URLs in text (http://, https://)",
396
- "Use markdown format: ![alt](url) or HTML: <img src='url'>",
397
- "Clear structure with headings and sections",
398
- "Link images close to their related text"
399
- ],
400
- "chat_settings": {
401
- "for_accuracy": {"temperature": 0.3, "use_advanced_rag": True, "use_reranking": True},
402
- "for_creativity": {"temperature": 0.8, "use_advanced_rag": False},
403
- "for_factual_answers": {"temperature": 0.3, "use_compression": True, "score_threshold": 0.6}
404
- },
405
- "retrieval_tuning": {
406
- "not_finding_info": "Lower score_threshold to 0.3-0.4, increase top_k to 7-10",
407
- "too_much_context": "Increase score_threshold to 0.6-0.7, decrease top_k to 3-5",
408
- "slow_responses": "Disable compression, use basic RAG, decrease top_k"
409
- }
410
- },
411
- "links": {
412
- "docs": "http://localhost:8000/docs",
413
- "redoc": "http://localhost:8000/redoc",
414
- "openapi": "http://localhost:8000/openapi.json",
415
- "guides": {
416
- "multimodal_pdf": "See MULTIMODAL_PDF_GUIDE.md",
417
- "advanced_rag": "See ADVANCED_RAG_GUIDE.md",
418
- "pdf_general": "See PDF_RAG_GUIDE.md",
419
- "quick_start": "See QUICK_START_PDF.md"
420
- }
421
- },
422
- "system_info": {
423
- "embedding_model": "Jina CLIP v2 (multimodal)",
424
- "vector_db": "Qdrant with HNSW index",
425
- "document_db": "MongoDB",
426
- "rag_pipeline": "Advanced RAG with query expansion, reranking, compression",
427
- "pdf_parser": "pypdfium2 with URL extraction",
428
- "max_inputs": "10 texts + 10 images per /index request"
429
- }
430
- }
431
-
432
- @app.post("/index", response_model=IndexResponse)
433
- async def index_data(
434
- id: str = Form(...),
435
- texts: Optional[List[str]] = Form(None),
436
- images: Optional[List[UploadFile]] = File(None),
437
- id_use: Optional[str] = Form(None),
438
- id_user: Optional[str] = Form(None)
439
- ):
440
- """
441
- Index data vào vector database (hỗ trợ nhiều texts và images)
442
-
443
- Body:
444
- - id: Document ID (primary ID)
445
- - texts: List of text contents (tiếng Việt supported) - Tối đa 10 texts
446
- - images: List of image files (optional) - Tối đa 10 images
447
- - id_use: ID của SocialMedia hoặc EventCode (optional)
448
- - id_user: ID của User (optional)
449
-
450
- Returns:
451
- - success: True/False
452
- - id: Document ID
453
- - message: Status message
454
-
455
- Example:
456
- ```bash
457
- curl -X POST '/index' \
458
- -F 'id=doc123' \
459
- -F 'id_use=social_media_456' \
460
- -F 'id_user=user_789' \
461
- -F 'texts=Post content 1' \
462
- -F 'texts=Post content 2' \
463
- -F 'images=@image1.jpg'
464
- ```
465
- """
466
- try:
467
- # Validation
468
- if texts is None and images is None:
469
- raise HTTPException(status_code=400, detail="Phải cung cấp ít nhất texts hoặc images")
470
-
471
- if texts and len(texts) > 10:
472
- raise HTTPException(status_code=400, detail="Tối đa 10 texts")
473
-
474
- if images and len(images) > 10:
475
- raise HTTPException(status_code=400, detail="Tối đa 10 images")
476
-
477
- # Prepare embeddings
478
- text_embeddings = []
479
- image_embeddings = []
480
-
481
- # Encode multiple texts (tiếng Việt)
482
- if texts:
483
- for text in texts:
484
- if text and text.strip():
485
- text_emb = embedding_service.encode_text(text)
486
- text_embeddings.append(text_emb)
487
-
488
- # Encode multiple images
489
- if images:
490
- for image in images:
491
- if image.filename: # Check if image is provided
492
- image_bytes = await image.read()
493
- pil_image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
494
- image_emb = embedding_service.encode_image(pil_image)
495
- image_embeddings.append(image_emb)
496
-
497
- # Combine embeddings
498
- all_embeddings = []
499
-
500
- if text_embeddings:
501
- # Average all text embeddings
502
- avg_text_embedding = np.mean(text_embeddings, axis=0)
503
- all_embeddings.append(avg_text_embedding)
504
-
505
- if image_embeddings:
506
- # Average all image embeddings
507
- avg_image_embedding = np.mean(image_embeddings, axis=0)
508
- all_embeddings.append(avg_image_embedding)
509
-
510
- if not all_embeddings:
511
- raise HTTPException(status_code=400, detail="Không có embedding nào được tạo từ texts hoặc images")
512
-
513
- # Final combined embedding
514
- combined_embedding = np.mean(all_embeddings, axis=0)
515
-
516
- # Normalize
517
- combined_embedding = combined_embedding / np.linalg.norm(combined_embedding, axis=1, keepdims=True)
518
-
519
- # Index vào Qdrant
520
- metadata = {
521
- "texts": texts if texts else [],
522
- "text_count": len(texts) if texts else 0,
523
- "image_count": len(images) if images else 0,
524
- "image_filenames": [img.filename for img in images] if images else [],
525
- "id_use": id_use if id_use else None, # ID của SocialMedia hoặc EventCode
526
- "id_user": id_user if id_user else None # ID của User
527
- }
528
-
529
- result = qdrant_service.index_data(
530
- doc_id=id,
531
- embedding=combined_embedding,
532
- metadata=metadata
533
- )
534
-
535
- return IndexResponse(
536
- success=True,
537
- id=result["original_id"], # Trả về MongoDB ObjectId
538
- message=f"Đã index thành công document {result['original_id']} với {len(texts) if texts else 0} texts và {len(images) if images else 0} images (Qdrant UUID: {result['qdrant_id']})"
539
- )
540
-
541
- except HTTPException:
542
- raise
543
- except Exception as e:
544
- raise HTTPException(status_code=500, detail=f"Lỗi khi index: {str(e)}")
545
-
546
-
547
- @app.post("/search", response_model=List[SearchResponse])
548
- async def search(
549
- text: Optional[str] = Form(None),
550
- image: Optional[UploadFile] = File(None),
551
- limit: int = Form(10),
552
- score_threshold: Optional[float] = Form(None),
553
- text_weight: float = Form(0.5),
554
- image_weight: float = Form(0.5)
555
- ):
556
- """
557
- Search similar documents bằng text và/hoặc image
558
-
559
- Body:
560
- - text: Query text (tiếng Việt supported)
561
- - image: Query image (optional)
562
- - limit: Số lượng kết quả (default: 10)
563
- - score_threshold: Minimum confidence score (0-1)
564
- - text_weight: Weight cho text search (default: 0.5)
565
- - image_weight: Weight cho image search (default: 0.5)
566
-
567
- Returns:
568
- - List of results với id, confidence, và metadata
569
- """
570
- try:
571
- # Prepare query embeddings
572
- text_embedding = None
573
- image_embedding = None
574
-
575
- # Encode text query
576
- if text and text.strip():
577
- text_embedding = embedding_service.encode_text(text)
578
-
579
- # Encode image query
580
- if image:
581
- image_bytes = await image.read()
582
- pil_image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
583
- image_embedding = embedding_service.encode_image(pil_image)
584
-
585
- # Validate input
586
- if text_embedding is None and image_embedding is None:
587
- raise HTTPException(status_code=400, detail="Phải cung cấp ít nhất text hoặc image để search")
588
-
589
- # Hybrid search với Qdrant
590
- results = qdrant_service.hybrid_search(
591
- text_embedding=text_embedding,
592
- image_embedding=image_embedding,
593
- text_weight=text_weight,
594
- image_weight=image_weight,
595
- limit=limit,
596
- score_threshold=score_threshold,
597
- ef=256 # High accuracy search
598
- )
599
-
600
- # Format response
601
- return [
602
- SearchResponse(
603
- id=result["id"],
604
- confidence=result["confidence"],
605
- metadata=result["metadata"]
606
- )
607
- for result in results
608
- ]
609
-
610
- except Exception as e:
611
- raise HTTPException(status_code=500, detail=f"Lỗi khi search: {str(e)}")
612
-
613
-
614
- @app.post("/search/text", response_model=List[SearchResponse])
615
- async def search_by_text(
616
- text: str = Form(...),
617
- limit: int = Form(10),
618
- score_threshold: Optional[float] = Form(None)
619
- ):
620
- """
621
- Search chỉ bằng text (tiếng Việt)
622
-
623
- Body:
624
- - text: Query text (tiếng Việt)
625
- - limit: Số lượng kết quả
626
- - score_threshold: Minimum confidence score
627
-
628
- Returns:
629
- - List of results
630
- """
631
- try:
632
- # Encode text
633
- text_embedding = embedding_service.encode_text(text)
634
-
635
- # Search
636
- results = qdrant_service.search(
637
- query_embedding=text_embedding,
638
- limit=limit,
639
- score_threshold=score_threshold,
640
- ef=256
641
- )
642
-
643
- return [
644
- SearchResponse(
645
- id=result["id"],
646
- confidence=result["confidence"],
647
- metadata=result["metadata"]
648
- )
649
- for result in results
650
- ]
651
-
652
- except Exception as e:
653
- raise HTTPException(status_code=500, detail=f"Lỗi khi search: {str(e)}")
654
-
655
-
656
- @app.post("/search/image", response_model=List[SearchResponse])
657
- async def search_by_image(
658
- image: UploadFile = File(...),
659
- limit: int = Form(10),
660
- score_threshold: Optional[float] = Form(None)
661
- ):
662
- """
663
- Search chỉ bằng image
664
-
665
- Body:
666
- - image: Query image
667
- - limit: Số lượng kết quả
668
- - score_threshold: Minimum confidence score
669
-
670
- Returns:
671
- - List of results
672
- """
673
- try:
674
- # Encode image
675
- image_bytes = await image.read()
676
- pil_image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
677
- image_embedding = embedding_service.encode_image(pil_image)
678
-
679
- # Search
680
- results = qdrant_service.search(
681
- query_embedding=image_embedding,
682
- limit=limit,
683
- score_threshold=score_threshold,
684
- ef=256
685
- )
686
-
687
- return [
688
- SearchResponse(
689
- id=result["id"],
690
- confidence=result["confidence"],
691
- metadata=result["metadata"]
692
- )
693
- for result in results
694
  ]
695
-
696
- except Exception as e:
697
- raise HTTPException(status_code=500, detail=f"Lỗi khi search: {str(e)}")
698
-
699
-
700
- @app.delete("/delete/{doc_id}")
701
- async def delete_document(doc_id: str):
702
- """
703
- Delete document by ID (MongoDB ObjectId hoặc UUID)
704
-
705
- Args:
706
- - doc_id: Document ID to delete
707
-
708
- Returns:
709
- - Success message
710
- """
711
- try:
712
- qdrant_service.delete_by_id(doc_id)
713
- return {"success": True, "message": f"Đã xóa document {doc_id}"}
714
- except Exception as e:
715
- raise HTTPException(status_code=500, detail=f"Lỗi khi xóa: {str(e)}")
716
-
717
-
718
- @app.get("/document/{doc_id}")
719
- async def get_document(doc_id: str):
720
- """
721
- Get document by ID (MongoDB ObjectId hoặc UUID)
722
-
723
- Args:
724
- - doc_id: Document ID (MongoDB ObjectId)
725
-
726
- Returns:
727
- - Document data
728
- """
729
- try:
730
- doc = qdrant_service.get_by_id(doc_id)
731
- if doc:
732
- return {
733
- "success": True,
734
- "data": doc
735
- }
736
- raise HTTPException(status_code=404, detail=f"Không tìm thấy document {doc_id}")
737
- except HTTPException:
738
- raise
739
- except Exception as e:
740
- raise HTTPException(status_code=500, detail=f"Lỗi khi get document: {str(e)}")
741
-
742
-
743
- @app.get("/stats")
744
- async def get_stats():
745
- """
746
- Lấy thông tin thống kê collection
747
-
748
- Returns:
749
- - Collection statistics
750
- """
751
- try:
752
- info = qdrant_service.get_collection_info()
753
- return info
754
- except Exception as e:
755
- raise HTTPException(status_code=500, detail=f"Lỗi khi lấy stats: {str(e)}")
756
-
757
-
758
- # ============================================
759
- # ChatbotRAG Endpoints
760
- # ============================================
761
-
762
- @app.post("/chat", response_model=ChatResponse)
763
- async def chat(request: ChatRequest):
764
- """
765
- Chat endpoint với Advanced RAG
766
-
767
- Body:
768
- - message: User message
769
- - use_rag: Enable RAG retrieval (default: true)
770
- - top_k: Number of documents to retrieve (default: 3)
771
- - system_message: System prompt (optional)
772
- - max_tokens: Max tokens for response (default: 512)
773
- - temperature: Temperature for generation (default: 0.7)
774
- - hf_token: Hugging Face token (optional, sẽ dùng env nếu không truyền)
775
- - use_advanced_rag: Use advanced RAG pipeline (default: true)
776
- - use_query_expansion: Enable query expansion (default: true)
777
- - use_reranking: Enable reranking (default: true)
778
- - use_compression: Enable context compression (default: true)
779
- - score_threshold: Minimum relevance score (default: 0.5)
780
-
781
- Returns:
782
- - response: Generated response
783
- - context_used: Retrieved context documents
784
- - timestamp: Response timestamp
785
- - rag_stats: Statistics from RAG pipeline
786
- """
787
- try:
788
- # Retrieve context if RAG enabled
789
- context_used = []
790
- rag_stats = None
791
-
792
- if request.use_rag:
793
- if request.use_advanced_rag:
794
- # Use Advanced RAG Pipeline
795
- documents, stats = advanced_rag.hybrid_rag_pipeline(
796
- query=request.message,
797
- top_k=request.top_k,
798
- score_threshold=request.score_threshold,
799
- use_reranking=request.use_reranking,
800
- use_compression=request.use_compression,
801
- max_context_tokens=500
802
- )
803
-
804
- # Convert to dict format for compatibility
805
- context_used = [
806
- {
807
- "id": doc.id,
808
- "confidence": doc.confidence,
809
- "metadata": doc.metadata
810
- }
811
- for doc in documents
812
- ]
813
- rag_stats = stats
814
-
815
- # Format context using advanced RAG formatter
816
- context_text = advanced_rag.format_context_for_llm(documents)
817
-
818
- else:
819
- # Use basic RAG (original implementation)
820
- query_embedding = embedding_service.encode_text(request.message)
821
-
822
- results = qdrant_service.search(
823
- query_embedding=query_embedding,
824
- limit=request.top_k,
825
- score_threshold=request.score_threshold
826
- )
827
- context_used = results
828
-
829
- # Build context text (basic format)
830
- context_text = "\n\nRelevant Context:\n"
831
- for i, doc in enumerate(context_used, 1):
832
- doc_text = doc["metadata"].get("text", "")
833
- confidence = doc["confidence"]
834
- context_text += f"\n[{i}] (Confidence: {confidence:.2f})\n{doc_text}\n"
835
-
836
- # Build system message with context
837
- if request.use_rag and context_used:
838
- if request.use_advanced_rag:
839
- # Use advanced prompt builder
840
- system_message = advanced_rag.build_rag_prompt(
841
- query=request.message,
842
- context=context_text,
843
- system_message=request.system_message
844
- )
845
- else:
846
- # Basic prompt
847
- system_message = f"{request.system_message}\n{context_text}\n\nPlease use the above context to answer the user's question when relevant."
848
- else:
849
- system_message = request.system_message
850
-
851
- # Use token from request or fallback to env
852
- token = request.hf_token or hf_token
853
- # Generate response
854
- if not token:
855
- response = f"""[LLM Response Placeholder]
856
-
857
- Context retrieved: {len(context_used)} documents
858
- User question: {request.message}
859
-
860
- To enable actual LLM generation:
861
- 1. Set HUGGINGFACE_TOKEN environment variable, OR
862
- 2. Pass hf_token in request body
863
-
864
- Example:
865
- {{
866
- "message": "Your question",
867
- "hf_token": "hf_xxxxxxxxxxxxx"
868
- }}
869
  """
870
- else:
871
- try:
872
- client = InferenceClient(
873
- token=hf_token,
874
- model="openai/gpt-oss-20b"
875
- )
876
-
877
- # Build messages
878
- messages = [
879
- {"role": "system", "content": system_message},
880
- {"role": "user", "content": request.message}
881
- ]
882
-
883
- # Generate response
884
- response = ""
885
- for msg in client.chat_completion(
886
- messages,
887
- max_tokens=request.max_tokens,
888
- stream=True,
889
- temperature=request.temperature,
890
- top_p=request.top_p,
891
- ):
892
- choices = msg.choices
893
- if len(choices) and choices[0].delta.content:
894
- response += choices[0].delta.content
895
-
896
- except Exception as e:
897
- response = f"Error generating response with LLM: {str(e)}\n\nContext was retrieved successfully, but LLM generation failed."
898
-
899
- # Save to history
900
- chat_data = {
901
- "user_message": request.message,
902
- "assistant_response": response,
903
- "context_used": context_used,
904
- "timestamp": datetime.utcnow()
905
- }
906
- chat_history_collection.insert_one(chat_data)
907
-
908
- return ChatResponse(
909
- response=response,
910
- context_used=context_used,
911
- timestamp=datetime.utcnow().isoformat(),
912
- rag_stats=rag_stats
913
- )
914
-
915
- except Exception as e:
916
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
917
-
918
-
919
- @app.post("/documents", response_model=AddDocumentResponse)
920
- async def add_document(request: AddDocumentRequest):
921
- """
922
- Add document to knowledge base
923
-
924
- Body:
925
- - text: Document text
926
- - metadata: Additional metadata (optional)
927
-
928
- Returns:
929
- - success: True/False
930
- - doc_id: MongoDB document ID
931
- - message: Status message
932
- """
933
- try:
934
- # Save to MongoDB
935
- doc_data = {
936
- "text": request.text,
937
- "metadata": request.metadata or {},
938
- "created_at": datetime.utcnow()
939
- }
940
- result = documents_collection.insert_one(doc_data)
941
- doc_id = str(result.inserted_id)
942
-
943
- # Generate embedding
944
- embedding = embedding_service.encode_text(request.text)
945
-
946
- # Index to Qdrant
947
- qdrant_service.index_data(
948
- doc_id=doc_id,
949
- embedding=embedding,
950
- metadata={
951
- "text": request.text,
952
- "source": "api",
953
- **(request.metadata or {})
954
- }
955
- )
956
-
957
- return AddDocumentResponse(
958
- success=True,
959
- doc_id=doc_id,
960
- message=f"Document added successfully with ID: {doc_id}"
961
- )
962
-
963
- except Exception as e:
964
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
965
-
966
-
967
- @app.post("/rag/search", response_model=List[SearchResponse])
968
- async def rag_search(
969
- query: str = Form(...),
970
- top_k: int = Form(5),
971
- score_threshold: Optional[float] = Form(0.5)
972
- ):
973
- """
974
- Search in knowledge base
975
-
976
- Body:
977
- - query: Search query
978
- - top_k: Number of results (default: 5)
979
- - score_threshold: Minimum score (default: 0.5)
980
-
981
- Returns:
982
- - results: List of matching documents
983
- """
984
- try:
985
- # Generate query embedding
986
- query_embedding = embedding_service.encode_text(query)
987
-
988
- # Search in Qdrant
989
- results = qdrant_service.search(
990
- query_embedding=query_embedding,
991
- limit=top_k,
992
- score_threshold=score_threshold
993
- )
994
 
995
- return [
996
- SearchResponse(
997
- id=result["id"],
998
- confidence=result["confidence"],
999
- metadata=result["metadata"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
  )
1001
- for result in results
1002
- ]
1003
-
1004
- except Exception as e:
1005
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1006
-
1007
-
1008
- @app.get("/history")
1009
- async def get_history(limit: int = 10, skip: int = 0):
1010
- """
1011
- Get chat history
1012
-
1013
- Query params:
1014
- - limit: Number of messages to return (default: 10)
1015
- - skip: Number of messages to skip (default: 0)
1016
-
1017
- Returns:
1018
- - history: List of chat messages
1019
- """
1020
- try:
1021
- history = list(
1022
- chat_history_collection
1023
- .find({}, {"_id": 0})
1024
- .sort("timestamp", -1)
1025
- .skip(skip)
1026
- .limit(limit)
1027
  )
1028
-
1029
- # Convert datetime to string
1030
- for msg in history:
1031
- if "timestamp" in msg:
1032
- msg["timestamp"] = msg["timestamp"].isoformat()
 
1033
 
1034
  return {
1035
- "history": history,
1036
- "total": chat_history_collection.count_documents({})
 
1037
  }
1038
-
1039
  except Exception as e:
1040
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1041
-
1042
-
1043
- @app.delete("/documents/{doc_id}")
1044
- async def delete_document_from_kb(doc_id: str):
1045
- """
1046
- Delete document from knowledge base
1047
-
1048
- Args:
1049
- - doc_id: Document ID (MongoDB ObjectId)
1050
-
1051
- Returns:
1052
- - success: True/False
1053
- - message: Status message
1054
- """
1055
- try:
1056
- # Delete from MongoDB
1057
- result = documents_collection.delete_one({"_id": doc_id})
1058
-
1059
- # Delete from Qdrant
1060
- if result.deleted_count > 0:
1061
- qdrant_service.delete_by_id(doc_id)
1062
- return {"success": True, "message": f"Document {doc_id} deleted from knowledge base"}
1063
- else:
1064
- raise HTTPException(status_code=404, detail=f"Document {doc_id} not found")
1065
-
1066
- except HTTPException:
1067
- raise
1068
- except Exception as e:
1069
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1070
-
1071
-
1072
- @app.post("/upload-pdf", response_model=UploadPDFResponse)
1073
- async def upload_pdf(
1074
- file: UploadFile = File(...),
1075
- document_id: Optional[str] = Form(None),
1076
- title: Optional[str] = Form(None),
1077
- description: Optional[str] = Form(None),
1078
- category: Optional[str] = Form(None)
1079
- ):
1080
- """
1081
- Upload and index PDF file into knowledge base
1082
-
1083
- Body (multipart/form-data):
1084
- - file: PDF file (required)
1085
- - document_id: Custom document ID (optional, auto-generated if not provided)
1086
- - title: Document title (optional)
1087
- - description: Document description (optional)
1088
- - category: Document category (optional, e.g., "user_guide", "faq")
1089
-
1090
- Returns:
1091
- - success: True/False
1092
- - document_id: Document ID
1093
- - filename: Original filename
1094
- - chunks_indexed: Number of chunks created
1095
- - message: Status message
1096
-
1097
- Example:
1098
- ```bash
1099
- curl -X POST "http://localhost:8000/upload-pdf" \
1100
- -F "file=@user_guide.pdf" \
1101
- -F "title=Hướng dẫn sử dụng ChatbotRAG" \
1102
- -F "category=user_guide"
1103
- ```
1104
- """
1105
- try:
1106
- # Validate file type
1107
- if not file.filename.endswith('.pdf'):
1108
- raise HTTPException(status_code=400, detail="Only PDF files are allowed")
1109
-
1110
- # Generate document ID if not provided
1111
- if not document_id:
1112
- from datetime import datetime
1113
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1114
- document_id = f"pdf_{timestamp}"
1115
-
1116
- # Read PDF bytes
1117
- pdf_bytes = await file.read()
1118
-
1119
- # Prepare metadata
1120
- metadata = {}
1121
- if title:
1122
- metadata['title'] = title
1123
- if description:
1124
- metadata['description'] = description
1125
- if category:
1126
- metadata['category'] = category
1127
-
1128
- # Index PDF
1129
- result = pdf_indexer.index_pdf_bytes(
1130
- pdf_bytes=pdf_bytes,
1131
- document_id=document_id,
1132
- filename=file.filename,
1133
- document_metadata=metadata
1134
- )
1135
-
1136
- return UploadPDFResponse(
1137
- success=True,
1138
- document_id=result['document_id'],
1139
- filename=result['filename'],
1140
- chunks_indexed=result['chunks_indexed'],
1141
- message=f"PDF '{file.filename}' đã được index thành công với {result['chunks_indexed']} chunks"
1142
- )
1143
-
1144
- except HTTPException:
1145
- raise
1146
- except Exception as e:
1147
- raise HTTPException(status_code=500, detail=f"Error uploading PDF: {str(e)}")
1148
-
1149
-
1150
- @app.get("/documents/pdf")
1151
- async def list_pdf_documents():
1152
- """
1153
- List all PDF documents in knowledge base
1154
-
1155
- Returns:
1156
- - documents: List of PDF documents with metadata
1157
- """
1158
- try:
1159
- docs = list(documents_collection.find(
1160
- {"type": "pdf"},
1161
- {"_id": 0}
1162
- ))
1163
- return {"documents": docs, "total": len(docs)}
1164
- except Exception as e:
1165
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1166
-
1167
-
1168
- @app.delete("/documents/pdf/{document_id}")
1169
- async def delete_pdf_document(document_id: str):
1170
- """
1171
- Delete PDF document and all its chunks from knowledge base
1172
-
1173
- Args:
1174
- - document_id: Document ID
1175
-
1176
- Returns:
1177
- - success: True/False
1178
- - message: Status message
1179
- """
1180
- try:
1181
- # Get document info
1182
- doc = documents_collection.find_one({"document_id": document_id, "type": "pdf"})
1183
-
1184
- if not doc:
1185
- raise HTTPException(status_code=404, detail=f"PDF document {document_id} not found")
1186
-
1187
- # Delete all chunks from Qdrant
1188
- chunk_ids = doc.get('chunk_ids', [])
1189
- for chunk_id in chunk_ids:
1190
- try:
1191
- qdrant_service.delete_by_id(chunk_id)
1192
- except:
1193
- pass # Chunk might already be deleted
1194
-
1195
- # Delete from MongoDB
1196
- documents_collection.delete_one({"document_id": document_id})
1197
-
1198
- return {
1199
- "success": True,
1200
- "message": f"PDF document {document_id} and {len(chunk_ids)} chunks deleted"
1201
- }
1202
-
1203
- except HTTPException:
1204
- raise
1205
- except Exception as e:
1206
- raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1207
-
1208
-
1209
- @app.post("/upload-pdf-multimodal", response_model=UploadPDFResponse)
1210
- async def upload_pdf_multimodal(
1211
- file: UploadFile = File(...),
1212
- document_id: Optional[str] = Form(None),
1213
- title: Optional[str] = Form(None),
1214
- description: Optional[str] = Form(None),
1215
- category: Optional[str] = Form(None)
1216
- ):
1217
- """
1218
- Upload PDF with text and image URLs (for user guides with screenshots)
1219
-
1220
- This endpoint is optimized for PDFs containing:
1221
- - Text instructions
1222
- - Image URLs (http://... or https://...)
1223
- - Markdown images: ![alt](url)
1224
- - HTML images: <img src="url">
1225
-
1226
- The system will:
1227
- 1. Extract text from PDF
1228
- 2. Detect all image URLs in the text
1229
- 3. Link images to their corresponding text chunks
1230
- 4. Store image URLs in metadata
1231
- 5. Return images along with text during chat
1232
-
1233
- Body (multipart/form-data):
1234
- - file: PDF file (required)
1235
- - document_id: Custom document ID (optional, auto-generated if not provided)
1236
- - title: Document title (optional)
1237
- - description: Document description (optional)
1238
- - category: Document category (optional, e.g., "user_guide", "tutorial")
1239
-
1240
- Returns:
1241
- - success: True/False
1242
- - document_id: Document ID
1243
- - filename: Original filename
1244
- - chunks_indexed: Number of chunks created
1245
- - message: Status message (includes image count)
1246
-
1247
- Example:
1248
- ```bash
1249
- curl -X POST "http://localhost:8000/upload-pdf-multimodal" \
1250
- -F "file=@user_guide_with_images.pdf" \
1251
- -F "title=Hướng dẫn có ảnh minh họa" \
1252
- -F "category=user_guide"
1253
- ```
1254
-
1255
- Example Response:
1256
- ```json
1257
- {
1258
- "success": true,
1259
- "document_id": "pdf_20251029_150000",
1260
- "filename": "user_guide_with_images.pdf",
1261
- "chunks_indexed": 25,
1262
- "message": "PDF 'user_guide_with_images.pdf' indexed with 25 chunks and 15 images"
1263
- }
1264
- ```
1265
- """
1266
- try:
1267
- # Validate file type
1268
- if not file.filename.endswith('.pdf'):
1269
- raise HTTPException(status_code=400, detail="Only PDF files are allowed")
1270
-
1271
- # Generate document ID if not provided
1272
- if not document_id:
1273
- from datetime import datetime
1274
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1275
- document_id = f"pdf_multimodal_{timestamp}"
1276
-
1277
- # Read PDF bytes
1278
- pdf_bytes = await file.read()
1279
-
1280
- # Prepare metadata
1281
- metadata = {'type': 'multimodal'}
1282
- if title:
1283
- metadata['title'] = title
1284
- if description:
1285
- metadata['description'] = description
1286
- if category:
1287
- metadata['category'] = category
1288
-
1289
- # Index PDF with multimodal parser
1290
- result = multimodal_pdf_indexer.index_pdf_bytes(
1291
- pdf_bytes=pdf_bytes,
1292
- document_id=document_id,
1293
- filename=file.filename,
1294
- document_metadata=metadata
1295
- )
1296
-
1297
- return UploadPDFResponse(
1298
- success=True,
1299
- document_id=result['document_id'],
1300
- filename=result['filename'],
1301
- chunks_indexed=result['chunks_indexed'],
1302
- message=f"PDF '{file.filename}' indexed successfully with {result['chunks_indexed']} chunks and {result.get('images_found', 0)} images"
1303
- )
1304
-
1305
- except HTTPException:
1306
- raise
1307
  except Exception as e:
1308
- raise HTTPException(status_code=500, detail=f"Error uploading multimodal PDF: {str(e)}")
1309
-
1310
 
1311
  if __name__ == "__main__":
1312
  import uvicorn
1313
- uvicorn.run(
1314
- app,
1315
- host="0.0.0.0",
1316
- port=8000,
1317
- log_level="info"
1318
- )
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import json
3
+ import uuid
4
+ import tempfile
5
  from datetime import datetime
6
+ from typing import List, Dict, Any, Optional
7
+ from fastapi import FastAPI, HTTPException, File, UploadFile
8
+ from fastapi.middleware.cors import CORSMiddleware
9
  from pymongo import MongoClient
10
+ from pydantic import BaseModel
11
+ from google import genai
12
+ from google.genai import types
 
 
 
 
13
 
14
+ app = FastAPI(title="Nomus AI Agent Calendar (V2 - Sync Tools)")
 
 
 
 
 
15
 
16
+ # Allow CORS for Frontend integration
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
 
22
  allow_headers=["*"],
23
  )
24
 
25
+ # MongoDB Configuration (Sync - Using pymongo for stable AI tools)
26
+ MONGO_URI = os.environ.get("MONGO_URI")
27
+ client = MongoClient(MONGO_URI)
28
+ db = client.nomus_db
29
+ tasks_collection = db.tasks
30
+ chat_collection = db.chat_history
31
+
32
+ # Gemini Configuration (New SDK: google-genai)
33
+ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
34
+ model_client = genai.Client(api_key=GOOGLE_API_KEY)
35
+
36
+ MODEL_NAME = "gemini-1.5-flash-lite-latest"
37
+
38
+ # --- TOOLS (SKILLS - SYNC) ---
39
+
40
+ def list_tasks() -> List[Dict]:
41
+ """Trả về danh sách tất cả công việc hiện tại trong lịch."""
42
+ print("Executing tool: list_tasks")
43
+ tasks = list(tasks_collection.find({}, {"_id": 0}))
44
+ return tasks
45
+
46
+ def check_conflicts(start_time: str, end_time: str) -> List[Dict]:
47
+ """Kiểm tra xem việc nào trùng trong khoảng thời gian (ISO format). Trả về danh sách xung đột."""
48
+ print(f"Executing tool: check_conflicts ({start_time} to {end_time})")
49
+ conflicts = list(tasks_collection.find({
50
+ "$or": [
51
+ {"start_time": {"$lt": end_time}, "end_time": {"$gt": start_time}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  ]
53
+ }, {"_id": 0}))
54
+ return conflicts
55
+
56
+ def add_task(title: str, description: str, start_time: str, end_time: str, priority: str = "medium", tags: List[str] = [], reminder: str = "") -> str:
57
+ """Thêm một công vi���c mới vào lịch. Yêu cầu tiêu đề, mô tả, giờ bắt đầu và kết thúc (ISO)."""
58
+ print(f"Executing tool: add_task ({title})")
59
+ task_data = {
60
+ "id": str(uuid.uuid4()),
61
+ "title": title,
62
+ "description": description,
63
+ "start_time": start_time,
64
+ "end_time": end_time,
65
+ "priority": priority,
66
+ "tags": tags,
67
+ "reminder": reminder or start_time
68
+ }
69
+ tasks_collection.insert_one(task_data)
70
+ return f"Đã thêm thành công: {title}"
71
+
72
+ # --- DB HELPERS ---
73
+
74
+ def save_chat_message(role: str, content: str):
75
+ chat_collection.insert_one({
76
+ "role": role,
77
+ "content": content,
78
+ "timestamp": datetime.now().isoformat(),
79
+ "date": datetime.now().strftime("%Y-%m-%d")
80
+ })
81
+
82
+ def get_daily_chat() -> List[Dict]:
83
+ today = datetime.now().strftime("%Y-%m-%d")
84
+ chats = list(chat_collection.find({"date": today}, {"_id": 0}).sort("timestamp", 1))
85
+ return chats
86
+
87
+ # --- AGENT LOGIC ---
88
+
89
+ SYSTEM_PROMPT = """Bạn là Trợ lý Lịch Nomus (Nomus AI Agent).
90
+ Nhiệm vụ: Giúp người dùng sắp xếp cuộc sống, kiểm tra xung đột và đề xuất lịch trình tối ưu.
91
+ Ngôn ngữ: Tiếng Việt.
92
+
93
+ HƯỚNG DẪN:
94
+ 1. Khi người dùng muốn tạo lịch, hãy liệt công việc (list_tasks) và kiểm tra xung đột (check_conflicts) trước.
95
+ 2. Sau khi sắp xếp xong, hãy gọi add_task để lưu vào DB.
96
+ 3. Luôn phản hồi lịch sự, ngắn gọn và xác nhận các việc đã làm.
97
+ 4. Trả về kết quả cuối cùng theo định dạng hội thoại.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ class ScheduleRequest(BaseModel):
101
+ text: str
102
+ current_time: Optional[str] = None
103
+
104
+ @app.post("/schedule")
105
+ async def handle_agent_request(req: ScheduleRequest):
106
+ if not GOOGLE_API_KEY:
107
+ raise HTTPException(status_code=500, detail="GOOGLE_API_KEY not set.")
108
+
109
+ curr_time = req.current_time or datetime.now().isoformat()
110
+ save_chat_message("user", req.text)
111
+
112
+ try:
113
+ # Using NEW google-genai SDK
114
+ response = model_client.models.generate_content(
115
+ model=MODEL_NAME,
116
+ contents=req.text,
117
+ config=types.GenerateContentConfig(
118
+ system_instruction=SYSTEM_PROMPT + f"\nThời gian hiện tại: {curr_time}",
119
+ tools=[list_tasks, check_conflicts, add_task],
120
+ automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=False)
121
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
123
+
124
+ assistant_msg = response.text
125
+ save_chat_message("assistant", assistant_msg)
126
+
127
+ # Fetch current tasks for FE sync
128
+ all_tasks = list(tasks_collection.find({}, {"_id": 0}))
129
 
130
  return {
131
+ "message": assistant_msg,
132
+ "tasks": all_tasks,
133
+ "suggestions": []
134
  }
 
135
  except Exception as e:
136
+ print(f"Gemini Error: {e}")
137
+ raise HTTPException(status_code=500, detail=str(e))
138
+
139
+ @app.get("/chat")
140
+ async def get_chat():
141
+ return {"history": get_daily_chat()}
142
+
143
+ @app.get("/tasks")
144
+ async def get_tasks():
145
+ tasks = list(tasks_collection.find({}, {"_id": 0}))
146
+ return {"tasks": tasks}
147
+
148
+ @app.get("/health")
149
+ async def health_check():
150
+ return {"status": "ok", "message": f"Nomus AI (GenAI SDK) is ready using {MODEL_NAME}"}
151
+
152
+ class ManualTaskRequest(BaseModel):
153
+ title: str
154
+ description: str
155
+ start_time: str
156
+ end_time: str
157
+ priority: str = "medium"
158
+ tags: List[str] = []
159
+ reminder: Optional[str] = None
160
+
161
+ @app.post("/tasks")
162
+ async def create_manual_task(task: ManualTaskRequest):
163
+ task_data = task.dict()
164
+ task_data["id"] = str(uuid.uuid4())
165
+ if not task_data["reminder"]:
166
+ task_data["reminder"] = task_data["start_time"]
167
+ tasks_collection.insert_one(task_data)
168
+ return {"message": "Task created", "id": task_data["id"]}
169
+
170
+ @app.patch("/tasks/{task_id}")
171
+ async def update_task(task_id: str, update: Dict):
172
+ if not update:
173
+ return {"message": "No data"}
174
+ result = tasks_collection.update_one({"id": task_id}, {"$set": update})
175
+ if result.modified_count == 0:
176
+ raise HTTPException(status_code=404, detail="Task not found")
177
+ return {"message": "Task updated"}
178
+
179
+ @app.delete("/tasks/{task_id}")
180
+ async def delete_task(task_id: str):
181
+ result = tasks_collection.delete_one({"id": task_id})
182
+ return {"message": "Task deleted"}
183
+
184
+ @app.post("/transcribe")
185
+ async def transcribe_audio(file: UploadFile = File(...)):
186
+ if not GOOGLE_API_KEY:
187
+ raise HTTPException(status_code=500, detail="GOOGLE_API_KEY not set.")
188
+
189
+ try:
190
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as tmp:
191
+ tmp.write(await file.read())
192
+ tmp_path = tmp.name
193
+
194
+ # New SDK upload and generate content
195
+ with open(tmp_path, "rb") as audio_file:
196
+ response = model_client.models.generate_content(
197
+ model=MODEL_NAME,
198
+ contents=[
199
+ "Chuyển đoạn âm thanh này thành văn bản tiếng Việt chính xác nhất. Chỉ trả về văn bản.",
200
+ types.Part.from_bytes(data=audio_file.read(), mime_type="audio/webm")
201
+ ]
202
+ )
203
+
204
+ os.remove(tmp_path)
205
+ return {"text": response.text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  except Exception as e:
207
+ raise HTTPException(status_code=500, detail=str(e))
 
208
 
209
  if __name__ == "__main__":
210
  import uvicorn
211
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)