File size: 12,039 Bytes
deafbd7
 
d31e1c0
 
 
 
deafbd7
 
96694a2
61f6a94
deafbd7
 
 
 
d31e1c0
61f6a94
18ddb05
d31e1c0
 
 
deafbd7
d31e1c0
deafbd7
d31e1c0
61f6a94
deafbd7
61f6a94
d31e1c0
61f6a94
d31e1c0
 
 
61f6a94
d31e1c0
 
61f6a94
 
5165223
 
61f6a94
 
 
 
d31e1c0
 
61f6a94
deafbd7
ec19d59
deafbd7
 
d31e1c0
96694a2
f326227
 
d31e1c0
f326227
 
61f6a94
5e8644b
61f6a94
 
 
d31e1c0
 
 
 
f326227
 
 
61f6a94
5e8644b
61f6a94
d31e1c0
 
 
 
f326227
 
 
61f6a94
d31e1c0
61f6a94
 
 
 
 
d31e1c0
61f6a94
 
 
 
d31e1c0
 
 
 
 
 
 
deafbd7
 
61f6a94
deafbd7
 
 
 
d31e1c0
 
 
 
 
 
 
 
 
 
 
 
deafbd7
61f6a94
d31e1c0
deafbd7
d31e1c0
5165223
d31e1c0
5165223
 
d31e1c0
5165223
 
 
d31e1c0
 
5165223
 
d31e1c0
 
5165223
 
 
 
a2227b6
 
 
ad2d8b8
a2227b6
ad2d8b8
a2227b6
 
ad2d8b8
 
 
a2227b6
 
 
 
 
 
 
 
d31e1c0
5165223
d31e1c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec19d59
 
d31e1c0
5e8644b
ec19d59
deafbd7
d31e1c0
deafbd7
61f6a94
d31e1c0
61f6a94
d31e1c0
 
 
 
 
 
 
deafbd7
61f6a94
d31e1c0
deafbd7
 
d31e1c0
 
61f6a94
 
 
 
d31e1c0
 
 
 
 
 
 
 
 
 
deafbd7
d31e1c0
 
61f6a94
 
d31e1c0
 
deafbd7
61f6a94
 
 
deafbd7
d31e1c0
 
 
 
 
 
deafbd7
d31e1c0
 
 
 
 
 
 
 
61f6a94
d31e1c0
 
 
 
 
 
deafbd7
61f6a94
deafbd7
 
d31e1c0
 
deafbd7
61f6a94
 
deafbd7
d31e1c0
 
61f6a94
 
 
18ddb05
61f6a94
deafbd7
61f6a94
d31e1c0
 
61f6a94
 
d31e1c0
 
 
61f6a94
d31e1c0
 
61f6a94
 
 
 
 
 
d31e1c0
deafbd7
61f6a94
 
 
 
 
d31e1c0
 
61f6a94
deafbd7
d31e1c0
 
 
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env python
# coding=utf-8
"""
Gradio UI for Travel Catalogue Creator
Production-ready interface with streaming agent responses
"""
import os
import re
from typing import Optional, List
from smolagents.agent_types import AgentText, handle_agent_output_types
from smolagents.agents import ActionStep, MultiStepAgent
from smolagents.memory import MemoryStep
from smolagents.utils import _is_package_available


