Repository: huggingface/smolagents

Issue:
[BUG]
**Describe the bug**
When loading an MCP tool (e.g. mcp_server_tavily), an exception occurs in _generate_tool_inputs(...). key "type" is not found.

**Code to reproduce the error**
# Load an MCP server like so
server_parameters = StdioServerParameters(
    command="python",
    args=["mcp_server_tavily.py"],
)

# then see what tools it exposes
with ToolCollection.from_mcp(server_parameters) as toolset:

    for tool in [*toolset.tools]:
        print(tool.name)

**Error logs (if any)**
  File ".../lib/python3.11/site-packages/mcpadapt/smolagents_adapter.py", line 30, in <dictcomp>
    k: {"type": v["type"], "description": v.get("description", "")}
                ~^^^^^^^^
KeyError: 'type'

**Expected behavior**
No exception

**Packages version:**
smolagents==1.8.1

**Additional context**
Add any other context about the problem here.


Discussion:

Patch:
diff --git a/docs/source/en/tutorials/building_good_agents.mdx b/docs/source/en/tutorials/building_good_agents.mdx
index 8c17de1..977c137 100644
--- a/docs/source/en/tutorials/building_good_agents.mdx
+++ b/docs/source/en/tutorials/building_good_agents.mdx
@@ -43,7 +43,7 @@ This leads to a few takeaways:
 
 ### Improve the information flow to the LLM engine
 
-Remember that your LLM engine is like an *intelligent* robot, tapped into a room with the only communication with the outside world being notes passed under a door.
+Remember that your LLM engine is like an *intelligent* robot, trapped into a room with the only communication with the outside world being notes passed under a door.
 
 It won't know of anything that happened if you don't explicitly put that into its prompt.
 
diff --git a/docs/source/zh/_toctree.yml b/docs/source/zh/_toctree.yml
index 4da8f48..5ebe325 100644
--- a/docs/source/zh/_toctree.yml
+++ b/docs/source/zh/_toctree.yml
@@ -8,10 +8,14 @@
   sections:
   - local: tutorials/building_good_agents
     title: ✨ 构建好用的 agents
+  - local: tutorials/inspect_runs
+    title: 📊 监控 Agent 的运行
   - local: tutorials/tools
     title: 🛠️ 工具 - 深度指南
   - local: tutorials/secure_code_execution
     title: 🛡️ 使用 E2B 保护你的代码执行
+  - local: tutorials/memory
+    title: 📚 管理 Agent 的记忆
 - title: Conceptual guides
   sections:
   - local: conceptual_guides/intro_agents
@@ -21,14 +25,18 @@
 - title: Examples
   sections:
   - local: examples/text_to_sql
-    title: Self-correcting Text-to-SQL
+    title: 自我修正 Text-to-SQL
   - local: examples/rag
-    title: Master you knowledge base with agentic RAG
+    title: 借助 agentic RAG 掌控知识库
   - local: examples/multiagents
-    title: Orchestrate a multi-agent system
+    title: 编排 multi-agent 系统
+  - local: examples/web_browser
+    title: 基于视觉模型构建能够浏览网页的agent
 - title: Reference
   sections:
   - local: reference/agents
     title: Agent-related objects
+  - local: reference/models
+    title: Model-related objects
   - local: reference/tools
     title: Tool-related objects
