Как создать AI-агента на Python с OpenAI для автоматического парсинга счетов

Все чаще разработчикам приходится решать рутинные задачи автоматизации. Одна из наиболее типичных это автоматический разбор документов (счетов, актов, накладных).

Мы создадим простого AI-агента для обработки счетов, в ввиде pdf-файлов или картинок, и научим его превращать их в структурированные данные. Для этого будем использовать python и OpenAI API c structured_output (это позволяет жестко задать структуру ответа без лишних деталей).

Агент будет иметь следующие функции:

  • принимает PDF и изображения;

  • преобразует в изображения;

  • отправляет в OpenAI API;

  • получает нормализованный JSON;

  • валидирует данные;

  • сохраняет в CSV или базу;

  • работает как Telegram-бот.


Подготовка окружения

Установим зависимости необходимые для работы агента:

pip install openai python-dotenv pydantic pdf2image pillow python-telegram-bot

Также потребуется Poppler для конвертации PDF → PNG (Ubuntu):

sudo apt install poppler-utils

Создаём .env:

OPENAI_API_KEY=ваш_ключ

Конвертация PDF в изображения

Хотя OpenAI умеет воспринимать PDF, но надежнее будет подавать информацию как изображения. Причина конвертации в том, что PDF может быть битый или иметь другие проблемы с layout, а картинку LLM проще распознать.

from pdf2image import convert_from_path

def pdf_to_images(pdf_path):
    pages = convert_from_path(pdf_path, dpi=200)  # dpi 200 = оптимум по качеству/скорости
    image_paths = []

    for i, page in enumerate(pages):
        path = f"/tmp/invoice_page_{i}.png"
        page.save(path, "PNG")
        image_paths.append(path)

    return image_paths

И функция, которая проверяет тип файла, и если нужно приводит к единому формату PNG

from PIL import Image

SUPPORTED_IMAGE_EXT = (".png", ".jpg", ".jpeg")

def prepare_images(path: str) -> list[str]:
    path = path.lower()

    # PDF → PNG
    if path.endswith(".pdf"):
        return pdf_to_images(path)

    # Картинка → используем напрямую
    if path.endswith(SUPPORTED_IMAGE_EXT):
        img = Image.open(path).convert("RGB")
        out_path = "/tmp/uploaded_image.png"
        img.save(out_path, "PNG")
        return [out_path]

    raise ValueError("Unsupported file format")

Определяем структуру данных (Pydantic)

В Responses API можно заставить модель возвращать только строго заданную структуру данных. Для этого описывается класс (через Pydantic), который определяет, какой именно JSON разрешён.

from pydantic import BaseModel

class Invoice(BaseModel):
    invoice_number: str | None
    invoice_date: str | None
    vendor: str | None
    total_amount: float | None
    currency: str | None
    line_items: list[dict] | None  # [{description, qty, price}]

Если в модели None, то AI не имеет права возвращать лишнее. Это даёт стабильность.


Парсинг документа через OpenAI Responses API

Используем модель семейства gpt-5.x, они лучше всего работают с документами.

from openai import OpenAI
client = OpenAI()

def parse_invoice(image_path):
    with open(image_path, "rb") as f:
        binary = f.read()

    response = client.responses.create(
        model="gpt-5.1",
        input=(
            "Extract invoice fields and return only valid JSON. "
            "If numeric values contain commas or spaces, normalize them. "
            "If document is rotated, still parse correctly."
        ),
        image={"data": binary, "mime_type": "image/png"},
        structured_output=Invoice,
    )

    return response.output_as(Invoice)

Почему это важно:

  • мы не используем чат-completion, только строгий responses;

  • модели серии gpt-5.* обеспечивают предсказуемость структуры;

  • минимизация галлюцинаций;


Валидатор

AI может ошибаться в мелочах, забыть сумму, подменить валюту, пропустить номер. Поэтому нужно добавить валидацию.

def validate_invoice(inv: Invoice) -> list[str]:
    errors = []

    if not inv.invoice_number:
        errors.append("Missing invoice_number")

    if not inv.total_amount:
        errors.append("Missing total_amount")

    if inv.currency and inv.currency.upper() not in ("USD", "EUR", "GBP", "BYN", "RUB"):
        errors.append(f"Unknown currency '{inv.currency}'")

    return errors

Сохранение в CSV

Реализуем функцию для сохрания результатов в csv-файл, но по желанию, можно сохранять также в базу данных (Mysql/PostgreSQL)

import csv, os

def save_invoice(inv: Invoice, path="invoices.csv"):
    is_new = not os.path.exists(path)
    with open(path, "a", newline="") as f:
        writer = csv.writer(f)

        if is_new:
            writer.writerow(inv.model_fields.keys())

        writer.writerow(inv.model_dump().values())

Основная функция обработки документа

Реализуем точку входа, где передаем путь к файлу, а все остальное код сделает сам.

def process_document(path: str):
    images = prepare_images(path)
    results = []

    for img in images:
        result = parse_invoice(img)
        errors = validate_invoice(result)

        if not errors:
            save_invoice(result)

        results.append((result, errors))

    return results

Интегрируем агента в Telegram-бот

Для начала нам нужно создать бота в телеграм, для этого нужно выполнить следующие шаги:

Открываем поиск → вводим: @BotFather

Затем выполняем команду:

/newbot

BotFather задаст два вопроса:

  1. Название бота — любое имя.

  2. username, заканчивающийся на _bot
    пример: invoice_reader_ai_bot

После создания BotFather выдаст API-ключ вашего бота:

Use this token to access the HTTP API:
1234567890:ABC123....XYZ

Создаем минимальный Telegram сервер на python-telegram-bot

import os
from telegram import Update
from telegram.ext import ApplicationBuilder, MessageHandler, filters, ContextTypes

TOKEN = "ваш_bot_token"

async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
    file = update.message.document

    # путь для временного хранения
    path = f"/tmp/{file.file_name}"

    tg_file = await file.get_file()
    await tg_file.download_to_drive(path)

    await update.message.reply_text("Файл получен, начинаю обработку...")

    if file.file_name.lower().endswith(".pdf"):
        results = process_pdf(path)

        errors = [e for _, e in results if e]
        if errors:
            await update.message.reply_text(
                "Есть ошибки:\n" + "\n".join(str(e) for e in errors)
            )
        else:
            await update.message.reply_text("Готово. Данные извлечены.")
    else:
        await update.message.reply_text("Поддерживаются только PDF.")

app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
app.run_polling()

Почему такая интеграция удобна?

Telegram становится:

  • интерфейсом загрузки документов;

  • интерфейсом получения результата;

  • интерфейсом логгирования;


Реальные сценарии использования

  • Автоматизация бухгалтерии - можно загрузить пачку PDF, а агент сам нормализует данные.

  • Сервис SaaS "AI-парсер документов" - на базе этой реализации можно создать небольшой сервис, который будет автоматизировать рутинные задачи.

Комментарии (0)

Войдите, чтобы оставить комментарий

Похожие статьи