def pull_messages_from_step(step_log: MemoryStep):
    """Extract ChatMessage objects from agent steps with proper nesting"""
    if not _is_package_available("gradio"):
        raise ModuleNotFoundError("Install gradio: `pip install 'smolagents[gradio]'`")
    
    import gradio as gr
    
    if isinstance(step_log, ActionStep):
        # Step header
        step_number = f"Step {step_log.step_number}" if step_log.step_number is not None else "Processing"
        yield gr.ChatMessage(role="assistant", content=f"**{step_number}**")
        
        # Show LLM reasoning/thinking
        if hasattr(step_log, "model_output") and step_log.model_output:
            model_output = step_log.model_output.strip()
            # Clean up code blocks
            model_output = re.sub(r"```\s*<end_code>.*", "```", model_output)
            model_output = re.sub(r"<end_code>\s*```", "```", model_output)
            
            if model_output:
                yield gr.ChatMessage(role="assistant", content=model_output)
        
        parent_id = None
        
        # Handle tool calls
        if hasattr(step_log, "tool_calls") and step_log.tool_calls:
            tool_call = step_log.tool_calls[0]
            parent_id = f"tool_{step_log.step_number}"
            
            # Format tool arguments
            args = tool_call.arguments
            if isinstance(args, dict):
                content = "\n".join(f"• {k}: {v}" for k, v in args.items() if v and k != 'self')
            else:
                content = str(args).strip()
            
            metadata = {
                "title": f"🛠️ Using: {tool_call.name}",
                "id": parent_id,
                "status": "pending",  # Gradio requires "pending", not "running"
            }
            yield gr.ChatMessage(role="assistant", content=content, metadata=metadata)
        
        # Show observations/results
        if hasattr(step_log, "observations") and step_log.observations:
            obs = step_log.observations.strip()
            if obs and not obs.startswith("Execution logs:"):
                metadata = {
                    "title": "✅ Result",
                    "status": "done"
                }
                if parent_id is not None:
                    metadata["parent_id"] = parent_id
                yield gr.ChatMessage(role="assistant", content=obs, metadata=metadata)
        
        # Show errors
        if hasattr(step_log, "error") and step_log.error:
            metadata = {
                "title": "⚠️ Warning",
                "status": "done"
            }
            if parent_id is not None:
                metadata["parent_id"] = parent_id
            yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata=metadata)
        
        # Step footer with timing and token info
        footer_parts = [step_number]
        if hasattr(step_log, "duration") and step_log.duration:
            footer_parts.append(f"⏱️ {float(step_log.duration):.1f}s")
        if hasattr(step_log, "input_token_count") and hasattr(step_log, "output_token_count"):
            footer_parts.append(f"💬 {step_log.input_token_count + step_log.output_token_count:,} tokens")
        
        yield gr.ChatMessage(
            role="assistant",
            content=f'<span style="color: #888; font-size: 0.85em;">{" | ".join(footer_parts)}</span>',
        )
        
        # Divider between steps
        yield gr.ChatMessage(
            role="assistant",
            content='<hr style="margin: 8px 0; border: 0; border-top: 1px solid #eee">'
        )


def stream_to_gradio(
    agent: MultiStepAgent,
    task: str,
    reset_agent_memory: bool = False,
    additional_args: Optional[dict] = None,
):
    """
    Runs agent and streams messages as gradio ChatMessages.
    
    Args:
        agent: The MultiStepAgent instance to run
        task: User's task/query string
        reset_agent_memory: Whether to clear agent memory before running
        additional_args: Optional additional arguments for the agent
    
    Yields:
        Gradio ChatMessage objects for UI display
    """
    if not _is_package_available("gradio"):
        raise ModuleNotFoundError("Install gradio: `pip install 'smolagents[gradio]'`")
    
    import gradio as gr
    
    try:
        # Run agent and stream steps
        for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
            if isinstance(step_log, ActionStep):
                # Track token usage if available
                if hasattr(agent.model, "last_input_token_count") and hasattr(agent.model, "last_output_token_count"):
                    step_log.input_token_count = agent.model.last_input_token_count
                    step_log.output_token_count = agent.model.last_output_token_count
                
                # Yield messages from this step
                for message in pull_messages_from_step(step_log):
                    yield message
        
        # Handle final output
        final = handle_agent_output_types(step_log)
        if isinstance(final, AgentText):
            content = final.to_string()
        else:
            # Extract the actual content from the final answer
            content = str(final)
            # Remove the wrapper if it exists (e.g., "FinalAnswerStep(final_answer='...')")
            if "final_answer=" in content or "FinalAnswerStep" in content:
                import re
                match = re.search(r"final_answer=['\"](.+?)['\"](?:\)|$)", content, re.DOTALL)
                if match:
                    content = match.group(1)
                elif "final_answer='" in content:
                    # Alternative extraction if regex fails
                    content = content.split("final_answer='", 1)[1].rsplit("')", 1)[0]
                    # Unescape newlines and other escape sequences
                    content = content.encode().decode('unicode_escape')
        
        yield gr.ChatMessage(
            role="assistant",
            content=content,
            metadata={"status": "done"}
        )
    
    except Exception as e:
        # Handle errors gracefully
        error_msg = str(e)
        
        # Provide helpful error messages
        if "500 Internal Server Error" in error_msg or "Bad Request" in error_msg:
            helpful_msg = (
                "⚠️ **API Error:** The model service encountered an issue.\n\n"
                "**Possible fixes:**\n"
                "• Try rephrasing your request more clearly\n"
                "• Include all required details: destination, dates, origin city, budget\n"
                "• Example: *'5-day Barcelona trip from NYC, Oct 15-19, budget $1500 USD'*\n\n"
                f"Technical details: {error_msg}"
            )
        elif "No results found" in error_msg:
            helpful_msg = (
                "⚠️ **Search Issue:** Couldn't find information about the destination.\n\n"
                "Please try again with a different destination or check the spelling."
            )
        else:
            helpful_msg = (
                f"⚠️ **Error:** {error_msg}\n\n"
                "Please try again with a clearer trip description including:\n"
                "• Destination city\n"
                "• Travel dates\n"
                "• Origin city\n"
                "• Budget amount + currency"
            )
        
        yield gr.ChatMessage(
            role="assistant",
            content=helpful_msg,
            metadata={"status": "done"}
        )


