출장 자동화 시스템
Agent-S grounding.py
myeongjaechoi
2025. 3. 30. 16:34
좌표 생성 메서드 (generate_coords)
def generate_coords(self, ref_expr: str, obs: Dict) -> List[int]:
self.grounding_model.reset()
prompt = f"Query:{ref_expr}\nOutput only the coordinate of one point in your response.\n"
self.grounding_model.add_message(
text_content=prompt, image_content=obs["screenshot"], put_text_last=True
)
response = call_llm_safe(self.grounding_model)
print("RAW GROUNDING MODEL RESPONSE:", response)
numericals = re.findall(r"\d+", response)
assert len(numericals) >= 2
return [int(numericals[0]), int(numericals[1])]
- ref_expr: 찾고자 하는 요소에 대한 설명(예: "로그인 버튼")
- obs: 화면 스크린샷을 포함한 관찰 데이터
- 언어 모델이 설명에 해당하는 화면 요소의 좌표를 생성하고 반환합니다
OCR 요소 추출 메서드 (get_ocr_elements)
이 메서드는 이미지에서 텍스트를 인식하고 각 텍스트 요소의 위치 정보를 추출합니다:
- b64_image_data: 화면 스크린샷 이미지 데이터
- OCR(광학 문자 인식)을 사용하여 이미지에서 텍스트를 추출하고, 각 텍스트의 위치와 크기 정보를 포함한 목록을 반환합니다
def get_ocr_elements(self, b64_image_data: str) -> Tuple[str, List]:
image = Image.open(BytesIO(b64_image_data))
image_data = pytesseract.image_to_data(image, output_type=Output.DICT)
# 텍스트 정리 (앞뒤 공백과 특수문자 제거)
for i, word in enumerate(image_data["text"]):
image_data["text"][i] = re.sub(
r"^[^a-zA-Z\s.,!?;:\-\+]+|[^a-zA-Z\s.,!?;:\-\+]+$", "", word
)
# OCR 요소 생성 및 테이블 형태로 정리
ocr_elements = []
ocr_table = "Text Table:\nWord id\tText\n"
grouping_map = defaultdict(list)
ocr_id = 0
for i in range(len(image_data["text"])):
block_num = image_data["block_num"][i]
if image_data["text"][i]:
grouping_map[block_num].append(image_data["text"][i])
ocr_table += f"{ocr_id}\t{image_data['text'][i]}\n"
ocr_elements.append({
"id": ocr_id,
"text": image_data["text"][i],
"group_num": block_num,
"word_num": len(grouping_map[block_num]),
"left": image_data["left"][i],
"top": image_data["top"][i],
"width": image_data["width"][i],
"height": image_data["height"][i],
})
ocr_id += 1
return ocr_table, ocr_elements
텍스트 좌표 생성 메서드 (generate_text_coords)
이 메서드는 특정 텍스트 문구의 좌표를 찾습니다
- phrase: 찾을 텍스트 문구
- obs: 화면 스크린샷을 포함한 관찰 데이터
- alignment: 좌표의 정렬 방식 ("start"는 문구의 시작, "end"는 문구의 끝)
- OCR로 추출된 텍스트 중에서 주어진 문구와 일치하는 텍스트의 좌표를 반환합니다
def generate_text_coords(self, phrase: str, obs: Dict, alignment: str = "") -> List[int]:
ocr_table, ocr_elements = self.get_ocr_elements(obs["screenshot"])
alignment_prompt = ""
if alignment == "start":
alignment_prompt = "**Important**: Output the word id of the FIRST word in the provided phrase.\n"
elif alignment == "end":
alignment_prompt = "**Important**: Output the word id of the LAST word in the provided phrase.\n"
# 언어 모델 프롬프트 설정
self.text_span_agent.reset()
self.text_span_agent.add_message(
alignment_prompt + "Phrase: " + phrase + "\n" + ocr_table, role="user"
)
self.text_span_agent.add_message(
"Screenshot:\n", image_content=obs["screenshot"], role="user"
)
# 응답 처리
response = call_llm_safe(self.text_span_agent)
print("TEXT SPAN AGENT RESPONSE:", response)
numericals = re.findall(r"\d+", response)
if len(numericals) > 0:
text_id = int(numericals[-1])
else:
text_id = 0
elem = ocr_elements[text_id]
# 좌표 계산
if alignment == "start":
coords = [elem["left"], elem["top"] + (elem["height"] // 2)]
elif alignment == "end":
coords = [elem["left"] + elem["width"], elem["top"] + (elem["height"] // 2)]
else:
coords = [
elem["left"] + (elem["width"] // 2),
elem["top"] + (elem["height"] // 2),
]
return coords
좌표 할당 메서드 (assign_coordinates)
- plan: 실행할 액션 계획
- obs: 화면 스크린샷을 포함한 관찰 데이터
- 액션 유형에 따라 적절한 좌표 생성 메서드를 호출하여 좌표를 할당합니다
def assign_coordinates(self, plan: str, obs: Dict):
# 이전 좌표 초기화
self.coords1, self.coords2 = None, None
try:
# 함수 이름과 인자 추출
action = parse_single_code_from_string(plan.split("Grounded Action")[-1])
function_name = re.match(r"(\w+\.\w+)\(", action).group(1)
args = self.parse_function_args(action)
except Exception as e:
raise RuntimeError(f"Error in parsing grounded action: {e}") from e
# 액션 유형에 따라 좌표 생성
if (
function_name in ["agent.click", "agent.type", "agent.scroll"]
and len(args) >= 1
and args[0] != None
):
self.coords1 = self.generate_coords(args[0], obs)
elif function_name == "agent.drag_and_drop" and len(args) >= 2:
self.coords1 = self.generate_coords(args[0], obs)
self.coords2 = self.generate_coords(args[1], obs)
elif function_name == "agent.highlight_text_span" and len(args) >= 2:
self.coords1 = self.generate_text_coords(args[0], obs, alignment="start")
self.coords2 = self.generate_text_coords(args[1], obs, alignment="end")
클릭 액션 (click)
- element_description: 클릭할 요소에 대한 설명
- num_clicks: 클릭 횟수
- button_type: 마우스 버튼 유형
- hold_keys: 클릭하는 동안 누르고 있을 키 목록
- PyAutoGUI 라이브러리를 사용하여 클릭 명령을 생성합니다
@agent_action
def click(
self,
element_description: str,
num_clicks: int = 1,
button_type: str = "left",
hold_keys: List = [],
):
"""요소 클릭
인자:
element_description:str, 클릭할 요소에 대한 상세한 설명. 최소 완전한 문장이어야 함
num_clicks:int, 요소를 클릭할 횟수
button_type:str, 사용할 마우스 버튼 ("left", "middle", "right" 중 하나)
hold_keys:List, 클릭하는 동안 누르고 있을 키 목록
"""
x, y = self.resize_coordinates(self.coords1)
command = "import pyautogui; "
# 키 누르기
for k in hold_keys:
command += f"pyautogui.keyDown({repr(k)}); "
# 클릭 명령 추가
command += f"""import pyautogui; pyautogui.click({x}, {y}, clicks={num_clicks}, button={repr(button_type)}); """
# 키 떼기
for k in hold_keys:
command += f"pyautogui.keyUp({repr(k)}); "
return command
애플리케이션 전환 액션 (switch_applications)
- app_code: 전환할 애플리케이션의 이름
- 각 운영체제(Mac, Ubuntu, Windows)에 맞는 전환 명령을 생성합니다
@agent_action
def switch_applications(self, app_code):
"""다른 열린 애플리케이션으로 전환
인자:
app_code:str 전환할 애플리케이션의 코드명
"""
if self.platform == "mac":
return f"import pyautogui; import time; pyautogui.hotkey('command', 'space', interval=0.5); pyautogui.typewrite({repr(app_code)}); pyautogui.press('enter'); time.sleep(1.0)"
elif self.platform == "ubuntu":
return UBUNTU_APP_SETUP.replace("APP_NAME", app_code)
elif self.platform == "windows":
return f"import pyautogui; import time; pyautogui.hotkey('win', 'd', interval=0.5); pyautogui.typewrite({repr(app_code)}); pyautogui.press('enter'); time.sleep(1.0)"
텍스트 입력 액션 (type)
- element_description: 텍스트를 입력할 요소에 대한 설명
- text: 입력할 텍스트
- overwrite: 기존 텍스트를 덮어쓸지 여부
- enter: 텍스트 입력 후 엔터키를 누를지 여부
- 요소 위치에 클릭한 후 텍스트를 입력하는 명령을 생성합니다13
@agent_action
def type(
self,
element_description: Optional[str] = None,
text: str = "",
overwrite: bool = False,
enter: bool = False,
):
"""특정 요소에 텍스트 입력
인자:
element_description:str, 텍스트를 입력할 요소에 대한 상세한 설명
text:str, 입력할 텍스트
overwrite:bool, 기존 텍스트를 덮어쓸지 여부
enter:bool, 텍스트 입력 후 엔터키를 누를지 여부
"""
if self.coords1 is not None:
x, y = self.resize_coordinates(self.coords1)
command = "import pyautogui; "
command += f"pyautogui.click({x}, {y}); "
if overwrite:
command += f"pyautogui.hotkey('ctrl', 'a'); pyautogui.press('backspace'); "
command += f"pyautogui.write({repr(text)}); "
if enter:
command += "pyautogui.press('enter'); "
else:
command = "import pyautogui; "
if overwrite:
command += f"pyautogui.hotkey('ctrl', 'a'); pyautogui.press('backspace'); "
command += f"pyautogui.write({repr(text)}); "
if enter:
command += "pyautogui.press('enter'); "
return command