v2.0.15: фиксы по ревью Соннета (10 находок)
Deploy contracts-flask / validate (push) Canceled after 0s
Deploy contracts-flask / validate (push) Canceled after 0s
- #1 process.py: target_id→target_hash (UPDATE/DELETE больше не UNRESOLVED) - #2 Dockerfile: COPY upload - #3 prompts.py: убран created_by из SELECT - #4 prompts_bp.py: save_new_version/delete_prompt - #5 llm.py/classify.py: LLM-конфиг из config.py - #6/#7 compare.js: escHtml (XSS) - #8-#10: комментарии + мёртвый gen_random_uuid
This commit is contained in:
@@ -7,6 +7,7 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY site /app/site
|
COPY site /app/site
|
||||||
|
COPY upload /app/upload
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"""Конфигурация приложения — все настройки в одном месте."""
|
"""Конфигурация приложения — все настройки в одном месте."""
|
||||||
import os
|
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_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
|
||||||
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
||||||
|
|||||||
@@ -206,10 +206,6 @@ def _pg_to_sqlite(sql):
|
|||||||
sql = sql.replace("%s", "?")
|
sql = sql.replace("%s", "?")
|
||||||
# ::jsonb → убрать (SQLite не типизирует)
|
# ::jsonb → убрать (SQLite не типизирует)
|
||||||
sql = sql.replace("::jsonb", "")
|
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
|
# BOOLEAN → INTEGER
|
||||||
sql = sql.replace(" BOOLEAN ", " INTEGER ")
|
sql = sql.replace(" BOOLEAN ", " INTEGER ")
|
||||||
sql = sql.replace(" bool ", " INTEGER ")
|
sql = sql.replace(" bool ", " INTEGER ")
|
||||||
|
|||||||
+1
-1
@@ -85,7 +85,7 @@ def seed_defaults():
|
|||||||
def list_by_role(role):
|
def list_by_role(role):
|
||||||
"""List all versions for a role, newest first."""
|
"""List all versions for a role, newest first."""
|
||||||
rows = query(
|
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,),
|
(role,),
|
||||||
)
|
)
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
|||||||
+1
-1
@@ -201,7 +201,7 @@ def _build_spec_text(current_spec: list) -> str:
|
|||||||
def build_prompt(current_spec: list, doc_text: str) -> tuple:
|
def build_prompt(current_spec: list, doc_text: str) -> tuple:
|
||||||
"""
|
"""
|
||||||
Формирует промпт для LLM.
|
Формирует промпт для LLM.
|
||||||
1. Пробует получить активный промпт из БД (Lucee API).
|
1. Пробует получить активный промпт из БД (SQLite, db.prompts).
|
||||||
2. При неудаче — fallback на хардкод.
|
2. При неудаче — fallback на хардкод.
|
||||||
Возвращает (текст_промпта, prompt_id).
|
Возвращает (текст_промпта, prompt_id).
|
||||||
prompt_id — UUID версии промпта из БД, или "" если fallback.
|
prompt_id — UUID версии промпта из БД, или "" если fallback.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def prompts_save():
|
|||||||
prompt_body = body.get("body", "")
|
prompt_body = body.get("body", "")
|
||||||
notes = body.get("notes", "")
|
notes = body.get("notes", "")
|
||||||
try:
|
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"])
|
return jsonify(ok=True, id=p["id"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify(ok=False, error=str(e)), 500
|
return jsonify(ok=False, error=str(e)), 500
|
||||||
@@ -65,7 +65,7 @@ def prompts_delete():
|
|||||||
return jsonify(ok=False, error="body required"), 400
|
return jsonify(ok=False, error="body required"), 400
|
||||||
prompt_id = body.get("id")
|
prompt_id = body.get("id")
|
||||||
try:
|
try:
|
||||||
db_prompts.delete(prompt_id)
|
db_prompts.delete_prompt(prompt_id)
|
||||||
return jsonify(ok=True)
|
return jsonify(ok=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify(ok=False, error=str(e)), 500
|
return jsonify(ok=False, error=str(e)), 500
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Classify service — LLM-based document classification.
|
|||||||
- Двухпроходная архитектура: LLM извлекает строки (тип/номер/дата/контрагент),
|
- Двухпроходная архитектура: LLM извлекает строки (тип/номер/дата/контрагент),
|
||||||
Python в grouping.py нормализует и группирует детерминированно.
|
Python в grouping.py нормализует и группирует детерминированно.
|
||||||
"""
|
"""
|
||||||
import json, re, os
|
import json, re
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -23,9 +23,7 @@ log = __import__("logging").getLogger(__name__)
|
|||||||
# Увеличивать осторожно — api.aillm.ru может троттлить
|
# Увеличивать осторожно — api.aillm.ru может троттлить
|
||||||
MAX_WORKERS = 4
|
MAX_WORKERS = 4
|
||||||
|
|
||||||
LLM_URL = "https://api.aillm.ru/v1/chat/completions"
|
from config import LLM_URL, LLM_KEY, LLM_MODEL
|
||||||
LLM_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "")
|
|
||||||
LLM_MODEL = "gpt-oss-120b"
|
|
||||||
|
|
||||||
# Ленивый singleton — обратная совместимость
|
# Ленивый singleton — обратная совместимость
|
||||||
_classify_llm = None
|
_classify_llm = None
|
||||||
@@ -91,7 +89,7 @@ def classify_batch(batch_id, llm_client=None, repo=None):
|
|||||||
_db = repo if repo else db_docs
|
_db = repo if repo else db_docs
|
||||||
_llm = llm_client if llm_client else _get_classify_client()
|
_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)
|
_db.reset_classify_status(batch_id)
|
||||||
pending = _db.list_pending(batch_id)
|
pending = _db.list_pending(batch_id)
|
||||||
if not pending:
|
if not pending:
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"""LLM service — call LLM API with optional DI."""
|
"""LLM service — call LLM API with optional DI."""
|
||||||
import os, json
|
import json
|
||||||
|
|
||||||
LLM_URL = "https://api.aillm.ru/v1/chat/completions"
|
from config import LLM_URL, LLM_KEY, LLM_MODEL
|
||||||
LLM_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "")
|
|
||||||
LLM_MODEL = "gpt-oss-120b"
|
|
||||||
|
|
||||||
# Ленивый singleton — обратная совместимость
|
# Ленивый singleton — обратная совместимость
|
||||||
_llm_client = None
|
_llm_client = None
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn):
|
|||||||
ops = result.get("ops", [])
|
ops = result.get("ops", [])
|
||||||
mode = result.get("mode", "llm")
|
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 {
|
yield {
|
||||||
"type": "llm_done",
|
"type": "llm_done",
|
||||||
"supplement_id": sid,
|
"supplement_id": sid,
|
||||||
|
|||||||
@@ -90,15 +90,15 @@ function applyCompareEvent(sections, event) {
|
|||||||
*/
|
*/
|
||||||
function renderCompareSectionHeader(sec) {
|
function renderCompareSectionHeader(sec) {
|
||||||
if (sec.status === 'extracting') {
|
if (sec.status === 'extracting') {
|
||||||
return '⏳ ' + sec.filename;
|
return '⏳ ' + escHtml(sec.filename);
|
||||||
}
|
}
|
||||||
if (sec.status === 'llm_done' || sec.status === 'applied') {
|
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') {
|
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=жёлтый
|
// Цвет строки зависит от действия: ADD=зелёный, DELETE=красный, UPDATE=жёлтый
|
||||||
var cls = action === 'ADD' ? 'diff-added' : action === 'DELETE' ? 'diff-deleted' : action === 'UPDATE' ? 'diff-changed' : '';
|
var cls = action === 'ADD' ? 'diff-added' : action === 'DELETE' ? 'diff-deleted' : action === 'UPDATE' ? 'diff-changed' : '';
|
||||||
var nr = op.new_row || {};
|
var nr = op.new_row || {};
|
||||||
html += '<tr class="' + cls + '"><td>' + action + '</td><td>' + (nr.name||'') + '</td><td class="num-cell">' + (nr.price!=null?nr.price:'') + '</td><td class="num-cell">' + (nr.qty!=null?nr.qty:'') + '</td><td class="num-cell">' + (nr.sum!=null?nr.sum:'') + '</td><td>' + (nr.date_start||'') + '</td></tr>';
|
html += '<tr class="' + cls + '"><td>' + action + '</td><td>' + escHtml(nr.name||'') + '</td><td class="num-cell">' + (nr.price!=null?nr.price:'') + '</td><td class="num-cell">' + (nr.qty!=null?nr.qty:'') + '</td><td class="num-cell">' + (nr.sum!=null?nr.sum:'') + '</td><td>' + (nr.date_start||'') + '</td></tr>';
|
||||||
});
|
});
|
||||||
html += '</tbody></table></div>';
|
html += '</tbody></table></div>';
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/static/logo.svg" alt="Nubes">
|
<img src="/static/logo.svg" alt="Nubes">
|
||||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.14</span></span>
|
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.15</span></span>
|
||||||
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
||||||
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
||||||
<span id="stepClassify">○ Классификация</span><span>→</span>
|
<span id="stepClassify">○ Классификация</span><span>→</span>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<script src="/static/app_utils.js?v=2.0.8"></script>
|
<script src="/static/app_utils.js?v=2.0.8"></script>
|
||||||
<script src="/static/files.js?v=2.0.14"></script>
|
<script src="/static/files.js?v=2.0.14"></script>
|
||||||
<script src="/static/groups.js?v=2.0.8"></script>
|
<script src="/static/groups.js?v=2.0.8"></script>
|
||||||
<script src="/static/compare.js?v=2.0.8"></script>
|
<script src="/static/compare.js?v=2.0.15"></script>
|
||||||
<script src="/static/app.js?v=2.0.13"></script>
|
<script src="/static/app.js?v=2.0.13"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user