"""llm_prompt.py — Формирование промпта для LLM-анализа ДС. Читает активный промпт из БД (через Lucee API). При ошибке — fallback на хардкод.""" import os import httpx LUCEE_URL = os.environ.get("LUCEE_URL", "https://contractor.luceek8s.dev.nubes.ru") # ── Fallback-промпты (если БД недоступна) ────────────────────── FALLBACK_EXTRACT = """Ты — анализатор договоров облачного провайдера. Ниже текст спецификации услуг из ПЕРВОГО документа (базовый договор). Извлеки ВСЕ строки спецификации в JSON-массив. Верни СТРОГО JSON без пояснений. Не используй markdown-блоки. ФОРМАТ: {{ "mode": "partial", "ops": [ {{"action": "ADD", "new_row": {{"name": "полное наименование", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}}, "comment": ""}} ] }} ПРАВИЛА: 1. Извлеки КАЖДУЮ строку таблицы спецификации как отдельную ADD-операцию. 2. Пропускай итоговые строки ("Итого...") и строки с подписями. 3. Если ячейка пустая — ставь null (не пиши 0). 4. price, qty, sum — ЧИСЛА, не строки. ТЕКСТ ДОКУМЕНТА: --- {doc_text} ---""" FALLBACK_DIFF = """Ты — анализатор допсоглашений к договорам облачного провайдера. У тебя есть текущая спецификация услуг и текст нового допсоглашения (ДС). Твоя задача — определить, какие изменения вносит ДС в текущую спецификацию. Верни СТРОГО JSON без пояснений. Не используй markdown-блоки. ФОРМАТ: {{ "mode": "partial" | "full_replace", "ops": [ {{"action": "ADD", "new_row": {{"name": "...", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}}, "comment": "..."}}, {{"action": "UPDATE", "target_id": "rN", "new_values": {{"price": число}}, "comment": "..."}}, {{"action": "DELETE", "target_id": "rN", "comment": "..."}}, {{"action": "UNRESOLVED", "new_values": {{"name": "...", "price": число, ...}}, "reason": "почему не смог сопоставить"}} ] }} ПРАВИЛА: 1. mode = "full_replace" — если в тексте есть фразы: «в следующей редакции», «заменить приложение», «излагается в следующей редакции». При full_replace — опиши ВСЕ новые строки как ADD. 2. mode = "partial" — если ДС меняет только отдельные строки. 3. Для UPDATE/DELETE — укажи target_id (r1, r2...) из списка текущей спецификации. НЕ придумывай новые id. 4. new_values в UPDATE — только ИЗМЕНЁННЫЕ поля (не все). 5. Если не можешь однозначно сопоставить строку — action: UNRESOLVED с reason. 6. Пропускай итоговые строки и подписи. 7. price, qty, sum — ЧИСЛА (не строки). ТЕКУЩАЯ СПЕЦИФИКАЦИЯ: {spec_current} ТЕКСТ ДОПСОГЛАШЕНИЯ: --- {doc_text} ---""" def _fetch_prompt(role: str) -> dict | None: """Получить активный промпт из БД. Возвращает {id, body} или None.""" try: with httpx.Client(http2=True, timeout=10, verify=False) as client: resp = client.get( f"{LUCEE_URL}/prompt.cfm", params={"action": "get_active", "role": role}, ) resp.raise_for_status() data = resp.json() # Lucee serializeJSON → UPPERCASE keys, normalize to lowercase data = {k.lower(): v for k, v in data.items()} if data.get("ok") and data.get("body"): return {"id": data.get("id", ""), "body": data["body"]} except Exception: pass return None def _build_spec_text(current_spec: list) -> str: """Перечисление строк спецификации для подстановки в {spec_current}.""" lines = [] for i, r in enumerate(current_spec): lines.append( f"[id: r{i+1}] {r.get('name', '?')} | " f"цена={r.get('price', '')} | объём={r.get('qty', '')} | " f"сумма={r.get('sum', '')} | начало={r.get('date_start', '')}" ) return "\n".join(lines) def build_prompt(current_spec: list, doc_text: str) -> tuple: """ Формирует промпт для LLM. 1. Пробует получить активный промпт из БД (Lucee API). 2. При неудаче — fallback на хардкод. Возвращает (текст_промпта, prompt_id). prompt_id — UUID версии промпта из БД, или "" если fallback. """ is_first = len(current_spec) == 0 role = "extract" if is_first else "diff" db = _fetch_prompt(role) if db: template = db["body"] prompt_id = db.get("id", "") else: template = FALLBACK_EXTRACT if is_first else FALLBACK_DIFF prompt_id = "" # Подстановка плейсхолдеров result = template.replace("{doc_text}", doc_text) result = result.replace("{spec_current}", _build_spec_text(current_spec)) return result, prompt_id