minhvtt commited on
Commit
91fe002
·
verified ·
1 Parent(s): 876300e

Upload 14 files

Browse files
Files changed (5) hide show
  1. chat_endpoint.py +261 -0
  2. chat_routes_integration.py +116 -0
  3. conversation_service.py +184 -0
  4. main.py +26 -1
  5. tools_service.py +164 -0
chat_endpoint.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chat endpoint với Multi-turn Conversation + Function Calling
3
+ """
4
+ from fastapi import HTTPException
5
+ from datetime import datetime
6
+ from huggingface_hub import InferenceClient
7
+ from typing import Dict, List
8
+ import json
9
+
10
+
11
+ async def chat_endpoint(
12
+ request, # ChatRequest
13
+ conversation_service,
14
+ tools_service,
15
+ advanced_rag,
16
+ embedding_service,
17
+ qdrant_service,
18
+ chat_history_collection,
19
+ hf_token
20
+ ):
21
+ """
22
+ Multi-turn conversational chatbot với RAG + Function Calling
23
+
24
+ Flow:
25
+ 1. Session management - create hoặc load existing session
26
+ 2. RAG search - retrieve context nếu enabled
27
+ 3. Build messages với conversation history + tools prompt
28
+ 4. LLM generation - có thể trigger tool calls
29
+ 5. Execute tools nếu cần
30
+ 6. Final LLM response với tool results
31
+ 7. Save to conversation history
32
+ """
33
+ try:
34
+ # ===== 1. SESSION MANAGEMENT =====
35
+ session_id = request.session_id
36
+ if not session_id:
37
+ # Create new session (server-side)
38
+ session_id = conversation_service.create_session(
39
+ metadata={"user_agent": "api", "created_via": "chat_endpoint"}
40
+ )
41
+ print(f"Created new session: {session_id}")
42
+ else:
43
+ # Validate existing session
44
+ if not conversation_service.session_exists(session_id):
45
+ raise HTTPException(
46
+ status_code=404,
47
+ detail=f"Session {session_id} not found. It may have expired."
48
+ )
49
+
50
+ # Load conversation history
51
+ conversation_history = conversation_service.get_conversation_history(session_id)
52
+
53
+ # ===== 2. RAG SEARCH =====
54
+ context_used = []
55
+ rag_stats = None
56
+ context_text = ""
57
+
58
+ if request.use_rag:
59
+ if request.use_advanced_rag:
60
+ # Use Advanced RAG Pipeline
61
+ hf_client = None
62
+ if request.hf_token or hf_token:
63
+ hf_client = InferenceClient(token=request.hf_token or hf_token)
64
+
65
+ documents, stats = advanced_rag.hybrid_rag_pipeline(
66
+ query=request.message,
67
+ top_k=request.top_k,
68
+ score_threshold=request.score_threshold,
69
+ use_reranking=request.use_reranking,
70
+ use_compression=request.use_compression,
71
+ use_query_expansion=request.use_query_expansion,
72
+ max_context_tokens=500,
73
+ hf_client=hf_client
74
+ )
75
+
76
+ # Convert to dict format
77
+ context_used = [
78
+ {
79
+ "id": doc.id,
80
+ "confidence": doc.confidence,
81
+ "metadata": doc.metadata
82
+ }
83
+ for doc in documents
84
+ ]
85
+ rag_stats = stats
86
+
87
+ # Format context
88
+ context_text = advanced_rag.format_context_for_llm(documents)
89
+ else:
90
+ # Basic RAG
91
+ query_embedding = embedding_service.encode_text(request.message)
92
+ results = qdrant_service.search(
93
+ query_embedding=query_embedding,
94
+ limit=request.top_k,
95
+ score_threshold=request.score_threshold
96
+ )
97
+ context_used = results
98
+
99
+ context_text = "\n\nRelevant Context:\n"
100
+ for i, doc in enumerate(context_used, 1):
101
+ doc_text = doc["metadata"].get("text", "")
102
+ if not doc_text:
103
+ doc_text = " ".join(doc["metadata"].get("texts", []))
104
+ confidence = doc["confidence"]
105
+ context_text += f"\n[{i}] (Confidence: {confidence:.2f})\n{doc_text}\n"
106
+
107
+ # ===== 3. BUILD MESSAGES với TOOLS PROMPT =====
108
+ messages = []
109
+
110
+ # System message với RAG context + Tools instruction
111
+ if request.use_rag and context_used:
112
+ if request.use_advanced_rag:
113
+ base_prompt = advanced_rag.build_rag_prompt(
114
+ query="", # Query sẽ đi trong user message
115
+ context=context_text,
116
+ system_message=request.system_message
117
+ )
118
+ else:
119
+ base_prompt = f"""{request.system_message}
120
+
121
+ {context_text}
122
+
123
+ HƯỚNG DẪN:
124
+ - Sử dụng thông tin từ context trên để trả lời câu hỏi.
125
+ - Trả lời tự nhiên, thân thiện, không copy nguyên văn.
126
+ - Nếu tìm thấy sự kiện, hãy tóm tắt các thông tin quan trọng nhất.
127
+ """
128
+ else:
129
+ base_prompt = request.system_message
130
+
131
+ # Add tools instruction nếu enabled
132
+ if request.enable_tools:
133
+ tools_prompt = tools_service.get_tools_prompt()
134
+ system_message_with_tools = f"{base_prompt}\n\n{tools_prompt}"
135
+ else:
136
+ system_message_with_tools = base_prompt
137
+
138
+ # Bắt đầu messages với system
139
+ messages.append({"role": "system", "content": system_message_with_tools})
140
+
141
+ # Add conversation history (past turns)
142
+ messages.extend(conversation_history)
143
+
144
+ # Add current user message
145
+ messages.append({"role": "user", "content": request.message})
146
+
147
+ # ===== 4. LLM GENERATION =====
148
+ token = request.hf_token or hf_token
149
+ tool_calls_made = []
150
+
151
+ if not token:
152
+ response = f"""[LLM Response Placeholder]
153
+
154
+ Context retrieved: {len(context_used)} documents
155
+ User question: {request.message}
156
+ Session: {session_id}
157
+
158
+ To enable actual LLM generation:
159
+ 1. Set HUGGINGFACE_TOKEN environment variable, OR
160
+ 2. Pass hf_token in request body
161
+ """
162
+ else:
163
+ try:
164
+ client = InferenceClient(
165
+ token=token,
166
+ model="openai/gpt-oss-20b" # Hoặc model khác
167
+ )
168
+
169
+ # First LLM call
170
+ first_response = ""
171
+ for msg in client.chat_completion(
172
+ messages,
173
+ max_tokens=request.max_tokens,
174
+ stream=True,
175
+ temperature=request.temperature,
176
+ top_p=request.top_p,
177
+ ):
178
+ choices = msg.choices
179
+ if len(choices) and choices[0].delta.content:
180
+ first_response += choices[0].delta.content
181
+
182
+ # ===== 5. PARSE & EXECUTE TOOLS =====
183
+ if request.enable_tools:
184
+ tool_result = await tools_service.parse_and_execute(first_response)
185
+
186
+ if tool_result:
187
+ # Tool was called!
188
+ tool_calls_made.append(tool_result)
189
+
190
+ # Add tool result to messages
191
+ messages.append({"role": "assistant", "content": first_response})
192
+ messages.append({
193
+ "role": "user",
194
+ "content": f"TOOL RESULT:\n{json.dumps(tool_result['result'], ensure_ascii=False, indent=2)}\n\nHãy dùng thông tin này để trả lời câu hỏi của user."
195
+ })
196
+
197
+ # Second LLM call với tool results
198
+ final_response = ""
199
+ for msg in client.chat_completion(
200
+ messages,
201
+ max_tokens=request.max_tokens,
202
+ stream=True,
203
+ temperature=request.temperature,
204
+ top_p=request.top_p,
205
+ ):
206
+ choices = msg.choices
207
+ if len(choices) and choices[0].delta.content:
208
+ final_response += choices[0].delta.content
209
+
210
+ response = final_response
211
+ else:
212
+ # No tool call, use first response
213
+ response = first_response
214
+ else:
215
+ response = first_response
216
+
217
+ except Exception as e:
218
+ response = f"Error generating response with LLM: {str(e)}\n\nContext was retrieved successfully, but LLM generation failed."
219
+
220
+ # ===== 6. SAVE TO CONVERSATION HISTORY =====
221
+ conversation_service.add_message(
222
+ session_id,
223
+ "user",
224
+ request.message
225
+ )
226
+ conversation_service.add_message(
227
+ session_id,
228
+ "assistant",
229
+ response,
230
+ metadata={
231
+ "rag_stats": rag_stats,
232
+ "tool_calls": tool_calls_made,
233
+ "context_count": len(context_used)
234
+ }
235
+ )
236
+
237
+ # Also save to legacy chat_history collection
238
+ chat_data = {
239
+ "session_id": session_id,
240
+ "user_message": request.message,
241
+ "assistant_response": response,
242
+ "context_used": context_used,
243
+ "tool_calls": tool_calls_made,
244
+ "timestamp": datetime.utcnow()
245
+ }
246
+ chat_history_collection.insert_one(chat_data)
247
+
248
+ # ===== 7. RETURN RESPONSE =====
249
+ return {
250
+ "response": response,
251
+ "context_used": context_used,
252
+ "timestamp": datetime.utcnow().isoformat(),
253
+ "rag_stats": rag_stats,
254
+ "session_id": session_id,
255
+ "tool_calls": tool_calls_made if tool_calls_made else None
256
+ }
257
+
258
+ except HTTPException:
259
+ raise
260
+ except Exception as e:
261
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
chat_routes_integration.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Integration code để thêm vào main.py
2
+
3
+ # ADD THIS IMPORT near line 20:
4
+ from chat_endpoint import chat_endpoint
5
+
6
+ # ADD THESE ROUTES before "if __name__ == '__main__':" (around line 1000):
7
+
8
+ @app.post("/chat", response_model=ChatResponse)
9
+ async def chat(request: ChatRequest):
10
+ """
11
+ Multi-turn conversational chatbot với RAG + Function Calling
12
+
13
+ Features:
14
+ - Server-side session management
15
+ - Conversation history tracking
16
+ - RAG context retrieval
17
+ - Function calling (API integration)
18
+
19
+ Example:
20
+ ```
21
+ # First message - creates session
22
+ POST /chat
23
+ {
24
+ "message": "Tìm sự kiện hòa nhạc",
25
+ "use_rag": true
26
+ }
27
+ Response: { "session_id": "abc-123", ... }
28
+
29
+ # Follow-up message - uses session
30
+ POST /chat
31
+ {
32
+ "message": "Ngày tổ chức chính xác là khi nào?",
33
+ "session_id": "abc-123"
34
+ }
35
+ # Bot understands context và calls API nếu cần
36
+ ```
37
+ """
38
+ return await chat_endpoint(
39
+ request=request,
40
+ conversation_service=conversation_service,
41
+ tools_service=tools_service,
42
+ advanced_rag=advanced_rag,
43
+ embedding_service=embedding_service,
44
+ qdrant_service=qdrant_service,
45
+ chat_history_collection=chat_history_collection,
46
+ hf_token=hf_token
47
+ )
48
+
49
+
50
+ @app.post("/chat/clear-session")
51
+ async def clear_chat_session(session_id: str):
52
+ """
53
+ Clear conversation history cho một session
54
+
55
+ Args:
56
+ session_id: Session identifier to clear
57
+
58
+ Returns:
59
+ Success message
60
+
61
+ Example:
62
+ ```
63
+ POST /chat/clear-session?session_id=abc-123
64
+ ```
65
+ """
66
+ success = conversation_service.clear_session(session_id)
67
+
68
+ if success:
69
+ return {
70
+ "success": True,
71
+ "message": f"Session {session_id} cleared successfully"
72
+ }
73
+ else:
74
+ raise HTTPException(
75
+ status_code=404,
76
+ detail=f"Session {session_id} not found or already cleared"
77
+ )
78
+
79
+
80
+ @app.get("/chat/session/{session_id}")
81
+ async def get_session_info(session_id: str):
82
+ """
83
+ Get thông tin về một conversation session
84
+
85
+ Args:
86
+ session_id: Session identifier
87
+
88
+ Returns:
89
+ Session metadata và message count
90
+
91
+ Example:
92
+ ```
93
+ GET /chat/session/abc-123
94
+ ```
95
+ """
96
+ session = conversation_service.get_session_info(session_id)
97
+
98
+ if not session:
99
+ raise HTTPException(
100
+ status_code=404,
101
+ detail=f"Session {session_id} not found"
102
+ )
103
+
104
+ # Get message count
105
+ history = conversation_service.get_conversation_history(
106
+ session_id,
107
+ include_metadata=True
108
+ )
109
+
110
+ return {
111
+ "session_id": session["session_id"],
112
+ "created_at": session["created_at"],
113
+ "updated_at": session["updated_at"],
114
+ "message_count": len(history),
115
+ "metadata": session.get("metadata", {})
116
+ }
conversation_service.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation Service for Multi-turn Chat
3
+ Server-side session management
4
+ """
5
+ from typing import List, Dict, Optional
6
+ from datetime import datetime
7
+ from pymongo.collection import Collection
8
+ import uuid
9
+
10
+
11
+ class ConversationService:
12
+ """
13
+ Manages multi-turn conversation history với server-side session
14
+ """
15
+
16
+ def __init__(self, mongo_collection: Collection, max_history: int = 10):
17
+ """
18
+ Args:
19
+ mongo_collection: MongoDB collection for storing conversations
20
+ max_history: Maximum số messages giữ lại (sliding window)
21
+ """
22
+ self.collection = mongo_collection
23
+ self.max_history = max_history
24
+
25
+ # Create indexes
26
+ self._ensure_indexes()
27
+
28
+ def _ensure_indexes(self):
29
+ """Create necessary indexes"""
30
+ try:
31
+ self.collection.create_index("session_id", unique=True)
32
+ # Auto-delete sessions sau 7 ngày không dùng
33
+ self.collection.create_index(
34
+ "updated_at",
35
+ expireAfterSeconds=604800 # 7 days
36
+ )
37
+ print("✓ Conversation indexes created")
38
+ except Exception as e:
39
+ print(f"Conversation indexes already exist or error: {e}")
40
+
41
+ def create_session(self, metadata: Optional[Dict] = None) -> str:
42
+ """
43
+ Create new conversation session
44
+
45
+ Returns:
46
+ session_id (UUID string)
47
+ """
48
+ session_id = str(uuid.uuid4())
49
+
50
+ self.collection.insert_one({
51
+ "session_id": session_id,
52
+ "messages": [],
53
+ "metadata": metadata or {},
54
+ "created_at": datetime.utcnow(),
55
+ "updated_at": datetime.utcnow()
56
+ })
57
+
58
+ return session_id
59
+
60
+ def add_message(
61
+ self,
62
+ session_id: str,
63
+ role: str,
64
+ content: str,
65
+ metadata: Optional[Dict] = None
66
+ ):
67
+ """
68
+ Add message to conversation history
69
+
70
+ Args:
71
+ session_id: Session identifier
72
+ role: "user" or "assistant"
73
+ content: Message text
74
+ metadata: Additional info (rag_stats, tool_calls, etc.)
75
+ """
76
+ message = {
77
+ "role": role,
78
+ "content": content,
79
+ "timestamp": datetime.utcnow().isoformat(),
80
+ "metadata": metadata or {}
81
+ }
82
+
83
+ # Upsert: tạo session nếu chưa tồn tại
84
+ self.collection.update_one(
85
+ {"session_id": session_id},
86
+ {
87
+ "$push": {
88
+ "messages": {
89
+ "$each": [message],
90
+ "$slice": -self.max_history # Keep only last N messages
91
+ }
92
+ },
93
+ "$set": {"updated_at": datetime.utcnow()}
94
+ },
95
+ upsert=True
96
+ )
97
+
98
+ def get_conversation_history(
99
+ self,
100
+ session_id: str,
101
+ limit: Optional[int] = None,
102
+ include_metadata: bool = False
103
+ ) -> List[Dict]:
104
+ """
105
+ Get conversation messages for LLM context
106
+
107
+ Args:
108
+ session_id: Session identifier
109
+ limit: Override max_history với số lượng tùy chỉnh
110
+ include_metadata: Include metadata trong response
111
+
112
+ Returns:
113
+ List of messages in format: [{"role": "user", "content": "..."}, ...]
114
+ """
115
+ session = self.collection.find_one({"session_id": session_id})
116
+
117
+ if not session:
118
+ return []
119
+
120
+ messages = session.get("messages", [])
121
+
122
+ # Limit to recent messages
123
+ if limit:
124
+ messages = messages[-limit:]
125
+ else:
126
+ messages = messages[-self.max_history:]
127
+
128
+ # Format for LLM
129
+ if include_metadata:
130
+ return messages
131
+ else:
132
+ return [
133
+ {
134
+ "role": msg["role"],
135
+ "content": msg["content"]
136
+ }
137
+ for msg in messages
138
+ ]
139
+
140
+ def get_session_info(self, session_id: str) -> Optional[Dict]:
141
+ """
142
+ Get session metadata
143
+
144
+ Returns:
145
+ Session info hoặc None nếu không tồn tại
146
+ """
147
+ session = self.collection.find_one(
148
+ {"session_id": session_id},
149
+ {"_id": 0, "session_id": 1, "created_at": 1, "updated_at": 1, "metadata": 1}
150
+ )
151
+ return session
152
+
153
+ def clear_session(self, session_id: str) -> bool:
154
+ """
155
+ Clear conversation history for session
156
+
157
+ Returns:
158
+ True nếu xóa thành công, False nếu session không tồn tại
159
+ """
160
+ result = self.collection.delete_one({"session_id": session_id})
161
+ return result.deleted_count > 0
162
+
163
+ def session_exists(self, session_id: str) -> bool:
164
+ """
165
+ Check if session exists
166
+ """
167
+ return self.collection.count_documents({"session_id": session_id}) > 0
168
+
169
+ def get_last_user_message(self, session_id: str) -> Optional[str]:
170
+ """
171
+ Get the last user message in conversation
172
+ Useful for context extraction
173
+ """
174
+ session = self.collection.find_one({"session_id": session_id})
175
+ if not session:
176
+ return None
177
+
178
+ messages = session.get("messages", [])
179
+ # Tìm message cuối cùng từ user
180
+ for msg in reversed(messages):
181
+ if msg["role"] == "user":
182
+ return msg["content"]
183
+
184
+ return None
main.py CHANGED
@@ -17,6 +17,8 @@ from advanced_rag import AdvancedRAG
17
  from cag_service import CAGService
