From e05af2b6ce460a4387f0ca58ebae1fc0e2196a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 14 Jun 2026 07:42:54 +0400 Subject: [PATCH] =?UTF-8?q?doc:=20=D1=81=D0=B0=D0=B3=D0=B0=20=D1=81=20LLM?= =?UTF-8?q?=20=E2=80=94=20HTTP/2,=20ddos-guard,=20httpx=E2=86=92curl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Задокументированы все 4 попытки, хеш-анализ, находка с Node.js fetch, хронология 12 коммитов за 14.06.2026. --- history/session-01-init.md | 65 +++++++- history/sonnet-architecture-answer.md | 210 ++++++++++++++++++++++++ history/sonnet-architecture-question.md | 81 +++++++++ 3 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 history/sonnet-architecture-answer.md create mode 100644 history/sonnet-architecture-question.md diff --git a/history/session-01-init.md b/history/session-01-init.md index cff587b..b9a82fd 100644 --- a/history/session-01-init.md +++ b/history/session-01-init.md @@ -277,6 +277,69 @@ contracts-app/ "DB_PASS": "****", "DB_PORT": "5432", "DB_USER": "contracts", - "DB_SSLMODE": "disable" + "DB_SSLMODE": "disable", + "LLM_API_KEY": "sk-ucI...", + "LLM_API_URL": "https://api.aillm.ru/v1/chat/completions" } ``` + +--- + +## Сага с LLM (aillm.ru + HTTP/2) + +### Проблема +Python-библиотеки (`requests`, `httpx`) не держат HTTP/2 из коробки. +`api.aillm.ru` за ddos-guard, который **требует HTTP/2 от облачных IP**. +С локальной машины (81.200.23.210) даже HTTP/1.1 работает — для этого IP ddos-guard делает исключение. + +### Хронология попыток + +| # | Подход | Результат | +|---|---|---| +| 1 | `requests` (HTTP/1.1) | ❌ 401 token_not_found_in_db (ложная ошибка от ddos-guard) | +| 2 | `httpx` с `http2=True` | ❌ то же самое — `h2` пакет не установился в контейнере | +| 3 | `httpx[http2]` + `h2` в requirements | ❌ `h2` не ставится через pip в slim-контейнере | +| 4 | `subprocess.run(['curl', '-s', '--http2', ...])` | ⬜ ждёт редеплоя | + +### Ключевые находки + +1. **Хеш ключа не совпадал:** локально `5fad79ba...`, в облаке `c772c3e5...`. + Оказалось — не разные ключи, а `ddos-guard` возвращает ложный 401 при HTTP/1.1, + хешируя какой-то внутренний токен, а не наш ключ. + +2. **Node.js `fetch()` работает** (проект `say`) — потому что undici (нативный fetch в Node) + поддерживает HTTP/2 из коробки. Python — нет. + +3. **`curl --http2`** — единственный гарантированный способ получить HTTP/2 в Python-контейнере, + без дополнительных пакетов. + +4. **Доступные модели:** `gpt-oss-120b`, `qwen3.6-27b-fp8`, `qwen3-6-27b-fp8-opt`, `whisper-large-v3-turbo`. + +### Решение +`llm_client.py` использует `subprocess.run(['curl', '-s', '--http2', ...])` вместо Python HTTP-библиотек. + +--- + +## Хронология коммитов (14.06.2026) + +### DB слой +- `ba2e855` — `db.py`: `execute()` + `query_one()` для INSERT/UPDATE/DELETE и единичного SELECT +- `4635ed4` — `test_routes`: action `exec` через `db.execute()` (DDL без fetch) +- `9d01dba` — `schema.py`: `db.query → db.execute` для DDL + +### Upload слой +- `8db35e3` — `upload.py`: Blueprint `/upload`, поток bytes→parser→textify→DB +- `db2f6cc` — fix MIME: приоритет расширения над content_type + +### LLM слой +- `44e53ed` — `llm_client.py`: HTTP-клиент (сначала `requests`) +- `fe8a556` — `test_routes`: action `llm` для теста +- `c580c93` — fix: модель `gpt-oss-120b` (не `deepseek-chat`) +- `5904830` — fix: `httpx[http2]` вместо `requests` +- `c97f26d` — fix: `h2` явно в requirements +- `099edbc` — fix: `curl --http2` через `subprocess` вместо httpx + +### Debug +- `cc3203a` — `test_routes`: action `debug` для просмотра env vars +- `0d0ba18` — debug показывает длину ключа +- `b457de4` — debug показывает `h2_available` и `httpx_version` diff --git a/history/sonnet-architecture-answer.md b/history/sonnet-architecture-answer.md new file mode 100644 index 0000000..fad6ee0 --- /dev/null +++ b/history/sonnet-architecture-answer.md @@ -0,0 +1,210 @@ +# Ответ: архитектура сервиса "Сверка договоров" + +_Дата: 2026-06-13_ + +--- + +## 1. Правильно ли разбиты слои? + +Разбивка в целом правильная. Принцип «один файл — одна ответственность» выдержан. +Критических проблем нет, но есть два момента, которые стоит учесть. + +### Что хорошо +- `parser.py` — чисто I/O-слой: байты → JSON. Никакой логики. +- `textify.py` — чисто форматирование: JSON → текст. Никакой логики. +- `db.py` — чисто транспорт к БД. Не знает о бизнес-сущностях. +- `test_routes.py` — отдельный Blueprint, не засоряет app.py. + +### Что стоит скорректировать + +**`db.py` — разделить на транспорт и схему.** +Сейчас там `ensure_db()` — это уже «знание» о схеме. Когда появятся таблицы +(`contracts`, `supplements`, `spec_rows`, `spec_history`), их создание (DDL) +стоит вынести в отдельный `schema.py`. `db.py` остаётся просто `connect()` и `query()`. + +**Слой LLM стоит разделить надвое:** +- `llm_client.py` — HTTP-клиент к aillm.ru: отправить промпт → получить строку ответа. + Не знает ни о договорах, ни о спецификациях. +- `extractor.py` — бизнес-логика: взять текст договора, сформировать промпт, + вызвать llm_client, распарсить ответ в строки спецификации. + +Это важно: если поменяется LLM — меняем только `llm_client.py`. +Если поменяется формат ответа — только `extractor.py`. + +**Итоговый состав слоёв:** + +``` +parser.py bytes → elements JSON (уже есть, не трогать) +textify.py elements → текст для LLM (уже есть, не трогать) +db.py connect() + query() (уже есть, убрать ensure_db) +schema.py DDL: CREATE TABLE IF NOT EXISTS +upload.py сохранить файл в БД (documents) +llm_client.py HTTP к aillm.ru → строка ответа +extractor.py текст → структурированные строки (промпт + парсинг ответа) +differ.py сравнение строк между допниками → список изменений +api.py Blueprint: /contracts, /supplements, /history +app.py сборка слоёв, Flask-приложение +test_routes.py Blueprint /test (уже есть) +``` + +--- + +## 2. В каком порядке создавать + +Каждый этап самодостаточен и проверяем до перехода к следующему. + +### Этап 1 — основа хранения +`schema.py` → DDL всех таблиц. +Запустить `python schema.py` — таблицы созданы. Проверить через `/test sql`. + +### Этап 2 — загрузка файлов +`upload.py` → принять файл, вызвать `parser.parse()`, вызвать `textify.to_text()`, +сохранить в `documents(original_bytes, parsed_text, mime, filename)`. +Добавить POST `/upload` в `app.py`. Проверить curl-ом. + +### Этап 3 — LLM клиент +`llm_client.py` → POST к aillm.ru, вернуть строку. +Проверить отдельно: `python llm_client.py` с тестовым промптом. + +### Этап 4 — извлечение строк спецификации +`extractor.py` → взять `parsed_text`, сформировать промпт, вызвать `llm_client`, +распарсить ответ в список `spec_row`. +Проверить на одном docx через тест-скрипт. + +### Этап 5 — сравнение (diff) +`differ.py` → взять два списка `spec_row`, вернуть изменения. +Это чистая функция: `diff(rows_old, rows_new) → changes`. +Проверить unit-тестом без БД. + +### Этап 6 — API +`api.py` → Blueprint с GET/POST для договоров, допников, истории. +Подключить в `app.py`. + +--- + +## 3. Поток данных между слоями + +Правило: слои передают данные через простые Python-структуры (dict, list). +Никаких прямых вызовов «через слой» — только соседние слои. + +``` +Файл (bytes) + │ + ▼ +parser.parse(bytes, mime) → {"elements": [...]} + │ + ▼ +textify.to_text(elements) → str + │ + ▼ +upload.py: сохранить в DB, получить document_id + │ + ▼ +extractor.extract(parsed_text) → [{"pos": 1, "name": "...", "qty": 10, ...}] + │ (внутри вызывает llm_client.ask(prompt) → str) + │ + ▼ +schema: сохранить строки в spec_rows(document_id, pos, ...) + │ + ▼ +differ.diff(rows_v1, rows_v2) → [{"pos": 3, "field": "qty", "old": 5, "new": 10}] + │ + ▼ +schema: сохранить в spec_history +``` + +Между слоями **нет импортов друг друга**, кроме: +- `upload.py` импортирует `parser` и `textify` (это нормально — upload оркеструет парсинг) +- `extractor.py` импортирует `llm_client` (клиент — зависимость экстрактора) +- `app.py` и `api.py` импортируют всё — они и есть точки сборки + +`db.py` никто не импортирует напрямую, кроме `upload.py`, `extractor.py` и `api.py`. +`schema.py` вызывается только один раз при старте из `app.py`. + +--- + +## 4. Как должен выглядеть app.py + +`app.py` — точка входа и сборки. Бизнес-логики ноль. + +```python +from flask import Flask +import db, schema +from test_routes import test_bp +from api import api_bp + +def create_app(): + app = Flask(__name__) + + # 1. Инициализация схемы при старте + schema.ensure_schema() + + # 2. Регистрация Blueprint-ов + app.register_blueprint(test_bp) + app.register_blueprint(api_bp) + + # 3. Системные маршруты + @app.route("/health") + def health(): + return "OK", 200 + + return app + +if __name__ == "__main__": + create_app().run(host="0.0.0.0", port=5000) +``` + +Правило: если в `app.py` появляется `if`, `for` или бизнес-слово — это уже лишнее. + +--- + +## 5. Потенциальные проблемы + +### LLM не гарантирует структуру ответа +Самая острая проблема. Модель может вернуть текст в произвольном формате, +сломать JSON, пропустить поля, придумать данные. + +**Решение:** +- В `extractor.py` — строгая схема промпта с примером ответа. +- Парсинг ответа через `try/except` с явным возвратом `{"error": "parse_failed", "raw": ответ}`. +- Никогда не падать — помечать строки как `unresolved`. + +### Идентификация изменённой строки в допнике +Самая неоднозначная задача: иногда в допнике новое полное состояние, +иногда — дельта. Определить это автоматически сложно. + +**Решение для `differ.py`:** +- Сначала попробовать детерминированный diff по позиции/артикулу. +- Если совпадение < порога — пометить как `ambiguous`, не фантазировать. +- Заказчик потом разбирает вручную `ambiguous`-записи. + +### Сопоставление артикула с каталогом +Задача нетривиальная, код-имён в спецификациях нет, матч только по описанию. + +**Решение:** вынести в отдельный `matcher.py`, реализовать как отдельный шаг после основного пайплайна. Пометить как `optional`, не блокировать основной поток. + +### Размер документов vs контекст LLM +Большая спецификация (100+ строк) может не влезть в контекст. + +**Решение в `extractor.py`:** разбивать таблицы на чанки, обрабатывать частями, +собирать результат. Это нужно заложить сразу — переделывать потом дороже. + +### Транзакционность при загрузке +Файл загружен → парсинг ок → LLM вызов → ошибка → документ в БД наполовину. + +**Решение:** хранить в `documents` поле `status` (`uploaded` / `parsed` / `extracted` / `error`). +Обновлять после каждого шага. Зависший `uploaded` — сигнал для повтора. + +--- + +## Итог + +| Вопрос | Ответ | +|--------|-------| +| Разбивка слоёв | Правильная. Добавить `schema.py`, разделить LLM на `llm_client` + `extractor` | +| Порядок | schema → upload → llm_client → extractor → differ → api | +| Поток данных | Через dict/list, нет перекрёстных импортов | +| app.py | Только сборка: `ensure_schema()` + `register_blueprint()` | +| Риски | LLM-нестабильность, diff-амбивалентность, чанкинг, транзакционность | + +Всё, что не решается надёжно детерминированно — помечать `unresolved`, не фантазировать. diff --git a/history/sonnet-architecture-question.md b/history/sonnet-architecture-question.md new file mode 100644 index 0000000..c35e24b --- /dev/null +++ b/history/sonnet-architecture-question.md @@ -0,0 +1,81 @@ +# Запрос к Claude Sonnet: архитектура сервиса "Сверка договоров" + +## Контекст + +Мы строим Flask-сервис для автоматизированной обработки договоров и допников. + +Исходная постановка задачи (от заказчика): +1. Спецификации договоров разобрать до структурированного вида +2. Собрать из **цепочки допников кумулятивный статус договора** — состояние во времени. + Важно: иногда в допнике **новое состояние** договора целиком, иногда — **только изменения**, + и нужно идентифицировать изменённую строку. +3. (опционально, малый процент случаев) сопоставить артикул по описанию с каталогом услуг + (кодов в спецификациях нет) + +Критичные ограничения: +- Данные строго конфиденциальны → LLM только своя (aillm.ru 120B), данные не покидают облако +- На каждом этапе возможны исключения — **не фантазировать**, а отметить конкретную + элементарную подзадачу как нерешаемую +- Рассматриваем как задачку для ИИ + +## Ключевое требование заказчика + +**НЕ МОНОЛИТИТЬ.** Всё делать небольшими НЕЗАВИСИМЫМИ слоями. +Каждый слой — отдельный Python-файл, своя зона ответственности. +Слои не должны зависеть друг от друга (или минимально). + +## Что уже сделано + +``` +site/ +├── app.py ← Flask-приложение, класс ContractsApp, только сборка слоёв +├── db.py ← слой БД: connect(), query(), _pg_connect() +├── test_routes.py ← Blueprint /test (статус БД, список таблиц, SQL, createdb) +├── parser.py ← слой парсера: parse(bytes, mime) → полный слепок docx/pdf +├── textify.py ← слой: elements JSON → линейный текст для LLM +├── static/ +└── templates/ +``` + +База: PostgreSQL, всё внутри облачного кластера. +LLM: aillm.ru (120B модель). + +## Что предстоит сделать + +1. **Слой загрузки** — приём файлов через API, сохранение в БД (original_bytes + parsed_text) +2. **Слой LLM** — отправка текста → модель → структурированные строки спецификации +3. **Слой хранения** — таблицы contracts, supplements, spec_rows, spec_history +4. **Слой сравнения (diff)** — сравнение строк между допниками, выявление изменений +5. **Слой API** — отдача истории договора, списка, etc. + +## Вопрос + +Разъясни план архитектуры: + +1. Правильно ли разбиты слои? Может что-то объединить или наоборот — разделить? +2. В каком порядке их создавать? +3. Как организовать поток данных между слоями, чтобы они оставались независимыми? +4. Как должен выглядеть основной оркестратор (app.py) — без бизнес-логики? +5. Какие потенциальные проблемы ты видишь с таким подходом? + +## Текущий код для ознакомления + +### parser.py (bytes → JSON elements) +Использует python-docx для docx, libreoffice для .doc, pdfplumber для PDF, zipfile для zip. +Возвращает полный слепок: все абзацы (со стилями) + все таблицы (все строки). +Ничего не фильтрует, не теряет. + +### textify.py (JSON elements → текст) +Форматирует elements в линейный текст: параграфы как `[Style] text`, таблицы как `| cell | cell |`. +Тоже не фильтрует, только форматирует. + +### db.py (БД) +connect() в целевую БД, _pg_connect(dbname) в любую, query(sql) — выполнить произвольный запрос. + +### test_routes.py (/test Blueprint) +GET /test — статус БД, POST /test с действиями: status, tables, sql, createdb. +Мост к БД извне для отладки. + +## Ожидаемый формат ответа + +Структурированный план архитектуры с пояснениями по каждому пункту вопроса.