一、背景
从 Web 诞生之日起,UI 主动化就成了测验的难点,到现在近 30 年,一直没有有用的手法处理Web UI测验的问题,尽管开展了许多的 webdriver 驱动,图片 diff 驱动的东西,可是这些东西的投入产出比一直被质疑,主动化率越多维护成本越高,大部分都做着就抛弃了,还有一部分在做与不做间纠结。
本文结合一些开源的项目探究运用GPT 主动做 UI 测验的或许性。
二、计划选型
当时UI 的首要问题:一个是经过 Webdriver 操控浏览器履行,这些东西都需求先查找到对应元素的 Elements,无论是录制的仍是自己编写的在面临 UI 变化,元素变化时都需求耗费很大的精力去重新辨认,解析 Dom 查找,这个工作庸俗且没有用率;另一种是经过图画进行点击,比方 Sikuli 这种东西,首要的问题也是复用性较差,换个分辨率的机器或许图片发生少的改动就不可用。
运用 GPT 做 UI 测验尝试了两种计划:
第一种将 Html 元素投喂给 GPT,首要办法获取 Html代码,对 Html 做开始缩减处理,再做向量化,然后喂给 GPT4 主动生成 Webdriver 驱动脚本,作用一般,并且由于 Html 比较大,Token 的消耗很大。
第二种思路是让 GPT 像人相同考虑和测验,比方一个人翻开一个网页后,他经过眼睛看到的页面文字或图标,然后用手完结点击和输入的操作,最后经过页面的弹窗或许文字来辨认是否有过错,这几个动作经过大脑一致和谐。
这儿首要介绍第二种.
三、新计划实践
1.新计划简介
新的计划首要结合 Playwright,SoM视觉符号,GPT4Vison,GPT4,AutoGen来完结。首要的原理
经过 Playwright进行浏览器操作,包含页面图画的获取、浏览器的各种操作,相当于‘‘手’’;
进行SoM 视觉数据符号,由于 GPT4Vison 在进行页面原始辨认时并不是很精确,参阅微软的论文可以经过视觉符号的手法来辅佐 GPT4V 辨认,相当于“眼睛”。
经过GPT4+AutoGen 将这些过程串起来完结和谐操控,相当于“大脑”。
2.首要架构
3.完结过程
1. 运用 Playwright 注入 JS
browser = playwright.chromium.launch(channel="chrome",headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://oa.jd.com/")
inject_js ="./pagemark.js"
withopen(inject_js,'r')asfile:
content =file.read()
page.evaluate(f"{content}")
2. SoM 视觉提示符号
如前文提到的 GPT4V 并不能有用的辨认 Web 的元素,所以在运用 GPT4V 之前进行图画符号,图画符号现在有两种方法,一种是经过 AI 辨认图片进行符号,这种首要运用在对静态图片图画的辨认,对于 Web 页面的符号,咱们可以采用注入 JS 修改页面元素的方法来符号。这儿经过在浏览器中注入 pagemark.js,运用 Playwright 履行 js 函数来完结页面的符号,该 JS 可以完结规范的coco annotation的标示。
// DOM Labelerlet labels =[];functionunmarkPage(){
for(const label of labels){
document.body.removeChild(label);
}
labels =[];
}
functionmarkPage(){
unmarkPage();
var bodyRect = document.body.getBoundingClientRect();
var items =Array.prototype.slice.call(
document.querySelectorAll('*')
).map(function(element){
var vw = Math.max(document.documentElement.clientWidth ||0, window.innerWidth ||0);
var vh = Math.max(document.documentElement.clientHeight ||0, window.innerHeight ||0);
var rects =[...element.getClientRects()].filter(bb=>{
var center_x = bb.left + bb.width /2;
var center_y = bb.top + bb.height /2;
var elAtCenter = document.elementFromPoint(center_x, center_y);
return elAtCenter === element || element.contains(elAtCenter)
}).map(bb=>{
const rect ={
left: Math.max(0, bb.left),
top: Math.max(0, bb.top),
right: Math.min(vw, bb.right),
bottom: Math.min(vh, bb.bottom)
};
return{
...rect,
width: rect.right - rect.left,
height: rect.bottom - rect.top
}
});
var area = rects.reduce((acc, rect)=> acc + rect.width * rect.height,0);
return{
element: element,
include:
(element.tagName ==="INPUT"|| element.tagName ==="TEXTAREA"|| element.tagName ==="SELECT")||
(element.tagName ==="BUTTON"|| element.tagName ==="A"||(element.onclick !=null)|| window.getComputedStyle(element).cursor =="pointer")||
(element.tagName ==="IFRAME"|| element.tagName ==="VIDEO")
,
area,
rects,
text: element.textContent.trim().replace(/s{2,}/g,' ')
};
}).filter(item=>
item.include &&(item.area >=20)
);
// Only keep inner clickable items
items = items.filter(x=>!items.some(y=> x.element.contains(y.element)&&!(x == y)))
// Function to generate random colors
functiongetRandomColor(){
var letters ='0123456789ABCDEF';
var color ='#';
for(var i =0; i <6; i++){
color += letters[Math.floor(Math.random()*16)];
}
return color;
}
// Lets create a floating border on top of these elements that will always be visible
items.forEach(function(item, index){
item.rects.forEach((bbox)=>{
newElement = document.createElement("div");
var borderColor =getRandomColor();
newElement.style.outline =`2px dashed ${borderColor}`;
newElement.style.position ="fixed";
newElement.style.left = bbox.left +"px";
newElement.style.top = bbox.top +"px";
newElement.style.width = bbox.width +"px";
newElement.style.height = bbox.height +"px";
newElement.style.pointerEvents ="none";
newElement.style.boxSizing ="border-box";
newElement.style.zIndex =2147483647;
// newElement.style.background = `${borderColor}80`;
// Add floating label at the corner
var label = document.createElement("span");
label.textContent = index;
label.style.position ="absolute";
label.style.top ="-19px";
label.style.left ="0px";
label.style.background = borderColor;
label.style.color ="white";
label.style.padding ="2px 4px";
label.style.fontSize ="12px";
label.style.borderRadius ="2px";
newElement.appendChild(label);
document.body.appendChild(newElement);
labels.push(newElement);
console.log(index)
});
})
}
以某系统为例,标示后的作用如下图:
将符号元素的方位编号保存到本地coco.json
{
"1":{"x":"10","y":"40","w":"8","h":"10","center_x":"30","center_y":"13"},
"2":{"x":"20","y":"40","w":"16","h":"10","center_x":"30","center_y":"21"},
"10":{"x":"40","y":"80","w":"16","h":"10","center_x":"80","center_y":"21"}}
其间 center_x, center_y 为元素的中心点坐标
3.GPT4V 辨认
Prompt
我上传了一张网页截图,页面元素现已运用COCO Annotator进行了数字符号。
请剖析这张图片,并辨认出网页的用处以及各个符号元素的功用。基于你的剖析,请生成一个规划路径来完结以下使命:
- 输入ERP查找。
- 查看列表是否为空
请运用图片中的数字符号来描绘完结使命的具体过程。
GPT4V Response
这张图片显现的是一个具有多个数字符号的网页截图。依据这些符号,咱们可以推测出网页的用处以及各个元素的功用。
为了完结您提出的使命,咱们可以依照以下过程操作:
nn1. 运用符号为10的查找框输入ERP查找词。这个查找框的标签显现为“查找框输入erp”,标明它用于输入查找词。
nn2. 点击符号为17的查找按钮。这个按钮的标签是“查找”,标明它用于提交查找恳求。
nn3. 查看列表是否为空。这个过程或许需求查看符号为19的区域,这个区域被符号为“列表内容”,假如这个区域没有显现任何内容或许有提示信息显现列表为空,则阐明查找成果为空。
nn以 上过程基于图片中的数字符号和元素标签来描绘完结使命的具体操作。"
提取元素坐标
运用正则表达式从GPT4V 的 response 中提取查询到的元素的 bbox id= 10,17,19, 结合在 SoM 符号中记录的 json 文件,找到编号 10 的元素坐标”10″:{“x”:”40″,”y”:”80″,”w”:”16″,”h”,”10″,”center_x”:”80″,”center_y”:”21″}
class GPT4VRecognize
def get_location(self, query):
coco_json='./coco.json'
withopen(coco_json,'r') as file:
content =file.read()
matches=re.findall(r'd+',gpt4v_response_content)
num=-1
iflen(matches)>0:
num=matches[0]
data = json.loads(json_str)
center_x = data[num]["center_x"]
center_y = data[num]["center_y"]
return center_x,center_y
4.Playwright操作页面
Playwright是一个非常强大的操作浏览器的东西,这儿由于前面现已经过 GPT4V 辨认了图片,所以咱们首要经过 坐标 的方法来操控元素,不再需求解析Dom 来操控,首要的包含,点击,双击,鼠标拖动,输入,截图等:
class Actions:
page=None
__init__(self,url):
global page
browser = playwright.chromium.launch(channel="chrome",headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://oa.jd.com/")
def mouse_move(self,x,y):
page.mouse.move(self,x,y)
def screenshot(self):
page.screenshot()
def mouse_click(self,x,y):
page.mouse.click(self,x,y)
def input_text(self,x,y,text):
page.mouse.click(self,x,y)
page.keyboard.type(text)
def mouse_db_click(self,x,y):
def select_option(self,x,y,option):
......
5.运用 AutoGen编列
AutoGen是一个代理东西,它可以代理多个 LLM在不同会话间切换,可以主动的调用预订的函数,并和谐这些 LLM 完结使命。
在上面的程序中,完结了:眼睛:经过 GPT4V 来辨认元素;手:经过 Playwright 来做各种操作;后边需求经过一个大脑来和谐手、眼完结使命,这儿经过 GPT4+AutoGen 来完结大脑的协同调度。
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant= autogen.AssistantAgent(
name="assistant",
system_message=
"""
You are the orchestrator responsible for completing a task involving the UI window.
Utilize the provided functions to take a screenshot after each action.
Remember to only use the functions you have been given and focus on the task at hand.
""",
llm_config={"config_list": config_list},
)
user_proxy = autogen.UserProxyAgent(
name="brain_proxy",
human_input_mode="NEVER",
code_execution_config={"work_dir":"coding"},
max_consecutive_auto_reply=10,
llm_config={"config_list": config_list},)
action = Actions(url)
gpt4v=GPT4VRecognize()
user_proxy.register_function(
function_map={
"get_location": gpt4v.get_location,
"mouse_move":action.mouse_move,
"screenshot":action.screenshot,
"mouse_click":action.mouse_click,
"mouse_dbclick":action.mouse_dbclick,
"select_option":action.select_option
})
def run(message_ask):
user_proxy.initiate_chat(assistant,message=message_ask)
if __name__ =="__main__":
run("经过 输入erp 'wwww30' 查找成果,并查看是否返回空列表")
四、问题和后续
1.当时的首要问题
本文首要抛砖引玉,经过一种完全按人类思维进行 UI测验的方法,在试验过程中还有许多问题待处理。
-
GPT 在中文语境下辨认的不太友好,在试验过程中对中文的 prompt 了解有误,需求不断的优化 prompt,尤其是页面中的中文元素。
-
AutoGen 对于处理预订义的动作也会有问题,需求不断调优。
-
GPT4V 真的很贵。
2.未来的想法
-
将每次向 GPT4V恳求的图画辨认做本地化处理,结合现有的一些测验办法,然后削减 Token,缩短履行耗时。
-
面向业务的 GPT需求不断训练,将系统运用手册和一有的 PRD 文档投喂给 GPT,增强 gpt 的系统辨认和测验才能。
五、参阅
1.Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V
作者:CHO武小平
来历:京东云开发者社区 转载请注明来历