UnvibeCodeGitHub ↗

Business Workflow Map

Understand end-to-end business operations reconstructed from the code.

smolagents-main · 30 workflows · 15 areas
Workflow area 1

Agent workflows

4 workflows
01
Workflow 1

agent memory and replay

What this workflow does

Agent run steps are recorded in AgentMemory, converted back to messages, and replayed through logging and code reconstruction.

Purpose

preserve and inspect an agent’s task, planning, action, and final-answer history

What it produces

AgentMemory steps and code actions

Who relies on it

agent developer / operator

Why this workflow matters

  • supports debugging and auditability of agent runs
  • enables reconstruction of executable code from prior actions
  • preserves step history for summaries and replays
View supporting code
src/smolagents/memory.py · lines 228–246 · AgentMemory
   228 |     def __init__(self, system_prompt: str):
   229 |         self.system_prompt: SystemPromptStep = SystemPromptStep(system_prompt=system_prompt)
   230 |         self.steps: list[TaskStep | ActionStep | PlanningStep] = []
   231 | 
   232 |     def reset(self):
   233 |         """Reset the agent's memory, clearing all steps and keeping the system prompt."""
   234 |         self.steps = []
   235 | 
   236 |     def get_succinct_steps(self) -> list[dict]:
   237 |         """Return a succinct representation of the agent's steps, excluding model input messages."""
   238 |         return [
   239 |             {key: value for key, value in step.dict().items() if key != "model_input_messages"} for step in self.steps
   240 |         ]
   241 | 
   242 |     def get_full_steps(self) -> list[dict]:
   243 |         """Return a full representation of the agent's steps, including model input messages."""
   244 |         if len(self.steps) == 0:
   245 |             return []
   246 |         return [step.dict() for step in self.steps]
src/smolagents/memory.py · lines 248–277 · AgentMemory
   248 |     def replay(self, logger: AgentLogger, detailed: bool = False):
   249 |         """Prints a pretty replay of the agent's steps.
   250 | 
   251 |         Args:
   252 |             logger (`AgentLogger`): The logger to print replay logs to.
   253 |             detailed (`bool`, default `False`): If True, also displays the memory at each step. Defaults to False.
   254 |                 Careful: will increase log length exponentially. Use only for debugging.
   255 |         """
   256 |         logger.console.log("Replaying the agent's steps:")
   257 |         logger.log_markdown(title="System prompt", content=self.system_prompt.system_prompt, level=LogLevel.ERROR)
   258 |         for step in self.steps:
   259 |             if isinstance(step, TaskStep):
   260 |                 logger.log_task(step.task, "", level=LogLevel.ERROR)
   261 |             elif isinstance(step, ActionStep):
   262 |                 logger.log_rule(f"Step {step.step_number}", level=LogLevel.ERROR)
   263 |                 if detailed and step.model_input_messages is not None:
   264 |                     logger.log_messages(step.model_input_messages, level=LogLevel.ERROR)
   265 |                 if step.model_output is not None:
   266 |                     logger.log_markdown(title="Agent output:", content=step.model_output, level=LogLevel.ERROR)
   267 |             elif isinstance(step, PlanningStep):
   268 |                 logger.log_rule("Planning step", level=LogLevel.ERROR)
   269 |                 if detailed and step.model_input_messages is not None:
   270 |                     logger.log_messages(step.model_input_messages, level=LogLevel.ERROR)
   271 |                 logger.log_markdown(title="Agent output:", content=step.plan, level=LogLevel.ERROR)
   272 | 
   273 |     def return_full_code(self) -> str:
   274 |         """Returns all code actions from the agent's steps, concatenated as a single script."""
   275 |         return "\n\n".join(
   276 |             [step.code_action for step in self.steps if isinstance(step, ActionStep) and step.code_action is not None]
   277 |         )
src/smolagents/memory.py · lines 214–230 · AgentMemory
   214 | class AgentMemory:
   215 |     """Memory for the agent, containing the system prompt and all steps taken by the agent.
   216 | 
   217 |     This class is used to store the agent's steps, including tasks, actions, and planning steps.
   218 |     It allows for resetting the memory, retrieving succinct or full step information, and replaying the agent's steps.
   219 | 
   220 |     Args:
   221 |         system_prompt (`str`): System prompt for the agent, which sets the context and instructions for the agent's behavior.
   222 | 
   223 |     **Attributes**:
   224 |         - **system_prompt** (`SystemPromptStep`) -- System prompt step for the agent.
   225 |         - **steps** (`list[TaskStep | ActionStep | PlanningStep]`) -- List of steps taken by the agent, which can include tasks, actions, and planning steps.
   226 |     """
   227 | 
   228 |     def __init__(self, system_prompt: str):
   229 |         self.system_prompt: SystemPromptStep = SystemPromptStep(system_prompt=system_prompt)
   230 |         self.steps: list[TaskStep | ActionStep | PlanningStep] = []
src/smolagents/memory.py · lines 224–230 · AgentMemory
   224 |         - **system_prompt** (`SystemPromptStep`) -- System prompt step for the agent.
   225 |         - **steps** (`list[TaskStep | ActionStep | PlanningStep]`) -- List of steps taken by the agent, which can include tasks, actions, and planning steps.
   226 |     """
   227 | 
   228 |     def __init__(self, system_prompt: str):
   229 |         self.system_prompt: SystemPromptStep = SystemPromptStep(system_prompt=system_prompt)
   230 |         self.steps: list[TaskStep | ActionStep | PlanningStep] = []
src/smolagents/memory.py · lines 51–64 · ActionStep
    51 | class ActionStep(MemoryStep):
    52 |     step_number: int
    53 |     timing: Timing
    54 |     model_input_messages: list[ChatMessage] | None = None
    55 |     tool_calls: list[ToolCall] | None = None
    56 |     error: AgentError | None = None
    57 |     model_output_message: ChatMessage | None = None
    58 |     model_output: str | list[dict[str, Any]] | None = None
    59 |     code_action: str | None = None
    60 |     observations: str | None = None
    61 |     observations_images: list["PIL.Image.Image"] | None = None
    62 |     action_output: Any = None
    63 |     token_usage: TokenUsage | None = None
    64 |     is_final_answer: bool = False
src/smolagents/memory.py · lines 273–277 · AgentMemory.return_full_code
   273 |     def return_full_code(self) -> str:
   274 |         """Returns all code actions from the agent's steps, concatenated as a single script."""
   275 |         return "\n\n".join(
   276 |             [step.code_action for step in self.steps if isinstance(step, ActionStep) and step.code_action is not None]
   277 |         )
02
Workflow 2

multi-step agent execution

What this workflow does

MultiStepAgent runs a task through planning, action/tool use, observation, and final-answer generation, producing a RunResult or output string.

Purpose

solve a user task step by step using an LLM and tools

What it produces

agent run / RunResult / memory steps

Who relies on it

task requester / downstream tool targets

Why this workflow matters

  • produces a final answer
  • records token usage, timing, and step history
  • can be exported or replayed for reuse and inspection
