diff --git a/Dockerfile b/Dockerfile index 174e281..36910c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY site /app/site +COPY upload /app/upload EXPOSE 5000 diff --git a/site/config.py b/site/config.py index 888ef1d..3601948 100644 --- a/site/config.py +++ b/site/config.py @@ -1,7 +1,7 @@ """Конфигурация приложения — все настройки в одном месте.""" import os -VERSION = "2.0.14" +VERSION = "2.0.15" LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions") LLM_KEY = os.getenv("LLM_API_KEY", "") diff --git a/site/db/connection.py b/site/db/connection.py index 3b96c15..fadf220 100644 --- a/site/db/connection.py +++ b/site/db/connection.py @@ -206,10 +206,6 @@ def _pg_to_sqlite(sql): sql = sql.replace("%s", "?") # ::jsonb → убрать (SQLite не типизирует) sql = sql.replace("::jsonb", "") - # gen_random_uuid() → хекс-UUID через randomblob - if "gen_random_uuid()" in sql: - import uuid - sql = sql.replace("gen_random_uuid()", "?") # BOOLEAN → INTEGER sql = sql.replace(" BOOLEAN ", " INTEGER ") sql = sql.replace(" bool ", " INTEGER ") diff --git a/site/db/prompts.py b/site/db/prompts.py index 605a817..c0a58a1 100644 --- a/site/db/prompts.py +++ b/site/db/prompts.py @@ -85,7 +85,7 @@ def seed_defaults(): def list_by_role(role): """List all versions for a role, newest first.""" rows = query( - "SELECT id, role, name, is_active, created_by, notes, created_at FROM prompts WHERE role=%s ORDER BY created_at DESC", + "SELECT id, role, name, is_active, notes, created_at FROM prompts WHERE role=%s ORDER BY created_at DESC", (role,), ) for r in rows: diff --git a/site/llm_prompt.py b/site/llm_prompt.py index 7b64351..3fb3f7c 100644 --- a/site/llm_prompt.py +++ b/site/llm_prompt.py @@ -201,7 +201,7 @@ def _build_spec_text(current_spec: list) -> str: def build_prompt(current_spec: list, doc_text: str) -> tuple: """ Формирует промпт для LLM. - 1. Пробует получить активный промпт из БД (Lucee API). + 1. Пробует получить активный промпт из БД (SQLite, db.prompts). 2. При неудаче — fallback на хардкод. Возвращает (текст_промпта, prompt_id). prompt_id — UUID версии промпта из БД, или "" если fallback. diff --git a/site/routes/prompts_bp.py b/site/routes/prompts_bp.py index 40cf565..5c70c5e 100644 --- a/site/routes/prompts_bp.py +++ b/site/routes/prompts_bp.py @@ -39,7 +39,7 @@ def prompts_save(): prompt_body = body.get("body", "") notes = body.get("notes", "") try: - p = db_prompts.insert(role, name, prompt_body, notes) + p = db_prompts.save_new_version(role, name, prompt_body, notes) return jsonify(ok=True, id=p["id"]) except Exception as e: return jsonify(ok=False, error=str(e)), 500 @@ -65,7 +65,7 @@ def prompts_delete(): return jsonify(ok=False, error="body required"), 400 prompt_id = body.get("id") try: - db_prompts.delete(prompt_id) + db_prompts.delete_prompt(prompt_id) return jsonify(ok=True) except Exception as e: return jsonify(ok=False, error=str(e)), 500 diff --git a/site/services/classify.py b/site/services/classify.py index f5cca6b..3498075 100644 --- a/site/services/classify.py +++ b/site/services/classify.py @@ -10,7 +10,7 @@ Classify service — LLM-based document classification. - Двухпроходная архитектура: LLM извлекает строки (тип/номер/дата/контрагент), Python в grouping.py нормализует и группирует детерминированно. """ -import json, re, os +import json, re from concurrent.futures import ThreadPoolExecutor, as_completed import httpx @@ -23,9 +23,7 @@ log = __import__("logging").getLogger(__name__) # Увеличивать осторожно — api.aillm.ru может троттлить MAX_WORKERS = 4 -LLM_URL = "https://api.aillm.ru/v1/chat/completions" -LLM_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "") -LLM_MODEL = "gpt-oss-120b" +from config import LLM_URL, LLM_KEY, LLM_MODEL # Ленивый singleton — обратная совместимость _classify_llm = None @@ -91,7 +89,7 @@ def classify_batch(batch_id, llm_client=None, repo=None): _db = repo if repo else db_docs _llm = llm_client if llm_client else _get_classify_client() - # Сбросить статус — allow re-classify after file changes + # Сбросить 'processing' -> 'pending' (crash recovery); уже классифицированные не трогаем _db.reset_classify_status(batch_id) pending = _db.list_pending(batch_id) if not pending: diff --git a/site/services/llm.py b/site/services/llm.py index 993d0d1..9cefa08 100644 --- a/site/services/llm.py +++ b/site/services/llm.py @@ -1,9 +1,7 @@ """LLM service — call LLM API with optional DI.""" -import os, json +import json -LLM_URL = "https://api.aillm.ru/v1/chat/completions" -LLM_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "") -LLM_MODEL = "gpt-oss-120b" +from config import LLM_URL, LLM_KEY, LLM_MODEL # Ленивый singleton — обратная совместимость _llm_client = None diff --git a/site/services/process.py b/site/services/process.py index 327bfb6..ea4a646 100644 --- a/site/services/process.py +++ b/site/services/process.py @@ -77,6 +77,18 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn): ops = result.get("ops", []) mode = result.get("mode", "llm") + # Трансляция target_id ("r1","r2"...) → target_hash (name_hash из current_spec). + # LLM возвращает target_id, а apply_ops() читает target_hash — без этого UPDATE/DELETE уходят в UNRESOLVED. + for _op in ops: + _tid = _op.get("target_id", "") + if _tid and isinstance(_tid, str) and _tid.startswith("r"): + try: + _idx = int(_tid[1:]) - 1 + if 0 <= _idx < len(current_spec): + _op["target_hash"] = current_spec[_idx]["hash"] + except ValueError: + pass + yield { "type": "llm_done", "supplement_id": sid, diff --git a/site/static/compare.js b/site/static/compare.js index 23daa56..692aa02 100644 --- a/site/static/compare.js +++ b/site/static/compare.js @@ -90,15 +90,15 @@ function applyCompareEvent(sections, event) { */ function renderCompareSectionHeader(sec) { if (sec.status === 'extracting') { - return '⏳ ' + sec.filename; + return '⏳ ' + escHtml(sec.filename); } if (sec.status === 'llm_done' || sec.status === 'applied') { - return '✓ ' + sec.filename + ' — ' + sec.ops_count + ' оп., ' + sec.mode + ' (' + sec.time_s + 'с)'; + return '✓ ' + escHtml(sec.filename) + ' — ' + sec.ops_count + ' оп., ' + sec.mode + ' (' + sec.time_s + 'с)'; } if (sec.status === 'error') { - return '✗ ' + sec.filename + ': ' + sec.error; + return '✗ ' + escHtml(sec.filename) + ': ' + sec.error; } - return sec.filename; + return escHtml(sec.filename); } /** @@ -117,7 +117,7 @@ function renderCompareOpsTable(ops) { // Цвет строки зависит от действия: ADD=зелёный, DELETE=красный, UPDATE=жёлтый var cls = action === 'ADD' ? 'diff-added' : action === 'DELETE' ? 'diff-deleted' : action === 'UPDATE' ? 'diff-changed' : ''; var nr = op.new_row || {}; - html += '' + action + '' + (nr.name||'') + '' + (nr.price!=null?nr.price:'') + '' + (nr.qty!=null?nr.qty:'') + '' + (nr.sum!=null?nr.sum:'') + '' + (nr.date_start||'') + ''; + html += '' + action + '' + escHtml(nr.name||'') + '' + (nr.price!=null?nr.price:'') + '' + (nr.qty!=null?nr.qty:'') + '' + (nr.sum!=null?nr.sum:'') + '' + (nr.date_start||'') + ''; }); html += ''; return html; diff --git a/site/templates/index.html b/site/templates/index.html index e8be084..00a714b 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -73,7 +73,7 @@
Nubes - Сверка договоров — LLM AI-driven Event Sourcing v2.0.14 + Сверка договоров — LLM AI-driven Event Sourcing v2.0.15
○ Загрузка ○ Классификация @@ -228,7 +228,7 @@ - +