AI-агент для автозаполнения веб-форм
Ручное заполнение однотипных веб-форм — регистрация поставщиков, подача заявок на тендеры, ввод данных в государственные порталы — занимает часы рабочего времени. AI-агент решает задачу: берёт данные из структурированного источника (БД, JSON, Excel), находит нужные поля на странице, правильно заполняет и отправляет. Ключевое преимущество перед обычным RPA — агент понимает смысл полей и умеет маппировать данные без жёсткого скрипта.
Агент заполнения форм с Playwright
from anthropic import Anthropic
from playwright.async_api import async_playwright, Page
import json
import asyncio
import base64
from io import BytesIO
client = Anthropic()
class FormFillerAgent:
"""AI-агент для заполнения веб-форм"""
FORM_TOOLS = [
{
"name": "analyze_form",
"description": "Анализирует структуру формы на странице: поля, типы, обязательность",
"input_schema": {
"type": "object",
"properties": {
"include_screenshot": {"type": "boolean", "default": True},
},
},
},
{
"name": "fill_field",
"description": "Заполняет конкретное поле формы",
"input_schema": {
"type": "object",
"properties": {
"selector": {"type": "string"},
"value": {"type": "string"},
"field_type": {
"type": "string",
"enum": ["text", "select", "checkbox", "radio", "date", "file"],
"default": "text",
},
},
"required": ["selector", "value"],
},
},
{
"name": "click_element",
"description": "Кликает на кнопку, ссылку или элемент",
"input_schema": {
"type": "object",
"properties": {
"selector": {"type": "string"},
"wait_after_ms": {"type": "integer", "default": 500},
},
"required": ["selector"],
},
},
{
"name": "get_form_errors",
"description": "Проверяет наличие ошибок валидации на форме",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "take_screenshot",
"description": "Делает скриншот текущего состояния страницы",
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "submit_form",
"description": "Отправляет форму",
"input_schema": {
"type": "object",
"properties": {
"submit_selector": {"type": "string", "default": "[type=submit], button[type=submit], .btn-submit"},
"confirm_dialog": {"type": "boolean", "default": False},
},
},
},
]
def __init__(self, page: Page, dry_run: bool = False):
self.page = page
self.dry_run = dry_run # не отправляем форму в режиме тестирования
async def get_form_structure(self) -> dict:
"""Извлекает структуру формы через DOM"""
return await self.page.evaluate("""
() => {
const form = document.querySelector('form') || document.body;
const inputs = form.querySelectorAll('input, select, textarea');
const fields = [];
inputs.forEach(input => {
if (input.type === 'hidden') return;
// Пытаемся найти label для поля
let label = '';
if (input.id) {
const labelEl = document.querySelector(`label[for="${input.id}"]`);
if (labelEl) label = labelEl.textContent.trim();
}
if (!label && input.placeholder) label = input.placeholder;
if (!label && input.name) label = input.name;
const options = [];
if (input.tagName === 'SELECT') {
input.querySelectorAll('option').forEach(opt => {
options.push({value: opt.value, text: opt.textContent.trim()});
});
}
fields.push({
tag: input.tagName.toLowerCase(),
type: input.type || 'text',
name: input.name || '',
id: input.id || '',
label: label,
required: input.required,
placeholder: input.placeholder || '',
current_value: input.value || '',
options: options,
css_path: input.id ? `#${input.id}` : (input.name ? `[name="${input.name}"]` : ''),
});
});
return {fields, url: location.href, title: document.title};
}
""")
async def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
if tool_name == "analyze_form":
structure = await self.get_form_structure()
result = {"form_structure": structure}
if tool_input.get("include_screenshot", True):
screenshot_bytes = await self.page.screenshot(type="png")
result["screenshot"] = base64.b64encode(screenshot_bytes).decode()
return json.dumps(result, ensure_ascii=False)
elif tool_name == "fill_field":
selector = tool_input["selector"]
value = tool_input["value"]
field_type = tool_input.get("field_type", "text")
try:
await self.page.wait_for_selector(selector, timeout=3000)
if field_type == "select":
await self.page.select_option(selector, label=value)
elif field_type == "checkbox":
is_checked = await self.page.is_checked(selector)
should_check = value.lower() in ("true", "yes", "да", "1")
if is_checked != should_check:
await self.page.click(selector)
elif field_type == "radio":
await self.page.check(f'{selector}[value="{value}"]')
elif field_type == "date":
await self.page.fill(selector, value)
else:
await self.page.fill(selector, "")
await self.page.type(selector, value, delay=20)
return f"Заполнено: {selector} = {value}"
except Exception as e:
return f"Ошибка заполнения {selector}: {e}"
elif tool_name == "click_element":
try:
await self.page.click(tool_input["selector"])
await asyncio.sleep(tool_input.get("wait_after_ms", 500) / 1000)
return f"Кликнул: {tool_input['selector']}"
except Exception as e:
return f"Ошибка клика: {e}"
elif tool_name == "get_form_errors":
errors = await self.page.evaluate("""
() => {
const errorElements = document.querySelectorAll(
'.error, .invalid-feedback, [class*="error"], [class*="invalid"], .help-block'
);
return Array.from(errorElements)
.map(el => el.textContent.trim())
.filter(t => t.length > 0);
}
""")
return json.dumps({"errors": errors})
elif tool_name == "take_screenshot":
screenshot_bytes = await self.page.screenshot(type="png")
return json.dumps({
"screenshot": base64.b64encode(screenshot_bytes).decode()
})
elif tool_name == "submit_form":
if self.dry_run:
return '{"dry_run": true, "message": "Форма не отправлена (dry run mode)"}'
try:
selector = tool_input.get("submit_selector", "[type=submit]")
await self.page.click(selector)
if tool_input.get("confirm_dialog"):
self.page.on("dialog", lambda d: d.accept())
await self.page.wait_for_load_state("networkidle", timeout=15000)
return json.dumps({"submitted": True, "url": self.page.url})
except Exception as e:
return json.dumps({"submitted": False, "error": str(e)})
return "Unknown tool"
async def fill_form(self, data: dict, context: str = "") -> dict:
"""Заполняет форму данными из словаря"""
data_text = json.dumps(data, ensure_ascii=False, indent=2)
messages = [{
"role": "user",
"content": f"""Заполни форму на странице следующими данными:
{data_text}
{f"Контекст: {context}" if context else ""}
Алгоритм:
1. Сначала analyze_form чтобы увидеть структуру
2. Сопоставь данные с полями формы по смыслу (маппинг может быть нелинейным)
3. Заполни каждое поле последовательно
4. Проверь ошибки валидации
5. Если ошибок нет — отправь форму
6. Сообщи о результате"""
}]
steps = 0
while steps < 25:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
system="Ты — агент заполнения веб-форм. Маппируй данные в поля по смыслу, а не только по имени.",
tools=self.FORM_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 = await self._execute_tool(block.name, block.input)
try:
result_data = json.loads(result)
if "screenshot" in result_data or (isinstance(result_data, dict) and "form_structure" in result_data):
content: list = [{"type": "text", "text": json.dumps(
{k: v for k, v in result_data.items() if k != "screenshot"}, ensure_ascii=False
)}]
if "screenshot" in result_data:
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": result_data["screenshot"],
}
})
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": content,
})
else:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except (json.JSONDecodeError, TypeError):
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
if done or response.stop_reason == "end_turn":
final_text = next((b.text for b in response.content if hasattr(b, "text")), "")
return {"success": True, "steps": steps, "message": final_text}
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
steps += 1
return {"success": False, "steps": steps}
Практический кейс: подача заявок на госзакупки
Задача: компания-поставщик участвует в 30–50 тендерах ежемесячно на Росэлторг и ЕИС. Заполнение заявки занимает 45–90 мин, часть полей повторяется (реквизиты, сертификаты, ценовое предложение).
Реализация:
- База данных с шаблонами (реквизиты компании, стандартные документы)
- Playwright агент обходит формы на площадках
- Умный маппинг: «Юридический адрес» → «Адрес места нахождения» → «Место регистрации» — одно поле, разные названия
- Dry-run режим для проверки до отправки
Результаты:
- Время на заявку: 60 мин → 8 мин (проверка + подпись)
- Ошибки из-за пропущенных полей: снизились на 91%
- Самая частая проблема: CAPTCHA на некоторых площадках — решается ручным прохождением + продолжением агентом
Сроки
- Базовый агент заполнения (одна форма): 3–5 дней
- Адаптивный маппинг для нескольких платформ: 1–2 недели
- Батчевая обработка очереди заявок: +1 неделя
- Интеграция с системой хранения документов: +1 неделя







