AppAgent 开源项目解读

开源地址:github.com/mnotgod96/A...

项目结构

核心模块script,我们通过script完成主要操作

项目配置

当前项目采用GPT4模型作为LLM引擎。

python 复制代码
OPENAI_API_BASE: "https://api.openai.com/v1/chat/completions"
OPENAI_API_KEY: "sk-"  # Set the value to sk-xxx if you host the openai interface for open llm model
OPENAI_API_MODEL: "gpt-4-vision-preview"  # The only OpenAI model by now that accepts visual input
MAX_TOKENS: 300  # The max token limit for the response completion
TEMPERATURE: 0.0  # The temperature of the model: the lower the value, the more consistent the output of the model
REQUEST_INTERVAL: 10  # Time in seconds between consecutive GPT-4V requests

ANDROID_SCREENSHOT_DIR: "/sdcard/Pictures/Screenshots"  # Set the directory on your Android device to store the intermediate screenshots. Make sure the directory EXISTS on your phone!
ANDROID_XML_DIR: "/sdcard"  # Set the directory on your Android device to store the intermediate XML files used for determining locations of UI elements on your screen. Make sure the directory EXISTS on your phone!

DOC_REFINE: false  # Set this to true will make the agent refine existing documentation based on the latest demonstration; otherwise, the agent will not regenerate a new documentation for elements with the same resource ID.
MAX_ROUNDS: 20  # Set the round limit for the agent to complete the task
DARK_MODE: false  # Set this to true if your app is in dark mode to enhance the element labeling
MIN_DIST: 30  # The minimum distance between elements to prevent overlapping during the labeling process

这里面包含了gpt4模型配置以及安卓控制器的配置

提示词设计

由于gpt在英文的表现更好,并且英文对token消耗更少,所以原项目采用的是英文的提示词。这里我们将其翻译为中文:

python 复制代码
tap_doc_template = """我将给你展示一个移动应用在点击屏幕上方标记为<ui_element>的UI元素前后的截图。每个元素的数字标签位于元素的中心。点击这个UI元素是进行更大任务的一部分,即<task_desc>。你的任务是用一两句话简洁地描述UI元素的功能。注意,你对UI元素的描述应该专注于一般功能。例如,如果UI元素用于导航到与约翰的聊天窗口,你的描述不应该包括特定人的名字。只需说:"点击这个区域将引导用户到聊天窗口"。永远不要在你的描述中包含UI元素的数字标签。你可以使用代词如"UI元素"来指代该元素。"""

text_doc_template = """我将给你展示一个移动应用在输入框中输入文本前后的截图,该输入框标记为屏幕上的数字<ui_element>。每个元素的数字标签位于元素的中心。在这个UI元素中输入文本是进行更大任务的一部分,即<task_desc>。你的任务是用一两句话简洁地描述UI元素的功能。注意,你对UI元素的描述应该专注于一般功能。例如,如果截图的变化显示用户在聊天框中输入了"你好吗?",你不需要提及实际的文本。只需说:"这个输入区域用于用户输入消息发送到聊天窗口"。永远不要在你的描述中包含UI元素的数字标签。你可以使用代词如"UI元素"来指代该元素。"""

long_press_doc_template = """我将给你展示一个移动应用在长按屏幕上方标记为<ui_element>的UI元素前后的截图。每个元素的数字标签位于元素的中心。长按这个UI元素是进行更大任务的一部分,即<task_desc>。你的任务是用一两句话简洁地描述UI元素的功能。注意,你对UI元素的描述应该专注于一般功能。例如,如果长按UI元素将用户重定向到与约翰的聊天窗口,你的描述不应该包括特定人的名字。只需说:"长按这个区域将重定向用户到聊天窗口"。永远不要在你的描述中包含UI元素的数字标签。你可以使用代词如"UI元素"来指代该元素。"""

swipe_doc_template = """我将给你展示一个移动应用在滑动屏幕上方标记为<ui_element>的UI元素前后的截图,滑动方向为<swipe_dir>。每个元素的数字标签位于元素的中心。滑动这个UI元素是进行更大任务的一部分,即<task_desc>。你的任务是用一两句话简洁地描述UI元素的功能。注意,你对UI元素的描述应该尽可能一般化。例如,如果滑动UI元素增加了建筑物图片的对比度,你的描述应该是这样的:"滑动这个区域允许用户调整图片的特定参数"。永远不要在你的描述中包含UI元素的数字标签。你可以使用代词如"UI元素"来指代该元素。"""

