Repository: huggingface/smolagents

Issue:
[BUG] AzureOpenAI content filter
**Describe the bug**
After upgrading to smolagents 1.13, Azure's content filter is blocking my requests.
This ONLY happens when planning_interval != 0 (ie. is enabled). If planning_interval is not set, everyting works just fine.
It usually complains about "self_harm"

**Code to reproduce the error**
```py
import os
from smolagents import CodeAgent, AzureOpenAIServerModel

model = AzureOpenAIServerModel(
    model_id = "gpt-4o",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
)
agent = CodeAgent(
    tools=[],
    model=model,
    planning_interval=2,
)
agent.run("what is e**3?")
```

**Error logs (if any)**
`
Error in generating model output:
Error code: 400 - {'error': {'message': "The response was filtered due to the prompt triggering Azure OpenAI's 
content management policy. Please modify your prompt and retry. To learn more about our content filtering policies 
please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766", 'type': None, 'param': 'prompt', 
'code': 'content_filter', 'status': 400, 'innererror': {'code': 'ResponsibleAIPolicyViolation', 
'content_filter_result': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 
'detected': False}, 'self_harm': {'filtered': True, 'severity': 'high'}, 'sexual': {'filtered': False, 'severity': 
'safe'}, 'violence': {'filtered': False, 'severity': 'medium'}}}}}
`

**Expected behavior**
I expected the agent to run normally after upgrading versions, both with and without planning.

**Packages version:**
smolagents==1.13.0

Discussion:

Solution patch:
diff --git a/examples/open_deep_research/run_gaia.py b/examples/open_deep_research/run_gaia.py
index b577211..52dd620 100644
--- a/examples/open_deep_research/run_gaia.py
+++ b/examples/open_deep_research/run_gaia.py
@@ -1,3 +1,4 @@
+# EXAMPLE COMMAND: python examples/open_deep_research/run_gaia.py --concurrency 32 --run-name generate-traces-03-apr-noplanning --model-id gpt-4o
 import argparse
 import json
 import os
@@ -23,7 +24,6 @@ from scripts.text_web_browser import (
     FindNextTool,
     PageDownTool,
     PageUpTool,
-    SearchInformationTool,
     SimpleTextBrowser,
     VisitTool,
 )
