File size: 6,124 Bytes
ca7a2c2
 
 
 
 
 
 
 
9e98b5a
 
 
 
ca7a2c2
9e98b5a
 
ca7a2c2
 
45b1ef5
ca7a2c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0140f42
ca7a2c2
 
0140f42
ca7a2c2
0140f42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38b5fe7
0140f42
 
38b5fe7
 
 
 
 
 
 
0140f42
 
 
 
 
38b5fe7
0140f42
 
38b5fe7
 
 
 
 
0140f42
 
 
 
 
 
 
ca7a2c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e98b5a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""ReAct Reasoning Module - Prompts and parsing for multi-step reasoning."""

import json
import re
from dataclasses import dataclass
from typing import Any

from app.shared.logger import agent_logger
from app.shared.prompts import (
    REACT_SYSTEM_PROMPT,
    TOOL_PURPOSES,
)

# Re-export for backward compatibility
__all__ = ["REACT_SYSTEM_PROMPT", "ReasoningResult", "parse_reasoning_response", "build_reasoning_prompt", "get_tool_purpose"]



@dataclass
class ReasoningResult:
    """Result from LLM reasoning step."""
    
    thought: str
    action: str
    action_input: dict
    raw_response: str
    parse_error: str | None = None


def parse_reasoning_response(response: str) -> ReasoningResult:
    """
    Parse LLM response into thought/action/action_input.
    
    Handles various formats:
    - Clean JSON
    - JSON in markdown code blocks
    - Partial/malformed JSON
    """
    raw = response.strip()
    
    # Try to extract JSON from code blocks
    json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', raw, re.DOTALL)
    if json_match:
        raw = json_match.group(1)
    
    # Try to find JSON object
    json_start = raw.find('{')
    json_end = raw.rfind('}')
    if json_start != -1 and json_end != -1:
        raw = raw[json_start:json_end + 1]
    
    try:
        data = json.loads(raw)
        return ReasoningResult(
            thought=data.get("thought", ""),
            action=data.get("action", "finish"),
            action_input=data.get("action_input", {}),
            raw_response=response,
        )
    except json.JSONDecodeError as e:
        agent_logger.error(f"Failed to parse reasoning response", e)
        
        # Fallback: try to extract key fields with regex
        thought_match = re.search(r'"thought"\s*:\s*"([^"]*)"', raw)
        action_match = re.search(r'"action"\s*:\s*"([^"]*)"', raw)
        
        thought = thought_match.group(1) if thought_match else "Parse error"
        action = action_match.group(1) if action_match else "finish"
        
        return ReasoningResult(
            thought=thought,
            action=action,
            action_input={},
            raw_response=response,
            parse_error=str(e),
        )


def build_reasoning_prompt(
    query: str,
    context_summary: str,
    previous_steps: list[dict],
    image_url: str | None = None,
) -> str:
    """Build the prompt for the next reasoning step."""
    
    # Previous steps summary with FULL observations
    steps_text = ""
    if previous_steps:
        steps_text = "\n**Các bước đã thực hiện và KẾT QUẢ:**\n"
        for step in previous_steps:
            action = step.get('action', 'unknown')
            thought = step.get('thought', '')[:100]
            observation = step.get('observation', [])
            
            steps_text += f"\n📍 **Step {step['step']}**: {thought}...\n"
            steps_text += f"   Action: `{action}`\n"
            
            # Show detailed observation data
            if action == "get_location_coordinates" and observation:
                if isinstance(observation, dict):
                    lat = observation.get('lat', 'N/A')
                    lng = observation.get('lng', 'N/A')
                    steps_text += f"   ✅ Kết quả: lat={lat}, lng={lng}\n"
                    steps_text += f"   ⚠️ ĐÃ CÓ TỌA ĐỘ - KHÔNG CẦN GỌI LẠI get_location_coordinates\n"
            
            elif action == "find_nearby_places" and observation:
                if isinstance(observation, list) and len(observation) > 0:
                    steps_text += f"   ✅ Tìm được {len(observation)} địa điểm:\n"
                    for i, place in enumerate(observation[:5], 1):
                        if isinstance(place, dict):
                            name = place.get('name', 'Unknown')
                            dist = place.get('distance_km', 'N/A')
                            rating = place.get('rating', 'N/A')
                            steps_text += f"      {i}. {name} ({dist}km, ⭐{rating})\n"
                        else:
                            steps_text += f"      {i}. {place}\n"
                    if len(observation) > 5:
                        steps_text += f"      ... và {len(observation) - 5} địa điểm khác\n"
                    steps_text += f"   ⚠️ ĐÃ CÓ DANH SÁCH - KHÔNG CẦN GỌI LẠI find_nearby_places\n"
            
            elif action == "retrieve_context_text" and observation:
                if isinstance(observation, list) and len(observation) > 0:
                    steps_text += f"   ✅ Tìm được {len(observation)} kết quả text:\n"
                    for i, item in enumerate(observation[:3], 1):
                        if isinstance(item, dict):
                            name = item.get('name', 'Unknown')
                            steps_text += f"      {i}. {name}\n"
                        else:
                            steps_text += f"      {i}. {item}\n"
                    steps_text += f"   ⚠️ ĐÃ CÓ KẾT QUẢ TEXT - KHÔNG CẦN GỌI LẠI retrieve_context_text\n"
            
            elif observation:
                result_count = len(observation) if isinstance(observation, list) else 1
                steps_text += f"   ✅ Kết quả: {result_count} items\n"
        
        steps_text += "\n**⚠️ QUAN TRỌNG:** Nếu đã có đủ thông tin từ các bước trên → action = 'finish'\n"
    
    # Image context
    image_text = ""
    if image_url:
        image_text = "\n**Lưu ý:** User đã gửi kèm ảnh. Có thể dùng retrieve_similar_visuals nếu cần.\n"
    
    prompt = f"""**Câu hỏi của user:** {query}
{image_text}
{context_summary}
{steps_text}
**Bước tiếp theo là gì?**

Trả lời theo format JSON:
```json
{{
  "thought": "...",
  "action": "tool_name hoặc finish",
  "action_input": {{...}}
}}
```"""
    
    return prompt


def get_tool_purpose(action: str) -> str:
    """Get human-readable purpose for a tool."""
    return TOOL_PURPOSES.get(action, action)