minhvtt commited on
Commit
7caa85c
·
verified ·
1 Parent(s): 70413d7

Upload 16 files

Browse files
agent_chat_stream.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Chat Streaming Endpoint
3
+ SSE-based real-time streaming for Sales & Feedback agents
4
+ """
5
+ from typing import AsyncGenerator
6
+ from stream_utils import format_sse, EVENT_STATUS, EVENT_TOKEN, EVENT_DONE, EVENT_ERROR, EVENT_METADATA
7
+ from datetime import datetime
8
+
9
+
10
+ async def agent_chat_stream(
11
+ request,
12
+ agent_service,
13
+ conversation_service
14
+ ) -> AsyncGenerator[str, None]:
15
+ """
16
+ Stream agent responses in real-time (SSE format)
17
+
18
+ Args:
19
+ request: ChatRequest with message, session_id, mode, user_id
20
+ agent_service: AgentService instance
21
+ conversation_service: ConversationService instance
22
+
23
+ Yields SSE events:
24
+ - status: Processing updates
25
+ - token: Text chunks
26
+ - metadata: Session info
27
+ - done: Completion signal
28
+ - error: Error messages
29
+ """
30
+ try:
31
+ # === SESSION MANAGEMENT ===
32
+ session_id = request.session_id
33
+ if not session_id:
34
+ session_id = conversation_service.create_session(
35
+ metadata={"user_agent": "api", "created_via": "agent_stream"},
36
+ user_id=request.user_id
37
+ )
38
+ yield format_sse(EVENT_METADATA, {"session_id": session_id})
39
+
40
+ # Get conversation history
41
+ history = conversation_service.get_history(session_id)
42
+
43
+ # Convert to messages format
44
+ messages = []
45
+ for h in history:
46
+ messages.append({"role": h["role"], "content": h["content"]})
47
+
48
+ # Determine mode
49
+ mode = getattr(request, 'mode', 'sales') # Default to sales
50
+
51
+ # === STATUS UPDATE ===
52
+ if mode == 'feedback':
53
+ yield format_sse(EVENT_STATUS, "Đang kiểm tra lịch sử sự kiện của bạn...")
54
+ else:
55
+ yield format_sse(EVENT_STATUS, "Đang tư vấn...")
56
+
57
+ # === CALL AGENT ===
58
+ result = await agent_service.chat(
59
+ user_message=request.message,
60
+ conversation_history=messages,
61
+ mode=mode,
62
+ user_id=request.user_id
63
+ )
64
+
65
+ agent_response = result["message"]
66
+
67
+ # === STREAM RESPONSE TOKEN BY TOKEN ===
68
+ # Simple character-by-character streaming
69
+ chunk_size = 5 # Characters per chunk
70
+ for i in range(0, len(agent_response), chunk_size):
71
+ chunk = agent_response[i:i+chunk_size]
72
+ yield format_sse(EVENT_TOKEN, chunk)
73
+ # Small delay for smoother streaming
74
+ import asyncio
75
+ await asyncio.sleep(0.02)
76
+
77
+ # === SAVE HISTORY ===
78
+ conversation_service.add_message(
79
+ session_id=session_id,
80
+ role="user",
81
+ content=request.message
82
+ )
83
+ conversation_service.add_message(
84
+ session_id=session_id,
85
+ role="assistant",
86
+ content=agent_response
87
+ )
88
+
89
+ # === DONE ===
90
+ yield format_sse(EVENT_DONE, {
91
+ "session_id": session_id,
92
+ "timestamp": datetime.utcnow().isoformat(),
93
+ "mode": mode,
94
+ "tool_calls": len(result.get("tool_calls", []))
95
+ })
96
+
97
+ except Exception as e:
98
+ print(f"⚠️ Agent Stream Error: {e}")
99
+ yield format_sse(EVENT_ERROR, str(e))
agent_service.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Service - Central Brain for Sales & Feedback Agents
3
+ Manages LLM conversation loop with tool calling
4
+ """
5
+ from typing import Dict, Any, List, Optional
6
+ import os
7
+ from tools_service import ToolsService
8
+
9
+
10
+ class AgentService:
11
+ """
12
+ Manages the conversation loop between User -> LLM -> Tools -> Response
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ tools_service: ToolsService,
18
+ embedding_service,
19
+ qdrant_service,
20
+ advanced_rag,
21
+ hf_token: str
22
+ ):
23
+ self.tools_service = tools_service
24
+ self.embedding_service = embedding_service
25
+ self.qdrant_service = qdrant_service
26
+ self.advanced_rag = advanced_rag
27
+ self.hf_token = hf_token
28
+
29
+ # Load system prompts
30
+ self.prompts = self._load_prompts()
31
+
32
+ def _load_prompts(self) -> Dict[str, str]:
33
+ """Load system prompts from files"""
34
+ prompts = {}
35
+ prompts_dir = "prompts"
36
+
37
+ for mode in ["sales_agent", "feedback_agent"]:
38
+ filepath = os.path.join(prompts_dir, f"{mode}.txt")
39
+ try:
40
+ with open(filepath, 'r', encoding='utf-8') as f:
41
+ prompts[mode] = f.read()
42
+ print(f"✓ Loaded prompt: {mode}")
43
+ except Exception as e:
44
+ print(f"⚠️ Error loading {mode} prompt: {e}")
45
+ prompts[mode] = ""
46
+
47
+ return prompts
48
+
49
+ async def chat(
50
+ self,
51
+ user_message: str,
52
+ conversation_history: List[Dict],
53
+ mode: str = "sales", # "sales" or "feedback"
54
+ user_id: Optional[str] = None,
55
+ max_iterations: int = 3
56
+ ) -> Dict[str, Any]:
57
+ """
58
+ Main conversation loop
59
+
60
+ Args:
61
+ user_message: User's input
62
+ conversation_history: Previous messages [{"role": "user", "content": ...}, ...]
63
+ mode: "sales" or "feedback"
64
+ user_id: User ID (for feedback mode to check purchase history)
65
+ max_iterations: Maximum tool call iterations to prevent infinite loops
66
+
67
+ Returns:
68
+ {
69
+ "message": "Bot response",
70
+ "tool_calls": [...], # List of tools called (for debugging)
71
+ "mode": mode
72
+ }
73
+ """
74
+ print(f"\n🤖 Agent Mode: {mode}")
75
+ print(f"👤 User Message: {user_message}")
76
+
77
+ # Select system prompt
78
+ system_prompt = self._get_system_prompt(mode)
79
+
80
+ # Build conversation context
81
+ messages = self._build_messages(system_prompt, conversation_history, user_message)
82
+
83
+ # Agentic loop: LLM may call tools multiple times
84
+ tool_calls_made = []
85
+ current_response = None
86
+
87
+ for iteration in range(max_iterations):
88
+ print(f"\n🔄 Iteration {iteration + 1}")
89
+
90
+ # Call LLM
91
+ llm_response = await self._call_llm(messages)
92
+ print(f"🧠 LLM Response: {llm_response[:200]}...")
93
+
94
+ # Check if LLM wants to call a tool
95
+ tool_result = await self.tools_service.parse_and_execute(llm_response)
96
+
97
+ if not tool_result:
98
+ # No tool call -> This is the final response
99
+ current_response = llm_response
100
+ break
101
+
102
+ # Tool was called
103
+ tool_calls_made.append(tool_result)
104
+ print(f"🔧 Tool Called: {tool_result.get('function')}")
105
+
106
+ # Add tool result to conversation
107
+ messages.append({
108
+ "role": "assistant",
109
+ "content": llm_response
110
+ })
111
+ messages.append({
112
+ "role": "system",
113
+ "content": f"Tool Result:\n{self._format_tool_result(tool_result)}"
114
+ })
115
+
116
+ # If tool returns "run_rag_search", handle it specially
117
+ if tool_result.get("result", {}).get("action") == "run_rag_search":
118
+ rag_results = await self._execute_rag_search(tool_result["result"]["query"])
119
+ messages[-1]["content"] = f"RAG Search Results:\n{rag_results}"
120
+
121
+ # Clean up response
122
+ final_response = current_response or llm_response
123
+ final_response = self._clean_response(final_response)
124
+
125
+ return {
126
+ "message": final_response,
127
+ "tool_calls": tool_calls_made,
128
+ "mode": mode
129
+ }
130
+
131
+ def _get_system_prompt(self, mode: str) -> str:
132
+ """Get system prompt for selected mode"""
133
+ prompt_key = f"{mode}_agent" if mode in ["sales", "feedback"] else "sales_agent"
134
+ return self.prompts.get(prompt_key, "")
135
+
136
+ def _build_messages(
137
+ self,
138
+ system_prompt: str,
139
+ history: List[Dict],
140
+ user_message: str
141
+ ) -> List[Dict]:
142
+ """Build messages array for LLM"""
143
+ messages = [{"role": "system", "content": system_prompt}]
144
+
145
+ # Add conversation history
146
+ messages.extend(history)
147
+
148
+ # Add current user message
149
+ messages.append({"role": "user", "content": user_message})
150
+
151
+ return messages
152
+
153
+ async def _call_llm(self, messages: List[Dict]) -> str:
154
+ """
155
+ Call HuggingFace LLM
156
+ Uses advanced_rag's chat method
157
+ """
158
+ try:
159
+ # Build prompt from messages
160
+ prompt = self._messages_to_prompt(messages)
161
+
162
+ # Call HF API via advanced_rag
163
+ response = await self.advanced_rag.chat_completion(
164
+ user_prompt=prompt,
165
+ context="", # Context is already in system prompt
166
+ chat_history=[], # History is in messages
167
+ token=self.hf_token
168
+ )
169
+
170
+ return response
171
+ except Exception as e:
172
+ print(f"⚠️ LLM Call Error: {e}")
173
+ return "Xin lỗi, tôi đang gặp chút vấn đề kỹ thuật. Bạn thử lại sau nhé!"
174
+
175
+ def _messages_to_prompt(self, messages: List[Dict]) -> str:
176
+ """Convert messages array to single prompt string"""
177
+ prompt_parts = []
178
+
179
+ for msg in messages:
180
+ role = msg["role"]
181
+ content = msg["content"]
182
+
183
+ if role == "system":
184
+ prompt_parts.append(f"[SYSTEM]\n{content}\n")
185
+ elif role == "user":
186
+ prompt_parts.append(f"[USER]\n{content}\n")
187
+ elif role == "assistant":
188
+ prompt_parts.append(f"[ASSISTANT]\n{content}\n")
189
+
190
+ return "\n".join(prompt_parts)
191
+
192
+ def _format_tool_result(self, tool_result: Dict) -> str:
193
+ """Format tool result for feeding back to LLM"""
194
+ result = tool_result.get("result", {})
195
+
196
+ if isinstance(result, dict):
197
+ # Pretty print key info
198
+ formatted = []
199
+ for key, value in result.items():
200
+ if key not in ["success", "error"]:
201
+ formatted.append(f"{key}: {value}")
202
+ return "\n".join(formatted)
203
+
204
+ return str(result)
205
+
206
+ async def _execute_rag_search(self, query_params: Dict) -> str:
207
+ """
208
+ Execute RAG search for event discovery
209
+ Called when LLM wants to search_events
210
+ """
211
+ query = query_params.get("query", "")
212
+ vibe = query_params.get("vibe", "")
213
+
214
+ # Build search query
215
+ search_text = f"{query} {vibe}".strip()
216
+
217
+ print(f"🔍 RAG Search: {search_text}")
218
+
219
+ # Use embedding + qdrant
220
+ embedding = self.embedding_service.encode_text(search_text)
221
+ results = self.qdrant_service.search(
222
+ collection_name="events",
223
+ query_vector=embedding,
224
+ limit=5
225
+ )
226
+
227
+ # Format results
228
+ formatted = []
229
+ for i, result in enumerate(results, 1):
230
+ payload = result.payload or {}
231
+ texts = payload.get("texts", [])
232
+ text = texts[0] if texts else ""
233
+ event_id = payload.get("id_use", "")
234
+
235
+ formatted.append(f"{i}. {text[:100]}... (ID: {event_id})")
236
+
237
+ return "\n".join(formatted) if formatted else "Không tìm thấy sự kiện phù hợp."
238
+
239
+ def _clean_response(self, response: str) -> str:
240
+ """Remove JSON artifacts from final response"""
241
+ # Remove JSON blocks
242
+ if "```json" in response:
243
+ response = response.split("```json")[0]
244
+ if "```" in response:
245
+ response = response.split("```")[0]
246
+
247
+ # Remove tool call markers
248
+ if "{" in response and "tool_call" in response:
249
+ # Find the last natural sentence before JSON
250
+ lines = response.split("\n")
251
+ cleaned = []
252
+ for line in lines:
253
+ if "{" in line and "tool_call" in line:
254
+ break
255
+ cleaned.append(line)
256
+ response = "\n".join(cleaned)
257
+
258
+ return response.strip()
main.py CHANGED
@@ -19,11 +19,8 @@ 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
- from intent_classifier import IntentClassifier # NEW
23
- from scenario_engine import ScenarioEngine # NEW
24
- from lead_storage_service import LeadStorageService # NEW
25
- from hybrid_chat_endpoint import hybrid_chat_endpoint # NEW
26
- from hybrid_chat_stream import hybrid_chat_stream # NEW: Streaming
27
 
28
  # Initialize FastAPI app
29
  app = FastAPI(
@@ -109,19 +106,18 @@ conversation_service = ConversationService(conversations_collection, max_history
109
  print("✓ Conversation Service initialized")
110
 
111
  # Initialize Tools Service
112
- tools_service = ToolsService(base_url="https://www.festavenue.site")
113
  print("✓ Tools Service initialized (Function Calling enabled)")
114
 
115
- # Initialize Hybrid Chat Components
116
- intent_classifier = IntentClassifier()
117
- print("✓ Intent Classifier initialized")
118
-
119
- scenario_engine = ScenarioEngine(scenarios_dir="scenarios")
120
- print("✓ Scenario Engine initialized")
121
-
122
- leads_collection = db["leads"]
123
- lead_storage = LeadStorageService(leads_collection)
124
- print("✓ Lead Storage Service initialized")
125
 
126
  print("✓ Services initialized successfully")
127
 
@@ -152,6 +148,7 @@ class ChatRequest(BaseModel):
152
  message: str
153
  session_id: Optional[str] = None # Multi-turn conversation
154
  user_id: Optional[str] = None # User identifier for session tracking
 
155
  use_rag: bool = True
156
  top_k: int = 3
157
  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é.
@@ -694,184 +691,11 @@ async def get_stats():
694
 
695
 
696
  # ============================================
697
- # ChatbotRAG Endpoints
 
698
  # ============================================
 
699
 
700
- # Import chat endpoint logic
701
- from hybrid_chat_endpoint import hybrid_chat_endpoint
702
-
703
- @app.post("/chat", response_model=ChatResponse)
704
- async def chat(request: ChatRequest):
705
- """
706
- Hybrid Conversational Chatbot: Scenario FSM + RAG
707
-
708
- Features:
709
- - ✅ Scenario-based flows (giá vé, đặt vé kịch bản)
710
- - ✅ RAG knowledge retrieval (PDF, documents)
711
- - ✅ Mid-scenario RAG interruption (answer off-topic questions)
712
- - ✅ Lead collection (email, phone → MongoDB)
713
- - ✅ Multi-turn conversations with state management
714
- - ✅ Function calling (external API integration)
715
-
716
- Flow:
717
- 1. User message → Intent classification
718
- 2. Route to: Scenario FSM OR RAG OR Hybrid
719
- 3. Execute flow + save state
720
- 4. Save conversation history
721
-
722
- Example 1 - Start Price Inquiry Scenario:
723
- ```
724
- POST /chat
725
- {
726
- "message": "giá vé bao nhiêu?",
727
- "use_rag": true
728
- }
729
-
730
- Response:
731
- {
732
- "response": "Hello 👋 Bạn muốn xem giá của show nào để mình báo đúng nè?",
733
- "session_id": "abc-123",
734
- "mode": "scenario",
735
- "scenario_active": true
736
- }
737
- ```
738
-
739
- Example 2 - Continue Scenario:
740
- ```
741
- POST /chat
742
- {
743
- "message": "Show A",
744
- "session_id": "abc-123"
745
- }
746
-
747
- Response:
748
- {
749
- "response": "Bạn đi 1 mình hay đi nhóm...",
750
- "mode": "scenario",
751
- "scenario_active": true
752
- }
753
- ```
754
-
755
- Example 3 - Mid-scenario RAG Question:
756
- ```
757
- POST /chat
758
- {
759
- "message": "sự kiện mấy giờ?",
760
- "session_id": "abc-123"
761
- }
762
- # Bot answers from RAG, then resumes scenario
763
- ```
764
-
765
- Example 4 - Pure RAG Query:
766
- ```
767
- POST /chat
768
- {
769
- "message": "địa điểm sự kiện ở đâu?",
770
- "use_rag": true
771
- }
772
- # Normal RAG response (không trigger scenario)
773
- ```
774
- """
775
- return await hybrid_chat_endpoint(
776
- request=request,
777
- conversation_service=conversation_service,
778
- intent_classifier=intent_classifier,
779
- embedding_service=embedding_service, # NEW: Required by handlers
780
- qdrant_service=qdrant_service, # NEW: Required by handlers
781
- tools_service=tools_service,
782
- advanced_rag=advanced_rag,
783
- chat_history_collection=chat_history_collection,
784
- hf_token=hf_token,
785
- lead_storage=lead_storage
786
- )
787
-
788
-
789
- @app.post("/chat/stream")
790
- async def chat_stream(request: ChatRequest):
791
- """
792
- Streaming Chat Endpoint (SSE - Server-Sent Events)
793
-
794
- Real-time token-by-token response display
795
-
796
- Features:
797
- - ✅ Real-time "typing" effect
798
- - ✅ Status updates (thinking, searching)
799
- - ✅ Scenario: Simulated streaming (smooth typing)
800
- - ✅ RAG: Real LLM streaming
801
- - ✅ HTTP/2 compatible
802
-
803
- Event Types:
804
- - status: Bot status ("Đang suy nghĩ...", "Đang tìm kiếm...")
805
- - token: Text chunks
806
- - metadata: Session ID, context info
807
- - done: Completion signal
808
- - error: Error messages
809
-
810
- Example - JavaScript Client:
811
- ```javascript
812
- const response = await fetch('/chat/stream', {
813
- method: 'POST',
814
- headers: { 'Content-Type': 'application/json' },
815
- body: JSON.stringify({
816
- message: "giá vé bao nhiêu?",
817
- use_rag: true
818
- })
819
- });
820
-
821
- const reader = response.body.getReader();
822
- const decoder = new TextDecoder();
823
-
824
- while (true) {
825
- const {done, value} = await reader.read();
826
- if (done) break;
827
-
828
- const chunk = decoder.decode(value);
829
- const lines = chunk.split('\n\n');
830
-
831
- for (const line of lines) {
832
- if (line.startsWith('event: token')) {
833
- const data = line.split('data: ')[1];
834
- displayToken(data); // Append to UI
835
- }
836
- else if (line.startsWith('event: done')) {
837
- console.log('Stream complete');
838
- }
839
- }
840
- }
841
- ```
842
-
843
- Example - EventSource (simpler but less control):
844
- ```javascript
845
- // Note: EventSource doesn't support POST, need to use fetch
846
- const eventSource = new EventSource('/chat/stream?message=hello');
847
-
848
- eventSource.addEventListener('token', (e) => {
849
- displayToken(e.data);
850
- });
851
-
852
- eventSource.addEventListener('done', (e) => {
853
- eventSource.close();
854
- });
855
- ```
856
- """
857
- return StreamingResponse(
858
- hybrid_chat_stream(
859
- request=request,
860
- conversation_service=conversation_service,
861
- intent_classifier=intent_classifier,
862
- embedding_service=embedding_service, # For handlers
863
- qdrant_service=qdrant_service, # For handlers
864
- advanced_rag=advanced_rag,
865
- hf_token=hf_token,
866
- lead_storage=lead_storage
867
- ),
868
- media_type="text/event-stream",
869
- headers={
870
- "Cache-Control": "no-cache",
871
- "Connection": "keep-alive",
872
- "X-Accel-Buffering": "no" # Disable nginx buffering
873
- }
874
- )
875
 
876
 
877
  @app.get("/chat/history/{session_id}")
@@ -1421,6 +1245,65 @@ async def delete_document_from_kb(doc_id: str):
1421
  raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1422
 
1423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1424
  if __name__ == "__main__":
1425
  import uvicorn
1426
  uvicorn.run(
 
19
  from multimodal_pdf_parser import MultimodalPDFIndexer
20
  from conversation_service import ConversationService
21
  from tools_service import ToolsService
22
+ from agent_service import AgentService
23
+ from agent_chat_stream import agent_chat_stream # NEW: Agent Streaming
 
 
 
24
 
25
  # Initialize FastAPI app
26
  app = FastAPI(
 
106
  print("✓ Conversation Service initialized")
107
 
108
  # Initialize Tools Service
109
+ tools_service = ToolsService(base_url="https://hoalacrent.io.vn/api/v0")
110
  print("✓ Tools Service initialized (Function Calling enabled)")
111
 
112
+ # Initialize Agent Service (Agentic Workflow)
113
+ agent_service = AgentService(
114
+ tools_service=tools_service,
115
+ embedding_service=embedding_service,
116
+ qdrant_service=qdrant_service,
117
+ advanced_rag=advanced_rag,
118
+ hf_token=hf_token
119
+ )
120
+ print("✓ Agent Service initialized (Agentic Workflow enabled)")
 
121
 
122
  print("✓ Services initialized successfully")
123
 
 
148
  message: str
149
  session_id: Optional[str] = None # Multi-turn conversation
150
  user_id: Optional[str] = None # User identifier for session tracking
151
+ mode: str = "sales" # NEW: "sales" or "feedback" for agent selection
152
  use_rag: bool = True
153
  top_k: int = 3
154
  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é.
 
691
 
692
 
693
  # ============================================
694
+ # ChatbotRAG Endpoints - DEPRECATED
695
+ # USE /agent/chat INSTEAD
696
  # ============================================
697
+ # Old endpoints removed - now using Agentic Workflow via /agent/chat
698
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
 
700
 
701
  @app.get("/chat/history/{session_id}")
 
1245
  raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
1246
 
1247
 
1248
+ # ===================================
1249
+ # AGENT CHAT STREAMING ENDPOINT (NEW)
1250
+ # ===================================
1251
+
1252
+ @app.post("/agent/chat")
1253
+ async def agent_chat(request: ChatRequest):
1254
+ """
1255
+ 🤖 **Agentic Chatbot với SSE Streaming**
1256
+
1257
+ **Modes:**
1258
+ - `sales`: Sales Agent - Tư vấn sự kiện, chốt sale
1259
+ - `feedback`: Feedback Agent - CSKH, thu thập đánh giá
1260
+
1261
+ **Features:**
1262
+ - ✅ LLM-driven conversation (no hard-coded scenarios)
1263
+ - ✅ Automatic tool calling (search, get_event_details, save_lead...)
1264
+ - ✅ Real-time SSE streaming
1265
+ - ✅ Purchase history check (for feedback mode)
1266
+
1267
+ **Example:**
1268
+ ```
1269
+ POST /agent/chat
1270
+ {
1271
+ "message": "Tìm event cho tôi",
1272
+ "mode": "sales",
1273
+ "user_id": "user_123"
1274
+ }
1275
+ ```
1276
+
1277
+ **SSE Stream:**
1278
+ ```
1279
+ event: status
1280
+ data: Đang tư vấn...
1281
+
1282
+ event: token
1283
+ data: Hello
1284
+
1285
+ event: token
1286
+ data: 👋
1287
+
1288
+ event: done
1289
+ data: {"session_id": "...", "mode": "sales"}
1290
+ ```
1291
+ """
1292
+ return StreamingResponse(
1293
+ agent_chat_stream(
1294
+ request=request,
1295
+ agent_service=agent_service,
1296
+ conversation_service=conversation_service
1297
+ ),
1298
+ media_type="text/event-stream",
1299
+ headers={
1300
+ "Cache-Control": "no-cache",
1301
+ "Connection": "keep-alive",
1302
+ "X-Accel-Buffering": "no"
1303
+ }
1304
+ )
1305
+
1306
+
1307
  if __name__ == "__main__":
1308
  import uvicorn
1309
  uvicorn.run(
prompts/feedback_agent.txt ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ROLE
2
+ Bạn là chuyên viên Chăm sóc khách hàng (CSKH) của nền tảng bán vé sự kiện.
3
+ Nhiệm vụ của bạn là lắng nghe phản hồi của khách hàng sau khi tham gia sự kiện và hỗ trợ họ.
4
+
5
+ # GOAL
6
+ 1. Kiểm tra xem khách hàng đã tham gia sự kiện nào chưa.
7
+ 2. Nếu CÓ: Xin đánh giá (feedback), cảm nhận để cải thiện dịch vụ.
8
+ 3. Nếu KHÔNG (hoặc đã feedback xong): Giới thiệu các sự kiện mới hấp dẫn (chuyển sang vai trò Sales).
9
+
10
+ # CAPABILITIES (TOOLS)
11
+ 1. `get_purchased_events(user_id)`: Kiểm tra lịch sử mua vé/tham gia sự kiện của khách hàng.
12
+ 2. `save_feedback(event_id, rating, comment)`: Lưu đánh giá của khách hàng (rating 1-5 sao).
13
+ 3. `search_events(...)`: Tìm sự kiện mới (nếu khách muốn đi tiếp).
14
+
15
+ # GUIDELINES
16
+
17
+ ## Phase 1: Check History (Luôn thực hiện đầu tiên)
18
+ - Ngay khi bắt đầu hội thoại, hãy gọi `get_purchased_events(user_id)` ngầm (không cần hỏi khách).
19
+ - **Trường hợp A: Khách chưa từng đi sự kiện nào (hoặc API trả về rỗng)**
20
+ - Chuyển ngay sang mode tư vấn: "Chào bạn! Bạn đang tìm kiếm sự kiện gì thú vị cho tuần này không? Bên mình đang có nhiều show hay lắm! 🎉"
21
+ - (Sau đó hành xử như Sales Agent).
22
+
23
+ - **Trường hợp B: Khách ĐÃ đi sự kiện (ví dụ: "Show Hà Anh Tuấn")**
24
+ - Mở đầu bằng lời chào ấm áp: "Chào bạn! Cảm ơn bạn đã tham gia show **Hà Anh Tuấn** vừa rồi. Hy vọng bạn đã có những giây phút tuyệt vời! 🥰"
25
+ - Hỏi thăm cảm nhận: "Bạn thấy không khí hôm đó thế nào? Có điều gì làm bạn chưa hài lòng không?"
26
+
27
+ ## Phase 2: Collect Feedback (Nếu khách đã đi)
28
+ - Lắng nghe khách chia sẻ.
29
+ - Nếu khách khen: "Tuyệt quá! Bạn chấm cho sự kiện mấy sao nè? (1-5 sao) ⭐"
30
+ - Nếu khách chê: Tỏ ra đồng cảm, xin lỗi và hứa cải thiện. "Dạ mình rất tiếc về trải nghiệm này. Mình sẽ ghi nhận ngay để BTC rút kinh nghiệm ạ."
31
+ - Sau khi khách chấm điểm/comment -> Gọi `save_feedback`.
32
+
33
+ ## Phase 3: Transition to Sales (Sau khi feedback xong)
34
+ - Sau khi đã lưu feedback, hãy khéo léo giới thiệu sự kiện mới:
35
+ "Cảm ơn bạn đã góp ý nha! À, sắp tới bên mình có show **Mỹ Tâm** cũng vibe tương tự, bạn có muốn xem qua không?"
36
+ - Nếu khách quan tâm -> Dùng `search_events` và tư vấn tiếp.
37
+
38
+ # EXAMPLES
39
+
40
+ **Case 1: Có lịch sử đi event**
41
+ System: (User ID 123 -> get_purchased_events -> ["Show Rock Việt"])
42
+ Agent: "Chào bạn! Cảm ơn bạn đã cháy hết mình tại **Show Rock Việt** hôm qua! 🤘 Bạn thấy ban nhạc diễn có sung không?"
43
+ User: "Sung lắm, nhưng âm thanh hơi rè."
44
+ Agent: "Dạ mình ghi nhận góp ý về âm thanh ạ. Cảm ơn bạn nhiều. Bạn chấm show này mấy điểm trên thang 5 sao nè?"
45
+ User: "4 sao thôi."
46
+ Agent (Call Tool): save_feedback(event_id="rock_viet", rating=4, comment="Sung nhưng âm thanh rè")
47
+ Agent: "Dạ mình đã lưu lại rồi ạ. À sắp tới có **RockStorm** âm thanh xịn hơn, bạn có hóng không? 🔥"
48
+
49
+ **Case 2: Không có lịch sử**
50
+ System: (User ID 456 -> get_purchased_events -> [])
51
+ Agent: "Chào bạn! 👋 Cuối tuần này bạn đã có kế hoạch đi đâu chơi chưa? Bên mình đang có mấy show Acoustic chill lắm nè!"
prompts/sales_agent.txt ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ROLE
2
+ Bạn là một chuyên viên tư vấn sự kiện (Sales Agent) nhiệt tình, am hiểu và khéo léo của nền tảng bán vé sự kiện.
3
+ Tên bạn là: "TicketBot" (hoặc xưng là "mình"/"tớ").
4
+
5
+ # GOAL
6
+ Mục tiêu của bạn là giúp khách hàng tìm được sự kiện phù hợp nhất và khuyến khích họ mua vé (hoặc để lại thông tin liên hệ).
7
+
8
+ # CAPABILITIES (TOOLS)
9
+ Bạn có quyền truy cập các công cụ sau (hãy sử dụng chúng khi cần thiết):
10
+ 1. `search_events(query, vibe, date)`: Tìm kiếm sự kiện theo từ khóa, tâm trạng (chill, sôi động...), hoặc thời gian.
11
+ 2. `get_event_details(event_id)`: Lấy thông tin chi tiết (giá vé, địa điểm, nghệ sĩ, thời gian) của một sự kiện cụ thể.
12
+ 3. `save_lead(email, phone, interest)`: Lưu thông tin khách hàng khi họ quan tâm hoặc muốn nhận tư vấn thêm.
13
+
14
+ # GUIDELINES
15
+ 1. **Khơi gợi nhu cầu (Consultative Selling):**
16
+ - Đừng chỉ hỏi "Bạn muốn gì?". Hãy hỏi mở: "Cuối tuần này bạn rảnh không? Bạn đang mood muốn 'quẩy' hay chill nhẹ nhàng?"
17
+ - Nếu khách chưa rõ, hãy gợi ý dựa trên các vibe phổ biến: Hài kịch, Nhạc Indie, Workshop, EDM...
18
+
19
+ 2. **Tư vấn thông minh:**
20
+ - Khi khách hỏi giá, đừng chỉ đưa con số. Hãy kèm giá trị: "Vé hạng A giá 500k nhưng view siêu đẹp, còn hạng B 300k thì tiết kiệm hơn."
21
+ - Luôn đề xuất thêm (Upsell/Cross-sell) nếu phù hợp: "Đi nhóm 4 người đang có combo giảm 10% đó ạ."
22
+
23
+ 3. **Sử dụng Tools khéo léo:**
24
+ - Khi khách hỏi "có sự kiện gì?", HÃY gọi `search_events`. Đừng tự bịa ra sự kiện.
25
+ - Khi trả về danh sách sự kiện, hãy tóm tắt ngắn gọn điểm hấp dẫn nhất của từng cái.
26
+
27
+ 4. **Chốt Deal (Closing):**
28
+ - Khi khách có vẻ ưng ý (hỏi chi tiết, giá, chỗ ngồi...), hãy khéo léo xin thông tin:
29
+ "Sự kiện này đang hot lắm, bạn cho mình xin email để mình gửi link đặt vé giữ chỗ ngay nhé?"
30
+ - Hoặc: "Mình gửi lịch diễn chi tiết qua Zalo/Email cho bạn tiện xem nha?" -> Gọi `save_lead`.
31
+
32
+ 5. **Tone & Voice:**
33
+ - Thân thiện, trẻ trung, dùng emoji tự nhiên (😄, 🎉, 🔥).
34
+ - Không quá cứng nhắc như robot.
35
+ - Nếu khách hỏi ngoài lề (off-topic), hãy trả lời ngắn gọn rồi khéo léo lái về chủ đề sự kiện.
36
+
37
+ # EXAMPLES
38
+
39
+ User: "Cuối tuần này có gì chơi không?"
40
+ Agent (Thought): Khách chưa nói rõ sở thích. Cần hỏi thêm vibe.
41
+ Agent: "Cuối tuần này Sài Gòn nhiều show hay lắm! Bạn đang mood muốn 'quẩy' hết mình hay tìm một góc chill chill nghe nhạc? 🎶"
42
+
43
+ User: "Chill thôi, nghe nhạc acoustic."
44
+ Agent (Thought): Gọi tool search_events(vibe="chill", category="acoustic").
45
+ Agent (Call Tool): search_events(vibe="chill", category="acoustic")
46
+ ... (Tool returns events) ...
47
+ Agent: "À, vậy thì **Mây Lang Thang** hôm thứ 7 này là chuẩn bài! Có Lê Hiếu hát, không gian cực lãng mạn. Hoặc **Lululola** thì view hoàng hôn đỉnh chóp. Bạn thích giọng ai hơn? 🎤"
tools_service.py CHANGED
@@ -7,52 +7,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 = ID sự kiện (LẤY TỪ metadata.id_use TRONG CONTEXT, KHÔNG PHẢI tên sự kiện!)
32
-
33
- VÍ DỤ QUAN TRỌNG:
34
- Context có:
35
- ```
36
- metadata: {
37
- "id_use": "69194cf61c0eda56688806f7", ← DÙNG CÁI NÀY!
38
- "texts": ["Y-CONCERT - Festival âm nhạc..."]
39
- }
40
- ```
41
- → Dùng event_code = "69194cf61c0eda56688806f7" (NOT "Y-CONCERT")
42
-
43
- CÚ PHÁP GỌI TOOL:
44
- Khi bạn cần gọi tool, hãy trả lời CHÍNH XÁC theo format JSON này:
45
- ```json
46
- {
47
- "tool_call": true,
48
- "function_name": "get_event_details",
49
- "arguments": {
50
- "event_code": "69194cf61c0eda56688806f7"
51
- },
52
- "reason": "Cần lấy thông tin chính xác về ngày giờ tổ chức"
53
- }
54
- ```
55
-
56
  QUAN TRỌNG:
57
  - event_code PHẢI LÀ metadata.id_use từ context (dạng MongoDB ObjectId)
58
  - KHÔNG dùng tên sự kiện như "Y-CONCERT" làm event_code
 
7
  import json
8
  import asyncio
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  QUAN TRỌNG:
11
  - event_code PHẢI LÀ metadata.id_use từ context (dạng MongoDB ObjectId)
12
  - KHÔNG dùng tên sự kiện như "Y-CONCERT" làm event_code