Cuong2004 commited on
Commit
ac07893
·
1 Parent(s): 38b5fe7

fix api response

Browse files
app/agent/react_agent.py CHANGED
@@ -9,6 +9,7 @@ Implements the ReAct (Reasoning + Acting) pattern:
9
 
10
  import time
11
  import json
 
12
  from typing import Any
13
 
14
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -157,15 +158,17 @@ class ReActAgent:
157
 
158
  if state.error:
159
  final_response = f"Xin lỗi, đã xảy ra lỗi: {state.error}"
 
160
  else:
161
- final_response = await self._synthesize(state, history)
162
 
163
  state.final_answer = final_response
 
164
 
165
  agent_logger.api_response(
166
  "/chat (ReAct)",
167
  200,
168
- {"steps": len(state.steps), "tools": list(state.context.keys())},
169
  state.total_duration_ms,
170
  )
171
 
@@ -291,15 +294,27 @@ class ReActAgent:
291
  else:
292
  return {"error": f"Unknown tool: {action}"}
293
 
294
- async def _synthesize(self, state: AgentState, history: str | None = None) -> str:
295
- """Synthesize final response from all collected information."""
 
 
 
 
 
296
  # Build context from all steps
297
  context_parts = []
 
 
298
  for step in state.steps:
299
  if step.observation and step.action != "finish":
300
  context_parts.append(
301
  f"Kết quả từ {step.action}:\n{json.dumps(step.observation, ensure_ascii=False, indent=2)}"
302
  )
 
 
 
 
 
303
 
304
  context = "\n\n".join(context_parts) if context_parts else "Không có kết quả."
305
 
@@ -324,15 +339,47 @@ Và kết quả thu thập được:
324
  Hãy trả lời câu hỏi của user một cách tự nhiên và hữu ích:
325
  "{state.query}"
326
 
327
- Trả lời tiếng Việt, thân thiện. Giới thiệu top 2-3 địa điểm phù hợp nhất với thông tin cụ thể."""
 
 
 
 
 
 
 
 
328
 
329
  response = await self.llm_client.generate(
330
  prompt=prompt,
331
  temperature=0.7,
332
- system_instruction="Bạn là trợ lý du lịch thông minh cho Đà Nẵng. Trả lời ngắn gọn, hữu ích.",
333
  )
334
 
335
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  def to_workflow(self, state: AgentState) -> AgentWorkflow:
338
  """Convert AgentState to AgentWorkflow for response."""
 
9
 
10
  import time
11
  import json
12
+ import re
13
  from typing import Any
14
 
15
  from sqlalchemy.ext.asyncio import AsyncSession
 
158
 
159
  if state.error:
160
  final_response = f"Xin lỗi, đã xảy ra lỗi: {state.error}"
161
+ selected_place_ids = []
162
  else:
163
+ final_response, selected_place_ids = await self._synthesize(state, history)
164
 
165
  state.final_answer = final_response
166
+ state.selected_place_ids = selected_place_ids # Store for later enrichment
167
 
168
  agent_logger.api_response(
169
  "/chat (ReAct)",
170
  200,
171
+ {"steps": len(state.steps), "tools": list(state.context.keys()), "places": len(selected_place_ids)},
172
  state.total_duration_ms,
173
  )
174
 
 
294
  else:
295
  return {"error": f"Unknown tool: {action}"}
296
 
297
+ async def _synthesize(self, state: AgentState, history: str | None = None) -> tuple[str, list[str]]:
298
+ """
299
+ Synthesize final response from all collected information.
300
+
301
+ Returns:
302
+ Tuple of (response_text, selected_place_ids)
303
+ """
304
  # Build context from all steps
305
  context_parts = []
306
+ all_place_ids = [] # Collect all available place_ids
307
+
308
  for step in state.steps:
309
  if step.observation and step.action != "finish":
310
  context_parts.append(
311
  f"Kết quả từ {step.action}:\n{json.dumps(step.observation, ensure_ascii=False, indent=2)}"
312
  )
313
+ # Collect place_ids from observations
314
+ if isinstance(step.observation, list):
315
+ for item in step.observation:
316
+ if isinstance(item, dict) and 'place_id' in item:
317
+ all_place_ids.append(item['place_id'])
318
 
319
  context = "\n\n".join(context_parts) if context_parts else "Không có kết quả."
320
 
 
339
  Hãy trả lời câu hỏi của user một cách tự nhiên và hữu ích:
340
  "{state.query}"
341
 
342
+ **QUAN TRỌNG:** Trả lời theo format JSON:
343
+ ```json
344
+ {{
345
+ "response": "Câu trả lời tiếng Việt, thân thiện. Giới thiệu top 2-3 địa điểm phù hợp nhất.",
346
+ "selected_place_ids": ["place_id_1", "place_id_2", "place_id_3"]
347
+ }}
348
+ ```
349
+
350
+ Chỉ chọn những place_id xuất hiện trong kết quả tìm kiếm ở trên. Nếu không có địa điểm, để mảng rỗng."""
351
 