class GradioUI:
    """Production-ready Gradio interface for travel agent"""
    
    def __init__(self, agent: MultiStepAgent, file_upload_folder: Optional[str] = None):
        """
        Initialize Gradio UI wrapper.
        
        Args:
            agent: The MultiStepAgent instance to use
            file_upload_folder: Optional folder path for file uploads
        """
        if not _is_package_available("gradio"):
            raise ModuleNotFoundError("Install gradio: `pip install 'smolagents[gradio]'`")
        
        self.agent = agent
        self.file_upload_folder = file_upload_folder
        
        # Create upload folder if specified
        if self.file_upload_folder and not os.path.exists(file_upload_folder):
            os.makedirs(file_upload_folder, exist_ok=True)
    
    def interact_with_agent(self, prompt: str, history: List):
        """
        Handle user interaction with the agent.
        
        Args:
            prompt: User's input text
            history: Conversation history
        
        Yields:
            Updated conversation history
        """
        import gradio as gr
        
        # Add user message to history
        history.append(gr.ChatMessage(role="user", content=prompt))
        yield history
        
        # Stream agent responses
        for msg in stream_to_gradio(self.agent, task=prompt, reset_agent_memory=False):
            history.append(msg)
            yield history
    
    def launch(self, **kwargs):
        """
        Launch the Gradio interface.
        
        Args:
            **kwargs: Additional arguments passed to demo.launch()
        """
        import gradio as gr
        
        # Build UI
        with gr.Blocks(
            title="✈️ Travel Catalogue Creator",
            fill_height=True,
            theme=gr.themes.Soft()
        ) as demo:
            # Header
            gr.Markdown("# ✈️ Smart Travel Catalogue Creator")
            gr.Markdown(
                "Plan your perfect trip with AI: weather forecasts, custom itineraries, "
                "packing lists & visual inspiration"
            )
            
            # Chat interface
            chatbot = gr.Chatbot(
                label="Your Travel Assistant",
                type="messages",
                avatar_images=(
                    None,  # User avatar (default)
                    "https://huggingface.co/datasets/agents-course/course-images/resolve/main/en/communication/Alfred.png",  # Agent avatar
                ),
                height=600,
                show_copy_button=True,
            )
            
            # Input area
            with gr.Row():
                text_input = gr.Textbox(
                    label="Describe your trip",
                    placeholder="e.g., '4-day Barcelona trip from NYC, Oct 15-19, budget $1500 USD'",
                    lines=2,
                )
                submit_btn = gr.Button("Plan My Trip", variant="primary")
            
            # Help text
            gr.Markdown("""
            ### 💡 Tips for best results:
            • **Include:** destination, dates, origin city, budget amount + currency
            • **Example:** *"5-day Lisbon trip from London, Sep 20-24, budget £800 GBP"*
            • **Example:** *"Weekend Paris getaway from Amsterdam, March 10-12, €600 budget"*
            """)
            
            # Connect interactions
            submit_btn.click(
                self.interact_with_agent,
                inputs=[text_input, chatbot],
                outputs=[chatbot],
                show_progress="full",
            )
            
            text_input.submit(
                self.interact_with_agent,
                inputs=[text_input, chatbot],
                outputs=[chatbot],
                show_progress="full",
            )
        
        # Launch
        demo.launch(**kwargs)


# Export public API
__all__ = ["stream_to_gradio", "GradioUI"]