refine_doc_suffix = """\n下面展示了之前演示中生成的这个UI元素的文档。你生成的描述应该基于这个之前的文档并进行优化。注意,你从给定的截图中得出的UI元素功能的理解可能与之前的文档相冲突,因为UI元素的功能可能是灵活的。在这种情况下,你的生成描述应该结合两者。旧的UI元素文档:<old_doc>"""

task_template = """你是一个被训练来在智能手机上执行一些基本任务的代理。你将获得一个智能手机的截图。截图上的交互式UI元素从1开始标记有数字标签。每个交互式元素的数字标签位于元素的中心。

你可以调用以下函数来控制智能手机:

1. tap(element: int)
这个函数用于点击智能手机屏幕上显示的UI元素。
"element"是分配给智能手机屏幕上显示的UI元素的数字标签。
一个简单的用例可以是tap(5),这将点击标记为数字5的UI元素。

2. text(text_input: str)
这个函数用于在输入字段/框中插入文本输入。text_input是你想要插入的字符串,并且必须用双引号括起来。一个简单的用例可以是text("Hello, world!"),这将在智能手机屏幕上的输入区域插入字符串"Hello, world!"。这个函数通常在你看到屏幕下半部分显示键盘时调用。

3. long_press(element: int)
这个函数用于长按智能手机屏幕上显示的UI元素。
"element"是分配给智能手机屏幕上显示的UI元素的数字标签。
一个简单的用例可以是long_press(5),这将长按标记为数字5的UI元素。

4. swipe(element: int, direction: str, dist: str)
这个函数用于在智能手机屏幕上滑动UI元素,通常是滚动视图或滑块。
"element"是分配给智能手机屏幕上显示的UI元素的数字标签。"direction"是一个字符串,代表四个方向之一:上、下、左、右。"direction"必须用双引号括起来。"dist"决定了滑动的距离,可以是三种选项之一:短、中、长。你应该根据需要选择合适的距离选项。
一个简单的用例可以是swipe(21, "up", "medium"),这将向上滑动标记为数字21的UI元素一段中等距离。

5. grid()
当你发现你想交互的元素没有数字标签,并且其他有数字标签的元素不能帮助你完成任务时,你应该调用这个函数。该函数将显示一个网格覆盖层,将智能手机屏幕分成小区域,这将给你更多的自由选择屏幕的任何部分进行点击、长按或滑动。
<ui_document>
你需要完成的任务是<task_description>。你为了完成这个任务而采取的过去行动总结如下:<last_act>
现在,根据以下文档和标记的截图,你需要思考并调用所需的函数来继续任务。你的输出应该包括三个部分,格式如下:
观察:<描述你在图像中观察到的内容>
思考:<为了完成给定的任务,我下一步应该做什么>
行动:<带有正确参数的函数调用以继续任务。如果你认为任务已经完成或者没有什么要做的,你应该输出FINISH。在这个字段中,你不能输出除了函数调用或FINISH之外的任何内容。>
总结:<用一两句话总结你的过去行动以及你最新的行动。不要在你的总结中包含数字标签>
你一次只能采取一个行动,所以请直接调用函数。"""

task_template_grid = """你是一个被训练来在智能手机上执行一些基本任务的代理。你将获得一个被网格覆盖的智能手机截图。网格将截图分成小的正方形区域。每个区域在左上角标记有一个整数。

你可以调用以下函数来控制智能手机:

1. tap(area: int, subarea: str)
这个函数用于点击智能手机屏幕上显示的网格区域。"area"是分配给智能手机屏幕上显示的网格区域的整数标签。"subarea"是一个字符串,代表在网格区域内点击的确切位置。它可以取九个值之一:中心、左上角、顶部、右上角、左侧、右侧、左下角、底部和右下角。
一个简单的用例可以是tap(5, "center"),这将点击标记为数字5的网格区域的确切中心。

2. long_press(area: int, subarea: str)
这个函数用于长按智能手机屏幕上显示的网格区域。"area"是分配给智能手机屏幕上显示的网格区域的整数标签。"subarea"是一个字符串,代表在网格区域内长按的确切位置。它可以取九个值之一:中心、左上角、顶部、右上角、左侧、右侧、左下角、底部和右下角。
一个简单的用例可以是long_press(7, "top-left"),这将在标记为数字7的网格区域的左上部分长按。

3. swipe(start_area: int, start_subarea: str, end_area: int, end_subarea: str)
这个函数用于在智能手机屏幕上执行滑动操作,特别是当你想要与滚动视图或滑块交互时。"start_area"是分配给滑动起始位置的网格区域的整数标签。"start_subarea"是一个字符串,代表在网格区域内开始滑动的确切位置。"end_area"是分配给滑动结束位置的网格区域的整数标签。"end_subarea"是一个字符串,代表在网格区域内结束滑动的确切位置。
两个子区域参数可以取九个值之一:中心、左上角、顶部、右上角、左侧、右侧、左下角、底部和右下角。
一个简单的用例可以是swipe(21, "center", 25, "right"),这将从标记为数字21的网格区域中心开始滑动到标记为数字25的网格区域的右侧。

你需要完成的任务是<task_description>。你为了完成这个任务而采取的过去行动总结如下:<last_act>
现在,根据以下标记的截图,你需要思考并调用所需的函数来继续任务。
你的输出应该包括三个部分,格式如下:
观察:<描述你在图像中观察到的内容>
思考:<为了完成给定的任务,我下一步应该做什么>
行动:<带有正确参数的函数调用以继续任务。如果你认为任务已经完成或者没有什么要做的,你应该输出FINISH。在这个字段中,你不能输出除了函数调用或FINISH之外的任何内容。>
总结:<用一两句话总结你的过去行动以及你最新的行动。不要在你的总结中包含网格区域编号>
你一次只能采取一个行动,所以请直接调用函数。"""