View supporting code
src/smolagents/agents.py · lines 268–352 · MultiStepAgent
   268 | class MultiStepAgent(ABC):
   269 |     """
   270 |     Agent class that solves the given task step by step, using the ReAct framework:
   271 |     While the objective is not reached, the agent will perform a cycle of action (given by the LLM) and observation (obtained from the environment).
   272 | 
   273 |     Args:
   274 |         tools (`list[Tool]`): [`Tool`]s that the agent can use.
   275 |         model (`Callable[[list[dict[str, str]]], ChatMessage]`): Model that will generate the agent's actions.
   276 |         prompt_templates ([`~agents.PromptTemplates`], *optional*): Prompt templates.
   277 |         instructions (`str`, *optional*): Custom instructions for the agent, will be inserted in the system prompt.
   278 |         max_steps (`int`, default `20`): Maximum number of steps the agent can take to solve the task.
   279 |         add_base_tools (`bool`, default `False`): Whether to add the base tools to the agent's tools.
   280 |         verbosity_level (`LogLevel`, default `LogLevel.INFO`): Level of verbosity of the agent's logs.
   281 |         managed_agents (`list`, *optional*): Managed agents that the agent can call.
   282 |         step_callbacks (`list[Callable]` | `dict[Type[MemoryStep], Callable | list[Callable]]`, *optional*): Callbacks that will be called at each step.
   283 |         planning_interval (`int`, *optional*): Interval at which the agent will run a planning step.
   284 |         name (`str`, *optional*): Necessary for a managed agent only - the name by which this agent can be called.
   285 |         description (`str`, *optional*): Necessary for a managed agent only - the description of this agent.
   286 |         provide_run_summary (`bool`, *optional*): Whether to provide a run summary when called as a managed agent.
   287 |         final_answer_checks (`list[Callable]`, *optional*): List of validation functions to run before accepting a final answer.
   288 |             Each function should:
   289 |             - Take the final answer, the agent's memory, and the agent itself as arguments.
   290 |             - Return a boolean indicating whether the final answer is valid.
   291 |         return_full_result (`bool`, default `False`): Whether to return the full [`RunResult`] object or just the final answer output from the agent run.
   292 |     """
   293 | 
   294 |     def __init__(
   295 |         self,
   296 |         tools: list[Tool],
   297 |         model: Model,
   298 |         prompt_templates: PromptTemplates | None = None,
   299 |         instructions: str | None = None,
       | … additional lines omitted from this preview …
src/smolagents/agents.py · lines 436–538 · run
   436 |     def run(
   437 |         self,
   438 |         task: str,
   439 |         stream: bool = False,
   440 |         reset: bool = True,
   441 |         images: list["PIL.Image.Image"] | None = None,
   442 |         additional_args: dict | None = None,
   443 |         max_steps: int | None = None,
   444 |         return_full_result: bool | None = None,
   445 |     ) -> Any | RunResult:
   446 |         """
   447 |         Run the agent for the given task.
   448 | 
   449 |         Args:
   450 |             task (`str`): Task to perform.
   451 |             stream (`bool`): Whether to run in streaming mode.
   452 |                 If `True`, returns a generator that yields each step as it is executed. You must iterate over this generator to process the individual steps (e.g., using a for loop or `next()`).
   453 |                 If `False`, executes all steps internally and returns only the final answer after completion.
   454 |             reset (`bool`): Whether to reset the conversation or keep it going from previous run.
   455 |             images (`list[PIL.Image.Image]`, *optional*): Image(s) objects.
   456 |             additional_args (`dict`, *optional*): Any other variables that you want to pass to the agent run, for instance images or dataframes. Give them clear names!
   457 |             max_steps (`int`, *optional*): Maximum number of steps the agent can take to solve the task. if not provided, will use the agent's default value.
   458 |             return_full_result (`bool`, *optional*): Whether to return the full [`RunResult`] object or just the final answer output.
   459 |                 If `None` (default), the agent's `self.return_full_result` setting is used.
   460 | 
   461 |         Example:
   462 |         ```py
   463 |         from smolagents import CodeAgent
   464 |         agent = CodeAgent(tools=[])
   465 |         agent.run("What is the result of 2 power 3.7384?")
   466 |         ```
   467 |         """
       | … additional lines omitted from this preview …
src/smolagents/agents.py · lines 331–343 · MultiStepAgent.__init__
   331 |         self.state: dict[str, Any] = {}
   332 |         self.name = self._validate_name(name)
   333 |         self.description = description
   334 |         self.provide_run_summary = provide_run_summary
   335 |         self.final_answer_checks = final_answer_checks if final_answer_checks is not None else []
   336 |         self.return_full_result = return_full_result
   337 |         self.instructions = instructions
   338 |         self._setup_managed_agents(managed_agents)
   339 |         self._setup_tools(tools, add_base_tools)
   340 |         self._validate_tools_and_managed_agents(tools, managed_agents)
   341 | 
   342 |         self.task: str | None = None
   343 |         self.memory = AgentMemory(self.system_prompt)
src/smolagents/agents.py · lines 468–488 · run
   468 |         max_steps = max_steps or self.max_steps
   469 |         self.task = task
   470 |         self.interrupt_switch = False
   471 |         if additional_args:
   472 |             self.state.update(additional_args)
   473 |             self.task += f"""
   474 | You have been provided with these additional arguments, that you can access directly using the keys as variables:
   475 | {str(additional_args)}."""
   476 | 
   477 |         self.memory.system_prompt = SystemPromptStep(system_prompt=self.system_prompt)
   478 |         if reset:
   479 |             self.memory.reset()
   480 |             self.monitor.reset()
   481 | 
   482 |         self.logger.log_task(
   483 |             content=self.task.strip(),
   484 |             subtitle=f"{type(self.model).__name__} - {(self.model.model_id if hasattr(self.model, 'model_id') else '')}",
   485 |             level=LogLevel.INFO,
   486 |             title=self.name if hasattr(self, "name") else None,
   487 |         )
   488 |         self.memory.steps.append(TaskStep(task=self.task, task_images=images))
src/smolagents/agents.py · lines 195–253 · RunResult
   195 | @dataclass
   196 | class RunResult:
   197 |     """Holds extended information about an agent run.
   198 | 
   199 |     Attributes:
   200 |         output (Any | None): The final output of the agent run, if available.
   201 |         state (Literal["success", "max_steps_error"]): The final state of the agent after the run.
   202 |         steps (list[dict]): The agent's memory, as a list of steps.
   203 |         token_usage (TokenUsage | None): Count of tokens used during the run.
   204 |         timing (Timing): Timing details of the agent run: start time, end time, duration.
   205 |         messages (list[dict]): The agent's memory, as a list of messages.
   206 |             <Deprecated version="1.22.0">
   207 |             Parameter 'messages' is deprecated and will be removed in version 1.25. Please use 'steps' instead.
   208 |             </Deprecated>
   209 |     """
   210 | 
   211 |     output: Any | None
   212 |     state: Literal["success", "max_steps_error"]
   213 |     steps: list[dict]
   214 |     token_usage: TokenUsage | None
   215 |     timing: Timing
   216 | 
   217 |     def __init__(self, output=None, state=None, steps=None, token_usage=None, timing=None, messages=None):
   218 |         # Handle deprecated 'messages' parameter
   219 |         if messages is not None:
   220 |             if steps is not None:
   221 |                 raise ValueError("Cannot specify both 'messages' and 'steps' parameters. Use 'steps' instead.")
   222 |             warnings.warn(
   223 |                 "Parameter 'messages' is deprecated and will be removed in version 1.25. Please use 'steps' instead.",
   224 |                 FutureWarning,
   225 |                 stacklevel=2,
   226 |             )
       | … additional lines omitted from this preview …
src/smolagents/agents.py · lines 523–536 · run
   523 |             if self.memory.steps and isinstance(getattr(self.memory.steps[-1], "error", None), AgentMaxStepsError):
   524 |                 state = "max_steps_error"
   525 |             else:
   526 |                 state = "success"
   527 | 
   528 |             step_dicts = self.memory.get_full_steps()
   529 | 
   530 |             return RunResult(
   531 |                 output=output,
   532 |                 token_usage=token_usage,
   533 |                 steps=step_dicts,
   534 |                 timing=Timing(start_time=run_start_time, end_time=time.time()),
   535 |                 state=state,
   536 |             )
03
Workflow 3

agent output typing and media serialization

What this workflow does

Agent-returned text, image, and audio values are normalized into agent-specific wrapper types and serialized to raw/string forms.

Purpose

make heterogeneous model outputs consumable by downstream UI/execution code

What it produces

AgentText, AgentImage, AgentAudio

Who relies on it

agent runtime and UI consumers

Why this workflow matters

  • preserves semantic type of outputs
  • serializes images/audio to filesystem paths for transport
  • supports notebook display and downstream tool handling
View supporting code
src/smolagents/agent_types.py · lines 263–281 · handle_agent_output_types
   263 | def handle_agent_output_types(output: Any, output_type: str | None = None) -> Any:
   264 |     if output_type in _AGENT_TYPE_MAPPING:
   265 |         # If the class has defined outputs, we can map directly according to the class definition
   266 |         decoded_outputs = _AGENT_TYPE_MAPPING[output_type](output)
   267 |         return decoded_outputs
   268 | 
   269 |     # If the class does not have defined output, then we map according to the type
   270 |     if isinstance(output, str):
   271 |         return AgentText(output)
   272 |     if isinstance(output, PIL.Image.Image):
   273 |         return AgentImage(output)
   274 |     try:
   275 |         import torch
   276 | 
   277 |         if isinstance(output, torch.Tensor):
   278 |             return AgentAudio(output)
   279 |     except ModuleNotFoundError:
   280 |         pass
   281 |     return output
tests/test_types.py · lines 112–118 · AgentTextTests.test_from_string
   112 | class AgentTextTests(unittest.TestCase):
   113 |     def test_from_string(self):
   114 |         string = "Hey!"
   115 |         agent_type = AgentText(string)
   116 | 
   117 |         self.assertEqual(string, agent_type.to_string())
   118 |         self.assertEqual(string, agent_type.to_raw())
src/smolagents/agent_types.py · lines 145–160 · AgentImage.to_string
   145 |             directory = tempfile.mkdtemp()
   146 |             self._path = os.path.join(directory, str(uuid.uuid4()) + ".png")
   147 |             self._raw.save(self._path, format="png")
   148 |             return self._path
   149 | 
   150 |         if self._tensor is not None:
   151 |             import numpy as np
   152 | 
   153 |             array = self._tensor.cpu().detach().numpy()
   154 | 
   155 |             # There is likely simpler than load into image into save
   156 |             img = PIL.Image.fromarray((255 - array * 255).astype(np.uint8))
   157 | 
   158 |             directory = tempfile.mkdtemp()
   159 |             self._path = os.path.join(directory, str(uuid.uuid4()) + ".png")
   160 |             img.save(self._path, format="png")
src/smolagents/agent_types.py · lines 247–251 · AgentAudio.to_string
   247 |         if self._tensor is not None:
   248 |             directory = tempfile.mkdtemp()
   249 |             self._path = os.path.join(directory, str(uuid.uuid4()) + ".wav")
   250 |             sf.write(self._path, self._tensor, samplerate=self.samplerate)
   251 |             return self._path
src/smolagents/agent_types.py · lines 62–176 · AgentText/AgentImage
    62 | class AgentText(AgentType, str):
    63 |     """
    64 |     Text type returned by the agent. Behaves as a string.
    65 |     """
    66 | 
    67 |     def to_raw(self):
    68 |         return self._value
    69 | 
    70 |     def to_string(self):
    71 |         return str(self._value)
    72 | 
    73 | 
    74 | class AgentImage(AgentType, PIL.Image.Image):
    75 |     """
    76 |     Image type returned by the agent. Behaves as a PIL.Image.Image.
    77 |     """
    78 | 
    79 |     def __init__(self, value):
    80 |         AgentType.__init__(self, value)
    81 |         PIL.Image.Image.__init__(self)
    82 | 
    83 |         self._path = None
    84 |         self._raw = None
    85 |         self._tensor = None
    86 | 
    87 |         if isinstance(value, AgentImage):
    88 |             self._raw, self._path, self._tensor = value._raw, value._path, value._tensor
    89 |         elif isinstance(value, PIL.Image.Image):
    90 |             self._raw = value
    91 |         elif isinstance(value, bytes):
    92 |             self._raw = PIL.Image.open(BytesIO(value))
    93 |         elif isinstance(value, (str, pathlib.Path)):
       | … additional lines omitted from this preview …
src/smolagents/agent_types.py · lines 181–185 · AgentAudio.__init__
   181 |     def __init__(self, value, samplerate=16_000):
   182 |         if not _is_package_available("soundfile") or not _is_package_available("torch"):
   183 |             raise ModuleNotFoundError(
   184 |                 "Please install 'audio' extra to use AgentAudio: `pip install 'smolagents[audio]'`"
   185 |             )
04
Workflow 4

chat-based agent inference and tool use

What this workflow does

Browser chat request posts a user message to /chat, the server runs a CodeAgent with MCP-provided tools in a worker thread, and returns the agent reply as JSON.

Purpose

ask the agent a question and receive an answer

What it produces

agent reply

Who relies on it

browser user

Why this workflow matters

  • interactive assistant response
  • tool-augmented answer generation
View supporting code
examples/server/main.py · lines 116–123 · homepage
   116 |         <div class="chat-container" id="chat-container">
   117 |             <div class="message agent-message">
   118 |                 Hello! I'm a code agent with access to MCP tools. Ask me anything!
   119 |             </div>
   120 |         </div>
   121 |         <div class="input-container">
   122 |             <input type="text" id="message-input" placeholder="Ask me anything..." autofocus>
   123 |             <button onclick="sendMessage()" id="send-button">Send</button>
examples/server/main.py · lines 140–164 · sendMessage
   140 |         async function sendMessage() {
   141 |             const message = messageInput.value.trim();
   142 |             if (!message) return;
   143 | 
   144 |             // Add user message
   145 |             addMessage(message, true);
   146 |             messageInput.value = '';
   147 |             sendButton.disabled = true;
   148 |             sendButton.textContent = 'Sending...';
   149 | 
   150 |             // Add loading indicator
   151 |             const loadingDiv = document.createElement('div');
   152 |             loadingDiv.className = 'message agent-message loading';
   153 |             loadingDiv.textContent = 'Thinking...';
   154 |             chatContainer.appendChild(loadingDiv);
   155 |             chatContainer.scrollTop = chatContainer.scrollHeight;
   156 | 
   157 |             try {
   158 |                 const response = await fetch('/chat', {
   159 |                     method: 'POST',
   160 |                     headers: {
   161 |                         'Content-Type': 'application/json',
   162 |                     },
   163 |                     body: JSON.stringify({ message }),
   164 |                 });
examples/server/main.py · lines 16–20 · agent
    16 | # Create a CodeAgent with a specific model and the tools from the MCP client
    17 | agent = CodeAgent(
    18 |     model=InferenceClientModel(model_id="Qwen/Qwen3-Next-80B-A3B-Thinking"),
    19 |     tools=mcp_client.get_tools(),
    20 | )
examples/server/main.py · lines 201–204 · chat
   201 |     result = await to_thread.run_sync(agent.run, message)
   202 |     # Format the result if it's a complex data structure
   203 |     reply = str(result)
   204 |     return JSONResponse({"reply": reply})
examples/server/main.py · lines 197–204 · chat
   197 | async def chat(request):
   198 |     data = await request.json()
   199 |     message = data.get("message", "").strip()
   200 |     # Run in a thread to avoid blocking the event loop
   201 |     result = await to_thread.run_sync(agent.run, message)
   202 |     # Format the result if it's a complex data structure
   203 |     reply = str(result)
   204 |     return JSONResponse({"reply": reply})
examples/server/main.py · lines 202–204 · chat
   202 |     # Format the result if it's a complex data structure
   203 |     reply = str(result)
   204 |     return JSONResponse({"reply": reply})
Workflow area 2

Streaming workflows

1 workflow
05
Workflow 5

UI streaming and run visualization

What this workflow does

Agent steps and streaming deltas are transformed into Gradio chat messages, including formatted code, logs, images, and footnotes.

Purpose

present agent execution progress to a human user

What it produces

Gradio chat messages and step logs

Who relies on it

human operator / UI consumer

Why this workflow matters

  • makes agent reasoning and tool use visible in the UI
  • packages logs, images, and errors into display-ready messages
  • preserves a readable final-answer stream
View supporting code
src/smolagents/gradio_ui.py · lines 22–26 · file_context
    22 | from smolagents.agent_types import AgentAudio, AgentImage, AgentText
    23 | from smolagents.agents import MultiStepAgent, PlanningStep
    24 | from smolagents.memory import ActionStep, FinalAnswerStep
    25 | from smolagents.models import ChatMessageStreamDelta, MessageRole, agglomerate_stream_deltas
    26 | from smolagents.utils import _is_package_available
src/smolagents/gradio_ui.py · lines 80–163 · _process_action_step
    80 | def _process_action_step(step_log: ActionStep, skip_model_outputs: bool = False) -> Generator:
    81 |     """
    82 |     Process an [`ActionStep`] and yield appropriate Gradio ChatMessage objects.
    83 | 
    84 |     Args:
    85 |         step_log ([`ActionStep`]): ActionStep to process.
    86 |         skip_model_outputs (`bool`): Whether to skip model outputs.
    87 | 
    88 |     Yields:
    89 |         `gradio.ChatMessage`: Gradio ChatMessages representing the action step.
    90 |     """
    91 |     import gradio as gr
    92 | 
    93 |     # Output the step number
    94 |     step_number = f"Step {step_log.step_number}"
    95 |     if not skip_model_outputs:
    96 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=f"**{step_number}**", metadata={"status": "done"})
    97 | 
    98 |     # First yield the thought/reasoning from the LLM
    99 |     if not skip_model_outputs and getattr(step_log, "model_output", ""):
   100 |         model_output = _clean_model_output(step_log.model_output)
   101 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=model_output, metadata={"status": "done"})
   102 | 
   103 |     # For tool calls, create a parent message
   104 |     if getattr(step_log, "tool_calls", []):
   105 |         first_tool_call = step_log.tool_calls[0]
   106 |         used_code = first_tool_call.name == "python_interpreter"
   107 | 
   108 |         # Process arguments based on type
   109 |         args = first_tool_call.arguments
   110 |         if isinstance(args, dict):
   111 |             content = str(args.get("answer", str(args)))
       | … additional lines omitted from this preview …
src/smolagents/gradio_ui.py · lines 94–163 · _process_action_step
    94 |     step_number = f"Step {step_log.step_number}"
    95 |     if not skip_model_outputs:
    96 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=f"**{step_number}**", metadata={"status": "done"})
    97 | 
    98 |     # First yield the thought/reasoning from the LLM
    99 |     if not skip_model_outputs and getattr(step_log, "model_output", ""):
   100 |         model_output = _clean_model_output(step_log.model_output)
   101 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=model_output, metadata={"status": "done"})
   102 | 
   103 |     # For tool calls, create a parent message
   104 |     if getattr(step_log, "tool_calls", []):
   105 |         first_tool_call = step_log.tool_calls[0]
   106 |         used_code = first_tool_call.name == "python_interpreter"
   107 | 
   108 |         # Process arguments based on type
   109 |         args = first_tool_call.arguments
   110 |         if isinstance(args, dict):
   111 |             content = str(args.get("answer", str(args)))
   112 |         else:
   113 |             content = str(args).strip()
   114 | 
   115 |         # Format code content if needed
   116 |         if used_code:
   117 |             content = _format_code_content(content)
   118 | 
   119 |         # Create the tool call message
   120 |         parent_message_tool = gr.ChatMessage(
   121 |             role=MessageRole.ASSISTANT,
   122 |             content=content,
   123 |             metadata={
   124 |                 "title": f"🛠️ Used tool {first_tool_call.name}",
   125 |                 "status": "done",
       | … additional lines omitted from this preview …
src/smolagents/models.py · lines 213–217 · ChatMessageStreamDelta
   213 | @dataclass
   214 | class ChatMessageStreamDelta:
   215 |     content: str | None = None
   216 |     tool_calls: list[ChatMessageToolCallStreamDelta] | None = None
   217 |     token_usage: TokenUsage | None = None
src/smolagents/gradio_ui.py · lines 39–56 · _clean_model_output
    39 | def _clean_model_output(model_output: str) -> str:
    40 |     """
    41 |     Clean up model output by removing trailing tags and extra backticks.
    42 | 
    43 |     Args:
    44 |         model_output (`str`): Raw model output.
    45 | 
    46 |     Returns:
    47 |         `str`: Cleaned model output.
    48 |     """
    49 |     if not model_output:
    50 |         return ""
    51 |     model_output = model_output.strip()
    52 |     # Remove any trailing <end_code> and extra backticks, handling multiple possible formats
    53 |     model_output = re.sub(r"```\s*<end_code>", "```", model_output)  # handles ```<end_code>
    54 |     model_output = re.sub(r"<end_code>\s*```", "```", model_output)  # handles <end_code>```
    55 |     model_output = re.sub(r"```\s*\n\s*<end_code>", "```", model_output)  # handles ```\n<end_code>
    56 |     return model_output.strip()
src/smolagents/gradio_ui.py · lines 59–77 · _format_code_content
    59 | def _format_code_content(content: str) -> str:
    60 |     """
    61 |     Format code content as Python code block if it's not already formatted.
    62 | 
    63 |     Args:
    64 |         content (`str`): Code content to format.
    65 | 
    66 |     Returns:
    67 |         `str`: Code content formatted as a Python code block.
    68 |     """
    69 |     content = content.strip()
    70 |     # Remove existing code blocks and end_code tags
    71 |     content = re.sub(r"```.*?\n", "", content)
    72 |     content = re.sub(r"\s*<end_code>\s*", "", content)
    73 |     content = content.strip()
    74 |     # Add Python code block formatting if not already present
    75 |     if not content.startswith("```python"):
    76 |         content = f"```python\n{content}\n```"
    77 |     return content
Workflow area 3

Remote workflows

4 workflows
06
Workflow 6

remote code execution transport

What this workflow does

RemotePythonExecutor serializes tool variables and final answers for remote code execution using safe JSON-first transport with optional pickle fallback.

Purpose

Send tools and variables into a remote Python kernel and recover final answers back out

What it produces

serialized execution state and final answer payload

Who relies on it

agent runtime / remote executor environment

Why this workflow matters

  • Allows agent code to run in an isolated remote environment
  • Preserves complex values when safe JSON encoding is possible
  • Falls back to pickle only in explicitly enabled legacy mode
View supporting code
src/smolagents/remote_executors.py · lines 53–79 · RemotePythonExecutor
    53 | class RemotePythonExecutor(PythonExecutor):
    54 |     """
    55 |     Executor of Python code in a remote environment.
    56 | 
    57 |     Args:
    58 |         additional_imports (`list[str]`): Additional Python packages to install.
    59 |         logger (`Logger`): Logger to use for output and errors.
    60 |         allow_pickle (`bool`, default `False`): Whether to allow pickle serialization for objects that cannot be safely serialized to JSON.
    61 |             - `False` (default, recommended): Only safe JSON serialization is used. Raises error if object cannot be safely serialized.
    62 |             - `True` (legacy mode): Tries safe JSON serialization first, falls back to pickle with warning if needed.
    63 | 
    64 |             **Security Warning:** Pickle deserialization can execute arbitrary code. Only set `allow_pickle=True`
    65 |             if you fully trust the execution environment and need backward compatibility with custom types.
    66 |     """
    67 | 
    68 |     FINAL_ANSWER_EXCEPTION = "FinalAnswerException"
    69 | 
    70 |     def __init__(
    71 |         self,
    72 |         additional_imports: list[str],
    73 |         logger,
    74 |         allow_pickle: bool = False,
    75 |     ):
    76 |         self.additional_imports = additional_imports
    77 |         self.logger = logger
    78 |         self.allow_pickle = allow_pickle
    79 |         self.logger.log("Initializing executor, hold on...")
src/smolagents/remote_executors.py · lines 115–131 · RemotePythonExecutor.send_variables
   115 |     def send_variables(self, variables: dict[str, Any]):
   116 |         """Send variables to the kernel namespace using SafeSerializer.
   117 | 
   118 |         Uses prefix-based format ("safe:..." or "pickle:...").
   119 |         When allow_pickle=False, only safe JSON serialization is allowed.
   120 |         When allow_pickle=True, pickle fallback is enabled for complex types.
   121 |         """
   122 |         if not variables:
   123 |             return
   124 | 
   125 |         serialized = SafeSerializer.dumps(variables, allow_pickle=self.allow_pickle)
   126 |         code = f"""
   127 | {SafeSerializer.get_deserializer_code(self.allow_pickle)}
   128 | vars_dict = _deserialize({repr(serialized)})
   129 | locals().update(vars_dict)
   130 | """
   131 |         self.run_code_raise_errors(code)
src/smolagents/remote_executors.py · lines 143–304 · RemotePythonExecutor._patch_final_answer_with_exception
   143 |     def _patch_final_answer_with_exception(self, final_answer_tool: FinalAnswerTool):
   144 |         """Patch the FinalAnswerTool to raise an exception.
   145 | 
   146 |         This is necessary because the remote executors
   147 |         rely on the FinalAnswerTool to detect the final answer.
   148 |         It modifies the `forward` method of the FinalAnswerTool to raise
   149 |         a `FinalAnswerException` with the final answer as a serialized value.
   150 |         This allows the executor to catch this exception and return the final answer.
   151 | 
   152 |         Uses prefix-based format ("safe:" or "pickle:") for serialization.
   153 | 
   154 |         Args:
   155 |             final_answer_tool (`FinalAnswerTool`): FinalAnswerTool instance to patch.
   156 |         """
   157 | 
   158 |         # Create a new class that inherits from the original FinalAnswerTool
   159 |         class _FinalAnswerTool(final_answer_tool.__class__):
   160 |             pass
   161 | 
   162 |         # Add a new forward method that raises the FinalAnswerException
   163 |         # NOTE: Serialization logic is inlined here because this method's source code
   164 |         # is extracted and sent to remote environments where external references don't exist
   165 |         # Capture settings via closure
   166 |         allow_pickle_setting = self.allow_pickle
   167 | 
   168 |         def forward(self, *args, **kwargs) -> Any:
   169 |             import base64
   170 |             import json
   171 |             from io import BytesIO
   172 | 
   173 |             # Baked in from closure at patch time
   174 |             ALLOW_PICKLE = allow_pickle_setting
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 60–66 · RemotePythonExecutor
    60 |         allow_pickle (`bool`, default `False`): Whether to allow pickle serialization for objects that cannot be safely serialized to JSON.
    61 |             - `False` (default, recommended): Only safe JSON serialization is used. Raises error if object cannot be safely serialized.
    62 |             - `True` (legacy mode): Tries safe JSON serialization first, falls back to pickle with warning if needed.
    63 | 
    64 |             **Security Warning:** Pickle deserialization can execute arbitrary code. Only set `allow_pickle=True`
    65 |             if you fully trust the execution environment and need backward compatibility with custom types.
    66 |     """
src/smolagents/remote_executors.py · lines 70–78 · RemotePythonExecutor.__init__
    70 |     def __init__(
    71 |         self,
    72 |         additional_imports: list[str],
    73 |         logger,
    74 |         allow_pickle: bool = False,
    75 |     ):
    76 |         self.additional_imports = additional_imports
    77 |         self.logger = logger
    78 |         self.allow_pickle = allow_pickle
src/smolagents/serialization.py · lines 267–346 · SafeSerializer.dumps/loads
   267 |         if not allow_pickle:
   268 |             # Safe ONLY mode - no pickle fallback
   269 |             json_safe = SafeSerializer.to_json_safe(obj)  # Raises SerializationError if fails
   270 |             return SafeSerializer.SAFE_PREFIX + json.dumps(json_safe)
   271 |         else:
   272 |             # Try safe first, fallback to pickle
   273 |             try:
   274 |                 json_safe = SafeSerializer.to_json_safe(obj)
   275 |                 return SafeSerializer.SAFE_PREFIX + json.dumps(json_safe)
   276 |             except SerializationError:
   277 |                 # Warn about insecure pickle usage
   278 |                 import warnings
   279 | 
   280 |                 warnings.warn(
   281 |                     "Falling back to insecure pickle serialization. "
   282 |                     "This is a security risk and will be removed in a future version. "
   283 |                     "Consider using only safe serializable types (primitives, lists, dicts, "
   284 |                     "numpy arrays, PIL images, datetime objects, dataclasses).",
   285 |                     FutureWarning,
   286 |                     stacklevel=2,
   287 |                 )
   288 |                 # Fallback to pickle (with prefix)
   289 |                 try:
   290 |                     return "pickle:" + base64.b64encode(pickle.dumps(obj)).decode()
   291 |                 except (pickle.PicklingError, TypeError, AttributeError) as e:
   292 |                     raise SerializationError(f"Cannot serialize object: {e}") from e
   293 | 
   294 |     @staticmethod
   295 |     def loads(data: str, allow_pickle: bool = False) -> Any:
   296 |         """
   297 |         Deserialize string with format detection.
   298 | 
       | … additional lines omitted from this preview …
07
Workflow 7

remote code execution sandbox

What this workflow does

Unknown caller initializes DockerExecutor, starts a Docker-hosted Jupyter kernel gateway, and executes Python code over a websocket before cleaning up the container.

Purpose

run Python code in an isolated Docker-backed environment

What it produces

DockerExecutor container and Jupyter kernel

Who relies on it

library consumer / agent runtime

Why this workflow matters

  • isolates execution state from the host process
  • returns executable results from remote Python snippets
  • reclaims container resources after use
View supporting code
src/smolagents/remote_executors.py · lines 572–692 · DockerExecutor.__init__ / run_code_raise_errors
   572 |     def __init__(
   573 |         self,
   574 |         additional_imports: list[str],
   575 |         logger,
   576 |         allow_pickle: bool = False,
   577 |         host: str = "127.0.0.1",
   578 |         port: int = 8888,
   579 |         image_name: str = "jupyter-kernel",
   580 |         build_new_image: bool = True,
   581 |         container_run_kwargs: dict[str, Any] | None = None,
   582 |         dockerfile_content: str | None = None,
   583 |     ):
   584 |         super().__init__(additional_imports, logger, allow_pickle)
   585 |         try:
   586 |             import docker
   587 |         except ModuleNotFoundError:
   588 |             raise ModuleNotFoundError(
   589 |                 "Please install 'docker' extra to use DockerExecutor: `pip install 'smolagents[docker]'`"
   590 |             )
   591 |         self.host = host
   592 |         self.port = port
   593 |         self.image_name = image_name
   594 | 
   595 |         self.dockerfile_content = dockerfile_content or dedent(
   596 |             """\
   597 |             FROM python:3.12-bullseye
   598 | 
   599 |             RUN pip install jupyter_kernel_gateway jupyter_client ipykernel
   600 | 
   601 |             EXPOSE 8888
   602 |             CMD ["jupyter", "kernelgateway", "--KernelGatewayApp.ip=0.0.0.0", "--KernelGatewayApp.port=8888"]
   603 |             """
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 606–668 · DockerExecutor.__init__
   606 |         # Initialize Docker
   607 |         try:
   608 |             self.client = docker.from_env()
   609 |         except docker.errors.DockerException as e:
   610 |             raise RuntimeError("Could not connect to Docker daemon: make sure Docker is running.") from e
   611 | 
   612 |         # Build and start container
   613 |         try:
   614 |             # Check if image exists, unless forced to rebuild
   615 |             if not build_new_image:
   616 |                 try:
   617 |                     self.client.images.get(self.image_name)
   618 |                     self.logger.log(f"Using existing Docker image: {self.image_name}", level=LogLevel.INFO)
   619 |                 except docker.errors.ImageNotFound:
   620 |                     self.logger.log(f"Image {self.image_name} not found, building...", level=LogLevel.INFO)
   621 |                     build_new_image = True
   622 | 
   623 |             if build_new_image:
   624 |                 self.logger.log(f"Building Docker image {self.image_name}...", level=LogLevel.INFO)
   625 |                 dockerfile_obj = BytesIO(self.dockerfile_content.encode("utf-8"))
   626 |                 _, build_logs = self.client.images.build(fileobj=dockerfile_obj, tag=self.image_name)
   627 |                 for log_chunk in build_logs:
   628 |                     # Only log non-empty messages
   629 |                     if log_message := log_chunk.get("stream", "").rstrip():
   630 |                         self.logger.log(log_message, level=LogLevel.DEBUG)
   631 | 
   632 |             self.logger.log(f"Starting container on {host}:{port}...", level=LogLevel.INFO)
   633 |             # Create base container parameters
   634 |             container_kwargs = {}
   635 |             if container_run_kwargs:
   636 |                 container_kwargs.update(container_run_kwargs)
   637 | 
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 654–659 · DockerExecutor.__init__
   654 |             retries = 0
   655 |             while self.container.status != "running" and retries < 5:
   656 |                 self.logger.log(f"Container status: {self.container.status}, waiting...", level=LogLevel.INFO)
   657 |                 time.sleep(1)
   658 |                 self.container.reload()
   659 |                 retries += 1
src/smolagents/remote_executors.py · lines 585–590 · DockerExecutor.__init__
   585 |         try:
   586 |             import docker
   587 |         except ModuleNotFoundError:
   588 |             raise ModuleNotFoundError(
   589 |                 "Please install 'docker' extra to use DockerExecutor: `pip install 'smolagents[docker]'`"
   590 |             )
src/smolagents/remote_executors.py · lines 644–650 · DockerExecutor.__init__
   644 |             # Generate auth token and pass it to the kernel gateway via the standard KG_AUTH_TOKEN env var
   645 |             <redacted>
   646 |             env = container_kwargs.get("environment") or {}
   647 |             if isinstance(env, list):
   648 |                 env = dict(kv.split("=", 1) for kv in env if "=" in kv)
   649 |             env["KG_AUTH_TOKEN"] = token
   650 |             container_kwargs["environment"] = env
src/smolagents/remote_executors.py · lines 694–704 · DockerExecutor.cleanup
   694 |     def cleanup(self):
   695 |         """Clean up the Docker container and resources."""
   696 |         try:
   697 |             if hasattr(self, "container"):
   698 |                 self.logger.log(f"Stopping and removing container {self.container.short_id}...", level=LogLevel.INFO)
   699 |                 self.container.stop()
   700 |                 self.container.remove()
   701 |                 self.logger.log("Container cleanup completed", level=LogLevel.INFO)
   702 |                 del self.container
   703 |         except Exception as e:
   704 |             self.logger.log_error(f"Error during cleanup: {e}")
08
Workflow 8

remote code execution sandbox

What this workflow does

Unknown caller initializes ModalExecutor, provisions a Modal sandbox, starts Jupyter inside it, installs packages, executes code over websocket, and terminates the sandbox.

Purpose

run Python code in a Modal-managed sandbox

What it produces

Modal sandbox and Jupyter kernel

Who relies on it

library consumer / agent runtime

Why this workflow matters

  • provides ephemeral remote compute for agent code
  • supports additional package installation in the sandbox
  • terminates the sandbox on cleanup
View supporting code
src/smolagents/remote_executors.py · lines 748–826 · ModalExecutor.__init__ / run_code_raise_errors
   748 |     def __init__(
   749 |         self,
   750 |         additional_imports: list[str],
   751 |         logger,
   752 |         allow_pickle: bool = False,
   753 |         app_name: str = "smolagent-executor",
   754 |         port: int = 8888,
   755 |         create_kwargs: Optional[dict] = None,
   756 |     ):
   757 |         super().__init__(additional_imports, logger, allow_pickle)
   758 |         self.port = port
   759 |         try:
   760 |             import modal
   761 |         except ModuleNotFoundError:
   762 |             raise ModuleNotFoundError(
   763 |                 """Please install 'modal' extra to use ModalExecutor: `pip install 'smolagents[modal]'`"""
   764 |             )
   765 | 
   766 |         if create_kwargs is None:
   767 |             create_kwargs = {}
   768 | 
   769 |         create_kwargs = {
   770 |             "image": modal.Image.debian_slim().uv_pip_install("jupyter_kernel_gateway", "ipykernel"),
   771 |             "timeout": 60 * 5,
   772 |             **create_kwargs,
   773 |         }
   774 | 
   775 |         if "app" not in create_kwargs:
   776 |             create_kwargs["app"] = modal.App.lookup(app_name, create_if_missing=True)
   777 | 
   778 |         if "encrypted_ports" not in create_kwargs:
   779 |             create_kwargs["encrypted_ports"] = [port]
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 798–811 · ModalExecutor.__init__
   798 |         self.logger.log("Starting Modal sandbox", level=LogLevel.INFO)
   799 |         self.sandbox = modal.Sandbox.create(
   800 |             *entrypoint,
   801 |             **create_kwargs,
   802 |         )
   803 | 
   804 |         tunnel = self.sandbox.tunnels()[port]
   805 |         self.logger.log(f"Waiting for Modal sandbox on {tunnel.host}:{port}", level=LogLevel.INFO)
   806 |         self._wait_for_server(tunnel.host, token)
   807 | 
   808 |         self.logger.log("Starting Jupyter kernel", level=LogLevel.INFO)
   809 |         kernel_id = _create_kernel_http(f"https://{tunnel.host}/api/kernels?<redacted>", logger)
   810 |         self.ws_url = f"wss://{tunnel.host}/api/kernels/{kernel_id}/channels?<redacted>"
   811 |         self.installed_packages = self.install_packages(additional_imports)
src/smolagents/remote_executors.py · lines 837–851 · ModalExecutor._wait_for_server
   837 |     def _wait_for_server(self, host: str, <redacted>
   838 |         """Wait for server to start up."""
   839 |         n_retries = 0
   840 |         while True:
   841 |             try:
   842 |                 resp = requests.get(f"https://{host}/api/kernelspecs?<redacted>")
   843 |                 if resp.status_code == 200:
   844 |                     break
   845 |             except RequestException:
   846 |                 n_retries += 1
   847 |                 if n_retries % 10 == 0:
   848 |                     self.logger.log("Waiting for server to startup, retrying...", level=LogLevel.INFO)
   849 |                 if n_retries > 60:
   850 |                     raise RuntimeError("Unable to connect to sandbox")
   851 |                 time.sleep(1.0)
src/smolagents/remote_executors.py · lines 759–764 · ModalExecutor.__init__
   759 |         try:
   760 |             import modal
   761 |         except ModuleNotFoundError:
   762 |             raise ModuleNotFoundError(
   763 |                 """Please install 'modal' extra to use ModalExecutor: `pip install 'smolagents[modal]'`"""
   764 |             )
src/smolagents/remote_executors.py · lines 766–789 · ModalExecutor.__init__
   766 |         if create_kwargs is None:
   767 |             create_kwargs = {}
   768 | 
   769 |         create_kwargs = {
   770 |             "image": modal.Image.debian_slim().uv_pip_install("jupyter_kernel_gateway", "ipykernel"),
   771 |             "timeout": 60 * 5,
   772 |             **create_kwargs,
   773 |         }
   774 | 
   775 |         if "app" not in create_kwargs:
   776 |             create_kwargs["app"] = modal.App.lookup(app_name, create_if_missing=True)
   777 | 
   778 |         if "encrypted_ports" not in create_kwargs:
   779 |             create_kwargs["encrypted_ports"] = [port]
   780 |         else:
   781 |             create_kwargs["encrypted_ports"] = create_kwargs["encrypted_ports"] + [port]
   782 | 
   783 |         <redacted>
   784 |         default_secrets = [modal.Secret.from_dict({"KG_AUTH_TOKEN": token})]
   785 | 
   786 |         if "secrets" not in create_kwargs:
   787 |             create_kwargs["secrets"] = default_secrets
   788 |         else:
   789 |             create_kwargs["secrets"] = create_kwargs["secrets"] + default_secrets
src/smolagents/remote_executors.py · lines 828–835 · ModalExecutor.cleanup
   828 |     def cleanup(self):
   829 |         """Clean up the Modal sandbox by terminating it."""
   830 |         if hasattr(self, "sandbox"):
   831 |             self.sandbox.terminate()
   832 | 
   833 |     def delete(self):
   834 |         """Ensure cleanup on deletion."""
   835 |         self.cleanup()
09
Workflow 9

remote code execution sandbox

What this workflow does

Unknown caller initializes BlaxelExecutor, provisions a Blaxel sandbox, creates a Jupyter kernel, installs packages, executes code over websocket, and deletes the sandbox during cleanup.

Purpose

run Python code in a Blaxel-managed sandbox

What it produces

Blaxel sandbox and Jupyter kernel

Who relies on it

library consumer / agent runtime

Why this workflow matters

  • supports fast-launching compute with retained memory state
  • allows package installation into the sandbox before execution
  • performs best-effort cleanup and deletion
View supporting code
src/smolagents/remote_executors.py · lines 882–980 · BlaxelExecutor.__init__ / run_code_raise_errors
   882 |     def __init__(
   883 |         self,
   884 |         additional_imports: list[str],
   885 |         logger,
   886 |         allow_pickle: bool = False,
   887 |         sandbox_name: str | None = None,
   888 |         image: str = "blaxel/jupyter-notebook",
   889 |         memory: int = 4096,
   890 |         ttl: str | None = None,
   891 |         region: Optional[str] = None,
   892 |     ):
   893 |         super().__init__(additional_imports, logger, allow_pickle=allow_pickle)
   894 | 
   895 |         try:
   896 |             import blaxel  # noqa: F401
   897 |         except ModuleNotFoundError:
   898 |             raise ModuleNotFoundError(
   899 |                 "Please install 'blaxel' extra to use BlaxelExecutor: `pip install 'smolagents[blaxel]'`"
   900 |             )
   901 | 
   902 |         self.sandbox_name = sandbox_name or f"smolagent-executor-{uuid.uuid4().hex[:8]}"
   903 |         self.image = image
   904 |         self.memory = memory
   905 |         self.region = region
   906 |         self.port = 8888
   907 |         self._cleaned_up = False  # Flag to prevent double cleanup
   908 | 
   909 |         # Prepare sandbox creation parameters
   910 |         <redacted>
   911 |         sandbox_config = {
   912 |             "metadata": {
   913 |                 "name": self.sandbox_name,
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 909–980 · BlaxelExecutor.__init__
   909 |         # Prepare sandbox creation parameters
   910 |         <redacted>
   911 |         sandbox_config = {
   912 |             "metadata": {
   913 |                 "name": self.sandbox_name,
   914 |             },
   915 |             "spec": {
   916 |                 "runtime": {"image": image, "memory": memory, "ports": [{"target": self.port}]},
   917 |             },
   918 |         }
   919 | 
   920 |         if region:
   921 |             sandbox_config["spec"]["region"] = region
   922 | 
   923 |         if ttl:
   924 |             sandbox_config["spec"]["runtime"]["ttl"] = ttl
   925 | 
   926 |         # Create the sandbox
   927 |         try:
   928 |             # Create sandbox environment on Blaxel
   929 |             self.sandbox = BlaxelExecutor._create_sandbox(sandbox_config)
   930 | 
   931 |             # Create kernel via HTTP
   932 |             from blaxel.core import settings
   933 | 
   934 |             kernel_id = _create_kernel_http(
   935 |                 f"{self.sandbox.metadata.url}/port/{self.port}/api/kernels?<redacted>",
   936 |                 self.logger,
   937 |                 headers=settings.headers,
   938 |             )
   939 | 
   940 |             # Set up websocket URL
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 907–1065 · BlaxelExecutor
   907 |         self._cleaned_up = False  # Flag to prevent double cleanup
   908 | 
   909 |         # Prepare sandbox creation parameters
   910 |         <redacted>
   911 |         sandbox_config = {
   912 |             "metadata": {
   913 |                 "name": self.sandbox_name,
   914 |             },
   915 |             "spec": {
   916 |                 "runtime": {"image": image, "memory": memory, "ports": [{"target": self.port}]},
   917 |             },
   918 |         }
   919 | 
   920 |         if region:
   921 |             sandbox_config["spec"]["region"] = region
   922 | 
   923 |         if ttl:
   924 |             sandbox_config["spec"]["runtime"]["ttl"] = ttl
   925 | 
   926 |         # Create the sandbox
   927 |         try:
   928 |             # Create sandbox environment on Blaxel
   929 |             self.sandbox = BlaxelExecutor._create_sandbox(sandbox_config)
   930 | 
   931 |             # Create kernel via HTTP
   932 |             from blaxel.core import settings
   933 | 
   934 |             kernel_id = _create_kernel_http(
   935 |                 f"{self.sandbox.metadata.url}/port/{self.port}/api/kernels?<redacted>",
   936 |                 self.logger,
   937 |                 headers=settings.headers,
   938 |             )
       | … additional lines omitted from this preview …
src/smolagents/remote_executors.py · lines 895–900 · BlaxelExecutor.__init__
   895 |         try:
   896 |             import blaxel  # noqa: F401
   897 |         except ModuleNotFoundError:
   898 |             raise ModuleNotFoundError(
   899 |                 "Please install 'blaxel' extra to use BlaxelExecutor: `pip install 'smolagents[blaxel]'`"
   900 |             )
src/smolagents/remote_executors.py · lines 1049–1055 · BlaxelExecutor.cleanup
  1049 |     def cleanup(self):
  1050 |         """Sync wrapper to clean up sandbox and resources."""
  1051 |         # Prevent double cleanup
  1052 |         if self._cleaned_up:
  1053 |             return
  1054 |         self.logger.log("Shutting down sandbox...", level=LogLevel.INFO)
  1055 |         self._cleaned_up = True
src/smolagents/remote_executors.py · lines 909–925 · BlaxelExecutor.__init__
   909 |         # Prepare sandbox creation parameters
   910 |         <redacted>
   911 |         sandbox_config = {
   912 |             "metadata": {
   913 |                 "name": self.sandbox_name,
   914 |             },
   915 |             "spec": {
   916 |                 "runtime": {"image": image, "memory": memory, "ports": [{"target": self.port}]},
   917 |             },
   918 |         }
   919 | 
   920 |         if region:
   921 |             sandbox_config["spec"]["region"] = region
   922 | 
   923 |         if ttl:
   924 |             sandbox_config["spec"]["runtime"]["ttl"] = ttl
   925 | 
Workflow area 4

Interactive workflows

2 workflows
10
Workflow 10

interactive agent runner

What this workflow does

A CLI user configures a model and tool set, then runs a CodeAgent or ToolCallingAgent to complete a prompt.

Purpose

run a prompt with selected tools and model backend

What it produces

agent configuration / prompt

Who relies on it

CLI user

Why this workflow matters

  • creates and executes a configured agent
  • selects between code-based and tool-calling execution modes
View supporting code
src/smolagents/cli.py · lines 45–107 · parse_arguments
    45 | def parse_arguments():
    46 |     parser = argparse.ArgumentParser(description="Run a CodeAgent with all specified parameters")
    47 |     parser.add_argument(
    48 |         "prompt",
    49 |         type=str,
    50 |         nargs="?",
    51 |         default=None,
    52 |         help="The prompt to run with the agent. If no prompt is provided, interactive mode will be launched to guide user through agent setup",
    53 |     )
    54 |     parser.add_argument(
    55 |         "--model-type",
    56 |         type=str,
    57 |         default="InferenceClientModel",
    58 |         help="The model type to use (e.g., InferenceClientModel, OpenAIModel, LiteLLMModel, TransformersModel)",
    59 |     )
    60 |     parser.add_argument(
    61 |         "--action-type",
    62 |         type=str,
    63 |         default="code",
    64 |         help="The action type to use (e.g., code, tool_calling)",
    65 |     )
    66 |     parser.add_argument(
    67 |         "--model-id",
    68 |         type=str,
    69 |         default="Qwen/Qwen3-Next-80B-A3B-Thinking",
    70 |         help="The model ID to use for the specified model type",
    71 |     )
    72 |     parser.add_argument(
    73 |         "--imports",
    74 |         nargs="*",  # accepts zero or more arguments
    75 |         default=[],
    76 |         help="Space-separated list of imports to authorize (e.g., 'numpy pandas')",
       | … additional lines omitted from this preview …
src/smolagents/cli.py · lines 262–290 · main
   262 | def main() -> None:
   263 |     args = parse_arguments()
   264 | 
   265 |     # Check if we should run in interactive mode
   266 |     # Interactive mode is triggered when no prompt is provided
   267 |     if args.prompt is None:
   268 |         prompt, tools, model_type, model_id, provider, api_base, api_key, imports, action_type = interactive_mode()
   269 |     else:
   270 |         prompt = args.prompt
   271 |         tools = args.tools
   272 |         model_type = args.model_type
   273 |         model_id = args.model_id
   274 |         provider = args.provider
   275 |         api_base = args.api_base
   276 |         <redacted>
   277 |         imports = args.imports
   278 |         action_type = args.action_type
   279 | 
   280 |     run_smolagent(
   281 |         prompt,
   282 |         tools,
   283 |         model_type,
   284 |         model_id,
   285 |         provider=provider,
   286 |         api_base=api_base,
   287 |         <redacted>,
   288 |         imports=imports,
   289 |         action_type=action_type,
   290 |     )
src/smolagents/cli.py · lines 219–259 · run_smolagent
   219 | def run_smolagent(
   220 |     prompt: str,
   221 |     tools: list[str],
   222 |     model_type: str,
   223 |     model_id: str,
   224 |     api_base: str | None = None,
   225 |     <redacted> | None = None,
   226 |     imports: list[str] | None = None,
   227 |     provider: str | None = None,
   228 |     action_type: str = "code",
   229 | ) -> None:
   230 |     load_dotenv()
   231 | 
   232 |     model = load_model(model_type, model_id, api_base=api_base, <redacted>, provider=provider)
   233 | 
   234 |     available_tools = []
   235 | 
   236 |     for tool_name in tools:
   237 |         if "/" in tool_name:
   238 |             space_name = tool_name.split("/")[-1].lower().replace("-", "_").replace(".", "_")
   239 |             description = f"Tool loaded from Hugging Face Space: {tool_name}"
   240 |             available_tools.append(Tool.from_space(space_id=tool_name, name=space_name, description=description))
   241 |         else:
   242 |             if tool_name in TOOL_MAPPING:
   243 |                 available_tools.append(TOOL_MAPPING[tool_name]())
   244 |             else:
   245 |                 raise ValueError(f"Tool {tool_name} is not recognized either as a default tool or a Space.")
   246 | 
   247 |     if action_type == "code":
   248 |         agent = CodeAgent(
   249 |             tools=available_tools,
   250 |             model=model,
       | … additional lines omitted from this preview …
src/smolagents/cli.py · lines 123–129 · interactive_mode
   123 |     # Get agent action type
   124 |     action_type = Prompt.ask(
   125 |         "[bold white]What action type would you like to use? 'code' or 'tool_calling'?[/]",
   126 |         default="code",
   127 |         choices=["code", "tool_calling"],
   128 |     )
   129 | 
src/smolagents/cli.py · lines 247–256 · run_smolagent
   247 |     if action_type == "code":
   248 |         agent = CodeAgent(
   249 |             tools=available_tools,
   250 |             model=model,
   251 |             additional_authorized_imports=imports,
   252 |             stream_outputs=True,
   253 |         )
   254 |     elif action_type == "tool_calling":
   255 |         agent = ToolCallingAgent(tools=available_tools, model=model, stream_outputs=True)
   256 |     else:
src/smolagents/cli.py · lines 236–245 · run_smolagent
   236 |     for tool_name in tools:
   237 |         if "/" in tool_name:
   238 |             space_name = tool_name.split("/")[-1].lower().replace("-", "_").replace(".", "_")
   239 |             description = f"Tool loaded from Hugging Face Space: {tool_name}"
   240 |             available_tools.append(Tool.from_space(space_id=tool_name, name=space_name, description=description))
   241 |         else:
   242 |             if tool_name in TOOL_MAPPING:
   243 |                 available_tools.append(TOOL_MAPPING[tool_name]())
   244 |             else:
   245 |                 raise ValueError(f"Tool {tool_name} is not recognized either as a default tool or a Space.")
11
Workflow 11

interactive agent UI streaming

What this workflow does

GradioUI converts agent planning, action, and final-answer steps into chat messages and streams them to a UI client.

Purpose

Display agent progress and final results live in a chat interface

What it produces

agent step logs and rendered chat messages

Who relies on it

end user operating the Gradio interface

Why this workflow matters

  • Users can watch plans, actions, and final answers as they are produced
  • Streamed deltas are aggregated into markdown text for display
  • The UI refuses to operate without the gradio extra
View supporting code
src/smolagents/gradio_ui.py · lines 248–276 · stream_to_gradio
   248 | def stream_to_gradio(
   249 |     agent,
   250 |     task: str,
   251 |     task_images: list | None = None,
   252 |     reset_agent_memory: bool = False,
   253 |     additional_args: dict | None = None,
   254 | ) -> Generator:
   255 |     """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
   256 | 
   257 |     if not _is_package_available("gradio"):
   258 |         raise ModuleNotFoundError(
   259 |             "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
   260 |         )
   261 |     accumulated_events: list[ChatMessageStreamDelta] = []
   262 |     for event in agent.run(
   263 |         task, images=task_images, stream=True, reset=reset_agent_memory, additional_args=additional_args
   264 |     ):
   265 |         if isinstance(event, ActionStep | PlanningStep | FinalAnswerStep):
   266 |             for message in pull_messages_from_step(
   267 |                 event,
   268 |                 # If we're streaming model outputs, no need to display them twice
   269 |                 skip_model_outputs=getattr(agent, "stream_outputs", False),
   270 |             ):
   271 |                 yield message
   272 |             accumulated_events = []
   273 |         elif isinstance(event, ChatMessageStreamDelta):
   274 |             accumulated_events.append(event)
   275 |             text = agglomerate_stream_deltas(accumulated_events).render_as_markdown()
   276 |             yield text
src/smolagents/gradio_ui.py · lines 234–245 · pull_messages_from_step
   234 |     if not _is_package_available("gradio"):
   235 |         raise ModuleNotFoundError(
   236 |             "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
   237 |         )
   238 |     if isinstance(step_log, ActionStep):
   239 |         yield from _process_action_step(step_log, skip_model_outputs)
   240 |     elif isinstance(step_log, PlanningStep):
   241 |         yield from _process_planning_step(step_log, skip_model_outputs)
   242 |     elif isinstance(step_log, FinalAnswerStep):
   243 |         yield from _process_final_answer_step(step_log)
   244 |     else:
   245 |         raise ValueError(f"Unsupported step type: {type(step_log)}")
src/smolagents/gradio_ui.py · lines 166–186 · _process_planning_step
   166 | def _process_planning_step(step_log: PlanningStep, skip_model_outputs: bool = False) -> Generator:
   167 |     """
   168 |     Process a [`PlanningStep`] and yield appropriate gradio.ChatMessage objects.
   169 | 
   170 |     Args:
   171 |         step_log ([`PlanningStep`]): PlanningStep to process.
   172 | 
   173 |     Yields:
   174 |         `gradio.ChatMessage`: Gradio ChatMessages representing the planning step.
   175 |     """
   176 |     import gradio as gr
   177 | 
   178 |     if not skip_model_outputs:
   179 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content="**Planning step**", metadata={"status": "done"})
   180 |         yield gr.ChatMessage(role=MessageRole.ASSISTANT, content=step_log.plan, metadata={"status": "done"})
   181 |     yield gr.ChatMessage(
   182 |         role=MessageRole.ASSISTANT,
   183 |         content=get_step_footnote_content(step_log, "Planning step"),
   184 |         metadata={"status": "done"},
   185 |     )
   186 |     yield gr.ChatMessage(role=MessageRole.ASSISTANT, content="-----", metadata={"status": "done"})
src/smolagents/gradio_ui.py · lines 189–223 · _process_final_answer_step
   189 | def _process_final_answer_step(step_log: FinalAnswerStep) -> Generator:
   190 |     """
   191 |     Process a [`FinalAnswerStep`] and yield appropriate gradio.ChatMessage objects.
   192 | 
   193 |     Args:
   194 |         step_log ([`FinalAnswerStep`]): FinalAnswerStep to process.
   195 | 
   196 |     Yields:
   197 |         `gradio.ChatMessage`: Gradio ChatMessages representing the final answer.
   198 |     """
   199 |     import gradio as gr
   200 | 
   201 |     final_answer = step_log.output
   202 |     if isinstance(final_answer, AgentText):
   203 |         yield gr.ChatMessage(
   204 |             role=MessageRole.ASSISTANT,
   205 |             content=f"**Final answer:**\n{final_answer.to_string()}\n",
   206 |             metadata={"status": "done"},
   207 |         )
   208 |     elif isinstance(final_answer, AgentImage):
   209 |         yield gr.ChatMessage(
   210 |             role=MessageRole.ASSISTANT,
   211 |             content={"path": final_answer.to_string(), "mime_type": "image/png"},
   212 |             metadata={"status": "done"},
   213 |         )
   214 |     elif isinstance(final_answer, AgentAudio):
   215 |         yield gr.ChatMessage(
   216 |             role=MessageRole.ASSISTANT,
   217 |             content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
   218 |             metadata={"status": "done"},
   219 |         )
   220 |     else:
       | … additional lines omitted from this preview …
src/smolagents/gradio_ui.py · lines 265–270 · stream_to_gradio
   265 |         if isinstance(event, ActionStep | PlanningStep | FinalAnswerStep):
   266 |             for message in pull_messages_from_step(
   267 |                 event,
   268 |                 # If we're streaming model outputs, no need to display them twice
   269 |                 skip_model_outputs=getattr(agent, "stream_outputs", False),
   270 |             ):
src/smolagents/gradio_ui.py · lines 234–237 · pull_messages_from_step
   234 |     if not _is_package_available("gradio"):
   235 |         raise ModuleNotFoundError(
   236 |             "Please install 'gradio' extra to use the GradioUI: `pip install 'smolagents[gradio]'`"
   237 |         )
Workflow area 5

Tool workflows

4 workflows
12
Workflow 12

tool wrapping and validation

What this workflow does

The @tool helper converts functions into Tool subclasses with generated schema, while MethodChecker/validate_tool_attributes enforce tool structure constraints.

Purpose

Turn user-defined functions into agent tools that are schema-safe and self-contained

What it produces

tool function, Tool subclass, JSON schema, and source code snapshot

Who relies on it

agent author / tool author

Why this workflow matters

  • Creates runnable tools from ordinary functions
  • Prevents invalid tool names and missing literal defaults
  • Captures source code for later reconstruction and remote execution
View supporting code
src/smolagents/tools.py · lines 1061–1168 · tool
  1061 | def tool(tool_function: Callable) -> Tool:
  1062 |     """
  1063 |     Convert a function into an instance of a dynamically created Tool subclass.
  1064 | 
  1065 |     Args:
  1066 |         tool_function (`Callable`): Function to convert into a Tool subclass.
  1067 |             Should have type hints for each input and a type hint for the output.
  1068 |             Should also have a docstring including the description of the function
  1069 |             and an 'Args:' part where each argument is described.
  1070 |     """
  1071 |     tool_json_schema = get_json_schema(tool_function)["function"]
  1072 |     if "return" not in tool_json_schema:
  1073 |         if len(tool_json_schema["parameters"]["properties"]) == 0:
  1074 |             tool_json_schema["return"] = {"type": "null"}
  1075 |         else:
  1076 |             raise TypeHintParsingException(
  1077 |                 "Tool return type not found: make sure your function has a return type hint!"
  1078 |             )
  1079 | 
  1080 |     class SimpleTool(Tool):
  1081 |         def __init__(self):
  1082 |             self.is_initialized = True
  1083 | 
  1084 |     # Set the class attributes
  1085 |     SimpleTool.name = tool_json_schema["name"]
  1086 |     SimpleTool.description = tool_json_schema["description"]
  1087 |     SimpleTool.inputs = tool_json_schema["parameters"]["properties"]
  1088 |     SimpleTool.output_type = tool_json_schema["return"]["type"]
  1089 | 
  1090 |     # Set output_schema if it exists in the JSON schema
  1091 |     if "output_schema" in tool_json_schema:
  1092 |         SimpleTool.output_schema = tool_json_schema["output_schema"]
       | … additional lines omitted from this preview …
src/smolagents/tool_validation.py · lines 157–263 · validate_tool_attributes
   157 | def validate_tool_attributes(cls, check_imports: bool = True) -> None:
   158 |     """
   159 |     Validates that a Tool class follows the proper patterns:
   160 |     0. Any argument of __init__ should have a default.
   161 |     Args chosen at init are not traceable, so we cannot rebuild the source code for them, thus any important arg should be defined as a class attribute.
   162 |     1. About the class:
   163 |         - Class attributes should only be strings or dicts
   164 |         - Class attributes cannot be complex attributes
   165 |     2. About all class methods:
   166 |         - Imports must be from packages, not local files
   167 |         - All methods must be self-contained
   168 | 
   169 |     Raises all errors encountered, if no error returns None.
   170 |     """
   171 | 
   172 |     class ClassLevelChecker(ast.NodeVisitor):
   173 |         def __init__(self):
   174 |             self.imported_names = set()
   175 |             self.complex_attributes = set()
   176 |             self.class_attributes = set()
   177 |             self.non_defaults = set()
   178 |             self.non_literal_defaults = set()
   179 |             self.in_method = False
   180 |             self.invalid_attributes = []
   181 | 
   182 |         def visit_FunctionDef(self, node):
   183 |             if node.name == "__init__":
   184 |                 self._check_init_function_parameters(node)
   185 |             old_context = self.in_method
   186 |             self.in_method = True
   187 |             self.generic_visit(node)
   188 |             self.in_method = old_context
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 1080–1168 · tool
  1080 |     class SimpleTool(Tool):
  1081 |         def __init__(self):
  1082 |             self.is_initialized = True
  1083 | 
  1084 |     # Set the class attributes
  1085 |     SimpleTool.name = tool_json_schema["name"]
  1086 |     SimpleTool.description = tool_json_schema["description"]
  1087 |     SimpleTool.inputs = tool_json_schema["parameters"]["properties"]
  1088 |     SimpleTool.output_type = tool_json_schema["return"]["type"]
  1089 | 
  1090 |     # Set output_schema if it exists in the JSON schema
  1091 |     if "output_schema" in tool_json_schema:
  1092 |         SimpleTool.output_schema = tool_json_schema["output_schema"]
  1093 |     elif "return" in tool_json_schema and "schema" in tool_json_schema["return"]:
  1094 |         SimpleTool.output_schema = tool_json_schema["return"]["schema"]
  1095 | 
  1096 |     @wraps(tool_function)
  1097 |     def wrapped_function(*args, **kwargs):
  1098 |         return tool_function(*args, **kwargs)
  1099 | 
  1100 |     # Bind the copied function to the forward method
  1101 |     SimpleTool.forward = staticmethod(wrapped_function)
  1102 | 
  1103 |     # Get the signature parameters of the tool function
  1104 |     sig = inspect.signature(tool_function)
  1105 |     # - Add "self" as first parameter to tool_function signature
  1106 |     new_sig = sig.replace(
  1107 |         parameters=[inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)] + list(sig.parameters.values())
  1108 |     )
  1109 |     # - Set the signature of the forward method
  1110 |     SimpleTool.forward.__signature__ = new_sig
  1111 | 
       | … additional lines omitted from this preview …
src/smolagents/tool_validation.py · lines 204–215 · ClassLevelChecker.visit_Assign
   204 |             # Check specific class attributes
   205 |             if getattr(node.targets[0], "id", "") == "name":
   206 |                 if not isinstance(node.value, ast.Constant):
   207 |                     self.invalid_attributes.append(f"Class attribute 'name' must be a constant, found '{node.value}'")
   208 |                 elif not isinstance(node.value.value, str):
   209 |                     self.invalid_attributes.append(
   210 |                         f"Class attribute 'name' must be a string, found '{node.value.value}'"
   211 |                     )
   212 |                 elif not is_valid_name(node.value.value):
   213 |                     self.invalid_attributes.append(
   214 |                         f"Class attribute 'name' must be a valid Python identifier and not a reserved keyword, found '{node.value.value}'"
   215 |                     )
src/smolagents/tools.py · lines 1112–1165 · tool
  1112 |     # Create and attach the source code of the dynamically created tool class and forward method
  1113 |     # - Get the source code of tool_function
  1114 |     tool_source = textwrap.dedent(inspect.getsource(tool_function))
  1115 |     # - Remove the tool decorator and function definition line
  1116 |     lines = tool_source.splitlines()
  1117 |     tree = ast.parse(tool_source)
  1118 |     #   - Find function definition
  1119 |     func_node = next((node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)), None)
  1120 |     if not func_node:
  1121 |         raise ValueError(
  1122 |             f"No function definition found in the provided source of {tool_function.__name__}. "
  1123 |             "Ensure the input is a standard function."
  1124 |         )
  1125 |     #   - Extract decorator lines
  1126 |     decorator_lines = ""
  1127 |     if func_node.decorator_list:
  1128 |         tool_decorators = [d for d in func_node.decorator_list if isinstance(d, ast.Name) and d.id == "tool"]
  1129 |         if len(tool_decorators) > 1:
  1130 |             raise ValueError(
  1131 |                 f"Multiple @tool decorators found on function '{func_node.name}'. Only one @tool decorator is allowed."
  1132 |             )
  1133 |         if len(tool_decorators) < len(func_node.decorator_list):
  1134 |             warnings.warn(
  1135 |                 f"Function '{func_node.name}' has decorators other than @tool. "
  1136 |                 "This may cause issues with serialization in the remote executor. See issue #1626."
  1137 |             )
  1138 |         decorator_start = tool_decorators[0].end_lineno if tool_decorators else 0
  1139 |         decorator_end = func_node.decorator_list[-1].end_lineno
  1140 |         decorator_lines = "\n".join(lines[decorator_start:decorator_end])
  1141 |     #   - Extract tool source body
  1142 |     body_start = func_node.body[0].lineno - 1  # AST lineno starts at 1
  1143 |     tool_source_body = "\n".join(lines[body_start:])
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 1071–1078 · tool
  1071 |     tool_json_schema = get_json_schema(tool_function)["function"]
  1072 |     if "return" not in tool_json_schema:
  1073 |         if len(tool_json_schema["parameters"]["properties"]) == 0:
  1074 |             tool_json_schema["return"] = {"type": "null"}
  1075 |         else:
  1076 |             raise TypeHintParsingException(
  1077 |                 "Tool return type not found: make sure your function has a return type hint!"
  1078 |             )
13
Workflow 13

tool schema generation

What this workflow does

Function introspection utilities derive JSON schemas from Python functions and docstrings for tool use.

Purpose

convert Python functions into structured tool definitions

What it produces

function signature / JSON schema / import list

Who relies on it

LLM tool caller and agent prompt renderer

Why this workflow matters

  • makes functions callable as structured tools
  • prevents malformed tool metadata by requiring docstrings and type hints
  • extracts required external packages for generated tool code
View supporting code
src/smolagents/_function_type_hints_utils.py · lines 97–231 · get_json_schema
    97 | def get_json_schema(func: Callable) -> dict:
    98 |     """
    99 |     This function generates a JSON schema for a given function, based on its docstring and type hints. This is
   100 |     mostly used for passing lists of tools to a chat template. The JSON schema contains the name and description of
   101 |     the function, as well as the names, types and descriptions for each of its arguments. `get_json_schema()` requires
   102 |     that the function has a docstring, and that each argument has a description in the docstring, in the standard
   103 |     Google docstring format shown below. It also requires that all the function arguments have a valid Python type hint.
   104 | 
   105 |     Although it is not required, a `Returns` block can also be added, which will be included in the schema. This is
   106 |     optional because most chat templates ignore the return value of the function.
   107 | 
   108 |     Args:
   109 |         func: The function to generate a JSON schema for.
   110 | 
   111 |     Returns:
   112 |         A dictionary containing the JSON schema for the function.
   113 | 
   114 |     Examples:
   115 |     ```python
   116 |     >>> def multiply(x: float, y: float):
   117 |     >>>    '''
   118 |     >>>    A function that multiplies two numbers
   119 |     >>>
   120 |     >>>    Args:
   121 |     >>>        x: The first number to multiply
   122 |     >>>        y: The second number to multiply
   123 |     >>>    '''
   124 |     >>>    return x * y
   125 |     >>>
   126 |     >>> print(get_json_schema(multiply))
   127 |     {
   128 |         "name": "multiply",
       | … additional lines omitted from this preview …
src/smolagents/_function_type_hints_utils.py · lines 256–288 · _parse_google_format_docstring
   256 | def _parse_google_format_docstring(
   257 |     docstring: str,
   258 | ) -> tuple[str | None, dict | None, str | None]:
   259 |     """
   260 |     Parses a Google-style docstring to extract the function description,
   261 |     argument descriptions, and return description.
   262 | 
   263 |     Args:
   264 |         docstring (str): The docstring to parse.
   265 | 
   266 |     Returns:
   267 |         The function description, arguments, and return description.
   268 |     """
   269 | 
   270 |     # Extract the sections
   271 |     description_match = description_re.search(docstring)
   272 |     args_match = args_re.search(docstring)
   273 |     returns_match = returns_re.search(docstring)
   274 | 
   275 |     # Clean and store the sections
   276 |     description = description_match.group(1).strip() if description_match else None
   277 |     docstring_args = args_match.group(1).strip() if args_match else None
   278 |     returns = returns_match.group(1).strip() if returns_match else None
   279 | 
   280 |     # Parsing the arguments into a dictionary
   281 |     if docstring_args is not None:
   282 |         docstring_args = "\n".join([line for line in docstring_args.split("\n") if line.strip()])  # Remove blank lines
   283 |         matches = args_split_re.findall(docstring_args)
   284 |         args_dict = {match[0]: re.sub(r"\s*\n+\s*", " ", match[1].strip()) for match in matches}
   285 |     else:
   286 |         args_dict = {}
   287 | 
       | … additional lines omitted from this preview …
src/smolagents/_function_type_hints_utils.py · lines 291–323 · _convert_type_hints_to_json_schema
   291 | def _convert_type_hints_to_json_schema(func: Callable, error_on_missing_type_hints: bool = True) -> dict:
   292 |     type_hints = get_type_hints(func)
   293 |     signature = inspect.signature(func)
   294 | 
   295 |     properties = {}
   296 |     for param_name, param_type in type_hints.items():
   297 |         properties[param_name] = _parse_type_hint(param_type)
   298 | 
   299 |     required = []
   300 |     for param_name, param in signature.parameters.items():
   301 |         if param.annotation == inspect.Parameter.empty and error_on_missing_type_hints:
   302 |             raise TypeHintParsingException(f"Argument {param.name} is missing a type hint in function {func.__name__}")
   303 |         if param_name not in properties:
   304 |             properties[param_name] = {}
   305 | 
   306 |         if param.default == inspect.Parameter.empty:
   307 |             required.append(param_name)
   308 |         else:
   309 |             properties[param_name]["nullable"] = True
   310 | 
   311 |     # Return: multi‐type union -> treat as any
   312 |     if (
   313 |         "return" in properties
   314 |         and (return_type := properties["return"].get("type"))
   315 |         and not isinstance(return_type, str)
   316 |     ):
   317 |         properties["return"]["type"] = "any"
   318 | 
   319 |     schema = {"type": "object", "properties": properties}
   320 |     if required:
   321 |         schema["required"] = required
   322 | 
       | … additional lines omitted from this preview …
src/smolagents/_function_type_hints_utils.py · lines 212–231 · get_json_schema
   212 |     json_schema = _convert_type_hints_to_json_schema(func)
   213 |     if (return_dict := json_schema["properties"].pop("return", None)) is not None:
   214 |         if return_doc is not None:  # We allow a missing return docstring since most templates ignore it
   215 |             return_dict["description"] = return_doc
   216 |     for arg, schema in json_schema["properties"].items():
   217 |         if arg not in param_descriptions:
   218 |             raise DocstringParsingException(
   219 |                 f"Cannot generate JSON schema for {func.__name__} because the docstring has no description for the argument '{arg}'"
   220 |             )
   221 |         desc = param_descriptions[arg]
   222 |         enum_choices = re.search(r"\(choices:\s*(.*?)\)\s*$", desc, flags=re.IGNORECASE)
   223 |         if enum_choices:
   224 |             schema["enum"] = [c.strip() for c in json.loads(enum_choices.group(1))]
   225 |             desc = enum_choices.string[: enum_choices.start()].strip()
   226 |         schema["description"] = desc
   227 | 
   228 |     output = {"name": func.__name__, "description": main_doc, "parameters": json_schema}
   229 |     if return_dict is not None:
   230 |         output["return"] = return_dict
   231 |     return {"type": "function", "function": output}
src/smolagents/models.py · lines 288–329 · get_tool_json_schema
   288 | def get_tool_json_schema(tool: Tool) -> dict:
   289 |     properties = deepcopy(tool.inputs)
   290 |     required = []
   291 |     for key, value in properties.items():
   292 |         if value["type"] == "any":
   293 |             value["type"] = "string"
   294 |         if not ("nullable" in value and value["nullable"]):
   295 |             required.append(key)
   296 | 
   297 |         # parse anyOf
   298 |         if "anyOf" in value:
   299 |             types = []
   300 |             enum = None
   301 |             for t in value["anyOf"]:
   302 |                 if t["type"] == "null":
   303 |                     value["nullable"] = True
   304 |                     continue
   305 |                 if t["type"] == "any":
   306 |                     types.append("string")
   307 |                 else:
   308 |                     types.append(t["type"])
   309 |                 if "enum" in t:  # assuming there is only one enum in anyOf
   310 |                     enum = t["enum"]
   311 | 
   312 |             value["type"] = types if len(types) > 1 else types[0]
   313 |             if enum is not None:
   314 |                 value["enum"] = enum
   315 | 
   316 |             value.pop("anyOf")
   317 | 
   318 |     return {
   319 |         "type": "function",
       | … additional lines omitted from this preview …
src/smolagents/_function_type_hints_utils.py · lines 204–208 · get_json_schema
   204 |     doc = inspect.getdoc(func)
   205 |     if not doc:
   206 |         raise DocstringParsingException(
   207 |             f"Cannot generate JSON schema for {func.__name__} because it has no docstring!"
   208 |         )
14
Workflow 14

Tool packaging and distribution

What this workflow does

A repo-native Tool is validated, serialized, saved, published, or reconstructed so an agent can reuse the tool locally or from remote sources.

Purpose

Prepare a tool for agent use, export it, or load it from a trusted remote source

What it produces

Tool / ToolCollection

Who relies on it

Agent developer or workflow author

Why this workflow matters

  • Reuses agent capabilities across runs
  • Moves tool logic between local code and Hub/Space/MCP sources
  • Produces a runnable Gradio app and requirements bundle when exporting
View supporting code
src/smolagents/tools.py · lines 137–249 · Tool
   137 |     def __init__(self, *args, **kwargs):
   138 |         self.is_initialized = False
   139 | 
   140 |     def __init_subclass__(cls, **kwargs):
   141 |         super().__init_subclass__(**kwargs)
   142 |         validate_after_init(cls)
   143 | 
   144 |     def validate_arguments(self):
   145 |         required_attributes = {
   146 |             "description": str,
   147 |             "name": str,
   148 |             "inputs": dict,
   149 |             "output_type": str,
   150 |         }
   151 |         # Validate class attributes
   152 |         for attr, expected_type in required_attributes.items():
   153 |             attr_value = getattr(self, attr, None)
   154 |             if attr_value is None:
   155 |                 raise TypeError(f"You must set an attribute {attr}.")
   156 |             if not isinstance(attr_value, expected_type):
   157 |                 raise TypeError(
   158 |                     f"Attribute {attr} should have type {expected_type.__name__}, got {type(attr_value)} instead."
   159 |                 )
   160 | 
   161 |         # Validate optional output_schema attribute
   162 |         output_schema = getattr(self, "output_schema", None)
   163 |         if output_schema is not None and not isinstance(output_schema, dict):
   164 |             raise TypeError(f"Attribute output_schema should have type dict, got {type(output_schema)} instead.")
   165 | 
   166 |         # - Validate name
   167 |         if not is_valid_name(self.name):
   168 |             raise Exception(
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 517–597 · Tool.from_hub
   517 |     def from_hub(
   518 |         cls,
   519 |         repo_id: str,
   520 |         <redacted> | None = None,
   521 |         trust_remote_code: bool = False,
   522 |         **kwargs,
   523 |     ):
   524 |         """
   525 |         Loads a tool defined on the Hub.
   526 | 
   527 |         <Tip warning={true}>
   528 | 
   529 |         Loading a tool from the Hub means that you'll download the tool and execute it locally.
   530 |         ALWAYS inspect the tool you're downloading before loading it within your runtime, as you would do when
   531 |         installing a package using pip/npm/apt.
   532 | 
   533 |         </Tip>
   534 | 
   535 |         Args:
   536 |             repo_id (`str`):
   537 |                 The name of the Space repo on the Hub where your tool is defined.
   538 |             token (`str`, *optional*):
   539 |                 The token to identify you on hf.co. If unset, will use the token generated when running
   540 |                 `huggingface-cli login` (stored in `~/.huggingface`).
   541 |             trust_remote_code(`str`, *optional*, defaults to False):
   542 |                 This flags marks that you understand the risk of running remote code and that you trust this tool.
   543 |                 If not setting this to True, loading the tool from Hub will fail.
   544 |             kwargs (additional keyword arguments, *optional*):
   545 |                 Additional keyword arguments that will be split in two: all arguments relevant to the Hub (such as
   546 |                 `cache_dir`, `revision`, `subfolder`) will be used when downloading the files for your tool, and the
   547 |                 others will be passed along to its init.
   548 |         """
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 906–1058 · ToolCollection
   906 |     def __init__(self, tools: list[Tool]):
   907 |         self.tools = tools
   908 | 
   909 |     @classmethod
   910 |     def from_hub(
   911 |         cls,
   912 |         collection_slug: str,
   913 |         <redacted> | None = None,
   914 |         trust_remote_code: bool = False,
   915 |     ) -> "ToolCollection":
   916 |         """Loads a tool collection from the Hub.
   917 | 
   918 |         it adds a collection of tools from all Spaces in the collection to the agent's toolbox
   919 | 
   920 |         > [!NOTE]
   921 |         > Only Spaces will be fetched, so you can feel free to add models and datasets to your collection if you'd
   922 |         > like for this collection to showcase them.
   923 | 
   924 |         Args:
   925 |             collection_slug (str): The collection slug referencing the collection.
   926 |             token (str, *optional*): The authentication token if the collection is private.
   927 |             trust_remote_code (bool, *optional*, defaults to False): Whether to trust the remote code.
   928 | 
   929 |         Returns:
   930 |             ToolCollection: A tool collection instance loaded with the tools.
   931 | 
   932 |         Example:
   933 |         ```py
   934 |         >>> from smolagents import ToolCollection, CodeAgent
   935 | 
   936 |         >>> image_tool_collection = ToolCollection.from_hub("huggingface-tools/diffusion-tools-6630bb19a942c2306a2cdb6f")
   937 |         >>> agent = CodeAgent(tools=[*image_tool_collection.tools], add_base_tools=True)
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 292–388 · Tool.to_dict/from_dict
   292 |     def to_dict(self) -> dict:
   293 |         """Returns a dictionary representing the tool"""
   294 |         class_name = self.__class__.__name__
   295 |         if type(self).__name__ == "SimpleTool":
   296 |             # Check that imports are self-contained
   297 |             source_code = get_source(self.forward).replace("@tool", "")
   298 |             forward_node = ast.parse(source_code)
   299 |             # If tool was created using '@tool' decorator, it has only a forward pass, so it's simpler to just get its code
   300 |             method_checker = MethodChecker(set())
   301 |             method_checker.visit(forward_node)
   302 | 
   303 |             if len(method_checker.errors) > 0:
   304 |                 errors = [f"- {error}" for error in method_checker.errors]
   305 |                 raise (ValueError(f"SimpleTool validation failed for {self.name}:\n" + "\n".join(errors)))
   306 | 
   307 |             forward_source_code = get_source(self.forward)
   308 |             tool_code = textwrap.dedent(
   309 |                 f"""
   310 |             from smolagents import Tool
   311 |             from typing import Any, Optional
   312 | 
   313 |             class {class_name}(Tool):
   314 |                 name = "{self.name}"
   315 |                 description = {json.dumps(textwrap.dedent(self.description).strip())}
   316 |                 inputs = {repr(self.inputs)}
   317 |                 output_type = "{self.output_type}"
   318 |             """
   319 |             ).strip()
   320 | 
   321 |             # Add output_schema if it exists
   322 |             if hasattr(self, "output_schema") and self.output_schema is not None:
   323 |                 tool_code += f"\n                output_schema = {repr(self.output_schema)}"
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 421–597 · Tool.push_to_hub/from_hub
   421 |     def push_to_hub(
   422 |         self,
   423 |         repo_id: str,
   424 |         commit_message: str = "Upload tool",
   425 |         private: bool | None = None,
   426 |         <redacted> | str | None = None,
   427 |         create_pr: bool = False,
   428 |     ) -> str:
   429 |         """
   430 |         Upload the tool to the Hub.
   431 | 
   432 |         Parameters:
   433 |             repo_id (`str`):
   434 |                 The name of the repository you want to push your tool to. It should contain your organization name when
   435 |                 pushing to a given organization.
   436 |             commit_message (`str`, *optional*, defaults to `"Upload tool"`):
   437 |                 Message to commit while pushing.
   438 |             private (`bool`, *optional*):
   439 |                 Whether to make the repo private. If `None` (default), the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.
   440 |             token (`bool` or `str`, *optional*):
   441 |                 The token to use as HTTP bearer authorization for remote files. If unset, will use the token generated
   442 |                 when running `huggingface-cli login` (stored in `~/.huggingface`).
   443 |             create_pr (`bool`, *optional*, defaults to `False`):
   444 |                 Whether to create a PR with the uploaded files or directly commit.
   445 |         """
   446 |         # Initialize repository
   447 |         repo_id = self._initialize_hub_repo(repo_id, token, private)
   448 |         # Prepare files for commit
   449 |         additions = self._prepare_hub_files()
   450 |         # Create commit
   451 |         return create_commit(
   452 |             repo_id=repo_id,
       | … additional lines omitted from this preview …
src/smolagents/tools.py · lines 643–739 · Tool.from_space
   643 |         from gradio_client import Client, handle_file
   644 | 
   645 |         class SpaceToolWrapper(Tool):
   646 |             skip_forward_signature_validation = True
   647 | 
   648 |             def __init__(
   649 |                 self,
   650 |                 space_id: str,
   651 |                 name: str,
   652 |                 description: str = "",
   653 |                 api_name: str | None = None,
   654 |                 <redacted> | None = None,
   655 |             ):
   656 |                 self.name = name
   657 |                 self.description = description
   658 |                 self.client = Client(space_id, hf_<redacted>
   659 |                 space_api = self.client.view_api(return_format="dict", print_info=False)
   660 |                 assert isinstance(space_api, dict)
   661 |                 space_description = space_api["named_endpoints"]
   662 | 
   663 |                 # If api_name is not defined, take the first of the available APIs for this space
   664 |                 if api_name is None:
   665 |                     api_name = list(space_description.keys())[0]
   666 |                     warnings.warn(
   667 |                         f"Since `api_name` was not defined, it was automatically set to the first available API: `{api_name}`."
   668 |                     )
   669 |                 self.api_name = api_name
   670 | 
   671 |                 try:
   672 |                     space_description_api = space_description[api_name]
   673 |                 except KeyError:
   674 |                     raise KeyError(f"Could not find specified {api_name=} among available api names.")
       | … additional lines omitted from this preview …
15
Workflow 15

tool session management

What this workflow does

MCPClient opens a session to an MCP server, exposes the server’s tools to the agent, and disconnects on shutdown or context exit.

Purpose

retrieve and use external MCP tools inside the agent runtime

What it produces

MCP server tools

Who relies on it

agent runtime

Why this workflow matters

  • enables external tool use
  • ensures cleanup of the MCP connection
View supporting code
src/smolagents/mcp_client.py · lines 33–42 · MCPClient
    33 | class MCPClient:
    34 |     """Manages the connection to an MCP server and make its tools available to SmolAgents.
    35 | 
    36 |     Note: tools can only be accessed after the connection has been started with the
    37 |         `connect()` method, done during the init. If you don't use the context manager
    38 |         we strongly encourage to use "try ... finally" to ensure the connection is cleaned up.
    39 | 
    40 |     Args:
    41 |         server_parameters (StdioServerParameters | dict[str, Any] | list[StdioServerParameters | dict[str, Any]]):
    42 |             Configuration parameters to connect to the MCP server. Can be a list if you want to connect multiple MCPs at once.
src/smolagents/mcp_client.py · lines 124–135 · MCPClient.connect/disconnect
   124 |     def connect(self):
   125 |         """Connect to the MCP server and initialize the tools."""
   126 |         self._tools: list[Tool] = self._adapter.__enter__()
   127 | 
   128 |     def disconnect(
   129 |         self,
   130 |         exc_type: type[BaseException] | None = None,
   131 |         exc_value: BaseException | None = None,
   132 |         exc_traceback: TracebackType | None = None,
   133 |     ):
   134 |         """Disconnect from the MCP server"""
   135 |         self._adapter.__exit__(exc_type, exc_value, exc_traceback)
src/smolagents/mcp_client.py · lines 41–49 · MCPClient.__init__
    41 |         server_parameters (StdioServerParameters | dict[str, Any] | list[StdioServerParameters | dict[str, Any]]):
    42 |             Configuration parameters to connect to the MCP server. Can be a list if you want to connect multiple MCPs at once.
    43 | 
    44 |             - An instance of `mcp.StdioServerParameters` for connecting a Stdio MCP server via standard input/output using a subprocess.
    45 | 
    46 |             - A `dict` with at least:
    47 |               - "url": URL of the server.
    48 |               - "transport": Transport protocol to use, one of:
    49 |                 - "streamable-http": Streamable HTTP transport (default).
src/smolagents/mcp_client.py · lines 137–154 · MCPClient.get_tools
   137 |     def get_tools(self) -> list[Tool]:
   138 |         """The SmolAgents tools available from the MCP server.
   139 | 
   140 |         Note: for now, this always returns the tools available at the creation of the session,
   141 |         but it will in a future release return also new tools available from the MCP server if
   142 |         any at call time.
   143 | 
   144 |         Raises:
   145 |             ValueError: If the MCP server tools is None (usually assuming the server is not started).
   146 | 
   147 |         Returns:
   148 |             list[Tool]: The SmolAgents tools available from the MCP server.
   149 |         """
   150 |         if self._tools is None:
   151 |             raise ValueError(
   152 |                 "Couldn't retrieve tools from MCP server, run `mcp_client.connect()` first before accessing `tools`"
   153 |             )
   154 |         return self._tools
src/smolagents/mcp_client.py · lines 121–154 · MCPClient._tools/get_tools
   121 |         self._tools: list[Tool] | None = None
   122 |         self.connect()
   123 | 
   124 |     def connect(self):
   125 |         """Connect to the MCP server and initialize the tools."""
   126 |         self._tools: list[Tool] = self._adapter.__enter__()
   127 | 
   128 |     def disconnect(
   129 |         self,
   130 |         exc_type: type[BaseException] | None = None,
   131 |         exc_value: BaseException | None = None,
   132 |         exc_traceback: TracebackType | None = None,
   133 |     ):
   134 |         """Disconnect from the MCP server"""
   135 |         self._adapter.__exit__(exc_type, exc_value, exc_traceback)
   136 | 
   137 |     def get_tools(self) -> list[Tool]:
   138 |         """The SmolAgents tools available from the MCP server.
   139 | 
   140 |         Note: for now, this always returns the tools available at the creation of the session,
   141 |         but it will in a future release return also new tools available from the MCP server if
   142 |         any at call time.
   143 | 
   144 |         Raises:
   145 |             ValueError: If the MCP server tools is None (usually assuming the server is not started).
   146 | 
   147 |         Returns:
   148 |             list[Tool]: The SmolAgents tools available from the MCP server.
   149 |         """
   150 |         if self._tools is None:
   151 |             raise ValueError(
   152 |                 "Couldn't retrieve tools from MCP server, run `mcp_client.connect()` first before accessing `tools`"
       | … additional lines omitted from this preview …
src/smolagents/mcp_client.py · lines 91–101 · MCPClient.__init__
    91 |         # Handle future warning for structured_output default value change
    92 |         if structured_output is None:
    93 |             warnings.warn(
    94 |                 "Parameter 'structured_output' was not specified. "
    95 |                 "Currently it defaults to False, but in version 1.25, the default will change to True. "
    96 |                 "To suppress this warning, explicitly set structured_output=True (new behavior) or structured_output=False (legacy behavior). "
    97 |                 "See documentation at https://huggingface.co/docs/smolagents/tutorials/tools#structured-output-and-output-schema-support for more details.",
    98 |                 FutureWarning,
    99 |                 stacklevel=2,
   100 |             )
   101 |             structured_output = False
Workflow area 6

Answering workflows

2 workflows
16
Workflow 16

web-assisted research answering

What this workflow does

A research user asks a question, and a multi-agent browser workflow searches the web, inspects pages/files, and returns an answer.

Purpose

answer a natural-language question using internet browsing and file inspection

What it produces

question / research answer

Who relies on it

CLI user

Why this workflow matters

  • returns a synthesized answer to the user
  • navigates external pages and documents to gather supporting information
View supporting code
examples/open_deep_research/run.py · lines 34–40 · parse_args
    34 | def parse_args():
    35 |     parser = argparse.ArgumentParser()
    36 |     parser.add_argument(
    37 |         "question", type=str, help="for example: 'How many studio albums did Mercedes Sosa release before 2007?'"
    38 |     )
    39 |     parser.add_argument("--model-id", type=str, default="o1")
    40 |     return parser.parse_args()
examples/open_deep_research/run.py · lines 114–121 · main
   114 | def main():
   115 |     args = parse_args()
   116 | 
   117 |     agent = create_agent(model_id=args.model_id)
   118 | 
   119 |     answer = agent.run(args.question)
   120 | 
   121 |     print(f"Got this answer: {answer}")
examples/open_deep_research/run.py · lines 60–111 · create_agent
    60 | def create_agent(model_id="o1"):
    61 |     model_params = {
    62 |         "model_id": model_id,
    63 |         "custom_role_conversions": custom_role_conversions,
    64 |         "max_completion_tokens": 8192,
    65 |     }
    66 |     if model_id == "o1":
    67 |         model_params["reasoning_effort"] = "high"
    68 |     model = LiteLLMModel(**model_params)
    69 | 
    70 |     text_limit = 100000
    71 |     browser = SimpleTextBrowser(**BROWSER_CONFIG)
    72 |     WEB_TOOLS = [
    73 |         GoogleSearchTool(provider="serper"),
    74 |         VisitTool(browser),
    75 |         PageUpTool(browser),
    76 |         PageDownTool(browser),
    77 |         FinderTool(browser),
    78 |         FindNextTool(browser),
    79 |         ArchiveSearchTool(browser),
    80 |         TextInspectorTool(model, text_limit),
    81 |     ]
    82 |     text_webbrowser_agent = ToolCallingAgent(
    83 |         model=model,
    84 |         tools=WEB_TOOLS,
    85 |         max_steps=20,
    86 |         verbosity_level=2,
    87 |         planning_interval=4,
    88 |         name="search_agent",
    89 |         description="""A team member that will search the internet to answer your question.
    90 |     Ask him for all your questions that require browsing the web.
    91 |     Provide him as much context as possible, in particular if you need to search on a specific timeframe!
       | … additional lines omitted from this preview …
examples/open_deep_research/run.py · lines 47–57 · BROWSER_CONFIG
    47 | BROWSER_CONFIG = {
    48 |     "viewport_size": 1024 * 5,
    49 |     "downloads_folder": "downloads_folder",
    50 |     "request_kwargs": {
    51 |         "headers": {"User-Agent": user_agent},
    52 |         "timeout": 300,
    53 |     },
    54 |     "serpapi_key": os.getenv("SERPAPI_API_KEY"),
    55 | }
    56 | 
    57 | os.makedirs(f"./{BROWSER_CONFIG['downloads_folder']}", exist_ok=True)
examples/open_deep_research/run.py · lines 117–120 · main
   117 |     agent = create_agent(model_id=args.model_id)
   118 | 
   119 |     answer = agent.run(args.question)
   120 | 
examples/open_deep_research/scripts/text_inspector_tool.py · lines 5–18 · TextInspectorTool
     5 | class TextInspectorTool(Tool):
     6 |     name = "inspect_file_as_text"
     7 |     description = """
     8 | You cannot load files yourself: instead call this tool to read a file as markdown text and ask questions about it.
     9 | This tool handles the following file extensions: [".html", ".htm", ".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".flac", ".pdf", ".docx"], and all other types of text files. IT DOES NOT HANDLE IMAGES."""
    10 | 
    11 |     inputs = {
    12 |         "file_path": {
    13 |             "description": "The path to the file you want to read as text. Must be a '.something' file, like '.pdf'. If it is an image, use the visualizer tool instead! DO NOT use this tool for an HTML webpage: use the web_search tool instead!",
    14 |             "type": "string",
    15 |         },
    16 |         "question": {
    17 |             "description": "[Optional]: Your question, as a natural language sentence. Provide as much context as possible. Do not pass this parameter if you just want to directly return the content of the file.",
    18 |             "type": "string",
17
Workflow 17

visual question answering

What this workflow does

An image is encoded and sent to a vision-capable external API to produce a detailed caption or answer.

Purpose

answer questions about an attached image

What it produces

image / image caption

Who relies on it

agent user

Why this workflow matters

  • produces an image-based answer or caption
  • downloads remote images when needed before inference
View supporting code
examples/open_deep_research/scripts/visual_qa.py · lines 141–180 · visualizer
   141 | @tool
   142 | def visualizer(image_path: str, question: str | None = None) -> str:
   143 |     """A tool that can answer questions about attached images.
   144 | 
   145 |     Args:
   146 |         image_path: The path to the image on which to answer the question. This should be a local path to downloaded image.
   147 |         question: The question to answer.
   148 |     """
   149 |     import mimetypes
   150 |     import os
   151 | 
   152 |     import requests
   153 | 
   154 |     from .visual_qa import encode_image
   155 | 
   156 |     add_note = False
   157 |     if not question:
   158 |         add_note = True
   159 |         question = "Please write a detailed caption for this image."
   160 |     if not isinstance(image_path, str):
   161 |         raise Exception("You should provide at least `image_path` string argument to this tool!")
   162 | 
   163 |     mime_type, _ = mimetypes.guess_type(image_path)
   164 |     base64_image = encode_image(image_path)
   165 | 
   166 |     payload = {
   167 |         "model": "gpt-4o",
   168 |         "messages": [
   169 |             {
   170 |                 "role": "user",
   171 |                 "content": [
   172 |                     {"type": "text", "text": question},
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/visual_qa.py · lines 66–93 · encode_image
    66 | def encode_image(image_path):
    67 |     if image_path.startswith("http"):
    68 |         user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"
    69 |         request_kwargs = {
    70 |             "headers": {"User-Agent": user_agent},
    71 |             "stream": True,
    72 |         }
    73 | 
    74 |         # Send a HTTP request to the URL
    75 |         response = requests.get(image_path, **request_kwargs)
    76 |         response.raise_for_status()
    77 |         content_type = response.headers.get("content-type", "")
    78 | 
    79 |         extension = mimetypes.guess_extension(content_type)
    80 |         if extension is None:
    81 |             extension = ".download"
    82 | 
    83 |         fname = str(uuid.uuid4()) + extension
    84 |         download_path = os.path.abspath(os.path.join("downloads", fname))
    85 | 
    86 |         with open(download_path, "wb") as fh:
    87 |             for chunk in response.iter_content(chunk_size=512):
    88 |                 fh.write(chunk)
    89 | 
    90 |         image_path = download_path
    91 | 
    92 |     with open(image_path, "rb") as image_file:
    93 |         return base64.b64encode(image_file.read()).decode("utf-8")
examples/open_deep_research/scripts/visual_qa.py · lines 119–139 · VisualQATool.forward
   119 |     def forward(self, image_path: str, question: str | None = None) -> str:
   120 |         output = ""
   121 |         add_note = False
   122 |         if not question:
   123 |             add_note = True
   124 |             question = "Please write a detailed caption for this image."
   125 |         try:
   126 |             output = process_images_and_text(image_path, question, self.client)
   127 |         except Exception as e:
   128 |             print(e)
   129 |             if "Payload Too Large" in str(e):
   130 |                 new_image_path = resize_image(image_path)
   131 |                 output = process_images_and_text(new_image_path, question, self.client)
   132 | 
   133 |         if add_note:
   134 |             output = (
   135 |                 f"You did not provide a particular question, so here is a detailed caption for the image: {output}"
   136 |             )
   137 | 
   138 |         return output
   139 | 
examples/open_deep_research/scripts/visual_qa.py · lines 125–132 · VisualQATool.forward
   125 |         try:
   126 |             output = process_images_and_text(image_path, question, self.client)
   127 |         except Exception as e:
   128 |             print(e)
   129 |             if "Payload Too Large" in str(e):
   130 |                 new_image_path = resize_image(image_path)
   131 |                 output = process_images_and_text(new_image_path, question, self.client)
   132 | 
examples/open_deep_research/scripts/visual_qa.py · lines 141–148 · visualizer
   141 | @tool
   142 | def visualizer(image_path: str, question: str | None = None) -> str:
   143 |     """A tool that can answer questions about attached images.
   144 | 
   145 |     Args:
   146 |         image_path: The path to the image on which to answer the question. This should be a local path to downloaded image.
   147 |         question: The question to answer.
   148 |     """
examples/open_deep_research/scripts/visual_qa.py · lines 166–180 · visualizer
   166 |     payload = {
   167 |         "model": "gpt-4o",
   168 |         "messages": [
   169 |             {
   170 |                 "role": "user",
   171 |                 "content": [
   172 |                     {"type": "text", "text": question},
   173 |                     {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{base64_image}"}},
   174 |                 ],
   175 |             }
   176 |         ],
   177 |         "max_tokens": 1000,
   178 |     }
   179 |     headers = {"Content-Type": "application/json", "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
   180 |     response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
Workflow area 7

Web workflows

2 workflows
18
Workflow 18

Web search retrieval

What this workflow does

A search tool accepts a query, applies rate limiting, calls DuckDuckGo, and returns formatted search results.

Purpose

Look up web results for a query and hand formatted results back to the agent

What it produces

search query / search results

Who relies on it

Agent user or downstream agent step

Why this workflow matters

  • Retrieves external information
  • Produces a human-readable results summary
  • Limits request rate to avoid overuse
View supporting code
src/smolagents/default_tools.py · lines 104–159 · DuckDuckGoSearchTool
   104 | class DuckDuckGoSearchTool(Tool):
   105 |     """Web search tool that performs searches using the DuckDuckGo search engine.
   106 | 
   107 |     Args:
   108 |         max_results (`int`, default `10`): Maximum number of search results to return.
   109 |         rate_limit (`float`, default `1.0`): Maximum queries per second. Set to `None` to disable rate limiting.
   110 |         **kwargs: Additional keyword arguments for the `DDGS` client.
   111 | 
   112 |     Examples:
   113 |         ```python
   114 |         >>> from smolagents import DuckDuckGoSearchTool
   115 |         >>> web_search_tool = DuckDuckGoSearchTool(max_results=5, rate_limit=2.0)
   116 |         >>> results = web_search_tool("Hugging Face")
   117 |         >>> print(results)
   118 |         ```
   119 |     """
   120 | 
   121 |     name = "web_search"
   122 |     description = """Performs a duckduckgo web search based on your query (think a Google search) then returns the top search results."""
   123 |     inputs = {"query": {"type": "string", "description": "The search query to perform."}}
   124 |     output_type = "string"
   125 | 
   126 |     def __init__(self, max_results: int = 10, rate_limit: float | None = 1.0, **kwargs):
   127 |         super().__init__()
   128 |         self.max_results = max_results
   129 |         self.rate_limit = rate_limit
   130 |         self._min_interval = 1.0 / rate_limit if rate_limit else 0.0
   131 |         self._last_request_time = 0.0
   132 |         try:
   133 |             from ddgs import DDGS
   134 |         except ImportError as e:
   135 |             raise ImportError(
       | … additional lines omitted from this preview …
src/smolagents/default_tools.py · lines 121–145 · DuckDuckGoSearchTool.forward
   121 |     name = "web_search"
   122 |     description = """Performs a duckduckgo web search based on your query (think a Google search) then returns the top search results."""
   123 |     inputs = {"query": {"type": "string", "description": "The search query to perform."}}
   124 |     output_type = "string"
   125 | 
   126 |     def __init__(self, max_results: int = 10, rate_limit: float | None = 1.0, **kwargs):
   127 |         super().__init__()
   128 |         self.max_results = max_results
   129 |         self.rate_limit = rate_limit
   130 |         self._min_interval = 1.0 / rate_limit if rate_limit else 0.0
   131 |         self._last_request_time = 0.0
   132 |         try:
   133 |             from ddgs import DDGS
   134 |         except ImportError as e:
   135 |             raise ImportError(
   136 |                 "You must install package `ddgs` to run this tool: for instance run `pip install ddgs`."
   137 |             ) from e
   138 |         self.ddgs = DDGS(**kwargs)
   139 | 
   140 |     def forward(self, query: str) -> str:
   141 |         self._enforce_rate_limit()
   142 |         results = self.ddgs.text(query, max_results=self.max_results)
   143 |         if len(results) == 0:
   144 |             raise Exception("No results found! Try a less restrictive/shorter query.")
   145 |         postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
src/smolagents/default_tools.py · lines 141–146 · DuckDuckGoSearchTool.forward
   141 |         self._enforce_rate_limit()
   142 |         results = self.ddgs.text(query, max_results=self.max_results)
   143 |         if len(results) == 0:
   144 |             raise Exception("No results found! Try a less restrictive/shorter query.")
   145 |         postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
   146 |         return "## Search Results\n\n" + "\n\n".join(postprocessed_results)
src/smolagents/default_tools.py · lines 126–159 · DuckDuckGoSearchTool.__init__/_enforce_rate_limit
   126 |     def __init__(self, max_results: int = 10, rate_limit: float | None = 1.0, **kwargs):
   127 |         super().__init__()
   128 |         self.max_results = max_results
   129 |         self.rate_limit = rate_limit
   130 |         self._min_interval = 1.0 / rate_limit if rate_limit else 0.0
   131 |         self._last_request_time = 0.0
   132 |         try:
   133 |             from ddgs import DDGS
   134 |         except ImportError as e:
   135 |             raise ImportError(
   136 |                 "You must install package `ddgs` to run this tool: for instance run `pip install ddgs`."
   137 |             ) from e
   138 |         self.ddgs = DDGS(**kwargs)
   139 | 
   140 |     def forward(self, query: str) -> str:
   141 |         self._enforce_rate_limit()
   142 |         results = self.ddgs.text(query, max_results=self.max_results)
   143 |         if len(results) == 0:
   144 |             raise Exception("No results found! Try a less restrictive/shorter query.")
   145 |         postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
   146 |         return "## Search Results\n\n" + "\n\n".join(postprocessed_results)
   147 | 
   148 |     def _enforce_rate_limit(self) -> None:
   149 |         import time
   150 | 
   151 |         # No rate limit enforced
   152 |         if not self.rate_limit:
   153 |             return
   154 | 
   155 |         now = time.time()
   156 |         elapsed = now - self._last_request_time
   157 |         if elapsed < self._min_interval:
       | … additional lines omitted from this preview …
src/smolagents/default_tools.py · lines 148–159 · DuckDuckGoSearchTool._enforce_rate_limit
   148 |     def _enforce_rate_limit(self) -> None:
   149 |         import time
   150 | 
   151 |         # No rate limit enforced
   152 |         if not self.rate_limit:
   153 |             return
   154 | 
   155 |         now = time.time()
   156 |         elapsed = now - self._last_request_time
   157 |         if elapsed < self._min_interval:
   158 |             time.sleep(self._min_interval - elapsed)
   159 |         self._last_request_time = time.time()
src/smolagents/default_tools.py · lines 142–145 · DuckDuckGoSearchTool.forward
   142 |         results = self.ddgs.text(query, max_results=self.max_results)
   143 |         if len(results) == 0:
   144 |             raise Exception("No results found! Try a less restrictive/shorter query.")
   145 |         postprocessed_results = [f"[{result['title']}]({result['href']})\n{result['body']}" for result in results]
19
Workflow 19

web search and webpage browsing

What this workflow does

CodeAgent uses browser/search tools to query the web, visit pages, and read text content from returned web results.

Purpose

retrieve web information and inspect page text in an agent loop

What it produces

web page content and search results

Who relies on it

CodeAgent / ToolCallingAgent user

Why this workflow matters

  • Enables agentic retrieval of external information
  • Returns markdown/text that downstream agent steps can reason over
View supporting code
examples/open_deep_research/scripts/text_web_browser.py · lines 384–391 · SearchInformationTool.forward
   384 |     def __init__(self, browser):
   385 |         super().__init__()
   386 |         self.browser = browser
   387 | 
   388 |     def forward(self, query: str, filter_year: int | None = None) -> str:
   389 |         self.browser.visit_page(f"google: {query}", filter_year=filter_year)
   390 |         header, content = self.browser._state()
   391 |         return header.strip() + "\n=======================\n" + content
examples/open_deep_research/scripts/text_web_browser.py · lines 400–407 · VisitTool.forward
   400 |     def __init__(self, browser=None):
   401 |         super().__init__()
   402 |         self.browser = browser
   403 | 
   404 |     def forward(self, url: str) -> str:
   405 |         self.browser.visit_page(url)
   406 |         header, content = self.browser._state()
   407 |         return header.strip() + "\n=======================\n" + content
examples/open_deep_research/scripts/text_web_browser.py · lines 55–76 · SimpleTextBrowser.set_address
    55 |     def set_address(self, uri_or_path: str, filter_year: int | None = None) -> None:
    56 |         # TODO: Handle anchors
    57 |         self.history.append((uri_or_path, time.time()))
    58 | 
    59 |         # Handle special URIs
    60 |         if uri_or_path == "about:blank":
    61 |             self._set_page_content("")
    62 |         elif uri_or_path.startswith("google:"):
    63 |             self._serpapi_search(uri_or_path[len("google:") :].strip(), filter_year=filter_year)
    64 |         else:
    65 |             if (
    66 |                 not uri_or_path.startswith("http:")
    67 |                 and not uri_or_path.startswith("https:")
    68 |                 and not uri_or_path.startswith("file:")
    69 |             ):
    70 |                 if len(self.history) > 1:
    71 |                     prior_address = self.history[-2][0]
    72 |                     uri_or_path = urljoin(prior_address, uri_or_path)
    73 |                     # Update the address with the fully-qualified path
    74 |                     self.history[-1] = (uri_or_path, self.history[-1][1])
    75 |             self._fetch_page(uri_or_path)
    76 | 
examples/open_deep_research/scripts/text_web_browser.py · lines 204–261 · SimpleTextBrowser._serpapi_search
   204 |     def _serpapi_search(self, query: str, filter_year: int | None = None) -> None:
   205 |         if self.serpapi_key is None:
   206 |             raise ValueError("Missing SerpAPI key.")
   207 | 
   208 |         params = {
   209 |             "engine": "google",
   210 |             "q": query,
   211 |             "api_key": self.serpapi_key,
   212 |         }
   213 |         if filter_year is not None:
   214 |             params["tbs"] = f"cdr:1,cd_min:01/01/{filter_year},cd_max:12/31/{filter_year}"
   215 | 
   216 |         search = GoogleSearch(params)
   217 |         results = search.get_dict()
   218 |         self.page_title = f"{query} - Search"
   219 |         if "organic_results" not in results.keys():
   220 |             raise Exception(f"No results found for query: '{query}'. Use a less specific query.")
   221 |         if len(results["organic_results"]) == 0:
   222 |             year_filter_message = f" with filter year={filter_year}" if filter_year is not None else ""
   223 |             self._set_page_content(
   224 |                 f"No results found for '{query}'{year_filter_message}. Try with a more general query, or remove the year filter."
   225 |             )
   226 |             return
   227 | 
   228 |         def _prev_visit(url):
   229 |             for i in range(len(self.history) - 1, -1, -1):
   230 |                 if self.history[i][0] == url:
   231 |                     return f"You previously visited this page {round(time.time() - self.history[i][1])} seconds ago.\n"
   232 |             return ""
   233 | 
   234 |         web_snippets: list[str] = list()
   235 |         idx = 0
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/text_web_browser.py · lines 92–101 · SimpleTextBrowser._set_page_content
    92 |     def _set_page_content(self, content: str) -> None:
    93 |         """Sets the text content of the current page."""
    94 |         self._page_content = content
    95 |         self._split_pages()
    96 |         if self.viewport_current_page >= len(self.viewport_pages):
    97 |             self.viewport_current_page = len(self.viewport_pages) - 1
    98 | 
    99 |     def page_down(self) -> None:
   100 |         self.viewport_current_page = min(self.viewport_current_page + 1, len(self.viewport_pages) - 1)
   101 | 
examples/open_deep_research/scripts/text_web_browser.py · lines 182–202 · SimpleTextBrowser._split_pages
   182 |     def _split_pages(self) -> None:
   183 |         # Do not split search results
   184 |         if self.address.startswith("google:"):
   185 |             self.viewport_pages = [(0, len(self._page_content))]
   186 |             return
   187 | 
   188 |         # Handle empty pages
   189 |         if len(self._page_content) == 0:
   190 |             self.viewport_pages = [(0, 0)]
   191 |             return
   192 | 
   193 |         # Break the viewport into pages
   194 |         self.viewport_pages = []
   195 |         start_idx = 0
   196 |         while start_idx < len(self._page_content):
   197 |             end_idx = min(start_idx + self.viewport_size, len(self._page_content))  # type: ignore[operator]
   198 |             # Adjust to end on a space
   199 |             while end_idx < len(self._page_content) and self._page_content[end_idx - 1] not in [" ", "\t", "\r", "\n"]:
   200 |                 end_idx += 1
   201 |             self.viewport_pages.append((start_idx, end_idx))
   202 |             start_idx = end_idx
Workflow area 8

Retrieval workflows

1 workflow
20
Workflow 20

external content retrieval and archiving

What this workflow does

CodeAgent uses file download and archive lookup tools to retrieve remote files or archived webpages and expose local paths or rendered archive content.

Purpose

obtain a file download or archived webpage snapshot

What it produces

remote file content / archived URL snapshot

Who relies on it

CodeAgent / human operator reviewing evidence

Why this workflow matters

  • Provides inspectable local artifacts for downstream analysis
  • Supports retrieval of historical webpage snapshots when current content is unavailable
View supporting code
examples/open_deep_research/scripts/text_web_browser.py · lines 410–442 · DownloadTool
   410 | class DownloadTool(Tool):
   411 |     name = "download_file"
   412 |     description = """
   413 | Download a file at a given URL. The file should be of this format: [".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".png", ".docx"]
   414 | After using this tool, for further inspection of this page you should return the download path to your manager via final_answer, and they will be able to inspect it.
   415 | DO NOT use this tool for .pdf or .txt or .htm files: for these types of files use visit_page with the file url instead."""
   416 |     inputs = {"url": {"type": "string", "description": "The relative or absolute url of the file to be downloaded."}}
   417 |     output_type = "string"
   418 | 
   419 |     def __init__(self, browser):
   420 |         super().__init__()
   421 |         self.browser = browser
   422 | 
   423 |     def forward(self, url: str) -> str:
   424 |         import requests
   425 | 
   426 |         if "arxiv" in url:
   427 |             url = url.replace("abs", "pdf")
   428 |         response = requests.get(url)
   429 |         content_type = response.headers.get("content-type", "")
   430 |         extension = mimetypes.guess_extension(content_type)
   431 |         if extension and isinstance(extension, str):
   432 |             new_path = f"./downloads/file{extension}"
   433 |         else:
   434 |             new_path = "./downloads/file.object"
   435 | 
   436 |         with open(new_path, "wb") as f:
   437 |             f.write(response.content)
   438 | 
   439 |         if "pdf" in extension or "txt" in extension or "htm" in extension:
   440 |             raise Exception("Do not use this tool for pdf or txt or html files: use visit_page instead.")
   441 | 
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/text_web_browser.py · lines 445–485 · ArchiveSearchTool
   445 | class ArchiveSearchTool(Tool):
   446 |     name = "find_archived_url"
   447 |     description = "Given a url, searches the Wayback Machine and returns the archived version of the url that's closest in time to the desired date."
   448 |     inputs = {
   449 |         "url": {"type": "string", "description": "The url you need the archive for."},
   450 |         "date": {
   451 |             "type": "string",
   452 |             "description": "The date that you want to find the archive for. Give this date in the format 'YYYYMMDD', for instance '27 June 2008' is written as '20080627'.",
   453 |         },
   454 |     }
   455 |     output_type = "string"
   456 | 
   457 |     def __init__(self, browser=None):
   458 |         super().__init__()
   459 |         self.browser = browser
   460 | 
   461 |     def forward(self, url, date) -> str:
   462 |         import requests
   463 | 
   464 |         no_timestamp_url = f"https://archive.org/wayback/available?url={url}"
   465 |         archive_url = no_timestamp_url + f"&timestamp={date}"
   466 |         response = requests.get(archive_url).json()
   467 |         response_notimestamp = requests.get(no_timestamp_url).json()
   468 |         if "archived_snapshots" in response and "closest" in response["archived_snapshots"]:
   469 |             closest = response["archived_snapshots"]["closest"]
   470 |             print("Archive found!", closest)
   471 | 
   472 |         elif "archived_snapshots" in response_notimestamp and "closest" in response_notimestamp["archived_snapshots"]:
   473 |             closest = response_notimestamp["archived_snapshots"]["closest"]
   474 |             print("Archive found!", closest)
   475 |         else:
   476 |             raise Exception(f"Your {url=} was not archived on Wayback Machine, try a different url.")
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/text_web_browser.py · lines 428–442 · DownloadTool.forward
   428 |         response = requests.get(url)
   429 |         content_type = response.headers.get("content-type", "")
   430 |         extension = mimetypes.guess_extension(content_type)
   431 |         if extension and isinstance(extension, str):
   432 |             new_path = f"./downloads/file{extension}"
   433 |         else:
   434 |             new_path = "./downloads/file.object"
   435 | 
   436 |         with open(new_path, "wb") as f:
   437 |             f.write(response.content)
   438 | 
   439 |         if "pdf" in extension or "txt" in extension or "htm" in extension:
   440 |             raise Exception("Do not use this tool for pdf or txt or html files: use visit_page instead.")
   441 | 
   442 |         return f"File was downloaded and saved under path {new_path}."
examples/open_deep_research/scripts/text_web_browser.py · lines 464–485 · ArchiveSearchTool.forward
   464 |         no_timestamp_url = f"https://archive.org/wayback/available?url={url}"
   465 |         archive_url = no_timestamp_url + f"&timestamp={date}"
   466 |         response = requests.get(archive_url).json()
   467 |         response_notimestamp = requests.get(no_timestamp_url).json()
   468 |         if "archived_snapshots" in response and "closest" in response["archived_snapshots"]:
   469 |             closest = response["archived_snapshots"]["closest"]
   470 |             print("Archive found!", closest)
   471 | 
   472 |         elif "archived_snapshots" in response_notimestamp and "closest" in response_notimestamp["archived_snapshots"]:
   473 |             closest = response_notimestamp["archived_snapshots"]["closest"]
   474 |             print("Archive found!", closest)
   475 |         else:
   476 |             raise Exception(f"Your {url=} was not archived on Wayback Machine, try a different url.")
   477 |         target_url = closest["url"]
   478 |         self.browser.visit_page(target_url)
   479 |         header, content = self.browser._state()
   480 |         return (
   481 |             f"Web archive for url {url}, snapshot taken at date {closest['timestamp'][:8]}:\n"
   482 |             + header.strip()
   483 |             + "\n=======================\n"
   484 |             + content
   485 |         )
examples/open_deep_research/scripts/text_web_browser.py · lines 478–485 · ArchiveSearchTool.forward
   478 |         self.browser.visit_page(target_url)
   479 |         header, content = self.browser._state()
   480 |         return (
   481 |             f"Web archive for url {url}, snapshot taken at date {closest['timestamp'][:8]}:\n"
   482 |             + header.strip()
   483 |             + "\n=======================\n"
   484 |             + content
   485 |         )
examples/open_deep_research/scripts/text_web_browser.py · lines 426–427 · DownloadTool.forward
   426 |         if "arxiv" in url:
   427 |             url = url.replace("abs", "pdf")
Workflow area 9

Code workflows

1 workflow
21
Workflow 21

restricted code execution / sandboxed Python code execution

What this workflow does

CodeAgent runs Python snippets through a constrained local executor that captures output, enforces authorized imports, and applies a timeout. / LocalPythonExecutor evaluates user-supplied Python code with tool/state handling while enforcing import and dangerous-operation restrictions.

Purpose

evaluate agent-generated Python for calculations or tool orchestration / run agent-authored Python snippets against a controlled tool/state environment

What it produces

Python code snippet and execution state / code execution state and toolset

Who relies on it

CodeAgent / ToolCallingAgent / agent runtime user / developer

Why this workflow matters

  • Produces stdout plus a final output value
  • Prevents unsupported imports and limits execution duration
  • returns computed values and updated state
  • prevents unauthorized imports and dangerous access
  • halts runaway execution with time and operation limits
View supporting code
src/smolagents/local_python_executor.py · lines 1688–1758 · LocalPythonExecutor
  1688 | class LocalPythonExecutor(PythonExecutor):
  1689 |     """
  1690 |     Executor of Python code in a local environment.
  1691 | 
  1692 |     This executor evaluates Python code with restricted access to imports and built-in functions.
  1693 |     It is not a security sandbox: for isolated execution of untrusted code, use a remote executor.
  1694 |     It maintains state between executions, allows for custom tools and functions to be made available
  1695 |     to the code, and captures print outputs separately from return values.
  1696 | 
  1697 |     Args:
  1698 |         additional_authorized_imports (`list[str]`):
  1699 |             Additional authorized imports for the executor.
  1700 |         max_print_outputs_length (`int`, defaults to `DEFAULT_MAX_LEN_OUTPUT=50_000`):
  1701 |             Maximum length of the print outputs.
  1702 |         additional_functions (`dict[str, Callable]`, *optional*):
  1703 |             Additional Python functions to be added to the executor.
  1704 |         timeout_seconds (`int`, *optional*, defaults to `MAX_EXECUTION_TIME_SECONDS`):
  1705 |             Maximum time in seconds allowed for code execution. Set to `None` to disable timeout.
  1706 |     """
  1707 | 
  1708 |     def __init__(
  1709 |         self,
  1710 |         additional_authorized_imports: list[str],
  1711 |         max_print_outputs_length: int | None = None,
  1712 |         additional_functions: dict[str, Callable] | None = None,
  1713 |         timeout_seconds: int | None = MAX_EXECUTION_TIME_SECONDS,
  1714 |     ):
  1715 |         self.custom_tools = {}
  1716 |         self.state = {"__name__": "__main__"}
  1717 |         self.max_print_outputs_length = max_print_outputs_length
  1718 |         if max_print_outputs_length is None:
  1719 |             self.max_print_outputs_length = DEFAULT_MAX_LEN_OUTPUT
       | … additional lines omitted from this preview …
src/smolagents/default_tools.py · lines 39–80 · PythonInterpreterTool
    39 | class PythonInterpreterTool(Tool):
    40 |     name = "python_interpreter"
    41 |     description = "This is a tool that evaluates python code. It can be used to perform calculations."
    42 |     inputs = {
    43 |         "code": {
    44 |             "type": "string",
    45 |             "description": "The python code to run in interpreter",
    46 |         }
    47 |     }
    48 |     output_type = "string"
    49 | 
    50 |     def __init__(self, *args, authorized_imports=None, timeout_seconds=MAX_EXECUTION_TIME_SECONDS, **kwargs):
    51 |         if authorized_imports is None:
    52 |             self.authorized_imports = list(set(BASE_BUILTIN_MODULES))
    53 |         else:
    54 |             self.authorized_imports = list(set(BASE_BUILTIN_MODULES) | set(authorized_imports))
    55 |         self.inputs = {
    56 |             "code": {
    57 |                 "type": "string",
    58 |                 "description": (
    59 |                     "The code snippet to evaluate. All variables used in this snippet must be defined in this same snippet, "
    60 |                     f"else you will get an error. This code can only import the following python libraries: {self.authorized_imports}."
    61 |                 ),
    62 |             }
    63 |         }
    64 |         self.base_python_tools = BASE_PYTHON_TOOLS
    65 |         self.python_evaluator = evaluate_python_code
    66 |         self.timeout_seconds = timeout_seconds
    67 |         super().__init__(*args, **kwargs)
    68 | 
    69 |     def forward(self, code: str) -> str:
    70 |         state = {}
       | … additional lines omitted from this preview …
src/smolagents/local_python_executor.py · lines 1715–1758 · LocalPythonExecutor.__call__
  1715 |         self.custom_tools = {}
  1716 |         self.state = {"__name__": "__main__"}
  1717 |         self.max_print_outputs_length = max_print_outputs_length
  1718 |         if max_print_outputs_length is None:
  1719 |             self.max_print_outputs_length = DEFAULT_MAX_LEN_OUTPUT
  1720 |         self.additional_authorized_imports = additional_authorized_imports
  1721 |         self.authorized_imports = list(set(BASE_BUILTIN_MODULES) | set(self.additional_authorized_imports))
  1722 |         self._check_authorized_imports_are_installed()
  1723 |         self.static_tools = None
  1724 |         self.additional_functions = additional_functions or {}
  1725 |         self.timeout_seconds = timeout_seconds
  1726 | 
  1727 |     def _check_authorized_imports_are_installed(self):
  1728 |         """
  1729 |         Check that all authorized imports are installed on the system.
  1730 | 
  1731 |         Handles wildcard imports ("*") and partial star-pattern imports (e.g., "os.*").
  1732 | 
  1733 |         Raises:
  1734 |             InterpreterError: If any of the authorized modules are not installed.
  1735 |         """
  1736 |         missing_modules = [
  1737 |             base_module
  1738 |             for imp in self.authorized_imports
  1739 |             if imp != "*" and find_spec(base_module := imp.split(".")[0]) is None
  1740 |         ]
  1741 |         if missing_modules:
  1742 |             raise InterpreterError(
  1743 |                 f"Non-installed authorized modules: {', '.join(missing_modules)}. "
  1744 |                 f"Please install these modules or remove them from the authorized imports list."
  1745 |             )
  1746 | 
       | … additional lines omitted from this preview …
src/smolagents/local_python_executor.py · lines 1623–1654 · evaluate_python_code
  1623 |     if state is None:
  1624 |         state = {}
  1625 |     static_tools = static_tools.copy() if static_tools is not None else {}
  1626 |     custom_tools = custom_tools if custom_tools is not None else {}
  1627 |     state["_print_outputs"] = PrintContainer()
  1628 |     state["_operations_count"] = {"counter": 0}
  1629 | 
  1630 |     if "final_answer" in static_tools:
  1631 |         previous_final_answer = static_tools["final_answer"]
  1632 | 
  1633 |         def final_answer(*args, **kwargs):  # Allow arbitrary arguments to be passed
  1634 |             raise FinalAnswerException(previous_final_answer(*args, **kwargs))
  1635 | 
  1636 |         static_tools["final_answer"] = final_answer
  1637 | 
  1638 |     # Define the actual execution logic
  1639 |     def _execute_code():
  1640 |         result = None
  1641 |         try:
  1642 |             for node in expression.body:
  1643 |                 result = evaluate_ast(node, state, static_tools, custom_tools, authorized_imports)
  1644 |             state["_print_outputs"].value = truncate_content(
  1645 |                 str(state["_print_outputs"]), max_length=max_print_outputs_length
  1646 |             )
  1647 |             is_final_answer = False
  1648 |             return result, is_final_answer
  1649 |         except FinalAnswerException as e:
  1650 |             state["_print_outputs"].value = truncate_content(
  1651 |                 str(state["_print_outputs"]), max_length=max_print_outputs_length
  1652 |             )
  1653 |             is_final_answer = True
  1654 |             return e.value, is_final_answer
src/smolagents/local_python_executor.py · lines 1627–1658 · evaluate_python_code
  1627 |     state["_print_outputs"] = PrintContainer()
  1628 |     state["_operations_count"] = {"counter": 0}
  1629 | 
  1630 |     if "final_answer" in static_tools:
  1631 |         previous_final_answer = static_tools["final_answer"]
  1632 | 
  1633 |         def final_answer(*args, **kwargs):  # Allow arbitrary arguments to be passed
  1634 |             raise FinalAnswerException(previous_final_answer(*args, **kwargs))
  1635 | 
  1636 |         static_tools["final_answer"] = final_answer
  1637 | 
  1638 |     # Define the actual execution logic
  1639 |     def _execute_code():
  1640 |         result = None
  1641 |         try:
  1642 |             for node in expression.body:
  1643 |                 result = evaluate_ast(node, state, static_tools, custom_tools, authorized_imports)
  1644 |             state["_print_outputs"].value = truncate_content(
  1645 |                 str(state["_print_outputs"]), max_length=max_print_outputs_length
  1646 |             )
  1647 |             is_final_answer = False
  1648 |             return result, is_final_answer
  1649 |         except FinalAnswerException as e:
  1650 |             state["_print_outputs"].value = truncate_content(
  1651 |                 str(state["_print_outputs"]), max_length=max_print_outputs_length
  1652 |             )
  1653 |             is_final_answer = True
  1654 |             return e.value, is_final_answer
  1655 |         except Exception as e:
  1656 |             state["_print_outputs"].value = truncate_content(
  1657 |                 str(state["_print_outputs"]), max_length=max_print_outputs_length
  1658 |             )
src/smolagents/local_python_executor.py · lines 240–255 · PrintContainer
   240 | class PrintContainer:
   241 |     def __init__(self):
   242 |         self.value = ""
   243 | 
   244 |     def append(self, text):
   245 |         self.value += text
   246 |         return self
   247 | 
   248 |     def __iadd__(self, other):
   249 |         """Implements the += operator"""
   250 |         self.value += str(other)
   251 |         return self
   252 | 
   253 |     def __str__(self):
   254 |         """String representation"""
   255 |         return self.value
Workflow area 10

Document workflows

4 workflows
22
Workflow 22

Document ingestion and Markdown conversion

What this workflow does

A document converter accepts a local file, URL, stream, or HTTP response, fetches or stages content, guesses formats, and returns Markdown or conversion errors.

Purpose

Turn a document or webpage into LLM-friendly Markdown text

What it produces

source file / URL / HTTP response / DocumentConverterResult

Who relies on it

Research workflow or agent consuming the text

Why this workflow matters

  • Normalizes heterogeneous inputs into a common text format
  • Makes web pages and files usable for downstream agent reasoning
  • Reports unsupported or failed formats explicitly
View supporting code
examples/open_deep_research/scripts/mdconvert.py · lines 768–803 · MarkdownConverter
   768 | class MarkdownConverter:
   769 |     """(In preview) An extremely simple text-based document reader, suitable for LLM use.
   770 |     This reader will convert common file-types or webpages to Markdown."""
   771 | 
   772 |     def __init__(
   773 |         self,
   774 |         requests_session: requests.Session | None = None,
   775 |         mlm_client: Any | None = None,
   776 |         mlm_model: Any | None = None,
   777 |     ):
   778 |         if requests_session is None:
   779 |             self._requests_session = requests.Session()
   780 |         else:
   781 |             self._requests_session = requests_session
   782 | 
   783 |         self._mlm_client = mlm_client
   784 |         self._mlm_model = mlm_model
   785 | 
   786 |         self._page_converters: list[DocumentConverter] = []
   787 | 
   788 |         # Register converters for successful browsing operations
   789 |         # Later registrations are tried first / take higher priority than earlier registrations
   790 |         # To this end, the most specific converters should appear below the most generic converters
   791 |         self.register_page_converter(PlainTextConverter())
   792 |         self.register_page_converter(HtmlConverter())
   793 |         self.register_page_converter(WikipediaConverter())
   794 |         self.register_page_converter(YouTubeConverter())
   795 |         self.register_page_converter(DocxConverter())
   796 |         self.register_page_converter(XlsxConverter())
   797 |         self.register_page_converter(PptxConverter())
   798 |         self.register_page_converter(WavConverter())
   799 |         self.register_page_converter(Mp3Converter())
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 804–925 · MarkdownConverter
   804 |     def convert(
   805 |         self, source: str | requests.Response, **kwargs: Any
   806 |     ) -> DocumentConverterResult:  # TODO: deal with kwargs
   807 |         """
   808 |         Args:
   809 |             - source: can be a string representing a path or url, or a requests.response object
   810 |             - extension: specifies the file extension to use when interpreting the file. If None, infer from source (path, uri, content-type, etc.)
   811 |         """
   812 | 
   813 |         # Local path or url
   814 |         if isinstance(source, str):
   815 |             if source.startswith("http://") or source.startswith("https://") or source.startswith("file://"):
   816 |                 return self.convert_url(source, **kwargs)
   817 |             else:
   818 |                 return self.convert_local(source, **kwargs)
   819 |         # Request response
   820 |         elif isinstance(source, requests.Response):
   821 |             return self.convert_response(source, **kwargs)
   822 | 
   823 |     def convert_local(self, path: str, **kwargs: Any) -> DocumentConverterResult:  # TODO: deal with kwargs
   824 |         # Prepare a list of extensions to try (in order of priority)
   825 |         ext = kwargs.get("file_extension")
   826 |         extensions = [ext] if ext is not None else []
   827 | 
   828 |         # Get extension alternatives from the path and puremagic
   829 |         base, ext = os.path.splitext(path)
   830 |         self._append_ext(extensions, ext)
   831 |         self._append_ext(extensions, self._guess_ext_magic(path))
   832 | 
   833 |         # Convert
   834 |         return self._convert(path, extensions, **kwargs)
   835 | 
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 842–866 · MarkdownConverter.convert_stream
   842 |         # Save the file locally to a temporary file. It will be deleted before this method exits
   843 |         handle, temp_path = tempfile.mkstemp()
   844 |         fh = os.fdopen(handle, "wb")
   845 |         result = None
   846 |         try:
   847 |             # Write to the temporary file
   848 |             content = stream.read()
   849 |             if isinstance(content, str):
   850 |                 fh.write(content.encode("utf-8"))
   851 |             else:
   852 |                 fh.write(content)
   853 |             fh.close()
   854 | 
   855 |             # Use puremagic to check for more extension options
   856 |             self._append_ext(extensions, self._guess_ext_magic(temp_path))
   857 | 
   858 |             # Convert
   859 |             result = self._convert(temp_path, extensions, **kwargs)
   860 |         # Clean up
   861 |         finally:
   862 |             try:
   863 |                 fh.close()
   864 |             except Exception:
   865 |                 pass
   866 |             os.unlink(temp_path)
examples/open_deep_research/scripts/mdconvert.py · lines 899–923 · MarkdownConverter.convert_response
   899 |         # Save the file locally to a temporary file. It will be deleted before this method exits
   900 |         handle, temp_path = tempfile.mkstemp()
   901 |         fh = os.fdopen(handle, "wb")
   902 |         result = None
   903 |         try:
   904 |             # Download the file
   905 |             for chunk in response.iter_content(chunk_size=512):
   906 |                 fh.write(chunk)
   907 |             fh.close()
   908 | 
   909 |             # Use puremagic to check for more extension options
   910 |             self._append_ext(extensions, self._guess_ext_magic(temp_path))
   911 | 
   912 |             # Convert
   913 |             result = self._convert(temp_path, extensions, url=response.url)
   914 |         except Exception as e:
   915 |             print(f"Error in converting: {e}")
   916 | 
   917 |         # Clean up
   918 |         finally:
   919 |             try:
   920 |                 fh.close()
   921 |             except Exception:
   922 |                 pass
   923 |             os.unlink(temp_path)
examples/open_deep_research/scripts/mdconvert.py · lines 823–970 · MarkdownConverter._convert
   823 |     def convert_local(self, path: str, **kwargs: Any) -> DocumentConverterResult:  # TODO: deal with kwargs
   824 |         # Prepare a list of extensions to try (in order of priority)
   825 |         ext = kwargs.get("file_extension")
   826 |         extensions = [ext] if ext is not None else []
   827 | 
   828 |         # Get extension alternatives from the path and puremagic
   829 |         base, ext = os.path.splitext(path)
   830 |         self._append_ext(extensions, ext)
   831 |         self._append_ext(extensions, self._guess_ext_magic(path))
   832 | 
   833 |         # Convert
   834 |         return self._convert(path, extensions, **kwargs)
   835 | 
   836 |     # TODO what should stream's type be?
   837 |     def convert_stream(self, stream: Any, **kwargs: Any) -> DocumentConverterResult:  # TODO: deal with kwargs
   838 |         # Prepare a list of extensions to try (in order of priority)
   839 |         ext = kwargs.get("file_extension")
   840 |         extensions = [ext] if ext is not None else []
   841 | 
   842 |         # Save the file locally to a temporary file. It will be deleted before this method exits
   843 |         handle, temp_path = tempfile.mkstemp()
   844 |         fh = os.fdopen(handle, "wb")
   845 |         result = None
   846 |         try:
   847 |             # Write to the temporary file
   848 |             content = stream.read()
   849 |             if isinstance(content, str):
   850 |                 fh.write(content.encode("utf-8"))
   851 |             else:
   852 |                 fh.write(content)
   853 |             fh.close()
   854 | 
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 870–875 · MarkdownConverter.convert_url
   870 |     def convert_url(self, url: str, **kwargs: Any) -> DocumentConverterResult:  # TODO: fix kwargs type
   871 |         # Send a HTTP request to the URL
   872 |         user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"
   873 |         response = self._requests_session.get(url, stream=True, headers={"User-Agent": user_agent})
   874 |         response.raise_for_status()
   875 |         return self.convert_response(response, **kwargs)
23
Workflow 23

multi-format document ingestion and enrichment

What this workflow does

Convert PDFs, DOCX, XLSX, PPTX, WAV/MP3/M4A, and images into markdown/text with metadata extraction, transcription, OCR/LLM description hooks, and sheet/slide formatting.

Purpose

turn heterogeneous files into readable markdown for analysis

What it produces

office/media/image file

Who relies on it

downstream agent or user reading extracted content

Why this workflow matters

  • Preserves document structure where possible
  • Adds searchable text from audio transcription or image description
  • Normalizes different file formats into one downstream representation
View supporting code
examples/open_deep_research/scripts/mdconvert.py · lines 359–367 · PdfConverter.convert
   359 |     def convert(self, local_path, **kwargs) -> None | DocumentConverterResult:
   360 |         # Bail if not a PDF
   361 |         extension = kwargs.get("file_extension", "")
   362 |         if extension.lower() != ".pdf":
   363 |             return None
   364 | 
   365 |         return DocumentConverterResult(
   366 |             title=None,
   367 |             text_content=pdfminer.high_level.extract_text(local_path),
examples/open_deep_research/scripts/mdconvert.py · lines 573–628 · Mp3Converter.convert
   573 |     def convert(self, local_path, **kwargs) -> None | DocumentConverterResult:
   574 |         # Bail if not a MP3
   575 |         extension = kwargs.get("file_extension", "")
   576 |         if extension.lower() not in [".mp3", ".m4a"]:
   577 |             return None
   578 | 
   579 |         md_content = ""
   580 | 
   581 |         # Add metadata
   582 |         metadata = self._get_metadata(local_path)
   583 |         if metadata:
   584 |             for f in [
   585 |                 "Title",
   586 |                 "Artist",
   587 |                 "Author",
   588 |                 "Band",
   589 |                 "Album",
   590 |                 "Genre",
   591 |                 "Track",
   592 |                 "DateTimeOriginal",
   593 |                 "CreateDate",
   594 |                 "Duration",
   595 |             ]:
   596 |                 if f in metadata:
   597 |                     md_content += f"{f}: {metadata[f]}\n"
   598 | 
   599 |         # Transcribe
   600 |         handle, temp_path = tempfile.mkstemp(suffix=".wav")
   601 |         os.close(handle)
   602 |         try:
   603 |             if extension.lower() == ".mp3":
   604 |                 sound = pydub.AudioSegment.from_mp3(local_path)
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 428–485 · PptxConverter.convert
   428 |         presentation = pptx.Presentation(local_path)
   429 |         slide_num = 0
   430 |         for slide in presentation.slides:
   431 |             slide_num += 1
   432 | 
   433 |             md_content += f"\n\n<!-- Slide number: {slide_num} -->\n"
   434 | 
   435 |             title = slide.shapes.title
   436 |             for shape in slide.shapes:
   437 |                 # Pictures
   438 |                 if self._is_picture(shape):
   439 |                     # https://github.com/scanny/python-pptx/pull/512#issuecomment-1713100069
   440 |                     alt_text = ""
   441 |                     try:
   442 |                         alt_text = shape._element._nvXxPr.cNvPr.attrib.get("descr", "")
   443 |                     except Exception:
   444 |                         pass
   445 | 
   446 |                     # A placeholder name
   447 |                     filename = re.sub(r"\W", "", shape.name) + ".jpg"
   448 |                     md_content += "\n![" + (alt_text if alt_text else shape.name) + "](" + filename + ")\n"
   449 | 
   450 |                 # Tables
   451 |                 if self._is_table(shape):
   452 |                     html_table = "<html><body><table>"
   453 |                     first_row = True
   454 |                     for row in shape.table.rows:
   455 |                         html_table += "<tr>"
   456 |                         for cell in row.cells:
   457 |                             if first_row:
   458 |                                 html_table += "<th>" + html.escape(cell.text) + "</th>"
   459 |                             else:
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 359–363 · PdfConverter.convert
   359 |     def convert(self, local_path, **kwargs) -> None | DocumentConverterResult:
   360 |         # Bail if not a PDF
   361 |         extension = kwargs.get("file_extension", "")
   362 |         if extension.lower() != ".pdf":
   363 |             return None
examples/open_deep_research/scripts/mdconvert.py · lines 376–380 · DocxConverter.convert
   376 |     def convert(self, local_path, **kwargs) -> None | DocumentConverterResult:
   377 |         # Bail if not a DOCX
   378 |         extension = kwargs.get("file_extension", "")
   379 |         if extension.lower() != ".docx":
   380 |             return None
examples/open_deep_research/scripts/mdconvert.py · lines 396–400 · XlsxConverter.convert
   396 |     def convert(self, local_path, **kwargs) -> None | DocumentConverterResult:
   397 |         # Bail if not a XLSX
   398 |         extension = kwargs.get("file_extension", "")
   399 |         if extension.lower() not in [".xlsx", ".xls"]:
   400 |             return None
24
Workflow 24

document-to-markdown conversion

What this workflow does

Convert local HTML, Wikipedia, or YouTube page captures into markdown/text and optionally enrich YouTube pages with transcript content.

Purpose

inspect a captured webpage or video page as readable text

What it produces

local HTML/page capture

Who relies on it

downstream agent or caller consuming the converted text

Why this workflow matters

  • Transforms web content into agent-readable markdown
  • Preserves titles and selected metadata
  • Can include transcript text for video pages
View supporting code
examples/open_deep_research/scripts/mdconvert.py · lines 149–186 · HtmlConverter
   149 | class HtmlConverter(DocumentConverter):
   150 |     """Anything with content type text/html"""
   151 | 
   152 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   153 |         # Bail if not html
   154 |         extension = kwargs.get("file_extension", "")
   155 |         if extension.lower() not in [".html", ".htm"]:
   156 |             return None
   157 | 
   158 |         result = None
   159 |         with open(local_path, "rt", encoding="utf-8") as fh:
   160 |             result = self._convert(fh.read())
   161 | 
   162 |         return result
   163 | 
   164 |     def _convert(self, html_content: str) -> None | DocumentConverterResult:
   165 |         """Helper function that converts and HTML string."""
   166 | 
   167 |         # Parse the string
   168 |         soup = BeautifulSoup(html_content, "html.parser")
   169 | 
   170 |         # Remove javascript and style blocks
   171 |         for script in soup(["script", "style"]):
   172 |             script.extract()
   173 | 
   174 |         # Print only the main content
   175 |         body_elm = soup.find("body")
   176 |         webpage_text = ""
   177 |         if body_elm:
   178 |             webpage_text = _CustomMarkdownify().convert_soup(body_elm)
   179 |         else:
   180 |             webpage_text = _CustomMarkdownify().convert_soup(soup)
       | … additional lines omitted from this preview …
examples/open_deep_research/scripts/mdconvert.py · lines 197–224 · WikipediaConverter.convert
   197 |         url = kwargs.get("url", "")
   198 |         if not re.search(r"^https?:\/\/[a-zA-Z]{2,3}\.wikipedia.org\/", url):
   199 |             return None
   200 | 
   201 |         # Parse the file
   202 |         soup = None
   203 |         with open(local_path, "rt", encoding="utf-8") as fh:
   204 |             soup = BeautifulSoup(fh.read(), "html.parser")
   205 | 
   206 |         # Remove javascript and style blocks
   207 |         for script in soup(["script", "style"]):
   208 |             script.extract()
   209 | 
   210 |         # Print only the main content
   211 |         body_elm = soup.find("div", {"id": "mw-content-text"})
   212 |         title_elm = soup.find("span", {"class": "mw-page-title-main"})
   213 | 
   214 |         webpage_text = ""
   215 |         main_title = None if soup.title is None else soup.title.string
   216 | 
   217 |         if body_elm:
   218 |             # What's the title
   219 |             if title_elm and len(title_elm) > 0:
   220 |                 main_title = title_elm.string  # type: ignore
   221 |                 assert isinstance(main_title, str)
   222 | 
   223 |             # Convert the page
   224 |             webpage_text = f"# {main_title}\n\n" + _CustomMarkdownify().convert_soup(body_elm)
examples/open_deep_research/scripts/mdconvert.py · lines 152–160 · HtmlConverter.convert
   152 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   153 |         # Bail if not html
   154 |         extension = kwargs.get("file_extension", "")
   155 |         if extension.lower() not in [".html", ".htm"]:
   156 |             return None
   157 | 
   158 |         result = None
   159 |         with open(local_path, "rt", encoding="utf-8") as fh:
   160 |             result = self._convert(fh.read())
examples/open_deep_research/scripts/mdconvert.py · lines 237–249 · YouTubeConverter.convert
   237 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   238 |         # Bail if not YouTube
   239 |         extension = kwargs.get("file_extension", "")
   240 |         if extension.lower() not in [".html", ".htm"]:
   241 |             return None
   242 |         url = kwargs.get("url", "")
   243 |         if not url.startswith("https://www.youtube.com/watch?"):
   244 |             return None
   245 | 
   246 |         # Parse the file
   247 |         soup = None
   248 |         with open(local_path, "rt", encoding="utf-8") as fh:
   249 |             soup = BeautifulSoup(fh.read(), "html.parser")
examples/open_deep_research/scripts/mdconvert.py · lines 152–156 · HtmlConverter.convert
   152 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   153 |         # Bail if not html
   154 |         extension = kwargs.get("file_extension", "")
   155 |         if extension.lower() not in [".html", ".htm"]:
   156 |             return None
examples/open_deep_research/scripts/mdconvert.py · lines 192–199 · WikipediaConverter.convert
   192 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   193 |         # Bail if not Wikipedia
   194 |         extension = kwargs.get("file_extension", "")
   195 |         if extension.lower() not in [".html", ".htm"]:
   196 |             return None
   197 |         url = kwargs.get("url", "")
   198 |         if not re.search(r"^https?:\/\/[a-zA-Z]{2,3}\.wikipedia.org\/", url):
   199 |             return None
25
Workflow 25

archive extraction

What this workflow does

Extract ZIP archives to a permanent local directory and return a deterministic listing of extracted files.

Purpose

unpack a ZIP file for later inspection

What it produces

ZIP archive

Who relies on it

downstream user or agent consuming the extracted file list

Why this workflow matters

  • Creates a permanent local extraction artifact
  • Produces a stable list of downloaded files
View supporting code
examples/open_deep_research/scripts/mdconvert.py · lines 647–676 · ZipConverter.convert
   647 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   648 |         # Bail if not a ZIP file
   649 |         extension = kwargs.get("file_extension", "")
   650 |         if extension.lower() != ".zip":
   651 |             return None
   652 | 
   653 |         # Verify it's actually a ZIP file
   654 |         if not zipfile.is_zipfile(local_path):
   655 |             return None
   656 | 
   657 |         # Extract all files and build list
   658 |         extracted_files = []
   659 |         with zipfile.ZipFile(local_path, "r") as zip_ref:
   660 |             # Extract all files
   661 |             zip_ref.extractall(self.extract_dir)
   662 |             # Get list of all files
   663 |             for file_path in zip_ref.namelist():
   664 |                 # Skip directories
   665 |                 if not file_path.endswith("/"):
   666 |                     extracted_files.append(self.extract_dir + "/" + file_path)
   667 | 
   668 |         # Sort files for consistent output
   669 |         extracted_files.sort()
   670 | 
   671 |         # Build the markdown content
   672 |         md_content = "Downloaded the following files:\n"
   673 |         for file in extracted_files:
   674 |             md_content += f"* {file}\n"
   675 | 
   676 |         return DocumentConverterResult(title="Extracted Files", text_content=md_content.strip())
examples/open_deep_research/scripts/mdconvert.py · lines 636–645 · ZipConverter.__init__
   636 |     def __init__(self, extract_dir: str = "downloads"):
   637 |         """
   638 |         Initialize with path to extraction directory.
   639 | 
   640 |         Args:
   641 |             extract_dir: The directory where files will be extracted. Defaults to "downloads"
   642 |         """
   643 |         self.extract_dir = extract_dir
   644 |         # Create the extraction directory if it doesn't exist
   645 |         os.makedirs(self.extract_dir, exist_ok=True)
examples/open_deep_research/scripts/mdconvert.py · lines 659–667 · ZipConverter.convert
   659 |         with zipfile.ZipFile(local_path, "r") as zip_ref:
   660 |             # Extract all files
   661 |             zip_ref.extractall(self.extract_dir)
   662 |             # Get list of all files
   663 |             for file_path in zip_ref.namelist():
   664 |                 # Skip directories
   665 |                 if not file_path.endswith("/"):
   666 |                     extracted_files.append(self.extract_dir + "/" + file_path)
   667 | 
examples/open_deep_research/scripts/mdconvert.py · lines 647–651 · ZipConverter.convert
   647 |     def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult:
   648 |         # Bail if not a ZIP file
   649 |         extension = kwargs.get("file_extension", "")
   650 |         if extension.lower() != ".zip":
   651 |             return None
examples/open_deep_research/scripts/mdconvert.py · lines 653–655 · ZipConverter.convert
   653 |         # Verify it's actually a ZIP file
   654 |         if not zipfile.is_zipfile(local_path):
   655 |             return None
examples/open_deep_research/scripts/mdconvert.py · lines 663–667 · ZipConverter.convert
   663 |             for file_path in zip_ref.namelist():
   664 |                 # Skip directories
   665 |                 if not file_path.endswith("/"):
   666 |                     extracted_files.append(self.extract_dir + "/" + file_path)
   667 | 
Workflow area 11

model invocation observability

1 workflow
26
Workflow 26

model invocation observability

What this workflow does

InferenceClientModel generation emits an OpenTelemetry span under SmolagentsInstrumentor instrumentation.

Purpose

trace model generation calls for monitoring and analysis

What it produces

generation span

Who relies on it

instrumentation consumer / observability system

Why this workflow matters

  • records model-call telemetry
  • enables tracing and monitoring of generation requests
View supporting code
tests/test_telemetry.py · lines 55–63 · instrument
    55 | @pytest.fixture(autouse=True)
    56 | def instrument(
    57 |     tracer_provider: trace_api.TracerProvider,
    58 |     in_memory_span_exporter: InMemorySpanExporter,
    59 | ) -> Generator[None, None, None]:
    60 |     SmolagentsInstrumentor().instrument(tracer_provider=tracer_provider, skip_dep_check=True)
    61 |     yield
    62 |     SmolagentsInstrumentor().uninstrument()
    63 |     in_memory_span_exporter.clear()
tests/test_telemetry.py · lines 68–87 · TestOpenTelemetry.test_model
    68 |     def test_model(self, in_memory_span_exporter: InMemorySpanExporter):
    69 |         model = InferenceClientModel()
    70 |         _ = model(
    71 |             messages=[
    72 |                 {
    73 |                     "role": "user",
    74 |                     "content": [
    75 |                         {
    76 |                             "type": "text",
    77 |                             "text": "Who won the World Cup in 2018? Answer in one word with no punctuation.",
    78 |                         }
    79 |                     ],
    80 |                 }
    81 |             ]
    82 |         )
    83 |         spans = in_memory_span_exporter.get_finished_spans()
    84 |         assert len(spans) == 1
    85 |         span = spans[0]
    86 |         assert span.name == "InferenceClientModel.generate"
    87 |         assert span.status.is_ok
tests/test_telemetry.py · lines 41–63 · in_memory_span_exporter / tracer_provider / instrument
    41 | @pytest.fixture
    42 | def in_memory_span_exporter() -> InMemorySpanExporter:
    43 |     return InMemorySpanExporter()
    44 | 
    45 | 
    46 | @pytest.fixture
    47 | def tracer_provider(in_memory_span_exporter: InMemorySpanExporter) -> trace_api.TracerProvider:
    48 |     resource = Resource(attributes={})
    49 |     tracer_provider = trace_sdk.TracerProvider(resource=resource)
    50 |     span_processor = SimpleSpanProcessor(span_exporter=in_memory_span_exporter)
    51 |     tracer_provider.add_span_processor(span_processor=span_processor)
    52 |     return tracer_provider
    53 | 
    54 | 
    55 | @pytest.fixture(autouse=True)
    56 | def instrument(
    57 |     tracer_provider: trace_api.TracerProvider,
    58 |     in_memory_span_exporter: InMemorySpanExporter,
    59 | ) -> Generator[None, None, None]:
    60 |     SmolagentsInstrumentor().instrument(tracer_provider=tracer_provider, skip_dep_check=True)
    61 |     yield
    62 |     SmolagentsInstrumentor().uninstrument()
    63 |     in_memory_span_exporter.clear()
tests/test_telemetry.py · lines 83–88 · TestOpenTelemetry.test_model
    83 |         spans = in_memory_span_exporter.get_finished_spans()
    84 |         assert len(spans) == 1
    85 |         span = spans[0]
    86 |         assert span.name == "InferenceClientModel.generate"
    87 |         assert span.status.is_ok
    88 |         assert span.attributes
tests/test_telemetry.py · lines 83–87 · TestOpenTelemetry.test_model
    83 |         spans = in_memory_span_exporter.get_finished_spans()
    84 |         assert len(spans) == 1
    85 |         span = spans[0]
    86 |         assert span.name == "InferenceClientModel.generate"
    87 |         assert span.status.is_ok
tests/test_telemetry.py · lines 83–85 · TestOpenTelemetry.test_model
    83 |         spans = in_memory_span_exporter.get_finished_spans()
    84 |         assert len(spans) == 1
    85 |         span = spans[0]
Workflow area 12

browser automation demo

1 workflow
27
Workflow 27

browser automation demo

What this workflow does

The browser-automation demo launches a CodeAgent with Selenium/Helium tools, takes screenshots, and enables page search/navigation for a user prompt.

Purpose

navigate web pages and extract visible information from a browser session

What it produces

browser session / screenshot / search result

Who relies on it

end user asking web navigation questions

Why this workflow matters

  • captures browser state for the agent
  • helps the agent search and navigate pages
  • returns a final answer to the prompt
View supporting code
src/smolagents/vision_web_browser.py · lines 148–157 · initialize_agent
   148 | def initialize_agent(model):
   149 |     """Initialize the CodeAgent with the specified model."""
   150 |     return CodeAgent(
   151 |         tools=[WebSearchTool(), go_back, close_popups, search_item_ctrl_f],
   152 |         model=model,
   153 |         additional_authorized_imports=["helium"],
   154 |         step_callbacks=[save_screenshot],
   155 |         max_steps=20,
   156 |         verbosity_level=2,
   157 |     )
src/smolagents/vision_web_browser.py · lines 231–237 · run_webagent
   231 |     global driver
   232 |     driver = initialize_driver()
   233 |     agent = initialize_agent(model)
   234 | 
   235 |     # Run the agent with the provided prompt
   236 |     agent.python_executor("from helium import *")
   237 |     agent.run(prompt + helium_instructions)
src/smolagents/vision_web_browser.py · lines 66–84 · save_screenshot
    66 | def save_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None:
    67 |     sleep(1.0)  # Let JavaScript animations happen before taking the screenshot
    68 |     driver = helium.get_driver()
    69 |     current_step = memory_step.step_number
    70 |     if driver is not None:
    71 |         for previous_memory_step in agent.memory.steps:  # Remove previous screenshots from logs for lean processing
    72 |             if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= current_step - 2:
    73 |                 previous_memory_step.observations_images = None
    74 |         png_bytes = driver.get_screenshot_as_png()
    75 |         image = PIL.Image.open(BytesIO(png_bytes))
    76 |         print(f"Captured a browser screenshot: {image.size} pixels")
    77 |         memory_step.observations_images = [image.copy()]  # Create a copy to ensure it persists, important!
    78 | 
    79 |     # Update observations with current URL
    80 |     url_info = f"Current url: {driver.current_url}"
    81 |     memory_step.observations = (
    82 |         url_info if memory_step.observations is None else memory_step.observations + "\n" + url_info
    83 |     )
    84 |     return
src/smolagents/vision_web_browser.py · lines 138–145 · initialize_driver
   138 | def initialize_driver():
   139 |     """Initialize the Selenium WebDriver."""
   140 |     chrome_options = webdriver.ChromeOptions()
   141 |     chrome_options.add_argument("--force-device-scale-factor=1")
   142 |     chrome_options.add_argument("--window-size=1000,1350")
   143 |     chrome_options.add_argument("--disable-pdf-viewer")
   144 |     chrome_options.add_argument("--window-position=0,0")
   145 |     return helium.start_chrome(headless=False, options=chrome_options)
src/smolagents/vision_web_browser.py · lines 66–78 · save_screenshot
    66 | def save_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None:
    67 |     sleep(1.0)  # Let JavaScript animations happen before taking the screenshot
    68 |     driver = helium.get_driver()
    69 |     current_step = memory_step.step_number
    70 |     if driver is not None:
    71 |         for previous_memory_step in agent.memory.steps:  # Remove previous screenshots from logs for lean processing
    72 |             if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= current_step - 2:
    73 |                 previous_memory_step.observations_images = None
    74 |         png_bytes = driver.get_screenshot_as_png()
    75 |         image = PIL.Image.open(BytesIO(png_bytes))
    76 |         print(f"Captured a browser screenshot: {image.size} pixels")
    77 |         memory_step.observations_images = [image.copy()]  # Create a copy to ensure it persists, important!
    78 | 
src/smolagents/vision_web_browser.py · lines 71–73 · save_screenshot
    71 |         for previous_memory_step in agent.memory.steps:  # Remove previous screenshots from logs for lean processing
    72 |             if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= current_step - 2:
    73 |                 previous_memory_step.observations_images = None
Workflow area 13

Remote workflows

1 workflow
28
Workflow 28

remote LLM chat completion

What this workflow does

Model wrapper prepares a chat completion request, applies rate limiting and retries, calls a remote LLM API, and returns the assistant message content.

Purpose

obtain model-generated assistant text from an external provider

What it produces

ChatMessage / completion response

Who relies on it

external API provider (OpenAI-compatible or Bedrock endpoint)

Why this workflow matters

  • Returns generated assistant content to downstream agents
  • Records token usage and supports retry/rate limiting for API interaction
View supporting code
src/smolagents/models.py · lines 1671–1690 · OpenAIModel.__init__
  1671 |     def __init__(
  1672 |         self,
  1673 |         model_id: str,
  1674 |         api_base: str | None = None,
  1675 |         <redacted> | None = None,
  1676 |         organization: str | None = None,
  1677 |         project: str | None = None,
  1678 |         client_kwargs: dict[str, Any] | None = None,
  1679 |         custom_role_conversions: dict[str, str] | None = None,
  1680 |         flatten_messages_as_text: bool = False,
  1681 |         **kwargs,
  1682 |     ):
  1683 |         self.client_kwargs = {
  1684 |             **(client_kwargs or {}),
  1685 |             "api_key": api_key,
  1686 |             "base_url": api_base,
  1687 |             "organization": organization,
  1688 |             "project": project,
  1689 |         }
  1690 |         super().__init__(
src/smolagents/models.py · lines 1171–1183 · ApiModel.__init__
  1171 |         self.custom_role_conversions = custom_role_conversions or {}
  1172 |         self.client = client or self.create_client()
  1173 |         self.rate_limiter = RateLimiter(requests_per_minute)
  1174 |         self.retryer = Retrying(
  1175 |             max_attempts=RETRY_MAX_ATTEMPTS if retry else 1,
  1176 |             wait_seconds=RETRY_WAIT,
  1177 |             exponential_base=RETRY_EXPONENTIAL_BASE,
  1178 |             jitter=RETRY_JITTER,
  1179 |             retry_predicate=is_rate_limit_error,
  1180 |             reraise=True,
  1181 |             before_sleep_logger=(logger, logging.INFO),
  1182 |             after_logger=(logger, logging.INFO),
  1183 |         )
src/smolagents/models.py · lines 1683–1705 · OpenAIModel.create_client
  1683 |         self.client_kwargs = {
  1684 |             **(client_kwargs or {}),
  1685 |             "api_key": api_key,
  1686 |             "base_url": api_base,
  1687 |             "organization": organization,
  1688 |             "project": project,
  1689 |         }
  1690 |         super().__init__(
  1691 |             model_id=model_id,
  1692 |             custom_role_conversions=custom_role_conversions,
  1693 |             flatten_messages_as_text=flatten_messages_as_text,
  1694 |             **kwargs,
  1695 |         )
  1696 | 
  1697 |     def create_client(self):
  1698 |         try:
  1699 |             import openai
  1700 |         except ModuleNotFoundError as e:
  1701 |             raise ModuleNotFoundError(
  1702 |                 "Please install 'openai' extra to use OpenAIModel: `pip install 'smolagents[openai]'`"
  1703 |             ) from e
  1704 | 
  1705 |         return openai.OpenAI(**self.client_kwargs)
src/smolagents/models.py · lines 2010–2018 · AmazonBedrockModel.create_client
  2010 |     def create_client(self):
  2011 |         try:
  2012 |             import boto3  # type: ignore
  2013 |         except ModuleNotFoundError as e:
  2014 |             raise ModuleNotFoundError(
  2015 |                 "Please install 'bedrock' extra to use AmazonBedrockServerModel: `pip install 'smolagents[bedrock]'`"
  2016 |             ) from e
  2017 | 
  2018 |         return boto3.client("bedrock-runtime", **self.client_kwargs)
src/smolagents/models.py · lines 1732–1739 · OpenAIModel.generate_stream
  1732 |             if event.usage:
  1733 |                 yield ChatMessageStreamDelta(
  1734 |                     content="",
  1735 |                     token_usage=TokenUsage(
  1736 |                         input_tokens=event.usage.prompt_tokens,
  1737 |                         output_tokens=event.usage.completion_tokens,
  1738 |                     ),
  1739 |                 )
src/smolagents/models.py · lines 1789–1792 · OpenAIModel.generate
  1789 |             token_usage=TokenUsage(
  1790 |                 input_tokens=response.usage.prompt_tokens,
  1791 |                 output_tokens=response.usage.completion_tokens,
  1792 |             ),
Workflow area 14

Llm workflows

1 workflow
29
Workflow 29

local LLM inference

What this workflow does

Local model wrappers load inference engines, apply chat templates, generate tokens, and return assistant messages or streamed deltas.

Purpose

produce assistant text from locally loaded model weights

What it produces

prompt / generated assistant message

Who relies on it

agent runtime consuming the model output

Why this workflow matters

  • Enables offline or self-hosted model execution
  • Returns text and token usage without a remote API dependency
View supporting code
src/smolagents/models.py · lines 932–978 · TransformersModel.__init__
   932 |         if not model_id:
   933 |             warnings.warn(
   934 |                 "The 'model_id' parameter will be required in version 2.0.0. "
   935 |                 "Please update your code to pass this parameter to avoid future errors. "
   936 |                 "For now, it defaults to 'HuggingFaceTB/SmolLM2-1.7B-Instruct'.",
   937 |                 FutureWarning,
   938 |             )
   939 |             model_id = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
   940 | 
   941 |         max_new_tokens = max_tokens if max_tokens is not None else max_new_tokens
   942 | 
   943 |         if device_map is None:
   944 |             device_map = "cuda" if torch.cuda.is_available() else "cpu"
   945 |         logger.info(f"Using device: {device_map}")
   946 |         self._is_vlm = False
   947 |         self.model_kwargs = model_kwargs or {}
   948 |         self.apply_chat_template_kwargs = apply_chat_template_kwargs or {}
   949 |         try:
   950 |             self.model = AutoModelForImageTextToText.from_pretrained(
   951 |                 model_id,
   952 |                 device_map=device_map,
   953 |                 torch_dtype=torch_dtype,
   954 |                 trust_remote_code=trust_remote_code,
   955 |                 **self.model_kwargs,
   956 |             )
   957 |             self.processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=trust_remote_code)
   958 |             self._is_vlm = True
   959 |             self.streamer = TextIteratorStreamer(self.processor.tokenizer, skip_prompt=True, skip_special_tokens=True)  # type: ignore
   960 | 
   961 |         except ValueError as e:
   962 |             if "Unrecognized configuration class" in str(e):
   963 |                 self.model = AutoModelForCausalLM.from_pretrained(
       | … additional lines omitted from this preview …
src/smolagents/models.py · lines 799–814 · MLXModel.__init__
   799 |         if not _is_package_available("mlx_lm"):
   800 |             raise ModuleNotFoundError(
   801 |                 "Please install 'mlx-lm' extra to use 'MLXModel': `pip install 'smolagents[mlx-lm]'`"
   802 |             )
   803 |         import mlx_lm
   804 | 
   805 |         self.load_kwargs = load_kwargs or {}
   806 |         self.load_kwargs.setdefault("tokenizer_config", {}).setdefault("trust_remote_code", trust_remote_code)
   807 |         self.apply_chat_template_kwargs = apply_chat_template_kwargs or {}
   808 |         self.apply_chat_template_kwargs.setdefault("add_generation_prompt", True)
   809 |         # mlx-lm doesn't support vision models: flatten_messages_as_text=True
   810 |         super().__init__(model_id=model_id, flatten_messages_as_text=True, **kwargs)
   811 | 
   812 |         self.model, self.tokenizer = mlx_lm.load(self.model_id, **self.load_kwargs)
   813 |         self.stream_generate = mlx_lm.stream_generate
   814 |         self.is_vlm = False  # mlx-lm doesn't support vision models
src/smolagents/models.py · lines 648–668 · VLLMModel.__init__
   648 |     def __init__(
   649 |         self,
   650 |         model_id,
   651 |         model_kwargs: dict[str, Any] | None = None,
   652 |         apply_chat_template_kwargs: dict[str, Any] | None = None,
   653 |         **kwargs,
   654 |     ):
   655 |         if not _is_package_available("vllm"):
   656 |             raise ModuleNotFoundError("Please install 'vllm' extra to use VLLMModel: `pip install 'smolagents[vllm]'`")
   657 | 
   658 |         from vllm import LLM  # type: ignore
   659 |         from vllm.transformers_utils.tokenizer import get_tokenizer  # type: ignore
   660 | 
   661 |         self.model_kwargs = model_kwargs or {}
   662 |         self.apply_chat_template_kwargs = apply_chat_template_kwargs or {}
   663 |         super().__init__(**kwargs)
   664 |         self.model_id = model_id
   665 |         self.model = LLM(model=model_id, **self.model_kwargs)
   666 |         assert self.model is not None
   667 |         self.tokenizer = get_tokenizer(model_id)
   668 |         self._is_vlm = False  # VLLMModel does not support vision models yet.
src/smolagents/models.py · lines 1016–1036 · TransformersModel._prepare_completion_args
  1016 |         messages = completion_kwargs.pop("messages")
  1017 |         stop_sequences = completion_kwargs.pop("stop", None)
  1018 |         tools = completion_kwargs.pop("tools", None)
  1019 | 
  1020 |         max_new_tokens = (
  1021 |             kwargs.get("max_new_tokens")
  1022 |             or kwargs.get("max_tokens")
  1023 |             or self.kwargs.get("max_new_tokens")
  1024 |             or self.kwargs.get("max_tokens")
  1025 |             or 1024
  1026 |         )
  1027 |         prompt_tensor = (self.processor if hasattr(self, "processor") else self.tokenizer).apply_chat_template(
  1028 |             messages,
  1029 |             tools=tools,
  1030 |             return_tensors="pt",
  1031 |             add_generation_prompt=True,
  1032 |             tokenize=True,
  1033 |             return_dict=True,
  1034 |             **self.apply_chat_template_kwargs,
  1035 |         )
  1036 |         prompt_tensor = prompt_tensor.to(self.model.device)  # type: ignore
src/smolagents/models.py · lines 648–667 · VLLMModel.__init__
   648 |     def __init__(
   649 |         self,
   650 |         model_id,
   651 |         model_kwargs: dict[str, Any] | None = None,
   652 |         apply_chat_template_kwargs: dict[str, Any] | None = None,
   653 |         **kwargs,
   654 |     ):
   655 |         if not _is_package_available("vllm"):
   656 |             raise ModuleNotFoundError("Please install 'vllm' extra to use VLLMModel: `pip install 'smolagents[vllm]'`")
   657 | 
   658 |         from vllm import LLM  # type: ignore
   659 |         from vllm.transformers_utils.tokenizer import get_tokenizer  # type: ignore
   660 | 
   661 |         self.model_kwargs = model_kwargs or {}
   662 |         self.apply_chat_template_kwargs = apply_chat_template_kwargs or {}
   663 |         super().__init__(**kwargs)
   664 |         self.model_id = model_id
   665 |         self.model = LLM(model=model_id, **self.model_kwargs)
   666 |         assert self.model is not None
   667 |         self.tokenizer = get_tokenizer(model_id)
src/smolagents/models.py · lines 932–940 · TransformersModel.__init__
   932 |         if not model_id:
   933 |             warnings.warn(
   934 |                 "The 'model_id' parameter will be required in version 2.0.0. "
   935 |                 "Please update your code to pass this parameter to avoid future errors. "
   936 |                 "For now, it defaults to 'HuggingFaceTB/SmolLM2-1.7B-Instruct'.",
   937 |                 FutureWarning,
   938 |             )
   939 |             model_id = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
   940 | 
Workflow area 15

Sandboxed workflows

1 workflow
30
Workflow 30

sandboxed code evaluation

What this workflow does

The local Python executor evaluates AST nodes, but blocks dunder access and unauthorized builtins/functions to keep code execution constrained.

Purpose

execute generated Python while restricting dangerous access

What it produces

AST expression / execution state

Who relies on it

agent runtime and tools invoked by the evaluator

Why this workflow matters

  • reduces unsafe access to internals
  • supports code-agent execution of generated code
View supporting code
src/smolagents/local_python_executor.py · lines 156–182 · check_safer_result
   156 | def check_safer_result(result: Any, static_tools: dict[str, Callable] = None, authorized_imports: list[str] = None):
   157 |     """
   158 |     Checks if a result is safer according to authorized imports and static tools.
   159 | 
   160 |     Args:
   161 |         result (Any): The result to check.
   162 |         static_tools (dict[str, Callable]): Dictionary of static tools.
   163 |         authorized_imports (list[str]): List of authorized imports.
   164 | 
   165 |     Raises:
   166 |         InterpreterError: If the result is not safe
   167 |     """
   168 |     if isinstance(result, ModuleType):
   169 |         if not check_import_authorized(result.__name__, authorized_imports):
   170 |             raise InterpreterError(f"Forbidden access to module: {result.__name__}")
   171 |     elif isinstance(result, dict) and result.get("__spec__"):
   172 |         if not check_import_authorized(result["__name__"], authorized_imports):
   173 |             raise InterpreterError(f"Forbidden access to module: {result['__name__']}")
   174 |     elif isinstance(result, (FunctionType, BuiltinFunctionType)):
   175 |         for qualified_function_name in DANGEROUS_FUNCTIONS:
   176 |             module_name, function_name = qualified_function_name.rsplit(".", 1)
   177 |             if (
   178 |                 (static_tools is None or function_name not in static_tools)
   179 |                 and result.__name__ == function_name
   180 |                 and result.__module__ == module_name
   181 |             ):
   182 |                 raise InterpreterError(f"Forbidden access to function: {function_name}")
src/smolagents/local_python_executor.py · lines 832–918 · evaluate_call
   832 |     if not isinstance(call.func, (ast.Call, ast.Lambda, ast.Attribute, ast.Name, ast.Subscript)):
   833 |         raise InterpreterError(f"This is not a correct function: {call.func}).")
   834 | 
   835 |     func, func_name = None, None
   836 | 
   837 |     if isinstance(call.func, ast.Call):
   838 |         func = evaluate_ast(call.func, state, static_tools, custom_tools, authorized_imports)
   839 |     elif isinstance(call.func, ast.Lambda):
   840 |         func = evaluate_ast(call.func, state, static_tools, custom_tools, authorized_imports)
   841 |     elif isinstance(call.func, ast.Attribute):
   842 |         obj = evaluate_ast(call.func.value, state, static_tools, custom_tools, authorized_imports)
   843 |         func_name = call.func.attr
   844 |         if not hasattr(obj, func_name):
   845 |             raise InterpreterError(f"Object {obj} has no attribute {func_name}")
   846 |         func = getattr(obj, func_name)
   847 |     elif isinstance(call.func, ast.Name):
   848 |         func_name = call.func.id
   849 |         if func_name in state:
   850 |             func = state[func_name]
   851 |         elif func_name in static_tools:
   852 |             func = static_tools[func_name]
   853 |         elif func_name in custom_tools:
   854 |             func = custom_tools[func_name]
   855 |         elif func_name in ERRORS:
   856 |             func = ERRORS[func_name]
   857 |         else:
   858 |             raise InterpreterError(
   859 |                 f"Forbidden function evaluation: '{call.func.id}' is not among the explicitly allowed tools or defined/imported in the preceding code"
   860 |             )
   861 |     elif isinstance(call.func, ast.Subscript):
   862 |         func = evaluate_ast(call.func, state, static_tools, custom_tools, authorized_imports)
   863 |         if not callable(func):
       | … additional lines omitted from this preview …
src/smolagents/local_python_executor.py · lines 471–519 · create_function
   471 |     def new_func(*args: Any, **kwargs: Any) -> Any:
   472 |         func_state = state.copy()
   473 |         arg_names = [arg.arg for arg in func_def.args.args]
   474 |         default_values = [
   475 |             evaluate_ast(d, state, static_tools, custom_tools, authorized_imports) for d in func_def.args.defaults
   476 |         ]
   477 | 
   478 |         # Apply default values
   479 |         defaults = dict(zip(arg_names[-len(default_values) :], default_values))
   480 | 
   481 |         # Set positional arguments
   482 |         for name, value in zip(arg_names, args):
   483 |             func_state[name] = value
   484 | 
   485 |         # Set keyword arguments
   486 |         for name, value in kwargs.items():
   487 |             func_state[name] = value
   488 | 
   489 |         # Handle variable arguments
   490 |         if func_def.args.vararg:
   491 |             vararg_name = func_def.args.vararg.arg
   492 |             func_state[vararg_name] = args
   493 | 
   494 |         if func_def.args.kwarg:
   495 |             kwarg_name = func_def.args.kwarg.arg
   496 |             func_state[kwarg_name] = kwargs
   497 | 
   498 |         # Set default values for arguments that were not provided
   499 |         for name, value in defaults.items():
   500 |             if name not in func_state:
   501 |                 func_state[name] = value
   502 | 
       | … additional lines omitted from this preview …
src/smolagents/local_python_executor.py · lines 794–822 · set_value
   794 | def set_value(
   795 |     target: ast.AST,
   796 |     value: Any,
   797 |     state: dict[str, Any],
   798 |     static_tools: dict[str, Callable],
   799 |     custom_tools: dict[str, Callable],
   800 |     authorized_imports: list[str],
   801 | ) -> None:
   802 |     if isinstance(target, ast.Name):
   803 |         if target.id in static_tools:
   804 |             raise InterpreterError(f"Cannot assign to name '{target.id}': doing this would erase the existing tool!")
   805 |         state[target.id] = value
   806 |     elif isinstance(target, ast.Tuple):
   807 |         if not isinstance(value, tuple):
   808 |             if hasattr(value, "__iter__") and not isinstance(value, (str, bytes)):
   809 |                 value = tuple(value)
   810 |             else:
   811 |                 raise InterpreterError("Cannot unpack non-tuple value")
   812 |         if len(target.elts) != len(value):
   813 |             raise InterpreterError("Cannot unpack tuple of wrong size")
   814 |         for i, elem in enumerate(target.elts):
   815 |             set_value(elem, value[i], state, static_tools, custom_tools, authorized_imports)
   816 |     elif isinstance(target, ast.Subscript):
   817 |         obj = evaluate_ast(target.value, state, static_tools, custom_tools, authorized_imports)
   818 |         key = evaluate_ast(target.slice, state, static_tools, custom_tools, authorized_imports)
   819 |         obj[key] = value
   820 |     elif isinstance(target, ast.Attribute):
   821 |         obj = evaluate_ast(target.value, state, static_tools, custom_tools, authorized_imports)
   822 |         setattr(obj, target.attr, value)
src/smolagents/local_python_executor.py · lines 777–791 · evaluate_assign
   777 |     result = evaluate_ast(assign.value, state, static_tools, custom_tools, authorized_imports)
   778 |     if len(assign.targets) == 1:
   779 |         target = assign.targets[0]
   780 |         set_value(target, result, state, static_tools, custom_tools, authorized_imports)
   781 |     else:
   782 |         expanded_values = []
   783 |         for tgt in assign.targets:
   784 |             if isinstance(tgt, ast.Starred):
   785 |                 expanded_values.extend(result)
   786 |             else:
   787 |                 expanded_values.append(result)
   788 | 
   789 |         for tgt, val in zip(assign.targets, expanded_values):
   790 |             set_value(tgt, val, state, static_tools, custom_tools, authorized_imports)
   791 |     return result
src/smolagents/local_python_executor.py · lines 68–71 · nodunder_getattr
    68 | def nodunder_getattr(obj, name, default=None):
    69 |     if name.startswith("__") and name.endswith("__"):
    70 |         raise InterpreterError(f"Forbidden access to dunder attribute: {name}")
    71 |     return getattr(obj, name, default)

Continue your review

Structured graph of your codebase linking different files and symbols.