docs: архитектурный анализ + History + gitignore (2026-06-27)

This commit is contained in:
“Naeel”
2026-06-27 13:00:18 +04:00
parent 4a21d77f51
commit 82c5c075f1
154 changed files with 4789 additions and 1443 deletions
+163
View File
@@ -0,0 +1,163 @@
"""
actions.py — API-обёртки для симулятора.
Все запросы к contracts.kube5s.ru с таймаутами.
Эмулирует действия пользователя через UI (загрузка, classify, groups, compare).
"""
import requests, json, time, sys, os, uuid
BASE = "https://contracts.kube5s.ru"
TIMEOUT = 120
# Убрать warnings
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def _post(url, **kw):
return requests.post(url, timeout=TIMEOUT, verify=False, **kw)
def _get(url, **kw):
return requests.get(url, timeout=TIMEOUT, verify=False, **kw)
# ═══════════════════════════════════════════════════════════════
# Действия
# ═══════════════════════════════════════════════════════════════
class Session:
"""Состояние сессии: batch_id, contract_id, files."""
def __init__(self):
self.batch_id = str(uuid.uuid4())
self.contract_id = None
self.files = [] # {name, doc_id, status}
def upload_file(self, path):
"""Загрузить один файл. Возвращает ответ API."""
fname = os.path.basename(path)
with open(path, 'rb') as f:
data = {'batch_id': self.batch_id}
if self.contract_id:
data['contract_id'] = self.contract_id
r = _post(f"{BASE}/upload", files={'files': (fname, f)}, data=data)
result = r.json()
if result.get('ok'):
doc_id = result.get('doc_id')
parsed = result.get('parsed', {})
self.files.append({
'name': fname, 'doc_id': doc_id,
'status': parsed.get('status', 'uploaded'),
'element_count': parsed.get('element_count', 0)
})
if result.get('contract_id'):
self.contract_id = result['contract_id']
return result
def upload_files(self, paths, delay=0.2):
"""Загрузить несколько файлов последовательно."""
for p in paths:
r = self.upload_file(p)
status = r.get('parsed', {}).get('status', r.get('ok', '?'))
print(f"{os.path.basename(p)}: {status}")
time.sleep(delay)
def classify(self):
"""Запустить классификацию, ждать завершения."""
r = _post(f"{BASE}/api/classify-batch", json={'batch_id': self.batch_id})
result = r.json()
if not result.get('ok'):
print(f" ✗ classify failed: {result}")
return result
# Поллить прогресс
total = result.get('total', 0)
print(f" ⏳ classify {total} файлов...")
for _ in range(300): # макс 10 минут (300 × 2с)
time.sleep(2)
pr = _get(f"{BASE}/api/batch-progress?batch={self.batch_id}").json()
done = pr.get('counts', {}).get('classified', 0) + pr.get('counts', {}).get('failed', 0)
if done >= total and total > 0:
print(f" ✓ classify done: {pr.get('counts')}")
return pr
return {"ok": False, "error": "timeout"}
def get_groups(self):
"""Получить группы после классификации."""
r = _get(f"{BASE}/api/groups?batch={self.batch_id}")
data = r.json()
groups = data.get('groups', [])
real = [g for g in groups if g.get('contract_number') != '__unresolved__']
unres = [g for g in groups if g.get('contract_number') == '__unresolved__']
print(f" 📋 группы: {len(real)} (+ {len(unres)} нераспознано)")
for g in real:
print(f"{g.get('contract_number')}{g.get('counterparty','?')} ({len(g.get('documents',[]))} док.)")
return data
def apply_and_compare(self, group_index=0):
"""Применить группу и запустить сравнение через SSE."""
r = _get(f"{BASE}/api/groups?batch={self.batch_id}")
groups = r.json().get('groups', [])
real = [g for g in groups if g.get('contract_number') != '__unresolved__']
if not real:
print(" ✗ нет групп для сравнения")
return None
group = real[group_index]
print(f" ⚖ compare группы №{group.get('contract_number')}...")
# Применить группу
ar = _post(f"{BASE}/api/apply-groups", json={'batch_id': self.batch_id, 'groups': [group]})
ad = ar.json()
if not ad.get('ok') or not ad.get('contract_ids'):
print(f" ✗ apply failed: {ad}")
return None
cid = ad['contract_ids'][0]
# SSE compare
import sseclient # pip install sseclient-py
url = f"{BASE}/process-v2?contract_id={cid}"
response = requests.get(url, stream=True, timeout=300, verify=False)
client = sseclient.SSEClient(response)
sections = {}
total_ops = 0
for event in client.events():
d = json.loads(event.data)
t = d.get('type')
if t == 'extract_start':
sections[d['supplement_id']] = d['filename']
elif t == 'llm_done':
print(f"{sections.get(d['supplement_id'],'?')}: {d.get('ops_count',0)} оп. {d.get('mode','?')} ({d.get('time_s',0)}с)")
elif t == 'applied':
ops = d.get('ops', [])
total_ops += len(ops)
s = d.get('summary', {})
print(f" 📊 +{s.get('added',0)} ~{s.get('updated',0)} -{s.get('deleted',0)}")
elif t == 'done':
print(f" ✓ compare done: {d.get('total_time_s',0)}с, всего {total_ops} оп.")
return d
elif t == 'error':
print(f" ✗ compare error: {d.get('message','?')}")
return None
return None
def cleanup(self):
"""Очистить БД после теста."""
try:
_post(f"{BASE}/api/cleanup")
except Exception:
pass
def upload_files_from_dir(session, dir_path, pattern="*", delay=0.2):
"""Загрузить все файлы из папки."""
import glob
files = sorted(glob.glob(os.path.join(dir_path, pattern)))
files = [f for f in files if f.endswith(('.docx', '.doc', '.pdf', '.zip'))]
if not files:
print(f" (нет файлов в {dir_path})")
return
print(f" Загрузка {len(files)} файлов из {dir_path}...")
session.upload_files(files, delay)