self_explore_task_template = """你是一个被训练来在智能手机上完成特定任务的代理。你将获得一个智能手机应用的截图

这里需要特别注意,GPT4V是支持图像的。它是一个涵盖视觉的多模态模型

文档生成

提示词部分,分为两个部分,一个是用于生成操作文档的,一个就是我们的函数描述,方便LLM作为一个agent干活。

生产文档的代码流程大致如下:

  1. 截图处理:首先,代码会读取智能手机操作的截图。这些截图通常包含了用户界面(UI)元素,如按钮、输入框等。
  2. 图像编码:然后,这些截图会被编码为Base64字符串,这是一种可以将图像数据编码为文本格式的方法,便于在网络传输或作为数据存储。
  3. 生成提示:接着,代码会根据截图和用户的操作(如点击、输入文本等)生成一个描述性的提示(prompt),这个提示会详细说明用户在截图中执行的操作以及预期的结果。
  4. GPT模型分析:生成的提示随后被发送给GPT-4模型。GPT-4模型是一个多模态模型,它能够理解文本和图像信息。在这种情况下,模型会分析文本提示,并可能结合图像内容(如果模型支持图像输入)来理解用户的操作意图。
  5. 生成文档:GPT-4模型根据分析的结果生成文档内容,描述UI元素的功能和用户操作的目的。这个文档内容会被保存下来,用于后续的参考或自动化任务。
  6. 文档优化:如果文档已经存在,代码会检查是否需要根据最新的操作记录来优化或更新文档。这可能涉及到使用旧文档内容作为上下文,让GPT-4模型生成更准确的描述。

步骤记录

在生成文档的过程当中,最主要的就是如何得到用户的操作,这里当前这个项目是直接通过终端,先进行终端交互操作,然后记录下来的图片等等之类的信息,将会被放到./目录下面,见到文档生成部分设置的默认路径。

python 复制代码
import argparse
import datetime

import cv2
import os
import shutil
import sys
import time

from and_controller import list_all_devices, AndroidController, traverse_tree
from config import load_config
from utils import print_with_color, draw_bbox_multi

# 设置命令行参数描述
arg_desc = "AppAgent - Human Demonstration"
parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter, description=arg_desc)
# 添加命令行参数
parser.add_argument("--app")
parser.add_argument("--demo")
parser.add_argument("--root_dir", default="./")
# 解析命令行参数
args = vars(parser.parse_args())

# 获取应用名称和演示名称
app = args["app"]
demo_name = args["demo"]
root_dir = args["root_dir"]

# 加载配置信息
configs = load_config()

# 如果应用名称未指定,提示用户输入
if not app:
    print_with_color("What is the name of the app you are going to demo?", "blue")
    app = input()
    app = app.replace(" ", "")
# 如果演示名称未指定,生成一个基于当前时间戳的名称
if not demo_name:
    demo_timestamp = int(time.time())
    demo_name = datetime.datetime.fromtimestamp(demo_timestamp).strftime(
        f"demo_{app}_%Y-%m-%d_%H-%M-%S")

# 创建工作目录结构
work_dir = os.path.join(root_dir, "apps")
if not os.path.exists(work_dir):
    os.mkdir(work_dir)
work_dir = os.path.join(work_dir, app)
if not os.path.exists(work_dir):
    os.mkdir(work_dir)
demo_dir = os.path.join(work_dir, "demos")
if not os.path.exists(demo_dir):
    os.mkdir(demo_dir)
task_dir = os.path.join(demo_dir, demo_name)
if os.path.exists(task_dir):
    shutil.rmtree(task_dir)  # 如果已存在,删除并重新创建
os.mkdir(task_dir)
raw_ss_dir = os.path.join(task_dir, "raw_screenshots")
os.mkdir(raw_ss_dir)
xml_dir = os.path.join(task_dir, "xml")
os.mkdir(xml_dir)
labeled_ss_dir = os.path.join(task_dir, "labeled_screenshots")
os.mkdir(labeled_ss_dir)
record_path = os.path.join(task_dir, "record.txt")
record_file = open(record_path, "w")
task_desc_path = os.path.join(task_dir, "task_desc.txt")

# 列出所有已连接的Android设备
device_list = list_all_devices()
if not device_list:
    print_with_color("ERROR: No device found!", "red")
    sys.exit()
print_with_color("List of devices attached:\n" + str(device_list), "yellow")
# 如果只有一个设备,自动选择;否则,让用户选择
if len(device_list) == 1:
    device = device_list[0]
    print_with_color(f"Device selected: {device}", "yellow")
else:
    print_with_color("Please choose the Android device to start demo by entering its ID:", "blue")
    device = input()

# 创建AndroidController实例,用于控制设备
controller = AndroidController(device)
# 获取设备屏幕尺寸
width, height = controller.get_device_size()
if not width and not height:
    print_with_color("ERROR: Invalid device size!", "red")
    sys.exit()
print_with_color(f"Screen resolution of {device}: {width}x{height}", "yellow")

# 用户输入演示目标
print_with_color("Please state the goal of your following demo actions clearly, e.g. send a message to John", "blue")
task_desc = input()
with open(task_desc_path, "w") as f:
    f.write(task_desc)

# 提示用户屏幕上的元素标记
print_with_color("All interactive elements on the screen are labeled with red and blue numeric tags. Elements "
                 "labeled with red tags are clickable elements; elements labeled with blue tags are scrollable "
                 "elements.", "blue")

# 用户操作循环
step = 0
while True:
    step += 1
    # 获取屏幕截图和XML布局信息
    screenshot_path = controller.get_screenshot(f"{demo_name}_{step}", raw_ss_dir)
    xml_path = controller.get_xml(f"{demo_name}_{step}", xml_dir)
    if screenshot_path == "ERROR" or xml_path == "ERROR":
        break
    # 遍历XML文件,标记可点击和可聚焦的元素
    clickable_list = []
    focusable_list = []
    traverse_tree(xml_path, clickable_list, "clickable", True)
    traverse_tree(xml_path, focusable_list, "focusable", True)
    # 合并列表,用于后续操作
    elem_list = clickable_list.copy()
    for elem in focusable_list:
        # 如果可聚焦元素与可点击元素相近,也将其添加到列表中
        bbox = elem.bbox
        center = (bbox[0][0] + bbox[1][0]) // 2, (bbox[0][1] + bbox[1][1]) // 2
        close = False
        for e in clickable_list:
            bbox = e.bbox
            center_ = (bbox[0][0] + bbox[1][0]) // 2, (bbox[0][1] + bbox[1][1]) // 2
            dist = (abs(center[0] - center_[0]) ** 2 + abs(center[1] - center_[1]) ** 2) ** 0.5
            if dist <= configs["MIN_DIST"]:
                close = True
                break
        if not close:
            elem_list.append(elem)
    # 在截图上绘制元素边界框
    labeled_img = draw_bbox_multi(screenshot_path, os.path.join(
        labeled_ss_dir, f"{demo_name}_{step}.png"), elem_list, True)
    cv2.imshow("image", labeled_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    # 提示用户选择操作
    user_input = "xxx"
    print_with_color("Choose one of the following actions you want to perform on the current screen:\ntap, text, long "
                     "press, swipe, stop", "blue")
    while user_input.lower() != "tap" and user_input.lower() != "text" and user_input.lower() != "long press" \
            and user_input.lower() != "swipe" and user_input.lower() != "stop":
        user_input = input()
    # 根据用户输入执行相应操作
    if user_input.lower() == "tap":
        # 用户选择点击操作
        # ...
    elif user_input.lower() == "text":
        # 用户选择输入文本操作
        # ...
    elif user_input.lower() == "long press":
        # 用户选择长按操作
        # ...
    elif user_input.lower() == "swipe":
        # 用户选择滑动操作
        # ...
    elif user_input.lower() == "stop":
        # 用户选择停止操作
        record_file.write("stop\n")
        record_file.close()
        break
    else:
        break
    # 等待3秒,以便用户观察屏幕
    time.sleep(3)

# 输出完成信息
print_with_color(f"Demonstration phase completed. {step} steps were recorded.", "yellow")

模型交互代码

这部分代码没什么好说的(model.py

  1. ​ask_gpt4v(content)​:

    • 这个函数用于向GPT-4模型发送请求。它首先设置了请求头,包括内容类型和授权令牌(API密钥)。
    • 然后构建了请求的负载(payload),包括模型名称、用户消息、温度(temperature,影响生成文本的随机性)、最大令牌数(max_tokens)。
    • 使用requests库发送POST请求到OpenAI的API端点。
    • 如果响应中没有错误,它会解析响应中的使用情况(usage),并计算请求的成本。
    • 最后,返回模型的响应。
  2. ​parse_explore_rsp(rsp)​:

    • 这个函数用于解析GPT-4模型在探索任务中的响应。它使用正则表达式提取响应中的观察(Observation)、思考(Thought)、行动(Action)和总结(Summary)。
    • 根据提取的信息,函数会返回一个列表,包含行动名称、参数(如点击的区域、输入的文本等)和总结。
  3. ​parse_grid_rsp(rsp)​:

    • 类似于parse_explore_rsp,这个函数用于解析在网格模式下的响应。它处理的是在网格界面上的操作,如点击、长按和滑动。
    • 返回的列表会包含行动名称、网格区域、子区域(如果适用)和总结。
  4. ​parse_reflect_rsp(rsp)​:

    • 这个函数用于解析GPT-4模型在反思任务中的响应。它提取决策(Decision)和思考(Thought),以及可能的文档描述(Documentation)。
    • 返回的列表包含决策、思考和文档描述。

这些函数的目的是将GPT-4模型的文本响应转换为更易于处理的结构化数据,以便后续的脚本或程序可以根据这些数据执行相应的操作。例如,这些操作可能包括在智能手机上模拟用户界面的交互,或者在自动化测试中记录用户的操作。通过这种方式,脚本能够自动化地根据GPT-4模型的指导来执行任务。

任务执行代码

这个脚本的目的是自动化地执行用户指定的任务,通过与Android设备交互,并根据GPT-4模型的指导来完成这些任务。脚本会记录每次操作的日志,以便在任务完成后进行分析。如果任务成功完成或者达到预设的最大轮数,脚本会输出相应的信息。

举个例子:比如在手机上打开一个应用并发送一条消息。这个示例假设你已经有一个名为"MyApp"的应用,并且你想要自动化地打开它,然后找到发送消息的按钮并输入一条消息。

首先,你需要确保你的Android设备已经连接到电脑,并且你的电脑上安装了ADB(Android Debug Bridge)工具。

然后,你可以在命令行中运行以下命令来启动脚本(假设你的脚本名为app_agent.py​):

css 复制代码
python app_agent.py --app MyApp --root_dir /path/to/your/root_dir

在脚本运行过程中,它会提示你输入任务描述,比如:

vbnet 复制代码
Please enter the description of the task you want me to complete in a few sentences:
Open the messaging app, find the conversation with John, and send a message saying "Hello, John! How are you?"

脚本会根据这个描述,通过GPT-4模型生成操作步骤。然后,它会模拟用户操作,比如点击打开应用、滚动到John的对话、点击输入框、输入消息并发送。

在执行过程中,脚本会记录每一步的操作和GPT-4模型的响应,以便在任务完成后你可以查看日志文件。如果任务成功完成,脚本会输出"Task completed successfully"。

执行架构图

相关推荐
靴子学长25 分钟前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
AI_NEW_COME1 小时前
知识库管理系统可扩展性深度测评
人工智能
海棠AI实验室2 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself2 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot
IT古董2 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
centurysee2 小时前
【最佳实践】Anthropic:Agentic系统实践案例
人工智能
mahuifa2 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai
四口鲸鱼爱吃盐3 小时前
Pytorch | 从零构建GoogleNet对CIFAR10进行分类
人工智能·pytorch·分类
蓝天星空3 小时前
Python调用open ai接口
人工智能·python
睡觉狂魔er3 小时前
自动驾驶控制与规划——Project 3: LQR车辆横向控制
人工智能·机器学习·自动驾驶