v1.0.113: prompt_version in spec_events — provenance (who created what with which prompt)

This commit is contained in:
2026-06-23 07:08:23 +04:00
parent 534f90a529
commit b75b206bfe
4 changed files with 40 additions and 22 deletions
+27 -12
View File
@@ -7,6 +7,15 @@
<cfset data = deserializeJSON(body)>
<cfset ops = structKeyExists(data, "ops") ? data.ops : []>
<cfset supplementId = structKeyExists(data, "supplement_id") ? data.supplement_id : "">
<cfset promptId = structKeyExists(data, "prompt_id") AND len(data.prompt_id) ? data.prompt_id : "">
<!--- ALTER TABLE: добавить prompt_version если ещё нет --->
<cftry>
<cfquery datasource="baza">
ALTER TABLE spec_events ADD COLUMN IF NOT EXISTS prompt_version UUID
</cfquery>
<cfcatch></cfcatch>
</cftry>
<cfset result = {ok: false, error: ""}>
@@ -44,7 +53,7 @@
<!--- full_replace: явные DELETE для аудита (массовый INSERT) --->
<cfif url.mode EQ "full_replace">
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status, prompt_version)
SELECT
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -52,7 +61,8 @@
'DELETE',
name_hash,
'full_replace',
'applied'
'applied',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
@@ -101,7 +111,7 @@
<cfset ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
<cfquery name="evt" datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status, prompt_version)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -109,7 +119,8 @@
'ADD',
<cfqueryparam value="#serializeJSON(nr)#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
'applied'
'applied',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
)
RETURNING id
</cfquery>
@@ -134,7 +145,7 @@
<cfif NOT structKeyExists(oldValues, th)>
<!--- target_hash не найден — создаём UNRESOLVED вместо падения --->
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status, prompt_version)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -142,7 +153,8 @@
'UNRESOLVED',
<cfqueryparam value="#serializeJSON(structKeyExists(op,'new_values') ? op.new_values : {})#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="UPDATE target_hash not found: #th#" cfsqltype="cf_sql_varchar">,
'pending'
'pending',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
)
</cfquery>
<cfset summary.unresolved++>
@@ -155,7 +167,7 @@
<cfset newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
<cfquery name="evt" datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, new_values, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, new_values, comment, status, prompt_version)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -164,7 +176,8 @@
<cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#serializeJSON(nv)#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
'applied'
'applied',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
)
RETURNING id
</cfquery>
@@ -186,7 +199,7 @@
<cfelseif action EQ "DELETE">
<cfset th = op.target_hash>
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status, prompt_version)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -194,7 +207,8 @@
'DELETE',
<cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
'applied'
'applied',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
)
</cfquery>
<cfquery datasource="baza">
@@ -206,7 +220,7 @@
<cfelseif action EQ "UNRESOLVED">
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status, prompt_version)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
@@ -214,7 +228,8 @@
'UNRESOLVED',
<cfqueryparam value="#serializeJSON(structKeyExists(op,'new_values') ? op.new_values : {})#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="#structKeyExists(op,'reason') ? op.reason : ''#" cfsqltype="cf_sql_varchar">,
'pending'
'pending',
<cfif len(promptId)><cfqueryparam value="#promptId#" cfsqltype="cf_sql_varchar">::uuid<cfelse>NULL</cfif>
)
</cfquery>
<cfset summary.unresolved++>
+6 -6
View File
@@ -19,8 +19,8 @@ LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
def call_llm(current_spec, doc_text):
"""Вызов LLM API. Возвращает {mode, ops} или кидает исключение."""
prompt = build_prompt(current_spec, doc_text)
"""Вызов LLM API. Возвращает ({mode, ops}, prompt_id) или кидает исключение."""
prompt, prompt_id = build_prompt(current_spec, doc_text)
payload = {
"model": LLM_MODEL,
"messages": [{"role": "user", "content": prompt}],
@@ -39,7 +39,7 @@ def call_llm(current_spec, doc_text):
json_text = json_text.split("```json")[1].split("```")[0]
elif "```" in json_text:
json_text = json_text.split("```")[1].split("```")[0]
return json.loads(json_text.strip())
return json.loads(json_text.strip()), prompt_id
class Handler(BaseHTTPRequestHandler):
@@ -215,7 +215,7 @@ class Handler(BaseHTTPRequestHandler):
"error": error[0], "time_s": elapsed})
continue
llm_result = result[0]
llm_result, prompt_id = result[0]
ops = llm_result.get("ops", [])
mode = llm_result.get("mode", "partial")
@@ -237,7 +237,7 @@ class Handler(BaseHTTPRequestHandler):
# Применить
apply_resp = httpx.post(
f"{LUCEE_URL}/apply_events.cfm?contract_id={cid}&mode={mode}",
json={"supplement_id": sid, "ops": ops}, timeout=30
json={"supplement_id": sid, "ops": ops, "prompt_id": prompt_id}, timeout=30
)
if apply_resp.status_code == 200:
ar = apply_resp.json()
@@ -258,7 +258,7 @@ class Handler(BaseHTTPRequestHandler):
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
try:
result = call_llm(body.get("current_spec", []), body.get("doc_text", ""))
result, prompt_id = call_llm(body.get("current_spec", []), body.get("doc_text", ""))
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
+6 -3
View File
@@ -101,12 +101,13 @@ def _build_spec_text(current_spec: list) -> str:
return "\n".join(lines)
def build_prompt(current_spec: list, doc_text: str) -> str:
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"
@@ -114,11 +115,13 @@ def build_prompt(current_spec: list, doc_text: str) -> str:
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
return result, prompt_id
+1 -1
View File
@@ -75,7 +75,7 @@
<body>
<div class="topbar">
<img src="/nubes-logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.112 — Lucee</span></span>
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.113 — Lucee</span></span>
</div>
<div class="content">