Files
contractor/deploy/llm_prompt.py
T

123 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()
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) -> str:
"""
Формирует промпт для LLM.
1. Пробует получить активный промпт из БД (Lucee API).
2. При неудаче — fallback на хардкод.
Возвращает готовый текст промпта.
"""
is_first = len(current_spec) == 0
role = "extract" if is_first else "diff"
db = _fetch_prompt(role)
if db:
template = db["body"]
else:
template = FALLBACK_EXTRACT if is_first else FALLBACK_DIFF
# Подстановка плейсхолдеров
result = template.replace("{doc_text}", doc_text)
result = result.replace("{spec_current}", _build_spec_text(current_spec))
return result