blackccpie commited on
Commit
7224e82
·
1 Parent(s): 4abedca

ini : added base files.

Browse files
Files changed (4) hide show
  1. agent.py +137 -0
  2. agent_ui.py +528 -0
  3. app.py +28 -0
  4. requirements.txt +2 -0
agent.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The MIT License
2
+
3
+ # Copyright (c) 2025 Albert Murienne
4
+
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import os
24
+
25
+ from smolagents import CodeAgent, InferenceClientModel, Tool
26
+ from tavily import TavilyClient
27
+
28
+ # Define a custom tool for Tavily search
29
+ from smolagents import Tool
30
+ from tavily import TavilyClient
31
+
32
+ tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
33
+
34
+ class TavilySearchTool(Tool):
35
+ name = "tavily_search"
36
+ description = "Search the web using Tavily."
37
+ inputs = {
38
+ "query": {
39
+ "type": "string",
40
+ "description": "The search query string.",
41
+ }
42
+ }
43
+ output_type = "string"
44
+
45
+ def forward(self, query: str):
46
+ response = tavily_client.search(query)
47
+ return response
48
+
49
+ class TavilyExtractTool(Tool):
50
+ name = "tavily_extract"
51
+ description = "Extract information from web pages using Tavily."
52
+ inputs = {
53
+ "url": {
54
+ "type": "string",
55
+ "description": "The URL of the web page to extract information from.",
56
+ }
57
+ }
58
+ output_type = "string"
59
+
60
+ def forward(self, url: str):
61
+ response = tavily_client.extract(url)
62
+ return response
63
+
64
+ class TavilyImageURLSearchTool(Tool):
65
+ name = "tavily_image_search"
66
+ description = "Search for most relevant image URL on the web using Tavily."
67
+ inputs = {
68
+ "query": {
69
+ "type": "string",
70
+ "description": "The search query string.",
71
+ }
72
+ }
73
+ output_type = "string"
74
+
75
+ def forward(self, query: str):
76
+ response = tavily_client.search(query, include_images=True)
77
+
78
+ images = response.get("images", [])
79
+
80
+ if images:
81
+ # Return the URL of the first image
82
+ first_image = images[0]
83
+ if isinstance(first_image, dict):
84
+ return first_image.get("url")
85
+ return first_image
86
+ return "none"
87
+
88
+ class SmolAlbert(CodeAgent):
89
+ """
90
+ A specialized CodeAgent that uses Tavily tools and a specific model.
91
+ """
92
+
93
+ #model_id = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
94
+ #model_id = "Qwen/Qwen3-30B-A3B-Thinking-2507"
95
+ model_id = "Qwen/Qwen3-235B-A22B-Instruct-2507"
96
+ provider = "auto"
97
+
98
+ def __init__(self):
99
+ """
100
+ Initialize the SmolAlbert agent with Tavily tools and a model.
101
+ """
102
+ # Set up the agent with the Tavily tool and a model
103
+ api_key = os.getenv("TAVILY_API_KEY")
104
+ search_tool = TavilySearchTool()
105
+ image_search_tool = TavilyImageURLSearchTool()
106
+ extract_tool = TavilyExtractTool()
107
+ model = InferenceClientModel(model_id=self.model_id, provider=self.provider)
108
+ self.agent = CodeAgent(
109
+ tools=[search_tool, image_search_tool, extract_tool],
110
+ model=model,
111
+ stream_outputs=True,
112
+ instructions=(
113
+ "When writing the final answer, including the most relevant URL(s) "
114
+ "from your search results as inline Markdown hyperlinks is MANDATORY. "
115
+ "Example format: ... (see [1](https://example1.com)) ... (see [2](https://example2.com)) ... "
116
+ "Do not invent URL(s) — only use the ones you were provided."
117
+ "If the answer includes an image URL, include it as an inline Markdown image: ![image](<image_url>)"
118
+ )
119
+ )
120
+
121
+ def run(self, task: str, additional_args: dict | None = None) -> str:
122
+ """
123
+ Run the agent with a given query and return the final answer.
124
+ """
125
+ return self.agent.run(
126
+ task=task,
127
+ stream=True,
128
+ reset=False,
129
+ max_steps=5,
130
+ additional_args=additional_args
131
+ )
132
+
133
+ def reset(self):
134
+ """
135
+ Reset the agent's internal state.
136
+ """
137
+ self.agent.memory.reset()
agent_ui.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # The MIT License
16
+
17
+ # Copyright (c) 2025 Albert Murienne
18
+
19
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ # of this software and associated documentation files (the "Software"), to deal
21
+ # in the Software without restriction, including without limitation the rights
22
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ # copies of the Software, and to permit persons to whom the Software is
24
+ # furnished to do so, subject to the following conditions:
25
+
26
+ # The above copyright notice and this permission notice shall be included in
27
+ # all copies or substantial portions of the Software.
28
+
29
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ # THE SOFTWARE.
36
+
37
+ import re
38
+ from typing import Generator
39
+
40
+ from smolagents.agent_types import AgentAudio, AgentImage, AgentText
41
+ from smolagents.agents import MultiStepAgent, PlanningStep
42
+ from smolagents.memory import ActionStep, FinalAnswerStep
43
+ from smolagents.models import ChatMessageStreamDelta, MessageRole, agglomerate_stream_deltas
44
+
45
+ def get_step_footnote_content(step_log: ActionStep | PlanningStep, step_name: str) -> str:
46
+ """Get a footnote string for a step log with duration and token information"""
47
+ step_footnote = f"**{step_name}**"
48
+ if step_log.token_usage is not None:
49
+ step_footnote += f" | Input tokens: {step_log.token_usage.input_tokens:,} | Output tokens: {step_log.token_usage.output_tokens:,}"
50
+ step_footnote += f" | Duration: {round(float(step_log.timing.duration), 2)}s" if step_log.timing.duration else ""
51
+ step_footnote_content = f"""<span style="color: #bbbbc2; font-size: 12px;">{step_footnote}</span> """
52
+ return step_footnote_content
53
+
54
+
55
+ def _clean_model_output(model_output: str) -> str:
56
+ """
57
+ Clean up model output by removing trailing tags and extra backticks.
58
+
59
+ Args:
60
+ model_output (`str`): Raw model output.
61
+
62
+ Returns:
63
+ `str`: Cleaned model output.
64
+ """
65
+ if not model_output:
66
+ return ""
67
+ model_output = model_output.strip()
68
+ # Remove any trailing <end_code> and extra backticks, handling multiple possible formats
69
+ model_output = re.sub(r"```\s*<end_code>", "```", model_output) # handles ```<end_code>
70
+ model_output = re.sub(r"<end_code>\s*```", "```", model_output) # handles <end_code>```
71
+ model_output = re.sub(r"```\s*\n\s*<end_code>", "```", model_output) # handles ```\n<end_code>
72
+ return model_output.strip()
73
+
74
+
75
+ def _format_code_content(content: str) -> str:
76
+ """
77
+ Format code content as Python code block if it's not already formatted.
78
+
79
+ Args:
80
+ content (`str`): Code content to format.
81
+
82
+ Returns:
83
+ `str`: Code content formatted as a Python code block.
84
+ """
85
+ content = content.strip()
86
+ # Remove existing code blocks and end_code tags
87
+ content = re.sub(r"```.*?\n", "", content)
88
+ content = re.sub(r"\s*<end_code>\s*", "", content)
89
+ content = content.strip()
90
+ # Add Python code block formatting if not already present
91
+ if not content.startswith("```python"):
92
+ content = f"```python\n{content}\n```"
93
+ return content
94
+
95
+
96
+ def _process_action_step(step_log: ActionStep, skip_model_outputs: bool = False) -> Generator:
97
+ """
98
+ Process an [`ActionStep`] and yield appropriate Gradio ChatMessage objects.
99
+
100
+ Args:
101
+ step_log ([`ActionStep`]): ActionStep to process.
102
+ skip_model_outputs (`bool`): Whether to skip model outputs.
103
+
104
+ Yields:
105
+ `gradio.ChatMessage`: Gradio ChatMessages representing the action step.
106
+ """
107
+ import gradio as gr
108
+
109
+ # Output the step number
110
+ step_number = f"Step {step_log.step_number}"
111
+ if not skip_model_outputs:
112
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=f"**{step_number}**", metadata={"status": "done"})
113
+
114
+ # First yield the thought/reasoning from the LLM
115
+ if not skip_model_outputs and getattr(step_log, "model_output", ""):
116
+ model_output = _clean_model_output(step_log.model_output)
117
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=model_output, metadata={"status": "done"})
118
+
119
+ # For tool calls, create a parent message
120
+ if getattr(step_log, "tool_calls", []):
121
+ first_tool_call = step_log.tool_calls[0]
122
+ used_code = first_tool_call.name == "python_interpreter"
123
+
124
+ # Process arguments based on type
125
+ args = first_tool_call.arguments
126
+ if isinstance(args, dict):
127
+ content = str(args.get("answer", str(args)))
128
+ else:
129
+ content = str(args).strip()
130
+
131
+ # Format code content if needed
132
+ if used_code:
133
+ content = _format_code_content(content)
134
+
135
+ # Create the tool call message
136
+ parent_message_tool = gr.ChatMessage(
137
+ role=MessageRole.ASSISTANT,
138
+ content=content,
139
+ metadata={
140
+ "title": f"🛠️ Used tool {first_tool_call.name}",
141
+ "status": "done",
142
+ },
143
+ )
144
+ yield parent_message_tool
145
+
146
+ # Display execution logs if they exist
147
+ if getattr(step_log, "observations", "") and step_log.observations.strip():
148
+ log_content = step_log.observations.strip()
149
+ if log_content:
150
+ log_content = re.sub(r"^Execution logs:\s*", "", log_content)
151
+ yield gr.ChatMessage(
152
+ role=MessageRole.ASSISTANT,
153
+ content=f"```bash\n{log_content}\n",
154
+ metadata={"title": "📝 Execution Logs", "status": "done"},
155
+ )
156
+
157
+ # Display any images in observations
158
+ if getattr(step_log, "observations_images", []):
159
+ for image in step_log.observations_images:
160
+ path_image = AgentImage(image).to_string()
161
+ yield gr.ChatMessage(
162
+ role=MessageRole.ASSISTANT,
163
+ content={"path": path_image, "mime_type": f"image/{path_image.split('.')[-1]}"},
164
+ metadata={"title": "🖼️ Output Image", "status": "done"},
165
+ )
166
+
167
+ # Handle errors
168
+ if getattr(step_log, "error", None):
169
+ yield gr.ChatMessage(
170
+ role=MessageRole.ASSISTANT, content=str(step_log.error), metadata={"title": "💥 Error", "status": "done"}
171
+ )
172
+
173
+ # Add step footnote and separator
174
+ yield gr.ChatMessage(
175
+ role=MessageRole.ASSISTANT,
176
+ content=get_step_footnote_content(step_log, step_number),
177
+ metadata={"status": "done"},
178
+ )
179
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content="-----", metadata={"status": "done"})
180
+
181
+
182
+ def _process_planning_step(step_log: PlanningStep, skip_model_outputs: bool = False) -> Generator:
183
+ """
184
+ Process a [`PlanningStep`] and yield appropriate gradio.ChatMessage objects.
185
+
186
+ Args:
187
+ step_log ([`PlanningStep`]): PlanningStep to process.
188
+
189
+ Yields:
190
+ `gradio.ChatMessage`: Gradio ChatMessages representing the planning step.
191
+ """
192
+ import gradio as gr
193
+
194
+ if not skip_model_outputs:
195
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content="**Planning step**", metadata={"status": "done"})
196
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=step_log.plan, metadata={"status": "done"})
197
+ yield gr.ChatMessage(
198
+ role=MessageRole.ASSISTANT,
199
+ content=get_step_footnote_content(step_log, "Planning step"),
200
+ metadata={"status": "done"},
201
+ )
202
+ yield gr.ChatMessage(role=MessageRole.ASSISTANT, content="-----", metadata={"status": "done"})
203
+
204
+
205
+ def _process_final_answer_step(step_log: FinalAnswerStep) -> Generator:
206
+ """
207
+ Process a [`FinalAnswerStep`] and yield appropriate gradio.ChatMessage objects.
208
+
209
+ Args:
210
+ step_log ([`FinalAnswerStep`]): FinalAnswerStep to process.
211
+
212
+ Yields:
213
+ `gradio.ChatMessage`: Gradio ChatMessages representing the final answer.
214
+ """
215
+ import gradio as gr
216
+
217
+ final_answer = step_log.output
218
+ if isinstance(final_answer, AgentText):
219
+ yield gr.ChatMessage(
220
+ role=MessageRole.ASSISTANT,
221
+ content=f"**Final answer:**\n{final_answer.to_string()}\n",
222
+ metadata={"status": "done"},
223
+ )
224
+ elif isinstance(final_answer, AgentImage):
225
+ yield gr.ChatMessage(
226
+ role=MessageRole.ASSISTANT,
227
+ content={"path": final_answer.to_string(), "mime_type": "image/png"},
228
+ metadata={"status": "done"},
229
+ )
230
+ elif isinstance(final_answer, AgentAudio):
231
+ yield gr.ChatMessage(
232
+ role=MessageRole.ASSISTANT,
233
+ content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
234
+ metadata={"status": "done"},
235
+ )
236
+ else:
237
+ yield gr.ChatMessage(
238
+ role=MessageRole.ASSISTANT, content=f"**Final answer:** {str(final_answer)}", metadata={"status": "done"}
239
+ )
240
+
241
+
242
+ def pull_messages_from_step(step_log: ActionStep | PlanningStep | FinalAnswerStep, skip_model_outputs: bool = False):
243
+ """Extract Gradio ChatMessage objects from agent steps with proper nesting.
244
+
245
+ Args:
246
+ step_log: The step log to display as gr.ChatMessage objects.
247
+ skip_model_outputs: If True, skip the model outputs when creating the gr.ChatMessage objects:
248
+ This is used for instance when streaming model outputs have already been displayed.
249
+ """
250
+ if isinstance(step_log, ActionStep):
251
+ yield from _process_action_step(step_log, skip_model_outputs)
252
+ elif isinstance(step_log, PlanningStep):
253
+ yield from _process_planning_step(step_log, skip_model_outputs)
254
+ elif isinstance(step_log, FinalAnswerStep):
255
+ yield from _process_final_answer_step(step_log)
256
+ else:
257
+ raise ValueError(f"Unsupported step type: {type(step_log)}")
258
+
259
+
260
+ def stream_to_gradio(
261
+ agent,
262
+ task: str,
263
+ additional_args: dict | None = None,
264
+ ) -> Generator:
265
+ """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
266
+
267
+ accumulated_events: list[ChatMessageStreamDelta] = []
268
+ for event in agent.run(task, additional_args=additional_args):
269
+ if isinstance(event, ActionStep | PlanningStep | FinalAnswerStep):
270
+ for message in pull_messages_from_step(
271
+ event,
272
+ # If we're streaming model outputs, no need to display them twice
273
+ skip_model_outputs=getattr(agent, "stream_outputs", False),
274
+ ):
275
+ yield message
276
+ accumulated_events = []
277
+ elif isinstance(event, ChatMessageStreamDelta):
278
+ accumulated_events.append(event)
279
+ text = agglomerate_stream_deltas(accumulated_events).render_as_markdown()
280
+ yield text
281
+
282
+
283
+ class AgentUI:
284
+ """
285
+ Gradio interface for interacting with a [`MultiStepAgent`].
286
+
287
+ This class provides a web interface to interact with the agent in real-time, allowing users to submit prompts, and receive responses in a chat-like format.
288
+ It can reset the agent's memory at the start of each interaction if desired.
289
+ It uses the [`gradio.Chatbot`] component to display the conversation history.
290
+ This class requires the `gradio` extra to be installed: `pip install 'smolagents[gradio]'`.
291
+
292
+ Args:
293
+ agent ([`MultiStepAgent`]): The agent to interact with.
294
+ """
295
+
296
+ def __init__(self, agent: MultiStepAgent):
297
+ self.agent = agent
298
+ self.description = getattr(agent, "description", None)
299
+
300
+ def interact_with_agent(self, prompt, verbose_messages, quiet_messages):
301
+ """
302
+ Interacts with the agent and streams results into two separate histories:
303
+ - verbose_messages: full reasoning stream (Chatterbox)
304
+ - quiet_messages: only user prompt + final answer (Quiet)
305
+ Quiet is enhanced with pending "Step N..." indicators only (no generic thinking text).
306
+ """
307
+ import gradio as gr
308
+
309
+ try:
310
+ # Append the user message to both histories (quiet keeps the user query)
311
+ user_msg = gr.ChatMessage(role="user", content=prompt, metadata={"status": "done"})
312
+ verbose_messages.append(user_msg)
313
+ quiet_messages.append(user_msg)
314
+
315
+ # yield initial state to update UI immediately
316
+ yield verbose_messages, quiet_messages
317
+
318
+ quiet_pending_idx = None
319
+
320
+ for msg in stream_to_gradio(self.agent, task=prompt):
321
+
322
+ # Full gr.ChatMessage object (from steps) — append to verbose always
323
+ if isinstance(msg, gr.ChatMessage):
324
+ # Mark last verbose pending -> done if needed and append
325
+ if verbose_messages and verbose_messages[-1].metadata.get("status") == "pending":
326
+ verbose_messages[-1].metadata["status"] = "done"
327
+ verbose_messages[-1].content = msg.content
328
+ else:
329
+ verbose_messages.append(msg)
330
+
331
+ content_text = msg.content if isinstance(msg.content, str) else ""
332
+
333
+ # Detect final answer messages and append to quiet
334
+ # HACK : FinalAnswerStep messages are produced by _process_final_answer_step and use "**Final answer:**" text
335
+ if "final answer" in content_text.lower():
336
+ # Replace pending with final answer in Quiet
337
+ final_msg = gr.ChatMessage(role=MessageRole.ASSISTANT, content=content_text, metadata={"status": "done"})
338
+ if quiet_pending_idx is not None:
339
+ quiet_messages[quiet_pending_idx] = final_msg
340
+ quiet_pending_idx = None
341
+ else:
342
+ quiet_messages.append(final_msg)
343
+ else:
344
+ # Look for "Step <number>" pattern
345
+ match = re.search(r"\bStep\s*(\d+)\b", content_text, re.IGNORECASE)
346
+ if match:
347
+ step_num = match.group(1)
348
+ pending_text = f"⏳ Step {step_num}..."
349
+ if quiet_pending_idx is None:
350
+ quiet_messages.append(
351
+ gr.ChatMessage(
352
+ role=MessageRole.ASSISTANT,
353
+ content=pending_text,
354
+ metadata={"status": "pending"},
355
+ )
356
+ )
357
+ quiet_pending_idx = len(quiet_messages) - 1
358
+ else:
359
+ quiet_messages[quiet_pending_idx].content = pending_text
360
+
361
+ elif isinstance(msg, str):
362
+ text = msg.replace("<", r"\<").replace(">", r"\>")
363
+ if verbose_messages and verbose_messages[-1].metadata.get("status") == "pending":
364
+ verbose_messages[-1].content = text
365
+ else:
366
+ verbose_messages.append(
367
+ gr.ChatMessage(role=MessageRole.ASSISTANT, content=text, metadata={"status": "pending"})
368
+ )
369
+ yield verbose_messages, quiet_messages
370
+
371
+ # final yield to ensure both UIs are up-to-date
372
+ yield verbose_messages, quiet_messages
373
+
374
+ except Exception as e:
375
+ # ensure UIs don't hang if something failed
376
+ yield verbose_messages, quiet_messages
377
+ raise gr.Error(f"Error in interaction: {str(e)}")
378
+
379
+ def clear_history(self):
380
+ """
381
+ Clear the chat history and reset the agent's memory.
382
+ """
383
+ self.agent.reset()
384
+ return [], []
385
+
386
+ def disable_query(self, text_input):
387
+ """
388
+ Disable the text input and submit button while the agent is processing.
389
+ """
390
+ import gradio as gr
391
+
392
+ return (
393
+ text_input,
394
+ gr.Textbox(
395
+ value="",
396
+ placeholder="Wait for answer completion before submitting a new prompt...",
397
+ interactive=False
398
+ ),
399
+ gr.Button(interactive=False),
400
+ )
401
+
402
+ def enable_query(self):
403
+ """
404
+ Enable the text input and submit button after the agent has finished processing.
405
+ """
406
+ import gradio as gr
407
+
408
+ return (
409
+ gr.Textbox(
410
+ interactive=True,
411
+ placeholder="Enter your prompt here and press Shift+Enter or the button"
412
+ ),
413
+ gr.Button(interactive=True),
414
+ )
415
+
416
+ def launch(self, share: bool = True, **kwargs):
417
+ """
418
+ Launch the Gradio app with the agent interface.
419
+
420
+ Args:
421
+ share (`bool`, defaults to `True`): Whether to share the app publicly.
422
+ **kwargs: Additional keyword arguments to pass to the Gradio launch method.
423
+ """
424
+ self.create_app().launch(debug=True, share=share, **kwargs)
425
+
426
+ def create_app(self):
427
+ import gradio as gr
428
+
429
+ with gr.Blocks(theme="glass", fill_height=True) as agent:
430
+
431
+ # Set up states to hold the session information
432
+ stored_query = gr.State("") # current user query
433
+ stored_messages_verbose = gr.State([]) # full reasoning history
434
+ stored_messages_quiet = gr.State([]) # only user + final answer
435
+
436
+ with gr.Sidebar():
437
+ gr.Markdown(
438
+ "# SmolAlbert 🤖"
439
+ )
440
+
441
+ with gr.Group():
442
+ gr.Markdown("**Your request**", container=True)
443
+ text_input = gr.Textbox(
444
+ lines=3,
445
+ label="Chat Message",
446
+ container=False,
447
+ placeholder="Enter your prompt here and press Shift+Enter or press the button",
448
+ )
449
+ submit_btn = gr.Button("Submit", variant="primary")
450
+
451
+ gr.HTML(
452
+ "<br><br><h4><center>Powered by <a target='_blank' href='https://github.com/huggingface/smolagents'><b>smolagents</b></a></center></h4>"
453
+ )
454
+
455
+ with gr.Tab("Quiet", scale=1):
456
+ quiet_chatbot = gr.Chatbot(
457
+ label="Agent",
458
+ type="messages",
459
+ avatar_images=(
460
+ None,
461
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
462
+ ),
463
+ resizeable=True,
464
+ scale=1,
465
+ latex_delimiters=[
466
+ {"left": r"$$", "right": r"$$", "display": True},
467
+ {"left": r"$", "right": r"$", "display": False},
468
+ {"left": r"\[", "right": r"\]", "display": True},
469
+ {"left": r"\(", "right": r"\)", "display": False},
470
+ ],
471
+ )
472
+
473
+ with gr.Tab("Chatterbox", scale=1):
474
+
475
+ # Main chat interface
476
+ verbose_chatbot = gr.Chatbot(
477
+ label="Agent",
478
+ type="messages",
479
+ avatar_images=(
480
+ None,
481
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
482
+ ),
483
+ resizeable=True,
484
+ scale=1,
485
+ latex_delimiters=[
486
+ {"left": r"$$", "right": r"$$", "display": True},
487
+ {"left": r"$", "right": r"$", "display": False},
488
+ {"left": r"\[", "right": r"\]", "display": True},
489
+ {"left": r"\(", "right": r"\)", "display": False},
490
+ ],
491
+ )
492
+
493
+ # Main input handlers: call interact_with_agent(prompt, verbose_state, quiet_state)
494
+ text_input.submit(
495
+ self.disable_query,
496
+ text_input,
497
+ [stored_query, text_input, submit_btn]
498
+ ).then(
499
+ self.interact_with_agent,
500
+ [stored_query, stored_messages_verbose, stored_messages_quiet],
501
+ [verbose_chatbot, quiet_chatbot],
502
+ ).then(
503
+ self.enable_query,
504
+ None,
505
+ [text_input, submit_btn],
506
+ )
507
+
508
+ submit_btn.click(
509
+ self.disable_query,
510
+ text_input,
511
+ [stored_query, text_input, submit_btn]
512
+ ).then(
513
+ self.interact_with_agent,
514
+ [stored_query, stored_messages_verbose, stored_messages_quiet],
515
+ [verbose_chatbot, quiet_chatbot],
516
+ ).then(
517
+ self.enable_query,
518
+ None,
519
+ [text_input, submit_btn],
520
+ )
521
+
522
+ # bind clears to both chat components so agent memory is reset
523
+ quiet_chatbot.clear(self.clear_history, inputs=None, outputs=[stored_messages_verbose, stored_messages_quiet])
524
+ verbose_chatbot.clear(self.clear_history, inputs=None, outputs=[stored_messages_verbose, stored_messages_quiet])
525
+
526
+ return agent
527
+
528
+ __all__ = ["stream_to_gradio", "AgentUI"]
app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The MIT License
2
+
3
+ # Copyright (c) 2025 Albert Murienne
4
+
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ from agent import SmolAlbert
24
+ from agent_ui import AgentUI
25
+
26
+ agent = SmolAlbert()
27
+ agent_ui = AgentUI(agent)
28
+ agent_ui.launch(share=False)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ smolagents==1.21.1
2
+ tavily-python==0.7.10