Repository: huggingface/smolagents

Issue:
[BUG] Example CLI command fails to parse positional aguments
**Describe the bug**
Hello,

After installing the package, I tried to run both examples of cli statements without success. The package was installed using `uv add smolagents`

**Code to reproduce the error**
```
smolagent "Plan a trip to Tokyo, Kyoto and Osaka between Mar 28 and Apr 7."  --model-type "HfApiModel" --model-id "Qwen/Qwen2.5-Coder-32B-Instruct" --imports "pandas numpy" --tools "web_search"
```

**Error logs (if any)**
```
Traceback (most recent call last):
  File "/home/agent_hell/agent_testbed/.venv/bin/smolagent", line 10, in <module>
    sys.exit(main())
             ^^^^^^
TypeError: main() missing 4 required positional arguments: 'prompt', 'tools', 'model_type', and 'model_id'
```

**Packages version:**
Fedora Linux 41 (Workstation Edition)
Python==3.11.11
smolagents==1.12.0
uv==0.6.11

Discussion:
@albertvillanova: Thanks for reporting. I am investigating this issue...

Solution patch:
diff --git a/README.md b/README.md
index b24ba30..069873f 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ limitations under the License.
 <h3 align="center">
   <div style="display:flex;flex-direction:row;">
     <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/smolagents.png" alt="Hugging Face mascot as James Bond" width=400px>
-    <p>A smol library to build great agents!</p>
+    <p>Agents that think in code!</p>
   </div>
 </h3>
 
diff --git a/examples/agent_from_any_llm.py b/examples/agent_from_any_llm.py
index f413a1b..fc5e224 100644
--- a/examples/agent_from_any_llm.py
+++ b/examples/agent_from_any_llm.py
@@ -7,7 +7,7 @@ from smolagents.agents import CodeAgent, ToolCallingAgent
 # Choose which inference type to use!
 
 available_inferences = ["hf_api", "hf_api_provider", "transformers", "ollama", "litellm", "openai"]
-chosen_inference = "openai"
+chosen_inference = "hf_api_provider"
 
 print(f"Chose model: '{chosen_inference}'")
 
diff --git a/examples/gradio_ui.py b/examples/gradio_ui.py
index 9383c89..d6866af 100644
--- a/examples/gradio_ui.py
+++ b/examples/gradio_ui.py
@@ -1,13 +1,25 @@
+from io import BytesIO
+
+import requests
+from PIL import Image
+
 from smolagents import CodeAgent, GradioUI, HfApiModel
 
 
+def add_agent_image(memory_step, agent):
+    url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/smolagents.png"
+    response = requests.get(url)
+    memory_step.observations_images = [Image.open(BytesIO(response.content))]
+
+
 agent = CodeAgent(
     tools=[],
     model=HfApiModel(),
     verbosity_level=1,
-    planning_interval=2,
+    planning_interval=3,
     name="example_agent",
-    description="This is an example agent that has no tools and uses only code.",
+    description="This is an example agent that has not tool but will always see an agent at the end of its step.",
+    step_callbacks=[add_agent_image],
 )
 
 GradioUI(agent, file_upload_folder="./data").launch()
