我们刚刚赋予了smolagents视觉能力

发布于 2025年1月24日
在 GitHub 上更新

你这假冒为善的人,先把你眼中的梁木取出来,然后你才能看得清楚,才能取出你弟兄眼中的刺。马太福音 7, 3-5

TL;DR

我们已经为smolagents添加了视觉支持,这使得在智能体管道中原生使用视觉语言模型成为可能。

目录

概述

在智能体世界中,许多能力都被视觉之墙所阻碍。一个常见的例子是网页浏览:网页具有丰富的视觉内容,仅仅提取文本永远无法完全恢复,无论是对象的相对位置、通过颜色传递的信息还是特定的图标……在这种情况下,视觉是智能体真正的超能力。所以我们刚刚将这项能力添加到了我们的smolagents中!

这带来了什么?一个能完全自主浏览网页的智能体浏览器!

以下是它看起来的样子

我们如何赋予smolagents视觉能力

🤔 我们如何将图像传递给智能体?传递图像有两种方式

  1. 你可以在智能体启动时直接获取图像。文档AI通常就是这种情况。
  2. 有时,图像需要动态添加。一个很好的例子是当网页浏览器刚刚执行了一个动作,需要查看其视口上的影响时。

1. 在智能体启动时一次性传递图像

对于需要一次性传递图像的情况,我们添加了在 `run` 方法中向智能体传递图像列表的功能:`agent.run("描述这些图像:", images=[image_1, image_2])`。

这些图像输入随后与您希望完成的任务提示一起存储在 `TaskStep` 的 `task_images` 属性中。

当运行智能体时,它们将被传递给模型。这在根据包含视觉元素的冗长 PDF 执行操作等情况下非常方便。

2. 在每个步骤中传递图像 ⇒ 使用回调

如何将图像动态添加到智能体的内存中?

为了弄清楚这一点,我们首先需要了解我们的智能体是如何工作的。

`smolagents` 中的所有智能体都基于单一的 `MultiStepAgent` 类,它是 ReAct 框架的抽象。在基本层面上,该类按照以下步骤循环执行操作,其中现有变量和知识被整合到智能体日志中:

  • 初始化:系统提示存储在 `SystemPromptStep` 中,用户查询记录在 `TaskStep` 中。
  • ReAct 循环(循环)
    1. 使用 `agent.write_inner_memory_from_logs()` 将代理日志写入一个可供 LLM 阅读的聊天消息列表。
    2. 将这些消息发送到 `Model` 对象以获取其完成。解析完成以获取操作(`ToolCallingAgent` 的 JSON blob,`CodeAgent` 的代码片段)。
    3. 执行动作并将结果记录到内存中(一个 `ActionStep`)。
    4. 在每个步骤结束时,运行 `agent.step_callbacks` 中定义的所有回调函数。⇒ 这就是我们添加图像支持的地方:创建一个将图像记录到内存中的回调!

下图详细说明了这一过程

如您所见,对于动态检索图像的用例(例如,网络浏览器代理),我们支持将图像添加到模型的 `ActionStep` 中,位于 `step_log.observation_images` 属性中。

这可以通过回调函数实现,该函数将在每个步骤结束时运行。

让我们演示如何制作这样一个回调,并用它来构建一个网页浏览器智能体。👇👇

如何创建具有视觉功能的网页浏览智能体

我们将使用 helium。它提供了基于 `selenium` 的浏览器自动化功能:这将使我们的智能体更容易操作网页。

pip install "smolagents[all]" helium selenium python-dotenv

智能体本身可以直接使用helium,因此不需要特定的工具:它可以直接使用helium执行操作,例如点击页面上可见的名为“top 10”的按钮。我们仍然需要制作一些工具来帮助智能体浏览网页:一个返回上一页的工具,另一个关闭弹出窗口的工具,因为这些弹出窗口由于其关闭按钮上没有文本,所以对于`helium`来说很难抓取。

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.common.exceptions import ElementNotInteractableException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

from smolagents import CodeAgent, LiteLLMModel, OpenAIServerModel, TransformersModel, tool
from smolagents.agents import ActionStep


load_dotenv()
import os

