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, тощо)",
"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 дні. Зв'яжіться з нами, ми підкажемо оптимальне рішення під ключ.







