Files
contracts-flask/site/db/prompts.py
T
naeel aafb038921
Deploy contracts-flask / validate (push) Successful in 0s
fix: db/*.py imports — from db.connection, not just connection
2026-07-15 11:40:59 +04:00

152 lines
8.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.
"""Prompts CRUD."""
from db.connection import query, execute, execute_returning
def _serialize(row):
"""Convert datetime fields to strings for JSON serialization (non-mutating)."""
if row and row.get("created_at"):
row = dict(row)
row["created_at"] = str(row["created_at"])
return row
def get_active(role):
rows = query(
"SELECT * FROM prompts WHERE role = %s AND is_active = true ORDER BY created_at DESC LIMIT 1",
(role,),
)
return _serialize(rows[0]) if rows else None
def get(prompt_id):
rows = query("SELECT * FROM prompts WHERE id = %s", (prompt_id,))
return rows[0] if rows else None
def seed_defaults():
"""Auto-seed default prompts if table is empty."""
# Check each role separately — don't skip if one is missing
existing_roles = set(r["role"] for r in query("SELECT DISTINCT role FROM prompts"))
if "extract" not in existing_roles:
extract_body = (
"Ты — анализатор договоров облачного провайдера.\n\n"
"Ниже текст спецификации услуг из ПЕРВОГО документа (базовый договор).\n"
"Извлеки ВСЕ строки спецификации в JSON-массив.\n\n"
"Верни СТРОГО JSON без пояснений. Не используй markdown-блоки.\n\n"
"ФОРМАТ:\n"
'{\n "mode": "partial",\n "ops": [\n'
' {"action": "ADD", "new_row": {"name": "полное наименование", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}, "comment": ""}\n'
" ]\n}\n\n"
"ПРАВИЛА:\n"
"1. Извлеки КАЖДУЮ строку таблицы спецификации как отдельную ADD-операцию.\n"
'2. Пропускай итоговые строки ("Итого...") и строки с подписями.\n'
"3. Если ячейка пустая — ставь null (не пиши 0).\n"
"4. price, qty, sum — ЧИСЛА, не строки.\n\n"
"ТЕКСТ ДОКУМЕНТА:\n---\n{doc_text}\n---"
)
execute(
"INSERT INTO prompts (role, name, body, is_active, notes) VALUES (%s, %s, %s, %s, %s)",
("extract", "default-v1", extract_body, True, "Авто-создан из llm_prompt.py _build_initial"),
)
if "diff" not in existing_roles:
diff_body = (
"Ты — анализатор допсоглашений к договорам облачного провайдера.\n\n"
"У тебя есть текущая спецификация услуг и текст нового допсоглашения (ДС).\n"
"Твоя задача — определить, какие изменения вносит ДС в текущую спецификацию.\n\n"
"Верни СТРОГО JSON без пояснений. Не используй markdown-блоки.\n\n"
"ФОРМАТ:\n"
'{\n "mode": "partial" | "full_replace",\n "ops": [\n'
' {"action": "ADD", "new_row": {"name": "...", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}, "comment": "..."},\n'
' {"action": "UPDATE", "target_id": "rN", "new_values": {"price": число}, "comment": "..."},\n'
' {"action": "DELETE", "target_id": "rN", "comment": "..."},\n'
' {"action": "UNRESOLVED", "new_values": {"name": "...", "price": число, ...}, "reason": "почему не смог сопоставить"}\n'
" ]\n}\n\n"
"ПРАВИЛА:\n"
'1. mode = "full_replace" — если в тексте есть фразы: «в следующей редакции», «заменить приложение», «излагается в следующей редакции». При full_replace — опиши ВСЕ новые строки как ADD.\n'
'2. mode = "partial" — если ДС меняет только отдельные строки.\n'
"3. Для UPDATE/DELETE — укажи target_id (r1, r2...) из списка текущей спецификации. НЕ придумывай новые id.\n"
"4. new_values в UPDATE — только ИЗМЕНЁННЫЕ поля (не все).\n"
"5. Если не можешь однозначно сопоставить строку — action: UNRESOLVED с reason.\n"
"6. Пропускай итоговые строки и подписи.\n"
"7. price, qty, sum — ЧИСЛА (не строки).\n\n"
"ТЕКУЩАЯ СПЕЦИФИКАЦИЯ:\n{spec_current}\n\n"
"ТЕКСТ ДОПСОГЛАШЕНИЯ:\n---\n{doc_text}\n---"
)
execute(
"INSERT INTO prompts (role, name, body, is_active, notes) VALUES (%s, %s, %s, %s, %s)",
("diff", "default-v1", diff_body, True, "Авто-создан из llm_prompt.py _build_diff"),
)
_ensure_classify_prompt()
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",
(role,),
)
for r in rows:
if r.get("created_at"):
r["created_at"] = str(r["created_at"])
return rows
def save_new_version(role, name, body, notes="", is_active=True):
"""Save new prompt version. Deactivates all others for this role, inserts new one."""
if is_active:
execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,))
return execute_returning(
"""INSERT INTO prompts (role, name, body, is_active, notes)
VALUES (%s, %s, %s, %s, %s) RETURNING *""",
(role, name, body, is_active, notes),
)
def activate(prompt_id):
"""Activate a prompt version (deactivates others for same role)."""
row = query("SELECT role FROM prompts WHERE id=%s", (prompt_id,))
if not row:
return False
role = row[0]["role"]
execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,))
execute("UPDATE prompts SET is_active=true WHERE id=%s", (prompt_id,))
return True
def delete_prompt(prompt_id):
"""Delete a prompt version (cannot delete active one)."""
row = query("SELECT is_active FROM prompts WHERE id=%s", (prompt_id,))
if not row:
return False
if row[0]["is_active"]:
return False # cannot delete active
execute("DELETE FROM prompts WHERE id=%s", (prompt_id,))
return True
def _ensure_classify_prompt():
"""Ensure classify prompt exists (idempotent)."""
existing = query("SELECT id FROM prompts WHERE role = 'classify' AND is_active = true LIMIT 1")
if existing:
return
body = (
"Ты — система классификации договорных документов. "
"Проанализируй текст и верни СТРОГО ВАЛИДНЫЙ JSON ОДНОЙ СТРОКОЙ "
"(без переносов строк, без markdown, без лишних пробелов в начале/конце).\n\n"
"Поля:\n"
'- doc_type: "contract" (договор) / "supplement" (допсоглашение) / "specification" (спецификация/приложение) / "other"\n'
"- own_number: номер ЭТОГО документа, строка без лишних пробелов (или null)\n"
'- parent_number: номер родительского договора из фразы «к Договору №...» (или null)\n'
'- doc_date: дата в YYYY-MM-DD. «27 февраля 2026» → 2026-02-27 (или null)\n'
"- counterparty: полное название контрагента (Заказчик/Арендатор), без сокращений (или null)\n\n"
"Пример вывода:\n"
'{"doc_type":"supplement","own_number":"1","parent_number":"01300_2","doc_date":"2026-02-27","counterparty":"АО XXX003"}\n\n'
"ДОКУМЕНТ:\n---\n{header_text}\n---"
)
execute(
"INSERT INTO prompts (role, name, body, is_active, notes) VALUES (%s, %s, %s, %s, %s)",
("classify", "default-v1", body, True, "Авто-создан: classify prompt v2 — LLM сам предложил"),
)