@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.
    """
    # Common selectors for modal close buttons and overlay elements
    modal_selectors = [
        "button[class*='close']",
        "[class*='modal']",
        "[class*='modal'] button",
        "[class*='CloseButton']",
        "[aria-label*='close']",
        ".modal-close",
        ".close-modal",
        ".modal .close",
        ".modal-backdrop",
        ".modal-overlay",
        "[class*='overlay']"
    ]

    wait = WebDriverWait(driver, timeout=0.5)

    for selector in modal_selectors:
        try:
            elements = wait.until(
                EC.presence_of_all_elements_located((By.CSS_SELECTOR, selector))
            )

            for element in elements:
                if element.is_displayed():
                    try:
                        # Try clicking with JavaScript as it's more reliable
                        driver.execute_script("arguments[0].click();", element)
                    except ElementNotInteractableException:
                        # If JavaScript click fails, try regular click
                        element.click()

        except TimeoutException:
            continue
        except Exception as e:
            print(f"Error handling selector {selector}: {str(e)}")
            continue
    return "Modals closed"

目前,智能体没有视觉输入。因此,让我们演示如何通过使用回调函数,将其图像动态地馈送到其步骤日志中。我们创建一个回调函数 `save_screenshot`,它将在每个步骤结束时运行。

def save_screenshot(step_log: ActionStep, agent: CodeAgent) -> None:
    sleep(1.0)  # Let JavaScript animations happen before taking the screenshot
    driver = helium.get_driver()
    current_step = step_log.step_number
    if driver is not None:
        for step_logs in agent.logs:  # Remove previous screenshots from logs for lean processing
            if isinstance(step_log, ActionStep) and step_log.step_number <= current_step - 2:
                step_logs.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")
        step_log.observations_images = [image.copy()]  # Create a copy to ensure it persists, important!

    # Update observations with current URL
    url_info = f"Current url: {driver.current_url}"
    step_log.observations = url_info if step_logs.observations is None else step_log.observations + "\n" + url_info
    return

这里最重要的一行是我们添加图像到观测图像中:`step_log.observations_images = [image.copy()]`。

此回调接受 `step_log` 和 `agent` 本身作为参数。将 `agent` 作为输入允许执行比仅仅修改最后日志更深层的操作。

我们来创建一个模型。我们已经在所有模型中添加了对图像的支持。需要精确的一点是:在使用带有 VLM 的 TransformersModel 时,为了使其正常工作,您需要在初始化时将 `flatten_messages_as_text` 设置为 `False`,例如

model = TransformersModel(model_id="HuggingFaceTB/SmolVLM-Instruct", device_map="auto", flatten_messages_as_text=False)

对于这个演示,我们使用Fireworks API中更大的Qwen2VL。

model = OpenAIServerModel(
    api_key=os.getenv("FIREWORKS_API_KEY"),
    api_base="https://api.fireworks.ai/inference/v1",
    model_id="accounts/fireworks/models/qwen2-vl-72b-instruct",
)

现在,让我们继续定义我们的代理。我们将 `verbosity_level` 设置为最高,以显示 LLM 的完整输出消息,从而查看其思考过程;并将 `max_steps` 增加到 20,以给代理更多步骤来探索网络。我们还为它提供了上面定义的 `save_screenshot` 回调。

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
)

最后,我们向代理提供了一些关于使用 helium 的指导。

helium_instructions = """
You can use helium to access websites. Don't bother about the helium driver, it's already managed.
First you need to import everything from helium, then you can do other actions!
Code:
```py
from helium import *
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>

Proceed in several steps rather than trying to solve the task in one shot.
And at the end, only when you have your answer, return your final answer.
Code:
```py
final_answer("YOUR_ANSWER_HERE")
```<end_code>

If pages seem stuck on loading, you might have to wait, for instance `import time` and run `time.sleep(5.0)`. But don't overuse this!
To list elements on page, DO NOT try code-based element searches like 'contributors = find_all(S("ol > li"))': just look at the latest screenshot you have and read it visually, or use your tool search_item_ctrl_f.
Of course, you can act on buttons like a user would do when navigating.
After each code blob you write, you will be automatically provided with an updated screenshot of the browser and the current browser url.
But beware that the screenshot will only be taken at the end of the whole action, it won't see intermediate states.
Don't kill the browser.
"""

运行智能体

现在一切就绪:让我们运行我们的智能体!

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.run(github_request + helium_instructions)

然而,请注意,这项任务非常困难:根据您使用的 VLM,这可能不总是奏效。像 Qwen2VL-72B 或 GPT-4o 这样强大的 VLM 通常更容易成功。

后续步骤

这将让您瞥见支持视觉功能的 `CodeAgent` 的强大能力,但还有更多工作要做!

我们期待看到您使用视觉语言模型和 smolagents 构建出什么样的作品!

社区

注意:关于模态关闭选择器,请注意模态框也称为对话框元素,如果构建得健壮,它们应该具有 role="dialog" 属性,这可以在识别这些弹出窗口时进行搜索。

此外,任何对话框/模态窗口都应该可以通过 Esc 键关闭!

希望这有助于更广泛地识别模态框/对话框和/或帮助更轻松地关闭它们!

上面相关代码块以供参考 :)



@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.
    """
    # Common selectors for modal close buttons and overlay elements
    modal_selectors = [
        "button[class*='close']",
        "[class*='modal']",
        "[class*='modal'] button",
        "[class*='CloseButton']",
        "[aria-label*='close']",
        ".modal-close",
        ".close-modal",
        ".modal .close",
        ".modal-backdrop",
        ".modal-overlay",
        "[class*='overlay']"
    ]
·
文章作者

哦,这是一个很好的观点,非常感谢!这很有用!

如果你有能力做到,那么
救命!救命!救命! https://arxiv.org/auth/endorse?x=VMBF4S

接受 cookie 弹窗。

@工具
def accept_cookie_popup() -> str
"""
接受任何可见的 cookie 同意横幅。
"""
wait = WebDriverWait(driver, timeout=0.5)
elements = wait.until(EC.presence_of_all_elements_located((By.ID, "onetrust-accept-btn-handler")))
elements[0].click()

代理 = CodeAgent(
tools=[go_back, close_popups, search_item_ctrl_f, close_cookie_popup],
model=model,
additional_authorized_imports=["helium"],
step_callbacks=[save_screenshot],
max_steps=20,
verbosity_level=2,
)

·
文章作者

我们已将其整合到主仓库中:https://github.com/huggingface/smolagents/blob/main/src/smolagents/vision_web_browser.py
因此它已从 examples/ 中删除

我收到此弃用警告:“'logs' 属性已弃用并将很快被移除。请改用 'self.memory.steps'。” 应该采取什么措施?

注册登录 以评论