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

项目结构

AppAgent 开源项目解读
中心模块script,咱们经过script完结主要操作

项目配置

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

AppAgent 开源项目解读

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消耗更少,所以原项目采用的是英文的提示词。这里咱们将其翻译为中文:

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模型生成更精确的描绘。

步骤记载

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

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("stopn")
        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​):

python app_agent.py --app MyApp --root_dir /path/to/your/root_dir

在脚本运转进程中,它会提示你输入使命描绘,比方:

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”。

履行架构图

AppAgent 开源项目解读