@@ -32,6 +32,7 @@ from tqdm import tqdm
 
 from smolagents import (
     CodeAgent,
+    GoogleSearchTool,
     # HfApiModel,
     LiteLLMModel,
     Model,
@@ -121,14 +122,14 @@ BROWSER_CONFIG = {
 os.makedirs(f"./{BROWSER_CONFIG['downloads_folder']}", exist_ok=True)
 
 
-def create_agent_hierarchy(model: Model):
+def create_agent_team(model: Model):
     text_limit = 100000
     ti_tool = TextInspectorTool(model, text_limit)
 
     browser = SimpleTextBrowser(**BROWSER_CONFIG)
 
     WEB_TOOLS = [
-        SearchInformationTool(browser),
+        GoogleSearchTool(provider="serper"),
         VisitTool(browser),
         PageUpTool(browser),
         PageDownTool(browser),
@@ -137,6 +138,7 @@ def create_agent_hierarchy(model: Model):
         ArchiveSearchTool(browser),
         TextInspectorTool(model, text_limit),
     ]
+
     text_webbrowser_agent = ToolCallingAgent(
         model=model,
         tools=WEB_TOOLS,
@@ -181,10 +183,12 @@ def answer_single_question(example, model_id, answers_file, visual_inspection_to
     model_params = {
         "model_id": model_id,
         "custom_role_conversions": custom_role_conversions,
-        "max_completion_tokens": 8192,
     }
     if model_id == "o1":
         model_params["reasoning_effort"] = "high"
+        model_params["max_completion_tokens"] = 8192
+    else:
+        model_params["max_tokens"] = 4096
     model = LiteLLMModel(**model_params)
     # model = HfApiModel(model_id="Qwen/Qwen2.5-72B-Instruct", provider="together")
     #     "https://lnxyuvj02bpe6mam.us-east-1.aws.endpoints.huggingface.cloud",
@@ -194,7 +198,7 @@ def answer_single_question(example, model_id, answers_file, visual_inspection_to
     # )
     document_inspection_tool = TextInspectorTool(model, 100000)
 
-    agent = create_agent_hierarchy(model)
+    agent = create_agent_team(model)
 
     augmented_question = """You have one question to answer. It is paramount that you provide a correct answer.
 Give it all you can: I know for a fact that you have access to all the relevant tools to solve it and find the correct answer (the answer does exist). Failure or 'I cannot answer' or 'None found' will not be tolerated, success will be rewarded.
@@ -227,7 +231,7 @@ Here is the task:
         output = str(final_result)
         for memory_step in agent.memory.steps:
             memory_step.model_input_messages = None
-        intermediate_steps = [str(step) for step in agent.memory.steps]
+        intermediate_steps = agent_memory
 
         # Check for parsing errors which indicate the LLM failed to follow the required format
         parsing_error = True if any(["AgentParsingError" in step for step in intermediate_steps]) else False
diff --git a/src/smolagents/_function_type_hints_utils.py b/src/smolagents/_function_type_hints_utils.py
index 90ffd2c..5c3bc4d 100644
--- a/src/smolagents/_function_type_hints_utils.py
+++ b/src/smolagents/_function_type_hints_utils.py
@@ -217,7 +217,7 @@ def get_json_schema(func: Callable) -> Dict:
 
 
 # Extracts the initial segment of the docstring, containing the function description
-description_re = re.compile(r"^(.*?)[\n\s]*(Args:|Returns:|Raises:|\Z)", re.DOTALL)
+description_re = re.compile(r"^(.*?)(?=\n\s*(Args:|Returns:|Raises:)|\Z)", re.DOTALL)
 # Extracts the Args: block from the docstring
 args_re = re.compile(r"\n\s*Args:\n\s*(.*?)[\n\s]*(Returns:|Raises:|\Z)", re.DOTALL)
 # Splits the Args: block into individual arguments
diff --git a/src/smolagents/gradio_ui.py b/src/smolagents/gradio_ui.py
index 8ec9925..c67361e 100644
--- a/src/smolagents/gradio_ui.py
+++ b/src/smolagents/gradio_ui.py
@@ -167,6 +167,7 @@ def pull_messages_from_step(
 def stream_to_gradio(
     agent,
     task: str,
+    task_images: list | None = None,
     reset_agent_memory: bool = False,
     additional_args: Optional[dict] = None,
 ):
@@ -174,7 +175,9 @@ def stream_to_gradio(
     total_input_tokens = 0
     total_output_tokens = 0
 
-    for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
+    for step_log in agent.run(
+        task, images=task_images, stream=True, reset=reset_agent_memory, additional_args=additional_args
+    ):
         # Track tokens if model provides them
         if getattr(agent.model, "last_input_token_count", None) is not None:
             total_input_tokens += agent.model.last_input_token_count
diff --git a/src/smolagents/memory.py b/src/smolagents/memory.py
index 93d1b81..520e20a 100644
--- a/src/smolagents/memory.py
+++ b/src/smolagents/memory.py
@@ -101,6 +101,20 @@ class ActionStep(MemoryStep):
                 )
             )
 
+        if self.observations_images:
+            messages.append(
+                Message(
+                    role=MessageRole.USER,
+                    content=[
+                        {
+                            "type": "image",
+                            "image": image,
+                        }
+                        for image in self.observations_images
+                    ],
+                )
+            )
+
         if self.observations is not None:
             messages.append(
                 Message(
@@ -108,8 +122,7 @@ class ActionStep(MemoryStep):
                     content=[
                         {
                             "type": "text",
-                            "text": (f"Call id: {self.tool_calls[0].id}\n" if self.tool_calls else "")
-                            + f"Observation:\n{self.observations}",
+                            "text": f"Observation:\n{self.observations}",
                         }
                     ],
                 )
@@ -126,20 +139,6 @@ class ActionStep(MemoryStep):
                 Message(role=MessageRole.TOOL_RESPONSE, content=[{"type": "text", "text": message_content}])
             )
 
-        if self.observations_images:
-            messages.append(
-                Message(
-                    role=MessageRole.USER,
-                    content=[{"type": "text", "text": "Here are the observed images:"}]
-                    + [
-                        {
-                            "type": "image",
-                            "image": image,
-                        }
-                        for image in self.observations_images
-                    ],
-                )
-            )
         return messages
 
 
@@ -154,7 +153,7 @@ class PlanningStep(MemoryStep):
             return []
         return [
             Message(role=MessageRole.ASSISTANT, content=[{"type": "text", "text": self.plan.strip()}]),
-            Message(role=MessageRole.USER, content=[{"type": "text", "text": "Now go on and execute this plan."}]),
+            Message(role=MessageRole.USER, content=[{"type": "text", "text": "Now proceed and carry out this plan."}]),
             # This second message creates a role change to prevent models models from simply continuing the plan message
         ]
 
diff --git a/src/smolagents/models.py b/src/smolagents/models.py
index 0bbfa42..35637a1 100644
--- a/src/smolagents/models.py
+++ b/src/smolagents/models.py
@@ -224,9 +224,14 @@ def get_clean_message_list(
         if len(output_message_list) > 0 and message["role"] == output_message_list[-1]["role"]:
             assert isinstance(message["content"], list), "Error: wrong content:" + str(message["content"])
             if flatten_messages_as_text:
-                output_message_list[-1]["content"] += message["content"][0]["text"]
+                output_message_list[-1]["content"] += "\n" + message["content"][0]["text"]
             else:
-                output_message_list[-1]["content"] += message["content"]
+                for el in message["content"]:
+                    if el["type"] == "text" and output_message_list[-1]["content"][-1]["type"] == "text":
+                        # Merge consecutive text messages rather than creating new ones
+                        output_message_list[-1]["content"][-1]["text"] += "\n" + el["text"]
+                    else:
+                        output_message_list[-1]["content"].append(el)
         else:
             if flatten_messages_as_text:
                 content = message["content"][0]["text"]


Test patch:
diff --git a/tests/test_function_type_hints_utils.py b/tests/test_function_type_hints_utils.py
index c352cf4..c262709 100644
--- a/tests/test_function_type_hints_utils.py
+++ b/tests/test_function_type_hints_utils.py
@@ -220,6 +220,23 @@ def complex_docstring_types_func():
     return process
 
 
+@pytest.fixture
+def keywords_in_description_func():
+    def process(value: str) -> str:
+        """
+        Function with Args: or Returns: keywords in its description.
+
+        Args:
+            value: A string value.
+
+        Returns:
+            str: Processed value.
+        """
+        return value.upper()
+
+    return process
+
+
 class TestGetJsonSchema:
     def test_get_json_schema_example(self):
         def fn(x: int, y: Optional[Tuple[str, str, float]] = None) -> None:
@@ -430,6 +447,10 @@ class TestGetJsonSchema:
         first_param_name = list(schema["function"]["parameters"]["properties"].keys())[0]
         assert schema["function"]["parameters"]["properties"][first_param_name]["description"] == expected_description
 
+    def test_with_special_words_in_description_func(self, keywords_in_description_func):
+        schema = get_json_schema(keywords_in_description_func)
+        assert schema["function"]["description"] == "Function with Args: or Returns: keywords in its description."
+
 
 class TestGetCode:
     @pytest.mark.parametrize(
diff --git a/tests/test_memory.py b/tests/test_memory.py
index ddc2297..04c6b7f 100644
--- a/tests/test_memory.py
+++ b/tests/test_memory.py
@@ -78,16 +78,16 @@ def test_action_step_to_messages():
     assert "type" in text_content
     assert "text" in text_content
 
-    observation_message = messages[2]
-    assert observation_message["role"] == MessageRole.TOOL_RESPONSE
-    assert "Observation:\nThis is a nice observation" in observation_message["content"][0]["text"]
-
-    image_message = messages[3]
-    image_content = image_message["content"][1]
+    image_message = messages[2]
+    image_content = image_message["content"][0]
     assert isinstance(image_content, dict)
     assert "type" in image_content
     assert "image" in image_content
 
+    observation_message = messages[3]
+    assert observation_message["role"] == MessageRole.TOOL_RESPONSE
+    assert "Observation:\nThis is a nice observation" in observation_message["content"][0]["text"]
+
 
 def test_action_step_to_messages_no_tool_calls_with_observations():
     action_step = ActionStep(
diff --git a/tests/test_models.py b/tests/test_models.py
index c7389b4..7b4536b 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -372,7 +372,7 @@ def test_get_clean_message_list_flatten_messages_as_text():
     result = get_clean_message_list(messages, flatten_messages_as_text=True)
     assert len(result) == 1
     assert result[0]["role"] == "user"
-    assert result[0]["content"] == "Hello!How are you?"
+    assert result[0]["content"] == "Hello!\nHow are you?"
 
 
 @pytest.mark.parametrize(