352
  response = await self.llm_client.generate(
353
  prompt=prompt,
354
  temperature=0.7,
355
+ system_instruction="Bạn là trợ lý du lịch thông minh cho Đà Nẵng. Trả lời format JSON.",
356
  )
357
 
358
+ # Parse JSON response
359
+ try:
360
+ # Extract JSON from response
361
+ json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', response, re.DOTALL)
362
+ if json_match:
363
+ response = json_match.group(1)
364
+
365
+ json_start = response.find('{')
366
+ json_end = response.rfind('}')
367
+ if json_start != -1 and json_end != -1:
368
+ response = response[json_start:json_end + 1]
369
+
370
+ data = json.loads(response)
371
+ text_response = data.get("response", response)
372
+ selected_ids = data.get("selected_place_ids", [])
373
+
374
+ # Validate selected_ids are in available places
375
+ valid_ids = [pid for pid in selected_ids if pid in all_place_ids]
376
+
377
+ return text_response, valid_ids
378
+
379
+ except (json.JSONDecodeError, KeyError):
380
+ # Fallback: return raw response with no places
381
+ agent_logger.error("Failed to parse synthesis JSON", None)
382
+ return response, []
383
 
384
  def to_workflow(self, state: AgentState) -> AgentWorkflow:
385
  """Convert AgentState to AgentWorkflow for response."""
app/agent/state.py CHANGED
@@ -46,6 +46,7 @@ class AgentState:
46
  max_steps: int = 5
47
  is_complete: bool = False
48
  final_answer: str = ""
 
49
  total_duration_ms: float = 0
50
  error: str | None = None
51
 
 
46
  max_steps: int = 5
47
  is_complete: bool = False
48
  final_answer: str = ""
49
+ selected_place_ids: list[str] = field(default_factory=list) # LLM-selected places
50
  total_duration_ms: float = 0
51
  error: str | None = None
52
 
app/api/router.py CHANGED
@@ -86,8 +86,21 @@ class WorkflowResponse(BaseModel):
86
  total_duration_ms: float = Field(..., description="Total processing time")
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  class ChatResponse(BaseModel):
90
- """Chat response model with workflow trace."""
91
 
92
  response: str = Field(..., description="Agent's response")
93
  status: str = Field(default="success", description="Response status")
@@ -95,7 +108,7 @@ class ChatResponse(BaseModel):
95
  model: str = Field(..., description="Model used")
96
  user_id: str = Field(..., description="User ID")
97
  session_id: str = Field(..., description="Session ID used")
98
- workflow: WorkflowResponse | None = Field(None, description="Workflow trace for debugging")
99
  tools_used: list[str] = Field(default_factory=list, description="MCP tools used")
100
  duration_ms: float = Field(default=0, description="Total processing time in ms")
101
 
@@ -225,6 +238,53 @@ async def find_nearby(request: NearbyRequest) -> NearbyResponse:
225
  )
