fix api response
Browse files- app/agent/react_agent.py +54 -7
- app/agent/state.py +1 -0
- app/api/router.py +82 -21
- docs/API_REFERENCE.md +23 -15
- tests/react_comparison_report.md +18 -12
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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 333 |
)
|
| 334 |
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 314 |
-
|
| 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 |
-
|
| 329 |
-
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 |
-
#
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 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 |
-
|
| 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": "
|
| 55 |
"status": "success",
|
| 56 |
"provider": "MegaLLM",
|
| 57 |
"model": "deepseek-ai/deepseek-v3.1-terminus",
|
| 58 |
"user_id": "user_123",
|
| 59 |
"session_id": "default",
|
| 60 |
-
"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 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 Võ 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:
|
| 4 |
|
| 5 |
## Summary
|
| 6 |
|
|
@@ -8,7 +8,7 @@
|
|
| 8 |
|--------|-------------|------------|
|
| 9 |
| Total Tests | 1 | 1 |
|
| 10 |
| Success | 1 | 1 |
|
| 11 |
-
| Avg Duration |
|
| 12 |
|
| 13 |
---
|
| 14 |
|
|
@@ -21,7 +21,7 @@
|
|
| 21 |
#### Single Mode
|
| 22 |
|
| 23 |
- **Status:** ✅ Success
|
| 24 |
-
- **Duration:**
|
| 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!
|
| 39 |
|
| 40 |
-
**Top nhà hàng gần biển Mỹ Khê:**
|
| 41 |
|
| 42 |
-
1.
|
| 43 |
-
* *Khoảng cách:* Chỉ ...
|
| 44 |
|
| 45 |
#### ReAct Mode
|
| 46 |
|
| 47 |
- **Status:** ✅ Success
|
| 48 |
-
- **Duration:**
|
| 49 |
- **Tools Used:** get_location_coordinates, find_nearby_places
|
| 50 |
-
- **Steps:**
|
| 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
|
| 57 |
Tool: `find_nearby_places` | Results: 5
|
|
|
|
|
|
|
| 58 |
|
| 59 |
**Response Preview:**
|
| 60 |
-
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
| 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 và đá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
|