18
  from pdf_parser import PDFIndexer
19
  from multimodal_pdf_parser import MultimodalPDFIndexer
 
 
20
 
21
  # Initialize FastAPI app
22
  app = FastAPI(
@@ -96,6 +98,15 @@ multimodal_pdf_indexer = MultimodalPDFIndexer(
96
  )
97
  print("✓ Multimodal PDF Indexer initialized")
98
 
 
 
 
 
 
 
 
 
 
99
  print("✓ Services initialized successfully")
100
 
101
 
@@ -123,6 +134,7 @@ class IndexResponse(BaseModel):
123
  # Pydantic models for ChatbotRAG
124
  class ChatRequest(BaseModel):
125
  message: str
 
126
  use_rag: bool = True
127
  top_k: int = 3
128
  system_message: Optional[str] = """Bạn là trợ lý AI chuyên biệt cho hệ thống quản lý sự kiện và bán vé.
@@ -143,6 +155,8 @@ Quy tắc tuyệt đối:
143
  use_reranking: bool = False # Disabled - Cross-Encoder not good for Vietnamese
144
  use_compression: bool = True
145
  score_threshold: float = 0.5
 
 
146
 
147
 
148
  class ChatResponse(BaseModel):
@@ -150,6 +164,8 @@ class ChatResponse(BaseModel):
150
  context_used: List[Dict]
151
  timestamp: str
152
  rag_stats: Optional[Dict] = None # Stats from advanced RAG pipeline
 
 
153
 
154
 
155
  class AddDocumentRequest(BaseModel):
@@ -748,7 +764,16 @@ async def chat(request: ChatRequest):
748
  )
749
  else:
750
  # Basic prompt
751
- system_message = f"{request.system_message}\n{context_text}\n\nPlease use the above context to answer the user's question when relevant."
 
 
 
 
 
 
 
 
 
752
  else:
753
  system_message = request.system_message
754
 
 
17
  from cag_service import CAGService
18
  from pdf_parser import PDFIndexer
19
  from multimodal_pdf_parser import MultimodalPDFIndexer
20
+ from conversation_service import ConversationService
21
+ from tools_service import ToolsService
22
 
23
  # Initialize FastAPI app
24
  app = FastAPI(
 
98
  )
99
  print("✓ Multimodal PDF Indexer initialized")
100
 
101
+ # Initialize Conversation Service
102
+ conversations_collection = db["conversations"]
103
+ conversation_service = ConversationService(conversations_collection, max_history=10)
104
+ print("✓ Conversation Service initialized")
105
+
106
+ # Initialize Tools Service
107
+ tools_service = ToolsService(base_url="https://www.festavenue.site")
108
+ print("✓ Tools Service initialized (Function Calling enabled)")
109
+
110
  print("✓ Services initialized successfully")
111
 
112
 
 
134
  # Pydantic models for ChatbotRAG
135
  class ChatRequest(BaseModel):
136
  message: str
137
+ session_id: Optional[str] = None # Multi-turn conversation
138
  use_rag: bool = True
139
  top_k: int = 3
140
  system_message: Optional[str] = """Bạn là trợ lý AI chuyên biệt cho hệ thống quản lý sự kiện và bán vé.
 
155
  use_reranking: bool = False # Disabled - Cross-Encoder not good for Vietnamese
156
  use_compression: bool = True
157
  score_threshold: float = 0.5
158
+ # Function calling
159
+ enable_tools: bool = True # Enable API tool calling
160
 
161
 
162
  class ChatResponse(BaseModel):
 
164
  context_used: List[Dict]
165
  timestamp: str
166
  rag_stats: Optional[Dict] = None # Stats from advanced RAG pipeline
167
+ session_id: str # NEW: Session identifier for multi-turn
168
+ tool_calls: Optional[List[Dict]] = None # NEW: Track API calls made
169
 
170
 
171
  class AddDocumentRequest(BaseModel):
 
764
  )