diff --git a/pyproject.toml b/pyproject.toml
index bff78b5..516e0b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ dependencies = [
   "requests>=2.32.3",
   "rich>=13.9.4",
   "jinja2>=3.1.4",
-  "pillow>=11.0.0",
+  "pillow>=11.0.0,<11.2.0",
   "markdownify>=0.14.1",
   "duckduckgo-search>=6.3.7",
   "python-dotenv"
diff --git a/src/smolagents/agents.py b/src/smolagents/agents.py
index 3fab3ff..545f70c 100644
--- a/src/smolagents/agents.py
+++ b/src/smolagents/agents.py
@@ -340,7 +340,9 @@ You have been provided with these additional arguments, that you can access usin
             if self.interrupt_switch:
                 raise AgentError("Agent interrupted.", self.logger)
             step_start_time = time.time()
-            if self.planning_interval is not None and self.step_number % self.planning_interval == 1:
+            if self.planning_interval is not None and (
+                self.step_number == 1 or (self.step_number - 1) % self.planning_interval == 0
+            ):
                 planning_step = self._create_planning_step(
                     task, is_first_step=(self.step_number == 1), step=self.step_number
                 )
@@ -357,6 +359,7 @@ You have been provided with these additional arguments, that you can access usin
                 action_step.error = e
             finally:
                 self._finalize_step(action_step, step_start_time)
+                self.memory.steps.append(action_step)
                 yield action_step
                 self.step_number += 1
 
@@ -385,7 +388,6 @@ You have been provided with these additional arguments, that you can access usin
     def _finalize_step(self, memory_step: ActionStep, step_start_time: float):
         memory_step.end_time = time.time()
         memory_step.duration = memory_step.end_time - step_start_time
-        self.memory.steps.append(memory_step)
         for callback in self.step_callbacks:
             # For compatibility with old callbacks that don't take the agent as an argument
             callback(memory_step) if len(inspect.signature(callback).parameters) == 1 else callback(
@@ -975,7 +977,7 @@ class ToolCallingAgent(MultiStepAgent):
             )
             memory_step.model_output_message = model_message
         except Exception as e:
-            raise AgentGenerationError(f"Error in generating tool call with model:\n{e}", self.logger) from e
+            raise AgentParsingError(f"Error while generating or parsing output:\n{e}", self.logger) from e
 
         self.logger.log_markdown(
             content=model_message.content if model_message.content else str(model_message.raw),
diff --git a/src/smolagents/cli.py b/src/smolagents/cli.py
index 132562b..260726f 100644
--- a/src/smolagents/cli.py
+++ b/src/smolagents/cli.py
@@ -103,7 +103,7 @@ def load_model(model_type: str, model_id: str, api_base: str | None = None, api_
         raise ValueError(f"Unsupported model type: {model_type}")
 
 
-def main(
+def run_smolagent(
     prompt: str,
     tools: list[str],
     model_type: str,
@@ -132,10 +132,9 @@ def main(
     agent.run(prompt)
 
 
-if __name__ == "__main__":
+def main() -> None:
     args = parse_arguments()
-
-    main(
+    run_smolagent(
         args.prompt,
         args.tools,
         args.model_type,
@@ -144,3 +143,7 @@ if __name__ == "__main__":
         api_key=args.api_key,
         imports=args.imports,
     )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/smolagents/gradio_ui.py b/src/smolagents/gradio_ui.py
index 1537fbc..8ec9925 100644
--- a/src/smolagents/gradio_ui.py
+++ b/src/smolagents/gradio_ui.py
@@ -91,37 +91,44 @@ def pull_messages_from_step(
                 metadata={
                     "title": f"🛠️ Used tool {first_tool_call.name}",
                     "id": parent_id,
-                    "status": "pending",
+                    "status": "done",
                 },
             )
             yield parent_message_tool
 
-            # Nesting execution logs under the tool call if they exist
-            if hasattr(step_log, "observations") and (
-                step_log.observations is not None and step_log.observations.strip()
-            ):  # Only yield execution logs if there's actual content
-                log_content = step_log.observations.strip()
-                if log_content:
-                    log_content = re.sub(r"^Execution logs:\s*", "", log_content)
-                    yield gr.ChatMessage(
-                        role="assistant",
-                        content=f"```bash\n{log_content}\n",
-                        metadata={"title": "📝 Execution Logs", "parent_id": parent_id, "status": "done"},
-                    )
-
-            # Nesting any errors under the tool call
-            if hasattr(step_log, "error") and step_log.error is not None:
+        # Display execution logs if they exist
+        if hasattr(step_log, "observations") and (
+            step_log.observations is not None and step_log.observations.strip()
+        ):  # Only yield execution logs if there's actual content
+            log_content = step_log.observations.strip()
+            if log_content:
+                log_content = re.sub(r"^Execution logs:\s*", "", log_content)
                 yield gr.ChatMessage(
                     role="assistant",
-                    content=str(step_log.error),
-                    metadata={"title": "💥 Error", "parent_id": parent_id, "status": "done"},
+                    content=f"```bash\n{log_content}\n",
+                    metadata={"title": "📝 Execution Logs", "status": "done"},
                 )
 
-            # Update parent message metadata to done status without yielding a new message
-            parent_message_tool.metadata["status"] = "done"
+        # Display any errors
+        if hasattr(step_log, "error") and step_log.error is not None:
+            yield gr.ChatMessage(
+                role="assistant",
+                content=str(step_log.error),
+                metadata={"title": "💥 Error", "status": "done"},
+            )
+
+        # Update parent message metadata to done status without yielding a new message
+        if getattr(step_log, "observations_images", []):
+            for image in step_log.observations_images:
+                path_image = AgentImage(image).to_string()
+                yield gr.ChatMessage(
+                    role="assistant",
+                    content={"path": path_image, "mime_type": f"image/{path_image.split('.')[-1]}"},
+                    metadata={"title": "🖼️ Output Image", "status": "done"},
+                )
 
         # Handle standalone errors but not from tool calls
-        elif hasattr(step_log, "error") and step_log.error is not None:
+        if hasattr(step_log, "error") and step_log.error is not None:
             yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error"})
 
         yield gr.ChatMessage(role="assistant", content=get_step_footnote_content(step_log, step_number))
diff --git a/src/smolagents/tools.py b/src/smolagents/tools.py
index 94c62d2..147b411 100644
--- a/src/smolagents/tools.py
+++ b/src/smolagents/tools.py
@@ -157,10 +157,12 @@ class Tool:
             and getattr(self, "skip_forward_signature_validation") is True
         ):
             signature = inspect.signature(self.forward)
-
-            if not set(key for key in signature.parameters.keys() if key != "self") == set(self.inputs.keys()):
+            actual_keys = set(key for key in signature.parameters.keys() if key != "self")
+            expected_keys = set(self.inputs.keys())
+            if actual_keys != expected_keys:
                 raise Exception(
-                    f"In tool '{self.name}', 'forward' method should take 'self' as its first argument, then its next arguments should match the keys of tool attribute 'inputs'."
+                    f"In tool '{self.name}', 'forward' method parameters were {actual_keys}, but expected {expected_keys}. "
+                    f"It should take 'self' as its first argument, then its next arguments should match the keys of tool attribute 'inputs'."
                 )
 
             json_schema = _convert_type_hints_to_json_schema(self.forward, error_on_missing_type_hints=False)[
diff --git a/src/smolagents/vision_web_browser.py b/src/smolagents/vision_web_browser.py
index 3e1b1ee..540a907 100644
--- a/src/smolagents/vision_web_browser.py
+++ b/src/smolagents/vision_web_browser.py
@@ -187,7 +187,7 @@ When you have modals or cookie banners on screen, you should get rid of them bef
 """
 
 
-def main(prompt: str, model_type: str, model_id: str) -> None:
+def run_webagent(prompt: str, model_type: str, model_id: str) -> None:
     # Load environment variables
     load_dotenv()
 
@@ -203,8 +203,11 @@ def main(prompt: str, model_type: str, model_id: str) -> None:
     agent.run(prompt + helium_instructions)
 
 
-if __name__ == "__main__":
+def main() -> None:
     # Parse command line arguments
     args = parse_arguments()
+    run_webagent(args.prompt, args.model_type, args.model_id)
+
 
-    main(args.prompt, args.model_type, args.model_id)
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/test_agents.py b/tests/test_agents.py
index 86ab308..da6ccee 100644
--- a/tests/test_agents.py
+++ b/tests/test_agents.py
@@ -913,6 +913,31 @@ class TestToolCallingAgent(unittest.TestCase):
         assert agent.memory.steps[1].tool_calls[0].arguments == {"location": "Paris", "date": "today"}
         assert agent.memory.steps[1].observations == "The weather in Paris on date:today is sunny."
 
+    @patch("huggingface_hub.InferenceClient")
+    def test_toolcalling_agent_api_misformatted_output(self, mock_inference_client):
+        """Test that even misformatted json blobs don't interrupt the run for a ToolCallingAgent."""
+        mock_client = mock_inference_client.return_value
+        mock_response = MagicMock()
+        mock_response.choices = [MagicMock()]
+        mock_response.choices[0].message = MagicMock()
+        mock_response.choices[
+            0
+        ].message.content = '{"name": weather_api", "arguments": {"location": "Paris", "date": "today"}}'
+        mock_client.chat_completion.return_value = mock_response
+        mock_response.usage.prompt_tokens = 10
+        mock_response.usage.completion_tokens = 20
+
+        model = HfApiModel(model_id="test-model")
+
+        agent = ToolCallingAgent(model=model, tools=[], max_steps=2, verbosity_level=1)
+        with agent.logger.console.capture() as capture:
+            agent.run("What's the weather in Paris?")
+        assert agent.memory.steps[0].task == "What's the weather in Paris?"
+        assert agent.memory.steps[1].tool_calls is None
+        assert "The JSON blob you used is invalid" in agent.memory.steps[1].error.message
+        assert "Error while generating or parsing output:" in capture.get()
+        assert len(agent.memory.steps) == 4
+
 
 class TestCodeAgent:
     @pytest.mark.parametrize("provide_run_summary", [False, True])
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 90207ec..dc0a8c7 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -63,9 +63,9 @@ def test_cli_main(capsys):
     with patch("smolagents.cli.load_model") as mock_load_model:
         mock_load_model.return_value = "mock_model"
         with patch("smolagents.cli.CodeAgent") as mock_code_agent:
-            from smolagents.cli import main
+            from smolagents.cli import run_smolagent
 
-            main("test_prompt", [], "HfApiModel", "test_model_id")
+            run_smolagent("test_prompt", [], "HfApiModel", "test_model_id")
     # load_model
     assert len(mock_load_model.call_args_list) == 1
     assert mock_load_model.call_args.args == ("HfApiModel", "test_model_id")
@@ -91,9 +91,9 @@ def test_vision_web_browser_main():
         with patch("smolagents.vision_web_browser.load_model") as mock_load_model:
             mock_load_model.return_value = "mock_model"
             with patch("smolagents.vision_web_browser.CodeAgent") as mock_code_agent:
-                from smolagents.vision_web_browser import helium_instructions, main
+                from smolagents.vision_web_browser import helium_instructions, run_webagent
 
-                main("test_prompt", "HfApiModel", "test_model_id")
+                run_webagent("test_prompt", "HfApiModel", "test_model_id")
     # load_model
     assert len(mock_load_model.call_args_list) == 1
     assert mock_load_model.call_args.args == ("HfApiModel", "test_model_id")
diff --git a/tests/test_monitoring.py b/tests/test_monitoring.py
index 3dd5704..32cd537 100644
--- a/tests/test_monitoring.py
+++ b/tests/test_monitoring.py
@@ -180,7 +180,7 @@ class MonitoringTester(unittest.TestCase):
         # Use stream_to_gradio to capture the output
         outputs = list(stream_to_gradio(agent, task="Test task"))
 
-        self.assertEqual(len(outputs), 11)
+        self.assertEqual(len(outputs), 13)
         final_message = outputs[-1]
         self.assertEqual(final_message.role, "assistant")
         self.assertIn("Malformed call", final_message.content)