diff --git a/docs/source/zh/examples/web_browser.mdx b/docs/source/zh/examples/web_browser.mdx
new file mode 100644
index 0000000..b72fe71
--- /dev/null
+++ b/docs/source/zh/examples/web_browser.mdx
@@ -0,0 +1,214 @@
+# 使用Agent实现网页浏览器自动化 🤖🌐
+
+[[open-in-colab]]
+
+在本notebook中，我们将创建一个**基于Agent的网页浏览器自动化系统**！该系统可以自动导航网站、与网页元素交互并提取信息。
+
+该Agent将能够：
+
+- [x] 导航到网页
+- [x] 点击元素
+- [x] 在页面内搜索
+- [x] 处理弹出窗口和模态框
+- [x] 提取信息
+
+让我们一步步搭建这个系统！
+
+首先运行以下命令安装所需依赖：
+
+```bash
+pip install smolagents selenium helium pillow -q
+```
+
+让我们导入所需的库并设置环境变量：
+
+```python
+from io import BytesIO
+from time import sleep
+
+import helium
+from dotenv import load_dotenv
+from PIL import Image
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+
+from smolagents import CodeAgent, tool
+from smolagents.agents import ActionStep
+
+# Load environment variables
+load_dotenv()
+```
+
+现在我们来创建核心的浏览器交互工具，使我们的Agent能够导航并与网页交互：
+
+```python
+@tool
+def search_item_ctrl_f(text: str, nth_result: int = 1) -> str:
+    """
+    Searches for text on the current page via Ctrl + F and jumps to the nth occurrence.
+    Args:
+        text: The text to search for
+        nth_result: Which occurrence to jump to (default: 1)
+    """
+    elements = driver.find_elements(By.XPATH, f"//*[contains(text(), '{text}')]")
+    if nth_result > len(elements):
+        raise Exception(f"Match n°{nth_result} not found (only {len(elements)} matches found)")
+    result = f"Found {len(elements)} matches for '{text}'."
+    elem = elements[nth_result - 1]
+    driver.execute_script("arguments[0].scrollIntoView(true);", elem)
+    result += f"Focused on element {nth_result} of {len(elements)}"
+    return result
+
+@tool
+def go_back() -> None:
+    """Goes back to previous page."""
+    driver.back()
+
+@tool
+def close_popups() -> str:
+    """
+    Closes any visible modal or pop-up on the page. Use this to dismiss pop-up windows!
+    This does not work on cookie consent banners.
+    """
+    webdriver.ActionChains(driver).send_keys(Keys.ESCAPE).perform()
+```
+
+让我们配置使用Chrome浏览器并设置截图功能：
+
+```python
+# Configure Chrome options
+chrome_options = webdriver.ChromeOptions()
+chrome_options.add_argument("--force-device-scale-factor=1")
+chrome_options.add_argument("--window-size=1000,1350")
+chrome_options.add_argument("--disable-pdf-viewer")
+chrome_options.add_argument("--window-position=0,0")
+
+# Initialize the browser
+driver = helium.start_chrome(headless=False, options=chrome_options)
+
+# Set up screenshot callback
+def save_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None:
+    sleep(1.0)  # Let JavaScript animations happen before taking the screenshot
+    driver = helium.get_driver()
+    current_step = memory_step.step_number
+    if driver is not None:
+        for previous_memory_step in agent.memory.steps:  # Remove previous screenshots for lean processing
+            if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= current_step - 2:
+                previous_memory_step.observations_images = None
+        png_bytes = driver.get_screenshot_as_png()
+        image = Image.open(BytesIO(png_bytes))
+        print(f"Captured a browser screenshot: {image.size} pixels")
+        memory_step.observations_images = [image.copy()]  # Create a copy to ensure it persists
+
+    # Update observations with current URL
+    url_info = f"Current url: {driver.current_url}"
+    memory_step.observations = (
+        url_info if memory_step.observations is None else memory_step.observations + "\n" + url_info
+    )
+```
+
+现在我们来创建网页自动化Agent：
+
+```python
+from smolagents import HfApiModel
+
+# Initialize the model
+model_id = "meta-llama/Llama-3.3-70B-Instruct"  # You can change this to your preferred model
+model = HfApiModel(model_id)
+
+# Create the agent
+agent = CodeAgent(
+    tools=[go_back, close_popups, search_item_ctrl_f],
+    model=model,
+    additional_authorized_imports=["helium"],
+    step_callbacks=[save_screenshot],
+    max_steps=20,
+    verbosity_level=2,
+)
+
+# Import helium for the agent
+agent.python_executor("from helium import *", agent.state)
+```
+
+Agent需要获得关于如何使用Helium进行网页自动化的指导。以下是我们将提供的操作说明：
+
+```python
+helium_instructions = """
+You can use helium to access websites. Don't bother about the helium driver, it's already managed.
+We've already ran "from helium import *"
+Then you can go to pages!
+Code:
+```py
+go_to('github.com/trending')
+```<end_code>
+
+You can directly click clickable elements by inputting the text that appears on them.
+Code:
+```py
+click("Top products")
+```<end_code>
+
+If it's a link:
+Code:
+```py
+click(Link("Top products"))
+```<end_code>
+
+If you try to interact with an element and it's not found, you'll get a LookupError.
+In general stop your action after each button click to see what happens on your screenshot.
+Never try to login in a page.
+
+To scroll up or down, use scroll_down or scroll_up with as an argument the number of pixels to scroll from.
+Code:
+```py
+scroll_down(num_pixels=1200) # This will scroll one viewport down
+```<end_code>
+
+When you have pop-ups with a cross icon to close, don't try to click the close icon by finding its element or targeting an 'X' element (this most often fails).
+Just use your built-in tool `close_popups` to close them:
+Code:
+```py
+close_popups()
+```<end_code>
+
+You can use .exists() to check for the existence of an element. For example:
+Code:
+```py
+if Text('Accept cookies?').exists():
+    click('I accept')
+```<end_code>
+"""
+```
+
+现在我们可以运行Agent执行任务了！让我们尝试在维基百科上查找信息：
+
+```python
+search_request = """
+Please navigate to https://en.wikipedia.org/wiki/Chicago and give me a sentence containing the word "1992" that mentions a construction accident.
+"""
+
+agent_output = agent.run(search_request + helium_instructions)
+print("Final output:")
+print(agent_output)
+```
+
+您可以通过修改请求参数执行不同任务。例如，以下请求可帮助我判断是否需要更加努力工作：
+
+```python
+github_request = """
+I'm trying to find how hard I have to work to get a repo in github.com/trending.
+Can you navigate to the profile for the top author of the top trending repo, and give me their total number of commits over the last year?
+"""
+
+agent_output = agent.run(github_request + helium_instructions)
+print("Final output:")
+print(agent_output)
+```
+
+该系统在以下任务中尤为有效：
+
+- 从网站提取数据
+- 网页研究自动化
+- 用户界面测试与验证
+- 内容监控
\ No newline at end of file
diff --git a/docs/source/zh/guided_tour.mdx b/docs/source/zh/guided_tour.mdx
index 2c9db3a..8289007 100644
--- a/docs/source/zh/guided_tour.mdx
+++ b/docs/source/zh/guided_tour.mdx
@@ -33,10 +33,12 @@ rendered properly in your Markdown viewer.
     - [`TransformersModel`] 使用预初始化的 `transformers` 管道在本地机器上运行推理
     - [`HfApiModel`] 在底层使用 `huggingface_hub.InferenceClient`
     - [`LiteLLMModel`] 让您通过 [LiteLLM](https://docs.litellm.ai/) 调用 100+ 不同的模型！
+    - [`AzureOpenAIServerModel`] 允许您使用部署在 [Azure](https://azure.microsoft.com/en-us/products/ai-services/openai-service) 中的 OpenAI 模型。
+    - [`MLXModel`] 可创建 [mlx-lm](https://pypi.org/project/mlx-lm/) 流水线，以便在本地机器上运行推理。
 
 - `tools`，agent 可以用来解决任务的 `Tools` 列表。它可以是一个空列表。您还可以通过定义可选参数 `add_base_tools=True` 在您的 `tools` 列表之上添加默认工具箱。
 
-一旦有了这两个参数 `tools` 和 `model`，您就可以创建一个 agent 并运行它。您可以使用任何您喜欢的 LLM，无论是通过 [Hugging Face API](https://huggingface.co/docs/api-inference/en/index)、[transformers](https://github.com/huggingface/transformers/)、[ollama](https://ollama.com/)，还是 [LiteLLM](https://www.litellm.ai/)。
+一旦有了这两个参数 `tools` 和 `model`，您就可以创建一个 agent 并运行它。您可以使用任何您喜欢的 LLM，无论是通过 [Hugging Face API](https://huggingface.co/docs/api-inference/en/index)、[transformers](https://github.com/huggingface/transformers/)、[ollama](https://ollama.com/)、[LiteLLM](https://www.litellm.ai/)、[Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)，还是[mlx-lm](https://pypi.org/project/mlx-lm/).。
 
 <hfoptions id="选择一个LLM">
 <hfoption id="Hugging Face API">
@@ -109,6 +111,62 @@ agent.run(
     "Could you give me the 118th number in the Fibonacci sequence?",
 )
 ```
+</hfoption>
+<hfoption id="Azure OpenAI">
+
+要连接到 Azure OpenAI，您可以直接使用 `AzureOpenAIServerModel`，或使用 `LiteLLMModel` 并进行相应配置。
+
+初始化 `AzureOpenAIServerModel` 实例时，需要传递模型部署名称，可选择以下任一种方式：1.传递 `azure_endpoint`、`api_key` 和 `api_version` 参数；2.设置环境变量 `AZURE_OPENAI_ENDPOINT`、`AZURE_OPENAI_API_KEY` 和 `OPENAI_API_VERSION`
+
+```python
+# !pip install smolagents[openai]
+from smolagents import CodeAgent, AzureOpenAIServerModel
+
+model = AzureOpenAIServerModel(model_id="gpt-4o-mini")
+agent = CodeAgent(tools=[], model=model, add_base_tools=True)
+
+agent.run(
+    "Could you give me the 118th number in the Fibonacci sequence?",
+)
+```
+
+也可按如下方式配置 `LiteLLMModel` 连接 Azure OpenAI：
+
+- 将模型部署名称作为 `model_id` 参数传递，并确保其前缀为 `azure/`
+- 确保设置环境变量 `AZURE_API_VERSION`
+- 任选其一：1.传递 `api_base` 和 `api_key` 参数；2.设置环境变量 `AZURE_API_KEY` 和 `AZURE_API_BASE`
+
+```python
+import os
+from smolagents import CodeAgent, LiteLLMModel
+
+AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="gpt-35-turbo-16k-deployment" # example of deployment name
+
+os.environ["AZURE_API_KEY"] = "" # api_key
+os.environ["AZURE_API_BASE"] = "" # "https://example-endpoint.openai.azure.com"
+os.environ["AZURE_API_VERSION"] = "" # "2024-10-01-preview"
+
+model = LiteLLMModel(model_id="azure/" + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME)
+agent = CodeAgent(tools=[], model=model, add_base_tools=True)
+
+agent.run(
+   "Could you give me the 118th number in the Fibonacci sequence?",
+)
+```
+
+</hfoption>
+<hfoption id="mlx-lm">
+
+```python
+# !pip install smolagents[mlx-lm]
+from smolagents import CodeAgent, MLXModel
+
+mlx_model = MLXModel("mlx-community/Qwen2.5-Coder-32B-Instruct-4bit")
+agent = CodeAgent(model=mlx_model, tools=[], add_base_tools=True)
+
+agent.run("Could you give me the 118th number in the Fibonacci sequence?")
+```
+
 </hfoption>
 </hfoptions>
 
@@ -125,6 +183,7 @@ Python 解释器默认也不允许在安全列表之外导入，所以所有最
 ```py
 from smolagents import CodeAgent
 
+model = HfApiModel()
 agent = CodeAgent(tools=[], model=model, additional_authorized_imports=['requests', 'bs4'])
 agent.run("Could you get me the title of the page at url 'https://huggingface.co/blog'?")
 ```
@@ -364,6 +423,18 @@ GradioUI(agent).launch()
 
 ## 下一步
 
+最后，当您按需配置好agent后，即可将其分享至 Hub！
+
+```py
+agent.push_to_hub("m-ric/my_agent")
+```
+
+类似地，若要加载已推送至 Hub 的agent，在信任其工具代码的前提下，可使用：
+
+```py
+agent.from_hub("m-ric/my_agent", trust_remote_code=True)
+```
+
 要更深入地使用，您将需要查看我们的教程：
 - [我们的代码 agent 如何工作的解释](./tutorials/secure_code_execution)
 - [本指南关于如何构建好的 agent](./tutorials/building_good_agents)。
diff --git a/docs/source/zh/tutorials/inspect_runs.mdx b/docs/source/zh/tutorials/inspect_runs.mdx
new file mode 100644
index 0000000..e7beb80
--- /dev/null
+++ b/docs/source/zh/tutorials/inspect_runs.mdx
@@ -0,0 +1,195 @@
+<!--Copyright 2024 The HuggingFace Team. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+⚠️ Note that this file is in Markdown but contain specific syntax for our doc-builder (similar to MDX) that may not be
+rendered properly in your Markdown viewer.
+
+-->
+# 使用 OpenTelemetry 检查运行记录
+
+[[open-in-colab]]
+
+> [!TIP]
+> 如果您是初次构建Agent，建议先阅读 [Agent 入门指南](../conceptual_guides/intro_agents) 和 [smolagents 导览](../guided_tour)。
+
+## 为什么需要记录Agent运行？
+
+调试Agent运行过程具有挑战性。
+
+验证运行是否正常进行很困难，因为Agent的工作流程本身具有 [设计上的不可预测性](../conceptual_guides/intro_agents)（如果可预测，直接使用传统代码即可）。
+
+检查运行记录同样困难：多步骤的Agent往往会快速在控制台生成大量日志，而大多数错误只是"LLM 低级错误"类型的问题，通常LLM会在后续步骤中通过生成更好的代码或工具调用来自我修正。
+
+因此，在生产环境中使用监控工具记录Agent运行过程，对于后续检查和分析至关重要！
+
+我们采用 [OpenTelemetry](https://opentelemetry.io/) 标准来实现Agent运行监控。
+
+这意味着您只需添加少量监控代码，即可在正常运行Agent时自动记录所有信息到监控平台。以下是在不同OpenTelemetry后端实现此功能的示例：
+
+在监控平台上的展示效果如下：
+
+<div class="flex justify-center">
+    <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/inspect_run_phoenix.gif"/>
+</div>
+
+
+## 使用 Arize AI Phoenix 配置遥测
+
+首先安装必要的软件包。这里我们选择安装 [Arize AI 的 Phoenix](https://github.com/Arize-ai/phoenix) 作为日志收集和检查方案，您也可以使用其他兼容 OpenTelemetry 的平台来完成收集与检查工作。
+
+```shell
+pip install 'smolagents[telemetry]'
+```
+
+接着在后台运行日志收集器：
+
+```shell
+python -m phoenix.server.main serve
+```
+
+最后配置 `SmolagentsInstrumentor` 来追踪Agent活动，并将追踪数据发送至 Phoenix 默认端点：
+
+```python
+from phoenix.otel import register
+from openinference.instrumentation.smolagents import SmolagentsInstrumentor
+
+register()
+SmolagentsInstrumentor().instrument()
+```
+
+完成上述配置后，即可正常运行您的Agent！
+
+```py
+from smolagents import (
+    CodeAgent,
+    ToolCallingAgent,
+    DuckDuckGoSearchTool,
+    VisitWebpageTool,
+    HfApiModel,
+)
+
+model = HfApiModel()
+
+search_agent = ToolCallingAgent(
+    tools=[DuckDuckGoSearchTool(), VisitWebpageTool()],
+    model=model,
+    name="search_agent",
+    description="This is an agent that can do web search.",
+)
+
+manager_agent = CodeAgent(
+    tools=[],
+    model=model,
+    managed_agents=[search_agent],
+)
+manager_agent.run(
+    "If the US keeps its 2024 growth rate, how many years will it take for the GDP to double?"
+)
+```
+Voilà!
+
+此时访问 `http://0.0.0.0:6006/projects/` 即可查看运行记录：
+
+<img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/inspect_run_phoenix.png">
+
+如图所示，CodeAgent 调用了其托管的 ToolCallingAgent（注：托管Agent也可以是另一个 CodeAgent）执行美国2024年经济增长率的网络搜索。托管Agent返回报告后，管理Agent根据结果计算出经济翻倍周期！是不是很智能？
+
+## 使用 Langfuse 配置遥测
+
+本部分演示如何通过 `SmolagentsInstrumentor` 使用 **Langfuse** 监控和调试 Hugging Face **smolagents**。
+
+> **Langfuse 是什么？** [Langfuse](https://langfuse.com) 是面向LLM工程的开源平台，提供AI Agent的追踪与监控功能，帮助开发者调试、分析和优化产品。该平台通过原生集成、OpenTelemetry 和 SDKs 与各类工具框架对接。
+
+### 步骤 1: 安装依赖
+
+```python
+%pip install smolagents
+%pip install opentelemetry-sdk opentelemetry-exporter-otlp openinference-instrumentation-smolagents
+```
+
+### 步骤 2: 配置环境变量
+
+设置 Langfuse API 密钥，并配置 OpenTelemetry 端点将追踪数据发送至 Langfuse。通过注册 [Langfuse Cloud](https://cloud.langfuse.com) 或 [自托管 Langfuse](https://langfuse.com/self-hosting) 获取 API 密钥。
+
+同时需添加 [Hugging Face 令牌](https://huggingface.co/settings/tokens) (`HF_TOKEN`) 作为环境变量：
+```python
+import os
+import base64
+
+LANGFUSE_PUBLIC_KEY="pk-lf-..."
+LANGFUSE_SECRET_KEY="sk-lf-..."
+LANGFUSE_AUTH=base64.b64encode(f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode()).decode()
+
+os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "https://cloud.langfuse.com/api/public/otel" # EU data region
+# os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "https://us.cloud.langfuse.com/api/public/otel" # US data region
+os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"
+
+# your Hugging Face token
+os.environ["HF_TOKEN"] = "hf_..."
+```
+
+### 步骤 3: 初始化 `SmolagentsInstrumentor`
+
+在应用程序代码执行前初始化 `SmolagentsInstrumentor`。配置 `tracer_provider` 并添加 span processor 将追踪数据导出至 Langfuse。`OTLPSpanExporter()` 会自动使用环境变量中配置的端点和请求头。
+
+
+```python
+from opentelemetry.sdk.trace import TracerProvider
+
+from openinference.instrumentation.smolagents import SmolagentsInstrumentor
+from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+
+trace_provider = TracerProvider()
+trace_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))
+
+SmolagentsInstrumentor().instrument(tracer_provider=trace_provider)
+```
+
+### 步骤 4: 运行 smolagent
+
+```python
+from smolagents import (
+    CodeAgent,
+    ToolCallingAgent,
+    DuckDuckGoSearchTool,
+    VisitWebpageTool,
+    HfApiModel,
+)
+
+model = HfApiModel(
+    model_id="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
+)
+
+search_agent = ToolCallingAgent(
+    tools=[DuckDuckGoSearchTool(), VisitWebpageTool()],
+    model=model,
+    name="search_agent",
+    description="This is an agent that can do web search.",
+)
+
+manager_agent = CodeAgent(
+    tools=[],
+    model=model,
+    managed_agents=[search_agent],
+)
+manager_agent.run(
+    "How can Langfuse be used to monitor and improve the reasoning and decision-making of smolagents when they execute multi-step tasks, like dynamically adjusting a recipe based on user feedback or available ingredients?"
+)
+```
+
+### 步骤 5: 在 Langfuse 中查看追踪记录
+
+运行Agent后，您可以在 [Langfuse](https://cloud.langfuse.com) 平台查看 smolagents 应用生成的追踪记录。这些记录会详细展示LLM的交互步骤，帮助您调试和优化AI代理。
+
+![smolagents 追踪示例](https://langfuse.com/images/cookbook/integration-smolagents/smolagent_example_trace.png)
+
+_[Langfuse 公开示例追踪](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/ce5160f9bfd5a6cd63b07d2bfcec6f54?timestamp=2025-02-11T09%3A25%3A45.163Z&display=details)_
\ No newline at end of file
diff --git a/docs/source/zh/tutorials/memory.mdx b/docs/source/zh/tutorials/memory.mdx
new file mode 100644
index 0000000..1718567
--- /dev/null
+++ b/docs/source/zh/tutorials/memory.mdx
@@ -0,0 +1,146 @@
+<!--Copyright 2024 The HuggingFace Team. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+
+⚠️ Note that this file is in Markdown but contain specific syntax for our doc-builder (similar to MDX) that may not be
+rendered properly in your Markdown viewer.
+
+-->
+# 📚 管理Agent的记忆
+
+[[open-in-colab]]
+
+归根结底，Agent可以定义为由几个简单组件构成：它拥有工具、提示词。最重要的是，它具备对过往步骤的记忆，能够追溯完整的规划、执行和错误历史。
+
+### 回放Agent的记忆
+
+我们提供了多项功能来审查Agent的过往运行记录。
+
+您可以通过插装（instrumentation）在可视化界面中查看Agent的运行过程，该界面支持对特定步骤进行缩放操作，具体方法参见[插装指南](./inspect_runs)。
+
+您也可以使用`agent.replay()`方法实现回放：
+
+当Agent完成运行后：
+```py
+from smolagents import HfApiModel, CodeAgent
+
+agent = CodeAgent(tools=[], model=HfApiModel(), verbosity_level=0)
+
+result = agent.run("What's the 20th Fibonacci number?")
+```
+
+若要回放最近一次运行，只需使用：
+```py
+agent.replay()
+```
+
+### 动态修改Agent的记忆
+
+许多高级应用场景需要对Agent的记忆进行动态修改。
+
+您可以通过以下方式访问Agent的记忆：
+
+```py
+from smolagents import ActionStep
+
+system_prompt_step = agent.memory.system_prompt
+print("The system prompt given to the agent was:")
+print(system_prompt_step.system_prompt)
+
+task_step = agent.memory.steps[0]
+print("\n\nThe first task step was:")
+print(task_step.task)
+
+for step in agent.memory.steps:
+    if isinstance(step, ActionStep):
+        if step.error is not None:
+            print(f"\nStep {step.step_number} got this error:\n{step.error}\n")
+        else:
+            print(f"\nStep {step.step_number} got these observations:\n{step.observations}\n")
+```
+
+使用`agent.memory.get_full_steps()`可获取完整步骤字典数据。
+
+您还可以通过步骤回调（step callbacks）实现记忆的动态修改。
+
+步骤回调函数可通过参数直接访问`agent`对象，因此能够访问所有记忆步骤并根据需要进行修改。例如，假设您正在监控网页浏览Agent每个步骤的屏幕截图，希望保留最新截图同时删除旧步骤的图片以节省token消耗。
+
+可参考以下代码示例：
+_注：此代码片段不完整，部分导入语句和对象定义已精简，完整代码请访问[原始脚本](https://github.com/huggingface/smolagents/blob/main/src/smolagents/vision_web_browser.py)_
+
+```py
+import helium
+from PIL import Image
+from io import BytesIO
+from time import sleep
+
+def update_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None:
+    sleep(1.0)  # Let JavaScript animations happen before taking the screenshot
+    driver = helium.get_driver()
+    latest_step = memory_step.step_number
+    for previous_memory_step in agent.memory.steps:  # Remove previous screenshots from logs for lean processing
+        if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= latest_step - 2:
+            previous_memory_step.observations_images = None
+    png_bytes = driver.get_screenshot_as_png()
+    image = Image.open(BytesIO(png_bytes))
+    memory_step.observations_images = [image.copy()]
+```
+
+最后在初始化Agent时，将此函数传入`step_callbacks`参数：
+
+```py
+CodeAgent(
+    tools=[DuckDuckGoSearchTool(), go_back, close_popups, search_item_ctrl_f],
+    model=model,
+    additional_authorized_imports=["helium"],
+    step_callbacks=[update_screenshot],
+    max_steps=20,
+    verbosity_level=2,
+)
+```
+
+请访问我们的 [vision web browser code](https://github.com/huggingface/smolagents/blob/main/src/smolagents/vision_web_browser.py) 查看完整可运行示例。
+
+### 分步运行 Agents
+
+当您需要处理耗时数天的工具调用时，这种方式特别有用：您可以逐步执行Agents。这还允许您在每一步更新记忆。
+
+```py
+from smolagents import HfApiModel, CodeAgent, ActionStep, TaskStep
+
+agent = CodeAgent(tools=[], model=HfApiModel(), verbosity_level=1)
+print(agent.memory.system_prompt)
+
+task = "What is the 20th Fibonacci number?"
+
+# You could modify the memory as needed here by inputting the memory of another agent.
+# agent.memory.steps = previous_agent.memory.steps
+
+# Let's start a new task!
+agent.memory.steps.append(TaskStep(task=task, task_images=[]))
+
+final_answer = None
+step_number = 1
+while final_answer is None and step_number <= 10:
+    memory_step = ActionStep(
+        step_number=step_number,
+        observations_images=[],
+    )
+    # Run one step.
+    final_answer = agent.step(memory_step)
+    agent.memory.steps.append(memory_step)
+    step_number += 1
+
+    # Change the memory as you please!
+    # For instance to update the latest step:
+    # agent.memory.steps[-1] = ...
+
+print("The final answer is:", final_answer)
+```
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 820d5d4..b7bc6a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,7 +47,7 @@ litellm = [
   "litellm>=1.60.2",
 ]
 mcp = [
-  "mcpadapt>=0.0.6",
+  "mcpadapt>=0.0.15",
   "mcp",
 ]
 mlx-lm = [
@@ -67,8 +67,12 @@ transformers = [
   "transformers>=4.0.0,<4.49.0",
   "smolagents[torch]",
 ]
+vision = [
+  "helium",
+  "selenium",
+]
 all = [
-  "smolagents[audio,docker,e2b,gradio,litellm,mcp,openai,telemetry,transformers]",
+  "smolagents[audio,docker,e2b,gradio,litellm,mcp,openai,telemetry,transformers,vision]",
 ]
 quality = [
   "ruff>=0.9.0",
diff --git a/src/smolagents/cli.py b/src/smolagents/cli.py
index 7d93a3b..83f1745 100644
--- a/src/smolagents/cli.py
+++ b/src/smolagents/cli.py
@@ -26,8 +26,8 @@ from smolagents.default_tools import TOOL_MAPPING
 leopard_prompt = "How many seconds would it take for a leopard at full speed to run through Pont des Arts?"
 
 
-def parse_arguments(description):
-    parser = argparse.ArgumentParser(description=description)
+def parse_arguments():
+    parser = argparse.ArgumentParser(description="Run a CodeAgent with all specified parameters")
     parser.add_argument(
         "prompt",
         type=str,
@@ -103,15 +103,21 @@ 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 main(
+    prompt: str,
+    tools: list[str],
+    model_type: str,
+    model_id: str,
+    api_base: str | None = None,
+    api_key: str | None = None,
+    imports: list[str] | None = None,
+) -> None:
     load_dotenv()
 
-    args = parse_arguments(description="Run a CodeAgent with all specified parameters")
-
-    model = load_model(args.model_type, args.model_id, args.api_base, args.api_key)
+    model = load_model(model_type, model_id, api_base=api_base, api_key=api_key)
 
     available_tools = []
-    for tool_name in args.tools:
+    for tool_name in tools:
         if "/" in tool_name:
             available_tools.append(Tool.from_space(tool_name))
         else:
@@ -120,11 +126,21 @@ def main():
             else:
                 raise ValueError(f"Tool {tool_name} is not recognized either as a default tool or a Space.")
 
-    print(f"Running agent with these tools: {args.tools}")
-    agent = CodeAgent(tools=available_tools, model=model, additional_authorized_imports=args.imports)
+    print(f"Running agent with these tools: {tools}")
+    agent = CodeAgent(tools=available_tools, model=model, additional_authorized_imports=imports)
 
-    agent.run(args.prompt)
+    agent.run(prompt)
 
 
 if __name__ == "__main__":
-    main()
+    args = parse_arguments()
+
+    main(
+        args.prompt,
+        args.tools,
+        args.model_type,
+        args.model_id,
+        api_base=args.api_base,
+        api_key=args.api_key,
+        imports=args.imports,
+    )
diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py
index 19ad59e..0dc3c1d 100644
--- a/src/smolagents/local_python_executor.py
+++ b/src/smolagents/local_python_executor.py
@@ -24,7 +24,7 @@ import re
 from collections.abc import Mapping
 from functools import wraps
 from importlib import import_module
-from types import ModuleType
+from types import BuiltinFunctionType, FunctionType, ModuleType
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple
 
 import numpy as np
@@ -116,25 +116,15 @@ BASE_PYTHON_TOOLS = {
     "complex": complex,
 }
 
-DANGEROUS_PATTERNS = (
-    "_os",
-    "os",
-    "subprocess",
-    "_subprocess",
-    "pty",
-    "system",
-    "popen",
-    "spawn",
-    "shutil",
-    "sys",
-    "pathlib",
-    "io",
-    "socket",
-    "compile",
-    "eval",
-    "exec",
-    "multiprocessing",
-)
+DANGEROUS_FUNCTIONS = [
+    "builtins.compile",
+    "builtins.eval",
+    "builtins.exec",
+    "builtins.globalsbuiltins.locals",
+    "builtins.__import__",
+    "os.popen",
+    "os.system",
+]
 
 DANGEROUS_MODULES = [
     "builtins",
@@ -248,23 +238,32 @@ def safer_eval(func: Callable):
         result = func(expression, state, static_tools, custom_tools, authorized_imports=authorized_imports)
         if "*" not in authorized_imports:
             if isinstance(result, ModuleType):
-                for module in DANGEROUS_MODULES:
+                for module_name in DANGEROUS_MODULES:
                     if (
-                        module not in authorized_imports
-                        and result.__name__ == module
+                        module_name not in authorized_imports
+                        and result.__name__ == module_name
                         # builtins has no __file__ attribute
-                        and getattr(result, "__file__", "") == getattr(import_module(module), "__file__", "")
+                        and getattr(result, "__file__", "") == getattr(import_module(module_name), "__file__", "")
                     ):
-                        raise InterpreterError(f"Forbidden return value: {module}")
+                        raise InterpreterError(f"Forbidden access to module: {module_name}")
             elif isinstance(result, dict) and result.get("__name__"):
-                for module in DANGEROUS_MODULES:
+                for module_name in DANGEROUS_MODULES:
                     if (
-                        module not in authorized_imports
-                        and result["__name__"] == module
+                        module_name not in authorized_imports
+                        and result["__name__"] == module_name
                         # builtins has no __file__ attribute
-                        and result.get("__file__", "") == getattr(import_module(module), "__file__", "")
+                        and result.get("__file__", "") == getattr(import_module(module_name), "__file__", "")
+                    ):
+                        raise InterpreterError(f"Forbidden access to module: {module_name}")
+            elif isinstance(result, (FunctionType, BuiltinFunctionType)):
+                for qualified_function_name in DANGEROUS_FUNCTIONS:
+                    module_name, function_name = qualified_function_name.rsplit(".", 1)
+                    if (
+                        function_name not in static_tools
+                        and result.__name__ == function_name
+                        and result.__module__ == module_name
                     ):
-                        raise InterpreterError(f"Forbidden return value: {module}")
+                        raise InterpreterError(f"Forbidden access to function: {function_name}")
         return result
 
     return _check_return
@@ -590,14 +589,13 @@ def evaluate_assign(
         target = assign.targets[0]
         set_value(target, result, state, static_tools, custom_tools, authorized_imports)
     else:
-        if len(assign.targets) != len(result):
-            raise InterpreterError(f"Assign failed: expected {len(result)} values but got {len(assign.targets)}.")
         expanded_values = []
         for tgt in assign.targets:
             if isinstance(tgt, ast.Starred):
                 expanded_values.extend(result)
             else:
                 expanded_values.append(result)
+
         for tgt, val in zip(assign.targets, expanded_values):
             set_value(tgt, val, state, static_tools, custom_tools, authorized_imports)
     return result
@@ -641,17 +639,21 @@ def evaluate_call(
     custom_tools: Dict[str, Callable],
     authorized_imports: List[str],
 ) -> Any:
-    if not (
-        isinstance(call.func, ast.Attribute) or isinstance(call.func, ast.Name) or isinstance(call.func, ast.Subscript)
-    ):
+    if not isinstance(call.func, (ast.Call, ast.Lambda, ast.Attribute, ast.Name, ast.Subscript)):
         raise InterpreterError(f"This is not a correct function: {call.func}).")
-    if isinstance(call.func, ast.Attribute):
+
+    func, func_name = None, None
+
+    if isinstance(call.func, ast.Call):
+        func = evaluate_call(call.func, state, static_tools, custom_tools, authorized_imports)
+    elif isinstance(call.func, ast.Lambda):
+        func = evaluate_lambda(call.func, state, static_tools, custom_tools, authorized_imports)
+    elif isinstance(call.func, ast.Attribute):
         obj = evaluate_ast(call.func.value, state, static_tools, custom_tools, authorized_imports)
         func_name = call.func.attr
         if not hasattr(obj, func_name):
             raise InterpreterError(f"Object {obj} has no attribute {func_name}")
         func = getattr(obj, func_name)
-
     elif isinstance(call.func, ast.Name):
         func_name = call.func.id
         if func_name in state:
@@ -666,12 +668,12 @@ def evaluate_call(
             raise InterpreterError(
                 f"It is not permitted to evaluate other functions than the provided tools or functions defined/imported in previous code (tried to execute {call.func.id})."
             )
-
     elif isinstance(call.func, ast.Subscript):
         func = evaluate_subscript(call.func, state, static_tools, custom_tools, authorized_imports)
         if not callable(func):
             raise InterpreterError(f"This is not a correct function: {call.func}).")
         func_name = None
+
     args = []
     for arg in call.args:
         if isinstance(arg, ast.Starred):
@@ -700,20 +702,15 @@ def evaluate_call(
             return super(cls, instance)
         else:
             raise InterpreterError("super() takes at most 2 arguments")
-    else:
-        if func_name == "print":
-            state["_print_outputs"] += " ".join(map(str, args)) + "\n"
-            return None
-        else:  # Assume it's a callable object
-            if (
-                (inspect.getmodule(func) == builtins)
-                and inspect.isbuiltin(func)
-                and (func not in static_tools.values())
-            ):
-                raise InterpreterError(
-                    f"Invoking a builtin function that has not been explicitly added as a tool is not allowed ({func_name})."
-                )
-            return func(*args, **kwargs)
+    elif func_name == "print":
+        state["_print_outputs"] += " ".join(map(str, args)) + "\n"
+        return None
+    else:  # Assume it's a callable object
+        if (inspect.getmodule(func) == builtins) and inspect.isbuiltin(func) and (func not in static_tools.values()):
+            raise InterpreterError(
+                f"Invoking a builtin function that has not been explicitly added as a tool is not allowed ({func_name})."
+            )
+        return func(*args, **kwargs)
 
 
 def evaluate_subscript(
@@ -1083,15 +1080,6 @@ def get_safe_module(raw_module, authorized_imports, visited=None):
 
     # Copy all attributes by reference, recursively checking modules
     for attr_name in dir(raw_module):
-        # Skip dangerous patterns at any level
-        if any(
-            pattern in raw_module.__name__.split(".") + [attr_name]
-            and not check_module_authorized(pattern, authorized_imports)
-            for pattern in DANGEROUS_PATTERNS
-        ):
-            logger.info(f"Skipping dangerous attribute {raw_module.__name__}.{attr_name}")
-            continue
-
         try:
             attr_value = getattr(raw_module, attr_name)
         except (ImportError, AttributeError) as e:
@@ -1114,8 +1102,6 @@ def check_module_authorized(module_name, authorized_imports):
         return True
     else:
         module_path = module_name.split(".")
-        if any([module in DANGEROUS_PATTERNS and module not in authorized_imports for module in module_path]):
-            return False
         # ["A", "B", "C"] -> ["A", "A.B", "A.B.C"]
         module_subpaths = [".".join(module_path[:i]) for i in range(1, len(module_path) + 1)]
         return any(subpath in authorized_imports for subpath in module_subpaths)
diff --git a/src/smolagents/vision_web_browser.py b/src/smolagents/vision_web_browser.py
index 29bcca0..d8216e4 100644
--- a/src/smolagents/vision_web_browser.py
+++ b/src/smolagents/vision_web_browser.py
@@ -187,15 +187,12 @@ When you have modals or cookie banners on screen, you should get rid of them bef
 """
 
 
-def main():
+def main(prompt: str, model_type: str, model_id: str) -> None:
     # Load environment variables
     load_dotenv()
 
-    # Parse command line arguments
-    args = parse_arguments()
-
     # Initialize the model based on the provided arguments
-    model = load_model(args.model_type, args.model_id)
+    model = load_model(model_type, model_id)
 
     global driver
     driver = initialize_driver()
@@ -203,8 +200,11 @@ def main():
 
     # Run the agent with the provided prompt
     agent.python_executor("from helium import *")
-    agent.run(args.prompt + helium_instructions)
+    agent.run(prompt + helium_instructions)
 
 
 if __name__ == "__main__":
-    main()
+    # Parse command line arguments
+    args = parse_arguments()
+
+    main(args.prompt, args.model_type, args.model_id)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 69832f0..eec13ed 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -3,6 +3,7 @@ from unittest.mock import patch
 import pytest
 
 from smolagents.cli import load_model
+from smolagents.local_python_executor import LocalPythonExecutor
 from smolagents.models import HfApiModel, LiteLLMModel, OpenAIServerModel, TransformersModel
 
 
@@ -52,3 +53,56 @@ def test_load_model_hf_api_model(set_env_vars):
 def test_load_model_invalid_model_type():
     with pytest.raises(ValueError, match="Unsupported model type: InvalidModel"):
         load_model("InvalidModel", "test_model_id")
+
+
+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
+
+            main("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")
+    assert mock_load_model.call_args.kwargs == {"api_base": None, "api_key": None}
+    # CodeAgent
+    assert len(mock_code_agent.call_args_list) == 1
+    assert mock_code_agent.call_args.args == ()
+    assert mock_code_agent.call_args.kwargs == {
+        "tools": [],
+        "model": "mock_model",
+        "additional_authorized_imports": None,
+    }
+    # agent.run
+    assert len(mock_code_agent.return_value.run.call_args_list) == 1
+    assert mock_code_agent.return_value.run.call_args.args == ("test_prompt",)
+    # print
+    captured = capsys.readouterr()
+    assert "Running agent with these tools: []" in captured.out
+
+
+def test_vision_web_browser_main():
+    with patch("smolagents.vision_web_browser.helium"):
+        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
+
+                main("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")
+    # CodeAgent
+    assert len(mock_code_agent.call_args_list) == 1
+    assert mock_code_agent.call_args.args == ()
+    assert len(mock_code_agent.call_args.kwargs["tools"]) == 4
+    assert mock_code_agent.call_args.kwargs["model"] == "mock_model"
+    assert mock_code_agent.call_args.kwargs["additional_authorized_imports"] == ["helium"]
+    # agent.python_executor
+    assert len(mock_code_agent.return_value.python_executor.call_args_list) == 1
+    assert mock_code_agent.return_value.python_executor.call_args.args == ("from helium import *",)
+    assert LocalPythonExecutor(["helium"])("from helium import *") == (None, "", False)
+    # agent.run
+    assert len(mock_code_agent.return_value.run.call_args_list) == 1
+    assert mock_code_agent.return_value.run.call_args.args == ("test_prompt" + helium_instructions,)
diff --git a/tests/test_local_python_executor.py b/tests/test_local_python_executor.py
index 06367f0..a34ac29 100644
--- a/tests/test_local_python_executor.py
+++ b/tests/test_local_python_executor.py
@@ -944,22 +944,6 @@ shift_intervals
     Got:      {result}
     """
 
-    def test_dangerous_subpackage_access_blocked(self):
-        # Direct imports with dangerous patterns should fail
-        code = "import random._os"
-        with pytest.raises(InterpreterError):
-            evaluate_python_code(code)
-
-        # Import of whitelisted modules should succeed but dangerous submodules should not exist
-        code = "import random;random._os.system('echo bad command passed')"
-        with pytest.raises(InterpreterError) as e:
-            evaluate_python_code(code)
-        assert "AttributeError: module 'random' has no attribute '_os'" in str(e)
-
-        code = "import doctest;doctest.inspect.os.system('echo bad command passed')"
-        with pytest.raises(InterpreterError):
-            evaluate_python_code(code, authorized_imports=["doctest"])
-
     def test_close_matches_subscript(self):
         code = 'capitals = {"Czech Republic": "Prague", "Monaco": "Monaco", "Bhutan": "Thimphu"};capitals["Butan"]'
         with pytest.raises(Exception) as e:
@@ -1400,7 +1384,7 @@ class TestPrintContainer:
         ("AnyModule", ["*"], True),
         ("os", ["os"], True),
         ("AnyModule", ["AnyModule"], True),
-        ("Module.os", ["Module"], False),
+        ("Module.os", ["Module"], True),
         ("Module.os", ["Module", "os"], True),
         ("os.path", ["os"], True),
         ("os", ["os.path"], False),
@@ -1423,6 +1407,44 @@ class TestLocalPythonExecutor:
         result, _, _ = executor(code)
         assert result == 11
 
+    @pytest.mark.parametrize(
+        "code",
+        [
+            "a = b = 1; a",
+            "a = b = 1; b",
+            "a, b = c, d = 1, 1; a",
+            "a, b = c, d = 1, 1; b",
+            "a, b = c, d = 1, 1; c",
+            "a, b = c, d = {1, 2}; a",
+            "a, b = c, d = {1, 2}; c",
+            "a, b = c, d = {1: 10, 2: 20}; a",
+            "a, b = c, d = {1: 10, 2: 20}; c",
+            "a = b = (lambda: 1)(); b",
+            "a = b = (lambda: 1)(); lambda x: 10; b",
+            "a = b = (lambda x: lambda y: x + y)(0)(1); b",
+            dedent("""
+            def foo():
+                return 1;
+            a = b = foo(); b"""),
+            dedent("""
+            def foo(*args, **kwargs):
+                return sum(args)
+            a = b = foo(1,-1,1); b"""),
+            "a, b = 1, 2; a, b = b, a; b",
+        ],
+    )
+    def test_chained_assignments(self, code):
+        executor = LocalPythonExecutor([])
+        executor.send_tools({})
+        result, _, _ = executor(code)
+        assert result == 1
+
+    def test_evaluate_assign_error(self):
+        code = "a, b = 1, 2, 3; a"
+        executor = LocalPythonExecutor([])
+        with pytest.raises(InterpreterError, match=".*Cannot unpack tuple of wrong size"):
+            executor(code)
+
 
 class TestLocalPythonExecutorSecurity:
     @pytest.mark.parametrize(
@@ -1451,6 +1473,63 @@ class TestLocalPythonExecutorSecurity:
         ):
             executor("import builtins")
 
+    @pytest.mark.parametrize(
+        "additional_authorized_imports, expected_error",
+        [([], InterpreterError("Import of builtins is not allowed")), (["builtins"], None)],
+    )
+    def test_vulnerability_builtins_safe_functions(self, additional_authorized_imports, expected_error):
+        executor = LocalPythonExecutor(additional_authorized_imports)
+        with (
+            pytest.raises(type(expected_error), match=f".*{expected_error}")
+            if isinstance(expected_error, Exception)
+            else does_not_raise()
+        ):
+            executor("import builtins; builtins.print(1)")
+
+    @pytest.mark.parametrize(
+        "additional_authorized_imports, additional_tools, expected_error",
+        [
+            ([], [], InterpreterError("Import of builtins is not allowed")),
+            (["builtins"], [], InterpreterError("Forbidden access to function: exec")),
+            (["builtins"], ["exec"], None),
+        ],
+    )
+    def test_vulnerability_builtins_dangerous_functions(
+        self, additional_authorized_imports, additional_tools, expected_error
+    ):
+        executor = LocalPythonExecutor(additional_authorized_imports)
+        if additional_tools:
+            from builtins import exec
+
+            executor.send_tools({"exec": exec})
+        with (
+            pytest.raises(type(expected_error), match=f".*{expected_error}")
+            if isinstance(expected_error, Exception)
+            else does_not_raise()
+        ):
+            executor("import builtins; builtins.exec")
+
+    @pytest.mark.parametrize(
+        "additional_authorized_imports, additional_tools, expected_error",
+        [
+            ([], [], InterpreterError("Import of os is not allowed")),
+            (["os"], [], InterpreterError("Forbidden access to function: popen")),
+            (["os"], ["popen"], None),
+        ],
+    )
+    def test_vulnerability_dangerous_functions(self, additional_authorized_imports, additional_tools, expected_error):
+        executor = LocalPythonExecutor(additional_authorized_imports)
+        if additional_tools:
+            from os import popen
+
+            executor.send_tools({"popen": popen})
+        with (
+            pytest.raises(type(expected_error), match=f".*{expected_error}")
+            if isinstance(expected_error, Exception)
+            else does_not_raise()
+        ):
+            executor("import os; os.popen")
+
     @pytest.mark.parametrize(
         "additional_authorized_imports, expected_error",
         [([], InterpreterError("Import of sys is not allowed")), (["os", "sys"], None)],
@@ -1473,7 +1552,7 @@ class TestLocalPythonExecutorSecurity:
 
     @pytest.mark.parametrize(
         "additional_authorized_imports, expected_error",
-        [(["importlib"], InterpreterError("Forbidden return value: os")), (["importlib", "os"], None)],
+        [(["importlib"], InterpreterError("Forbidden access to module: os")), (["importlib", "os"], None)],
     )
     def test_vulnerability_via_importlib(self, additional_authorized_imports, expected_error):
         executor = LocalPythonExecutor(additional_authorized_imports)
@@ -1491,19 +1570,58 @@ class TestLocalPythonExecutorSecurity:
                 )
             )
 
+    @pytest.mark.parametrize(
+        "code, additional_authorized_imports, expected_error",
+        [
+            # os submodule
+            ("import queue; queue.threading._os.system(':')", [], InterpreterError("Forbidden access to module: os")),
+            ("import random; random._os.system(':')", [], InterpreterError("Forbidden access to module: os")),
+            (
+                "import random; random.__dict__['_os'].system(':')",
+                [],
+                InterpreterError("Forbidden access to module: os"),
+            ),
+            (
+                "import doctest; doctest.inspect.os.system(':')",
+                ["doctest"],
+                InterpreterError("Forbidden access to module: os"),
+            ),
+            # subprocess submodule
+            (
+                "import asyncio; asyncio.base_events.events.subprocess",
+                ["asyncio"],
+                InterpreterError("Forbidden access to module: subprocess"),
+            ),
+            # sys submodule
+            (
+                "import queue; queue.threading._sys.modules['os'].system(':')",
+                [],
+                InterpreterError("Forbidden access to module: sys"),
+            ),
+            # Allowed
+            ("import pandas; pandas.io", ["pandas"], None),
+        ],
+    )
+    def test_vulnerability_via_submodules(self, code, additional_authorized_imports, expected_error):
+        executor = LocalPythonExecutor(additional_authorized_imports)
+        with (
+            pytest.raises(type(expected_error), match=f".*{expected_error}")
+            if isinstance(expected_error, Exception)
+            else does_not_raise()
+        ):
+            executor(code)
+
     @pytest.mark.parametrize(
         "additional_authorized_imports, additional_tools, expected_error",
         [
             ([], [], InterpreterError("Import of sys is not allowed")),
-            (["sys"], [], InterpreterError("Forbidden return value: builtins")),
+            (["sys"], [], InterpreterError("Forbidden access to module: builtins")),
             (
                 ["sys", "builtins"],
                 [],
-                InterpreterError(
-                    "Invoking a builtin function that has not been explicitly added as a tool is not allowed"
-                ),
+                InterpreterError("Forbidden access to function: __import__"),
             ),
-            (["sys", "builtins"], ["__import__"], InterpreterError("Forbidden return value: os")),
+            (["sys", "builtins"], ["__import__"], InterpreterError("Forbidden access to module: os")),
             (["sys", "builtins", "os"], ["__import__"], None),
         ],
     )
@@ -1533,7 +1651,10 @@ class TestLocalPythonExecutorSecurity:
     @pytest.mark.parametrize("patch_builtin_import_module", [False, True])  # builtins_import.__module__ = None
     @pytest.mark.parametrize(
         "additional_authorized_imports, additional_tools, expected_error",
-        [([], [], InterpreterError("Forbidden return value: builtins")), (["builtins", "os"], ["__import__"], None)],
+        [
+            ([], [], InterpreterError("Forbidden access to module: builtins")),
+            (["builtins", "os"], ["__import__"], None),
+        ],
     )
     def test_vulnerability_builtins_via_traceback(
         self, patch_builtin_import_module, additional_authorized_imports, additional_tools, expected_error, monkeypatch
@@ -1567,7 +1688,10 @@ class TestLocalPythonExecutorSecurity:
     @pytest.mark.parametrize("patch_builtin_import_module", [False, True])  # builtins_import.__module__ = None
     @pytest.mark.parametrize(
         "additional_authorized_imports, additional_tools, expected_error",
-        [([], [], InterpreterError("Forbidden return value: builtins")), (["builtins", "os"], ["__import__"], None)],
+        [
+            ([], [], InterpreterError("Forbidden access to module: builtins")),
+            (["builtins", "os"], ["__import__"], None),
+        ],
     )
     def test_vulnerability_builtins_via_class_catch_warnings(
         self, patch_builtin_import_module, additional_authorized_imports, additional_tools, expected_error, monkeypatch
@@ -1602,7 +1726,7 @@ class TestLocalPythonExecutorSecurity:
     @pytest.mark.filterwarnings("ignore::DeprecationWarning")
     @pytest.mark.parametrize(
         "additional_authorized_imports, expected_error",
-        [([], InterpreterError("Forbidden return value: os")), (["os"], None)],
+        [([], InterpreterError("Forbidden access to module: os")), (["os"], None)],
     )
     def test_vulnerability_load_module_via_builtin_importer(self, additional_authorized_imports, expected_error):
         executor = LocalPythonExecutor(additional_authorized_imports)