226
 
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  @router.post(
229
  "/chat",
230
  response_model=ChatResponse,
@@ -310,13 +370,8 @@ async def chat(
310
  session_id=session_id,
311
  )
312
 
313
- workflow_response = WorkflowResponse(
314
- query=workflow_data["query"],
315
- intent_detected=workflow_data["intent_detected"],
316
- tools_used=workflow_data["tools_used"],
317
- steps=[WorkflowStepResponse(**s) for s in workflow_data["steps"]],
318
- total_duration_ms=workflow_data["total_duration_ms"],
319
- )
320
 
321
  return ChatResponse(
322
  response=response_text,
@@ -325,8 +380,8 @@ async def chat(
325
  model=model,
326
  user_id=request.user_id,
327
  session_id=session_id,
328
- workflow=workflow_response,
329
- tools_used=workflow.tools_used,
330
  duration_ms=agent_state.total_duration_ms,
331
  )
332
 
@@ -350,15 +405,21 @@ async def chat(
350
  session_id=session_id,
351
  )
352
 
353
- # Build workflow response
354
- workflow_data = result.workflow.to_dict()
355
- workflow_response = WorkflowResponse(
356
- query=workflow_data["query"],
357
- intent_detected=workflow_data["intent_detected"],
358
- tools_used=workflow_data["tools_used"],
359
- steps=[WorkflowStepResponse(**s) for s in workflow_data["steps"]],
360
- total_duration_ms=workflow_data["total_duration_ms"],
361
- )
 
 
 
 
 
 
362
 
363
  return ChatResponse(
364
  response=result.response,
@@ -367,7 +428,7 @@ async def chat(
367
  model=model,
368
  user_id=request.user_id,
369
  session_id=session_id,
370
- workflow=workflow_response,
371
  tools_used=result.tools_used,
372
  duration_ms=result.total_duration_ms,
373
  )
 
86
  total_duration_ms: float = Field(..., description="Total processing time")
87
 
88
 
89
+ class PlaceItem(BaseModel):
90
+ """Place item for FE rendering."""
91
+ place_id: str
92
+ name: str
93
+ category: str | None = None
94
+ lat: float | None = None
95
+ lng: float | None = None
96
+ rating: float | None = None
97
+ distance_km: float | None = None
98
+ address: str | None = None
99
+ image_url: str | None = None
100
+
101
+
102
  class ChatResponse(BaseModel):
103
+ """Chat response model."""
104
 
105
  response: str = Field(..., description="Agent's response")
106
  status: str = Field(default="success", description="Response status")
 
108
  model: str = Field(..., description="Model used")
109
  user_id: str = Field(..., description="User ID")
110
  session_id: str = Field(..., description="Session ID used")
111
+ places: list[PlaceItem] = Field(default_factory=list, description="LLM-selected places for FE rendering")
112
  tools_used: list[str] = Field(default_factory=list, description="MCP tools used")
113
  duration_ms: float = Field(default=0, description="Total processing time in ms")
114
 
 
238
  )
239
 
240
 
241
+ async def enrich_places_from_ids(place_ids: list[str], db: AsyncSession) -> list[PlaceItem]:
242
+ """
243
+ Enrich LLM-selected place_ids with full details from DB.
244
+
245
+ Args:
246
+ place_ids: List of place_ids selected by LLM in synthesis
247
+ db: Database session
248
+
249
+ Returns:
250
+ List of PlaceItem with full details
251
+ """
252
+ if not place_ids:
253
+ return []
254
+
255
+ # Fetch full details from DB
256
+ from sqlalchemy import text
257
+ result = await db.execute(
258
+ text("""
259
+ SELECT place_id, name, category, address, rating,
260
+ ST_X(coordinates::geometry) as lng,
261
+ ST_Y(coordinates::geometry) as lat
262
+ FROM places_metadata
263
+ WHERE place_id = ANY(:place_ids)
264
+ """),
265
+ {"place_ids": place_ids}
266
+ )
267
+ rows = result.fetchall()
268
+
269
+ # Build PlaceItem list preserving LLM order
270
+ places_dict = {row.place_id: row for row in rows}
271
+ places = []
272
+ for pid in place_ids:
273
+ if pid in places_dict:
274
+ row = places_dict[pid]
275
+ places.append(PlaceItem(
276
+ place_id=row.place_id,
277
+ name=row.name,
278
+ category=row.category,
279
+ lat=row.lat,
280
+ lng=row.lng,
281
+ rating=float(row.rating) if row.rating else None,
282
+ address=row.address,
283
+ ))
284
+
285
+ return places
286
+
287
+
288
  @router.post(
289
  "/chat",
290
  response_model=ChatResponse,
 
370
  session_id=session_id,
371
  )
372
 
373
+ # Enrich LLM-selected place_ids with DB data
374
+ places = await enrich_places_from_ids(agent_state.selected_place_ids, db)
 
 
 
 
 
375
 
376
  return ChatResponse(
377
  response=response_text,
 
380
  model=model,
381
  user_id=request.user_id,
382
  session_id=session_id,
383
+ places=places,
384
+ tools_used=list(agent_state.context.keys()),
385
  duration_ms=agent_state.total_duration_ms,
386
  )
387
 
 
405
  session_id=session_id,
406
  )
407
 
408
+ # Extract places from tool results if available
409
+ places = []
410
+ if hasattr(result, 'selected_place_ids') and result.selected_place_ids:
411
+ # If agent provides selected_place_ids, enrich from DB
412
+ places = await enrich_places_from_ids(result.selected_place_ids, db)
413
+ elif hasattr(result, 'tool_results') and result.tool_results:
414
+ # Fallback: extract all place_ids from tool results
415
+ place_ids = []
416
+ for tool_result in result.tool_results:
417
+ if isinstance(tool_result, list):
418
+ for item in tool_result:
419
+ if isinstance(item, dict) and 'place_id' in item:
420
+ place_ids.append(item['place_id'])
421
+ if place_ids:
422
+ places = await enrich_places_from_ids(place_ids[:5], db) # Limit to top 5
423
 
424
  return ChatResponse(
425
  response=result.response,
 
428
  model=model,
429
  user_id=request.user_id,
430
  session_id=session_id,
431
+ places=places,
432
  tools_used=result.tools_used,
433
  duration_ms=result.total_duration_ms,
434
  )
docs/API_REFERENCE.md CHANGED
@@ -51,31 +51,39 @@ Main endpoint for interacting with the AI assistant.
51
  **Response:**
52
  ```json
53
  {
54
- "response": "Dựa trên yêu cầu của bạn, tôi tìm thấy...",
55
  "status": "success",
56
  "provider": "MegaLLM",
57
  "model": "deepseek-ai/deepseek-v3.1-terminus",
58
  "user_id": "user_123",
59
  "session_id": "default",
60
- "workflow": {
61
- "query": "Quán cafe view đẹp gần Mỹ Khê",
62
- "intent_detected": "location_search",
63
- "tools_used": ["find_nearby_places"],
64
- "steps": [
65
- {
66
- "step": "Execute find_nearby_places",
67
- "tool": "find_nearby_places",
68
- "purpose": "Tìm địa điểm gần vị trí được nhắc đến",
69
- "results": 5
70
- }
71
- ],
72
- "total_duration_ms": 5748.23
73
- },
 
 
 
 
 
 
74
  "tools_used": ["find_nearby_places"],
75
  "duration_ms": 5748.23
76
  }
77
  ```
78
 
 
 
79
  ---
80
 
81
  ### POST `/chat/clear`
 
51
  **Response:**
52
  ```json
53
  {
54
+ "response": "Mình gợi ý 3 quán cafe rất đẹp gần Mỹ Khê...",
55
  "status": "success",
56
  "provider": "MegaLLM",
57
  "model": "deepseek-ai/deepseek-v3.1-terminus",
58
  "user_id": "user_123",
59
  "session_id": "default",
60
+ "places": [
61
+ {
62
+ "place_id": "cafe_001",
63
+ "name": "Cabanon Palace",
64
+ "category": "restaurant",
65
+ "lat": 16.06,
66
+ "lng": 108.24,
67
+ "rating": 4.8,
68
+ "address": "123 Nguyên Giáp"
69
+ },
70
+ {
71
+ "place_id": "cafe_002",
72
+ "name": "Be Man Restaurant",
73
+ "category": "restaurant",
74
+ "lat": 16.07,
75
+ "lng": 108.25,
76
+ "rating": 4.5,
77
+ "address": "456 Phạm Văn Đồng"
78
+ }
79
+ ],
80
  "tools_used": ["find_nearby_places"],
81
  "duration_ms": 5748.23
82
  }
83
  ```
84
 
85
+ > **Note:** `places` array contains LLM-selected places with full details. FE can render these as cards separately from text response.
86
+
87
  ---
88
 
89
  ### POST `/chat/clear`
tests/react_comparison_report.md CHANGED
@@ -1,6 +1,6 @@
1
  # LocalMate Agent Test Report
2
 
3
- **Generated:** 2025-12-18 01:06:05
4
 
5
  ## Summary
6
 
@@ -8,7 +8,7 @@
8
  |--------|-------------|------------|
9
  | Total Tests | 1 | 1 |
10
  | Success | 1 | 1 |
11
- | Avg Duration | 31219ms | 10765ms |
12
 
13
  ---
14
 
@@ -21,7 +21,7 @@
21
  #### Single Mode
22
 
23
  - **Status:** ✅ Success
24
- - **Duration:** 31219ms
25
  - **Tools Used:** find_nearby_places
26
 
27
  **Workflow:**
@@ -35,29 +35,35 @@
35
  Tool: `None` | Results: 0
36
 
37
  **Response Preview:**
38
- > Chào bạn! Dựa trên vị trí gần bãi biển Mỹ Khê, mình tìm được một số nhà hàng rất hay cho bạn đây:
39
 
40
- **Top nhà hàng gần biển Mỹ Khê:**
41
 
42
- 1. **Cabanon Palace** (Nhà hàng Pháp)
43
- * *Khoảng cách:* Chỉ ...
44
 
45
  #### ReAct Mode
46
 
47
  - **Status:** ✅ Success
48
- - **Duration:** 10765ms
49
  - **Tools Used:** get_location_coordinates, find_nearby_places
50
- - **Steps:** 2
51
  - **Intent Detected:** react_multi_step
52
 
53
  **Workflow Steps:**
54
  - Step 1: Để tìm nhà hàng gần bãi biển Mỹ Khê, trước tiên cầ...
55
  Tool: `get_location_coordinates` | Results: 0
56
- - Step 2: Đã có tọa độ của bãi biển Mỹ Khê. Bây giờ cần tìm ...
57
  Tool: `find_nearby_places` | Results: 5
 
 
58
 
59
  **Response Preview:**
60
- > Xin lỗi, đã xảy ra lỗi: 'str' object has no attribute 'get'...
 
 
 
 
 
61
 
62
  ---
63
 
@@ -67,7 +73,7 @@
67
 
68
  | Test | Single Mode Tools | ReAct Mode Tools | ReAct Steps |
69
  |------|-------------------|------------------|-------------|
70
- | 2 | find_nearby_places | get_location_coordinates, find_nearby_places | 2 |
71
 
72
 
73
  ### Key Observations
 
1
  # LocalMate Agent Test Report
2
 
3
+ **Generated:** 2025-12-18 01:17:38
4
 
5
  ## Summary
6
 
 
8
  |--------|-------------|------------|
9
  | Total Tests | 1 | 1 |
10
  | Success | 1 | 1 |
11
+ | Avg Duration | 7584ms | 23328ms |
12
 
13
  ---
14
 
 
21
  #### Single Mode
22
 
23
  - **Status:** ✅ Success
24
+ - **Duration:** 7584ms
25
  - **Tools Used:** find_nearby_places
26
 
27
  **Workflow:**
 
35
  Tool: `None` | Results: 0
36
 
37
  **Response Preview:**
38
+ > Chào bạn! Mình đã tìm được một số nhà hàng ngon và gần bãi biển Mỹ Khê cho bạn đây. Dựa trên khoảng cách đánh giá, đây là những gợi ý nổi bật nhất:
39
 
40
+ 🍽️ **Top 3 nhà hàng gần bãi biển Mỹ Khê:**
41
 
42
+ 1. *...
 
43
 
44
  #### ReAct Mode
45
 
46
  - **Status:** ✅ Success
47
+ - **Duration:** 23328ms
48
  - **Tools Used:** get_location_coordinates, find_nearby_places
49
+ - **Steps:** 3
50
  - **Intent Detected:** react_multi_step
51
 
52
  **Workflow Steps:**
53
  - Step 1: Để tìm nhà hàng gần bãi biển Mỹ Khê, trước tiên cầ...
54
  Tool: `get_location_coordinates` | Results: 0
55
+ - Step 2: Đã có tọa độ của bãi biển Mỹ Khê, bây giờ cần tìm ...
56
  Tool: `find_nearby_places` | Results: 5
57
+ - Step 3: Tôi đã có tọa độ của bãi biển Mỹ Khê và danh sách ...
58
+ Tool: `None` | Results: 0
59
 
60
  **Response Preview:**
61
+ > Dạ chào bạn! Mình gợi ý một số nhà hàng ngon và gần bãi biển Mỹ Khê nhé:
62
+
63
+ 🍽️ **Cabanon Palace**
64
+ - *Chuyên:* Ẩm thực Pháp sang trọng
65
+ - *Khoảng cách:* ~0.94km từ biển
66
+ - *Đánh giá:* 4.8/5 – lý tưởn...
67
 
68
  ---
69
 
 
73
 
74
  | Test | Single Mode Tools | ReAct Mode Tools | ReAct Steps |
75
  |------|-------------------|------------------|-------------|
76
+ | 2 | find_nearby_places | get_location_coordinates, find_nearby_places | 3 |
77
 
78
 
79
  ### Key Observations