AI-агент для автоматизации desktop-приложений
Desktop-приложения без API — классическая головная боль. Legacy ERP, CAD-программы, банк-клиенты, 1C в режиме thick client. Selenium их не видит, REST API нет. AI-агент решает задачу через два подхода: либо Computer Use (скриншот + управление мышью/клавиатурой), либо accessibility API (pywinauto, UI Automation) — более надёжный вариант для Windows-приложений.
Подход 1: pywinauto + LLM для Windows-приложений
pywinauto обращается к Windows UI Automation API — та же технология, что использует экранный диктор. Элементы находятся по accessibility-атрибутам (AutomationId, Name, ControlType), а не по пикселям. Значительно надёжнее скриншотов.
from anthropic import Anthropic
import pywinauto
from pywinauto.application import Application
from pywinauto.findwindows import ElementNotFoundError
import json
import subprocess
import time
client = Anthropic()
class DesktopAppAgent:
"""AI-агент для автоматизации Windows desktop-приложений"""
def __init__(self, app_path: str = None, app_title: str = None):
self.app_path = app_path
self.app_title = app_title
self.app = None
self.main_window = None
def launch_or_connect(self):
"""Запускает приложение или подключается к запущенному"""
try:
if self.app_title:
self.app = Application(backend="uia").connect(title_re=f".*{self.app_title}.*")
elif self.app_path:
self.app = Application(backend="uia").start(self.app_path)
time.sleep(2) # ждём инициализации
except pywinauto.findwindows.ElementNotFoundError:
if self.app_path:
self.app = Application(backend="uia").start(self.app_path)
time.sleep(2)
self.main_window = self.app.top_window()
def get_ui_tree(self, max_depth: int = 4) -> dict:
"""Получает дерево UI-элементов"""
def extract_element(element, depth=0):
if depth > max_depth:
return None
try:
info = {
"name": element.window_text()[:100] if element.window_text() else "",
"control_type": element.element_info.control_type,
"automation_id": element.element_info.automation_id or "",
"enabled": element.is_enabled(),
"visible": element.is_visible(),
"rect": str(element.rectangle()),
}
children = []
for child in element.children():
child_info = extract_element(child, depth + 1)
if child_info and (child_info["visible"] or child_info["enabled"]):
children.append(child_info)
if children:
info["children"] = children[:20] # не более 20 дочерних
return info
except Exception:
return None
return extract_element(self.main_window)
def find_and_interact(self, instruction: str) -> str:
"""LLM определяет какой элемент нужен и что с ним делать"""
ui_tree = self.get_ui_tree()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": f"""Проанализируй дерево UI и верни JSON с действием:
{{
"action": "click|type|select|get_value|find",
"automation_id": "ID элемента если есть",
"name": "имя элемента",
"control_type": "тип элемента",
"value": "значение для type/select"
}}
Инструкция: {instruction}
UI-дерево:
{json.dumps(ui_tree, ensure_ascii=False)[:4000]}
Только JSON."""
}],
)
try:
text = response.content[0].text
action = json.loads(text[text.find("{"):text.rfind("}") + 1])
return self._execute_ui_action(action)
except Exception as e:
return f"Ошибка парсинга: {e}"
def _execute_ui_action(self, action: dict) -> str:
"""Выполняет действие с UI-элементом"""
try:
# Ищем элемент по automation_id или имени
element = None
if action.get("automation_id"):
element = self.main_window.child_window(
auto_id=action["automation_id"]
)
elif action.get("name"):
element = self.main_window.child_window(
title=action["name"],
control_type=action.get("control_type"),
)
if not element:
return "Элемент не найден"
act = action.get("action", "click")
if act == "click":
element.click_input()
return f"Кликнул на {action.get('name', action.get('automation_id'))}"
elif act == "type":
element.set_edit_text(action.get("value", ""))
return f"Ввёл: {action.get('value', '')}"
elif act == "select":
element.select(action.get("value", ""))
return f"Выбрал: {action.get('value', '')}"
elif act == "get_value":
return element.window_text() or element.get_value()
except ElementNotFoundError:
return f"Элемент не найден: {action}"
except Exception as e:
return f"Ошибка: {type(e).__name__}: {e}"
return "Действие выполнено"
class DesktopWorkflowAgent:
"""Высокоуровневый агент для выполнения задач в desktop-приложении"""
TOOLS = [
{
"name": "get_ui_state",
"description": "Получает текущее состояние UI-дерева приложения",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "interact_with_element",
"description": "Взаимодействует с UI-элементом (клик, ввод текста, выбор)",
"input_schema": {
"type": "object",
"properties": {
"automation_id": {"type": "string"},
"action": {"type": "string", "enum": ["click", "type", "select", "get_value"]},
"value": {"type": "string"},
},
"required": ["action"],
},
},
{
"name": "wait",
"description": "Ждёт изменения состояния приложения",
"input_schema": {
"type": "object",
"properties": {
"seconds": {"type": "number", "default": 1.0},
"wait_for_element": {"type": "string"},
},
},
},
{
"name": "keyboard_shortcut",
"description": "Нажимает комбинацию клавиш (Ctrl+S, Alt+F4, etc.)",
"input_schema": {
"type": "object",
"properties": {
"shortcut": {"type": "string", "description": "Например: Ctrl+S, Alt+Tab, F2"},
},
"required": ["shortcut"],
},
},
]
def __init__(self, desktop_agent: DesktopAppAgent):
self.agent = desktop_agent
async def run(self, task: str) -> dict:
messages = [{"role": "user", "content": task}]
steps = 0
while steps < 40:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system="Ты — агент автоматизации desktop-приложения. Используй инструменты последовательно.",
tools=self.TOOLS,
messages=messages,
)
tool_results = []
done = False
for block in response.content:
if hasattr(block, "text") and block.text:
if "выполнено" in block.text.lower() or "завершено" in block.text.lower():
done = True
elif block.type == "tool_use":
result = ""
inp = block.input
if block.name == "get_ui_state":
result = json.dumps(self.agent.get_ui_tree(max_depth=3), ensure_ascii=False)[:3000]
elif block.name == "interact_with_element":
result = self.agent._execute_ui_action(inp)
elif block.name == "wait":
time.sleep(inp.get("seconds", 1.0))
result = "Waited"
elif block.name == "keyboard_shortcut":
import pyautogui
keys = inp["shortcut"].replace("+", " ").split()
pyautogui.hotkey(*[k.lower() for k in keys])
result = f"Pressed {inp['shortcut']}"
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
if done or response.stop_reason == "end_turn":
return {"success": True, "steps": steps}
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
steps += 1
return {"success": False, "steps": steps}
Практический кейс: автоматизация 1С:Бухгалтерия
Задача: ежемесячное формирование 40 отчётов в 1С для 12 юрлиц. Процесс занимал 3 рабочих дня двух бухгалтеров.
Подход: pywinauto для 1С desktop-клиента (версия 8.3). UI Automation работает с 1С через COM-объекты и accessibility API.
Результаты:
- 40 отчётов × 12 юрлиц: 3 рабочих дня → 4 часа (ночной запуск)
- Ошибки ручного ввода: снизились до 0
- Сложность: 1С периодически меняет automation_id при обновлениях — пришлось добавить fallback-поиск по имени элемента
Ключевой момент при работе с 1С: приложение использует кастомный движок, не все элементы видны через стандартный UI Automation. Часть автоматизации реализована через COM-интерфейс 1С напрямую.
Сроки
- Анализ UI-дерево приложения + базовая автоматизация: 3–5 дней
- Конкретный workflow (форма → обработка → результат): 1–2 недели
- 1С/SAP специфика (COM + pywinauto): +1 неделя
- Батчевая обработка + мониторинг: +1 неделя