765
  else:
766
  # Basic prompt
767
+ # Basic prompt with better instructions
768
+ system_message = f"""{request.system_message}
769
+
770
+ {context_text}
771
+
772
+ HƯỚNG DẪN:
773
+ - Sử dụng thông tin từ context trên để trả lời câu hỏi.
774
+ - Trả lời tự nhiên, thân thiện, không copy nguyên văn.
775
+ - Nếu tìm thấy sự kiện, hãy tóm tắt các thông tin quan trọng nhất.
776
+ """
777
  else:
778
  system_message = request.system_message
779
 
tools_service.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tools Service for LLM Function Calling
3
+ HuggingFace-compatible với prompt engineering
4
+ """
5
+ import httpx
6
+ from typing import List, Dict, Any, Optional
7
+ import json
8
+ import asyncio
9
+
10
+
11
+ class ToolsService:
12
+ """
13
+ Manages external API tools that LLM can call via prompt engineering
14
+ """
15
+
16
+ def __init__(self, base_url: str = "https://www.festavenue.site"):
17
+ self.base_url = base_url
18
+ self.client = httpx.AsyncClient(timeout=10.0)
19
+
20
+ def get_tools_prompt(self) -> str:
21
+ """
22
+ Return prompt instruction for HuggingFace LLM về available tools
23
+ """
24
+ return """
25
+ AVAILABLE TOOLS:
26
+ Bạn có thể sử dụng các công cụ sau để lấy thông tin chi tiết:
27
+
28
+ 1. get_event_details(event_code: str)
29
+ - Mô tả: Lấy thông tin đầy đủ về một sự kiện từ hệ thống
30
+ - Khi nào dùng: Khi user hỏi về ngày giờ chính xác, địa điểm cụ thể, thông tin liên hệ, hoặc chi tiết khác về một sự kiện
31
+ - Tham số: event_code (mã sự kiện, ví dụ: "Y-CONCERT", "EVT001")
32
+ - Ví dụ: User hỏi "Ngày tổ chức Y-CONCERT là khi nào?" → Dùng get_event_details("Y-CONCERT")
33
+
34
+ CÚ PHÁP GỌI TOOL:
35
+ Khi bạn cần gọi tool, hãy trả lời CHÍNH XÁC theo format JSON này:
36
+ ```json
37
+ {
38
+ "tool_call": true,
39
+ "function_name": "get_event_details",
40
+ "arguments": {
41
+ "event_code": "Y-CONCERT"
42
+ },
43
+ "reason": "Cần lấy thông tin chính xác về ngày giờ tổ chức"
44
+ }
45
+ ```
46
+
47
+ QUAN TRỌNG:
48
+ - CHỈ trả JSON khi BẮT BUỘC cần gọi tool
49
+ - Nếu có thể trả lời từ context sẵn có, đừng gọi tool
50
+ - Sau khi nhận kết quả từ tool, hãy trả lời user bằng ngôn ngữ tự nhiên
51
+ """
52
+
53
+ async def parse_and_execute(self, llm_response: str) -> Optional[Dict[str, Any]]:
54
+ """
55
+ Parse LLM response và execute tool nếu có
56
+
57
+ Returns:
58
+ None nếu không có tool call
59
+ Dict với tool result nếu có tool call
60
+ """
61
+ # Try to extract JSON from response
62
+ try:
63
+ # Tìm JSON block trong response
64
+ if "```json" in llm_response:
65
+ json_start = llm_response.find("```json") + 7
66
+ json_end = llm_response.find("```", json_start)
67
+ json_str = llm_response[json_start:json_end].strip()
68
+ elif "{" in llm_response and "}" in llm_response:
69
+ # Fallback: tìm JSON object đầu tiên
70
+ json_start = llm_response.find("{")
71
+ json_end = llm_response.rfind("}") + 1
72
+ json_str = llm_response[json_start:json_end]
73
+ else:
74
+ return None
75
+
76
+ tool_call = json.loads(json_str)
77
+
78
+ # Validate tool call structure
79
+ if not tool_call.get("tool_call"):
80
+ return None
81
+
82
+ function_name = tool_call.get("function_name")
83
+ arguments = tool_call.get("arguments", {})
84
+
85
+ # Execute tool
86
+ if function_name == "get_event_details":
87
+ result = await self._get_event_details(arguments.get("event_code"))
88
+ return {
89
+ "function": function_name,
90
+ "arguments": arguments,
91
+ "result": result
92
+ }
93
+ else:
94
+ return {
95
+ "function": function_name,
96
+ "arguments": arguments,
97
+ "result": {"success": False, "error": f"Unknown function: {function_name}"}
98
+ }
99
+
100
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
101
+ # Không phải tool call, response bình thường
102
+ return None
103
+
104
+ async def _get_event_details(self, event_code: str) -> Dict[str, Any]:
105
+ """
106
+ Call getEventByEventCode API
107
+ """
108
+ try:
109
+ response = await self.client.get(
110
+ f"{self.base_url}/event/get-event-by-event-code",
111
+ params={"eventCode": event_code}
112
+ )
113
+ response.raise_for_status()
114
+ data = response.json()
115
+
116
+ # Extract relevant fields
117
+ event = data.get("data", {})
118
+
119
+ if not event:
120
+ return {
121
+ "success": False,
122
+ "error": "Event not found",
123
+ "message": f"Không tìm thấy sự kiện với mã {event_code}"
124
+ }
125
+
126
+ return {
127
+ "success": True,
128
+ "event_code": event.get("eventCode"),
129
+ "event_name": event.get("eventName"),
130
+ "description": event.get("description"),
131
+ "short_description": event.get("shortDescription"),
132
+ "start_time": event.get("startTimeEventTime"),
133
+ "end_time": event.get("endTimeEventTime"),
134
+ "start_sale": event.get("startTicketSaleTime"),
135
+ "end_sale": event.get("endTicketSaleTime"),
136
+ "location": {
137
+ "address": event.get("location", {}).get("address"),
138
+ "city": event.get("location", {}).get("city"),
139
+ },
140
+ "contact": {
141
+ "email": event.get("publicContactEmail"),
142
+ "phone": event.get("publicContactPhone"),
143
+ "website": event.get("website")
144
+ },
145
+ "capacity": event.get("capacity"),
146
+ "hashtags": event.get("hashtags", [])
147
+ }
148
+
149
+ except httpx.HTTPStatusError as e:
150
+ return {
151
+ "success": False,
152
+ "error": f"HTTP {e.response.status_code}",
153
+ "message": f"API trả về lỗi khi truy vấn sự kiện {event_code}"
154
+ }
155
+ except Exception as e:
156
+ return {
157
+ "success": False,
158
+ "error": str(e),
159
+ "message": "Không thể kết nối đến API để lấy thông tin sự kiện"
160
+ }
161
+
162
+ async def close(self):
163
+ """Close HTTP client"""
164
+ await self.client.aclose()