123 lines
5.5 KiB
Python
123 lines
5.5 KiB
Python
"""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
|