Files
2026-07-13 13:06:23 +08:00

346 lines
14 KiB
Python

from autoagent.environment.markdown_browser import RequestsMarkdownBrowser
from autoagent.environment import LocalEnv
from functools import partial, update_wrapper
from inspect import signature
from typing import Tuple
import time
from autoagent.registry import register_tool, register_plugin_tool
from typing import Union, Optional
from typing import Optional
from autoagent.types import Result
import requests
import mimetypes
import base64
import uuid
import os
from litellm import completion
import cv2
import tempfile
from typing import List
from moviepy import *
import time
import base64
from faster_whisper import WhisperModel
from constant import COMPLETION_MODEL, API_BASE_URL
def with_env(env: RequestsMarkdownBrowser):
"""将env注入到工具函数中的装饰器"""
def decorator(func):
def wrapped(*args, **kwargs):
return func(env=env, *args, **kwargs)
# 保留原始函数的所有属性
update_wrapper(wrapped, func)
# 修改signature,移除env参数
wrapped.__signature__ = signature(func).replace(
parameters=[p for p in signature(func).parameters.values() if p.name != 'env']
)
return wrapped
return decorator
def _get_browser_state(env: RequestsMarkdownBrowser) -> Tuple[str, str]:
"""
Get the current state of the browser, including the header and content.
"""
# print(env.address)
header = f"Address: {env._convert_local_to_docker(env.address)}\n"
# header = f"Address: {env.address}\n"
if env.page_title is not None:
header += f"Title: {env.page_title}\n"
current_page = env.viewport_current_page
total_pages = len(env.viewport_pages)
address = env.address
for i in range(len(env.history) - 2, -1, -1): # Start from the second last
if env.history[i][0] == address:
header += f"You previously visited this page {round(time.time() - env.history[i][1])} seconds ago.\n"
break
prefix = f"Your browser is currently open to the page '{env.page_title}'\n" if env.page_title is not None else ""
header = prefix + header
header += f"Viewport position: Showing page {current_page+1} of {total_pages}.\n"
return (header, env.viewport)
@register_tool("open_local_file")
def open_local_file(context_variables, path: str):
"""
Open a local file at a path in the text-based browser and return current viewport content.
Args:
path: The absolute path of a local file to visit.
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
try:
# assert DOCKER_WORKPLACE_NAME in path, f"The path must be a absolute path from `/{DOCKER_WORKPLACE_NAME}/` directory"
# local_path = path.replace('/' + DOCKER_WORKPLACE_NAME, LOCAL_ROOT + f'/{DOCKER_WORKPLACE_NAME}')
# print(local_path)
path = env._convert_docker_to_local(path)
env.open_local_file(path)
header, content = _get_browser_state(env)
final_response = header.strip() + "\n=======================\n" + content
return final_response
except Exception as e:
return f"Error in `open_local_file`: {e}"
@register_tool("page_up_markdown")
def page_up_markdown(context_variables):
"""
Scroll the viewport UP one page-length in the current file and return the new viewport content.
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
try:
env.page_up()
header, content = _get_browser_state(env)
final_response = header.strip() + "\n=======================\n" + content
return final_response
except Exception as e:
return f"Error in `page_up`: {e}"
@register_tool("page_down_markdown")
def page_down_markdown(context_variables):
"""
Scroll the viewport DOWN one page-length in the current file and return the new viewport content.
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
try:
env.page_down()
header, content = _get_browser_state(env)
final_response = header.strip() + "\n=======================\n" + content
return final_response
except Exception as e:
return f"Error in `page_down`: {e}"
@register_tool("find_on_page_ctrl_f")
def find_on_page_ctrl_f(context_variables, search_string: str):
"""
Scroll the viewport to the first occurrence of the search string. This is equivalent to Ctrl+F.
Args:
search_string: The string to search for on the page. This search string supports wildcards like '*'
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
try:
env.find_on_page(search_string)
header, content = _get_browser_state(env)
final_response = header.strip() + "\n=======================\n" + content
return final_response
except Exception as e:
return f"Error in `find_on_page_ctrl_f`: {e}"
@register_tool("find_next")
def find_next(context_variables):
"""
Scroll the viewport to next occurrence of the search string.
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
try:
env.find_next()
header, content = _get_browser_state(env)
final_response = header.strip() + "\n=======================\n" + content
return final_response
except Exception as e:
return f"Error in `find_next`: {e}"
def _encode_image(context_variables, image_path: str):
"""
Encode an image to base64.
"""
env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
assert env is not None, "file_env is not set"
if image_path.startswith("http"):
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"
request_kwargs = {
"headers": {"User-Agent": user_agent},
"stream": True,
}
# Send a HTTP request to the URL
response = requests.get(image_path, **request_kwargs)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
extension = mimetypes.guess_extension(content_type)
if extension is None:
extension = ".download"
fname = str(uuid.uuid4()) + extension
download_path = os.path.abspath(os.path.join(env.local_workplace, "downloads", fname))
with open(download_path, "wb") as fh:
for chunk in response.iter_content(chunk_size=512):
fh.write(chunk)
image_path = download_path
elif env.local_workplace in image_path:
image_path = image_path
else:
image_path = env._convert_docker_to_local(image_path)
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
# @register_tool("visual_question_answering")
# def visual_question_answering(context_variables, image_path: str, question: Optional[str] = None) -> Result:
# """
# A tool that can answer questions about attached images.
# Args:
# image_path: The path to the image on which to answer the question. This should be a local path to downloaded image.
# question: the question to answer (default: "Please write a detailed caption for this image.")
# """
# env: RequestsMarkdownBrowser = context_variables.get("file_env", None)
# assert env is not None, "file_env is not set"
# try:
# if not question:
# question = "Please write a detailed caption for this image."
# if not isinstance(image_path, str):
# raise Exception("You should provide only one string as argument to this tool!")
# base64_image = _encode_image(context_variables, image_path)
# ret_str = question
# msg = [{"role": "user", "content": [
# {"type": "text", "text": question},
# {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}}
# ]}]
# res = completion(model="gpt-4o-2024-08-06", messages=msg)
# ret_str = res.choices[0].message.content
# return Result(
# value=ret_str,
# # image=base64_image
# )
# except Exception as e:
# return Result(
# value=f"Error in `visual_question_answering`: {e}",
# )
@register_tool("visual_question_answering")
@register_plugin_tool("visual_question_answering")
def visual_question_answering(context_variables, file_path: str, question: Optional[str] = None) -> Result:
"""
This tool is used to answer questions about attached images or videos.
Args:
file_path: File path of the image or video.
question: The question to answer (default: "Please describe the image or video content.")
"""
env: Union[RequestsMarkdownBrowser, LocalEnv] = context_variables.get("file_env", LocalEnv())
if env is None:
env = LocalEnv()
try:
if not isinstance(file_path, str):
raise Exception("File path must be a string!")
# 判断文件类型
file_extension = os.path.splitext(file_path)[1].lower()
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm'}
if file_extension in video_extensions:
# 视频处理逻辑
if not question:
question = "Please describe the video content."
# 1. 提取关键帧
local_file_path = env._convert_docker_to_local(file_path)
base64Frames, audio_path = process_video(local_file_path)
if audio_path is not None:
audio_text = process_audio(audio_path)
else:
audio_text = "No audio found in the video."
messages=[
{"role": "system", "content":"""Use the video and transcription to answer the provided question."""},
{"role": "user", "content": [
{"type": "text", "text": "These are the frames from the video."},
*map(lambda x: {"type": "image_url",
"image_url": {"url": f'data:image/png;base64,{x}', "detail": "low"}}, base64Frames),
{"type": "text", "text": f"The audio transcription is: {audio_text}\nQuestion: {question}"},
],
}
]
final_res = completion(model=COMPLETION_MODEL, messages=messages, base_url=API_BASE_URL)
return Result(value=final_res.choices[0].message.content)
else:
# 原有的图片处理逻辑
if not question:
question = "Please describe the image content."
base64_image = _encode_image(context_variables, file_path)
msg = [{"role": "user", "content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}}
]}]
res = completion(model=COMPLETION_MODEL, messages=msg, base_url=API_BASE_URL)
return Result(value=res.choices[0].message.content)
except Exception as e:
return Result(value=f"Error in `visual_question_answering`: {e}")
def process_audio(audio_path):
model = WhisperModel("large-v3-turbo")
print(f"Processing batch: {audio_path}")
segments, info = model.transcribe(audio_path)
transcribed_text = ""
for segment in segments:
transcribed_text += "[%.2fs -> %.2fs] %s\n" % (segment.start, segment.end, segment.text)
return transcribed_text
def process_video(video_path, seconds_per_frame=2):
base64Frames = []
base_video_path, _ = os.path.splitext(video_path)
video = cv2.VideoCapture(video_path)
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
fps = video.get(cv2.CAP_PROP_FPS)
frames_to_skip = int(fps * seconds_per_frame)
curr_frame=0
# Loop through the video and extract frames at specified sampling rate
while curr_frame < total_frames - 1:
video.set(cv2.CAP_PROP_POS_FRAMES, curr_frame)
success, frame = video.read()
if not success:
break
_, buffer = cv2.imencode(".png", frame)
base64Frames.append(base64.b64encode(buffer).decode("utf-8"))
curr_frame += frames_to_skip
video.release()
# Extract audio from video
clip = VideoFileClip(video_path)
if clip.audio is not None:
audio_path = f"{base_video_path}.mp3"
clip.audio.write_audiofile(audio_path, bitrate="32k")
clip.audio.close()
clip.close()
else:
audio_path = None
clip.close()
print(f"Extracted {len(base64Frames)} frames")
print(f"Extracted audio to {audio_path}")
return base64Frames, audio_path
if __name__ == "__main__":
local_root = os.getcwd()
workplace_name = 'workplace_gaia_meta'
env = RequestsMarkdownBrowser(viewport_size=1024 * 5, local_root=local_root, workplace_name=workplace_name, downloads_folder=os.path.join(local_root, workplace_name, "downloads"))
# print("Open file", "~"*100)
# print(open_local_file(env, f"/{workplace_name}/downloads/2207.01510v1.pdf"))
# print("Page down", "~"*100)
# print(page_down_markdown(env))
# print("Find on page", "~"*100)
# print(find_on_page_ctrl_f(env, "Chain-of-Thought"))
# print("Find next", "~"*100)
# print(find_next(env))
print(visual_question_answering(context_variables = {"file_env": env}, file_path = "//workplace_gaia_meta/autoagent/downloaded_video.mp4", question="What is the highest number of bird species to be on camera simultaneously?").value)
# print(visual_question_answering("/workplace_meta/downloads/workflow.png", "What is the main idea of this paper?").image)