Desktop-приложения без API — классическая головная боль инженера по автоматизации. Legacy ERP, CAD-программы, банк-клиенты, 1С в режиме thick client. Selenium их не видит, REST API нет. Мы решаем эту задачу через AI-агента, который либо использует Computer Use (скриншот + управление мышью/клавиатурой), либо обращается к accessibility API (pywinauto, UI Automation) — более надёжный вариант для Windows-приложений. В этой статье разберём, как построить такого агента, какие подводные камни и сколько это экономит.
Почему AI-агент, а не RPA?
Классические RPA-системы (UiPath, Blue Prism) требуют жёсткой разметки интерфейса и ломаются при любом обновлении. AI-агент на базе LLM понимает UI на уровне семантики: ему не нужен заранее заданный селектор — он анализирует дерево элементов и принимает решение о действии. Это снижает стоимость поддержки на 40–60% на дистанции. Мы опираемся на стандарты Windows UI Automation API и документацию pywinauto.
Как мы строим такого агента?
В основе лежит связка pywinauto + Anthropic API. 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}
Какой подход выбрать: pywinauto vs Computer Use?
| Критерий | pywinauto + UI Automation | Computer Use (скриншот) |
|---|---|---|
| Надёжность | Высокая — работа с элементами по атрибутам | Средняя — зависит от разрешения экрана и контраста |
| Скорость | Быстро — без рендеринга изображений | Медленно — требуется генерация скриншота и его анализ |
| Поддержка изменений UI | Частичная — при обновлении automation_id может измениться | Высокая — не зависит от атрибутов |
| Поддерживаемые приложения | Windows (WPF, WinForms, 1C, SAP) | Любые с графическим интерфейсом |
| Доступ к сложным элементам (таблицы, гриды) | Есть через UI Automation pattern | Ограничен (только видимая область) |
Мы используем оба подхода: pywinauto как основной, Computer Use как fallback. Если приложение имеет доступную модель UI — предпочитаем pywinauto, иначе Computer Use.
Когда нужен Computer Use как fallback?
Computer Use выручает, когда приложение использует кастомную графику или не предоставляет accessibility-дерево. Например, старые CAD-системы или визуализаторы данных. В таких случаях агент делает скриншот области и передаёт его LLM для анализа. Однако это медленнее и менее надёжно: latency p99 выше, и модель может ошибиться в определении координат. Поэтому применяем Computer Use только там, где pywinauto бессилен.
Практический кейс: автоматизация 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С напрямую. Наш опыт показывает, что гибридная схема (pywinauto + COM) даёт лучший результат.
Что входит в работу?
- Анализ UI-дерева приложения — выявление доступных элементов, составление карты автоматизации.
- Разработка агента для конкретного workflow — реализация последовательности действий с fallback-логикой.
- Интеграция с LLM — выбор модели (Claude или GPT), настройка промптов для устойчивой генерации JSON-команд.
- Батчевая обработка + мониторинг — запуск по расписанию, отправка алертов об ошибках.
- Документация — описание архитектуры, инструкция по запуску, поддержка.
Ориентировочные сроки
| Этап | Длительность |
|---|---|
| Анализ UI-дерева + базовая автоматизация | 3–5 дней |
| Конкретный workflow (форма → обработка → результат) | 1–2 недели |
| 1С/SAP специфика (COM + pywinauto) | +1 неделя |
| Батчевая обработка + мониторинг | +1 неделя |
Мы гарантируем стабильность агента на протяжении всего срока эксплуатации — в случае обновления приложения корректируем селекторы и промпты. Работаем с desktop-автоматизацией более 5 лет, выполнено свыше 20 проектов для банков, ретейла и логистики.
Получите консультацию по вашему сценарию — оценим проект за 1–2 дня. Свяжитесь с нами, мы подскажем оптимальное решение под ключ.







