v1.0.110: prompts versioning — DB schema + API + UI (extract/diff tabs, history, activate)
This commit is contained in:
+232
-26
@@ -6,48 +6,254 @@
|
||||
|
||||
<!--- Создать таблицу при первом обращении --->
|
||||
<cftry>
|
||||
<cfquery datasource="baza">
|
||||
DROP TABLE IF EXISTS prompts CASCADE
|
||||
</cfquery>
|
||||
<cfquery datasource="baza">
|
||||
CREATE TABLE IF NOT EXISTS prompts (
|
||||
contract_id UUID PRIMARY KEY REFERENCES contracts(id),
|
||||
prompt_text TEXT NOT NULL
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
parent_id UUID REFERENCES prompts(id),
|
||||
is_active BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
created_by VARCHAR(50) DEFAULT 'system',
|
||||
notes TEXT
|
||||
)
|
||||
</cfquery>
|
||||
<cfcatch></cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfparam name="url.contract_id" default="">
|
||||
<!--- Авто-сид: если таблица пуста — создать дефолтные промпты --->
|
||||
<cftry>
|
||||
<cfquery name="cntCheck" datasource="baza">SELECT COUNT(*) as cnt FROM prompts</cfquery>
|
||||
<cfif cntCheck.cnt EQ 0>
|
||||
<!--- Дефолтный промпт для extract (первый документ) --->
|
||||
<cfquery datasource="baza">
|
||||
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
||||
'extract', 'default-v1',
|
||||
'Ты — анализатор договоров облачного провайдера.
|
||||
|
||||
Ниже текст спецификации услуг из ПЕРВОГО документа (базовый договор).
|
||||
Извлеки ВСЕ строки спецификации в 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}
|
||||
---',
|
||||
true, 'Авто-создан из llm_prompt.py _build_initial'
|
||||
)
|
||||
</cfquery>
|
||||
<!--- Дефолтный промпт для diff (сравнение с текущей спецификацией) --->
|
||||
<cfquery datasource="baza">
|
||||
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
||||
'diff', 'default-v1',
|
||||
'Ты — анализатор допсоглашений к договорам облачного провайдера.
|
||||
|
||||
У тебя есть текущая спецификация услуг и текст нового допсоглашения (ДС).
|
||||
Твоя задача — определить, какие изменения вносит ДС в текущую спецификацию.
|
||||
|
||||
Верни СТРОГО 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}
|
||||
---',
|
||||
true, 'Авто-создан из llm_prompt.py _build_diff'
|
||||
)
|
||||
</cfquery>
|
||||
</cfif>
|
||||
<cfcatch></cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfparam name="url.action" default="get_active">
|
||||
|
||||
<cfset result = {ok: false, error: ""}>
|
||||
|
||||
<cftry>
|
||||
<cfif NOT len(url.contract_id)>
|
||||
<cfset result.error = "contract_id required">
|
||||
<cfelseif cgi.request_method EQ "POST">
|
||||
<!--- Сохранить промпт --->
|
||||
<cfif url.action EQ "get_active" AND structKeyExists(url, "role")>
|
||||
<!--- Получить активный промпт для роли --->
|
||||
<cfquery name="q" datasource="baza">
|
||||
SELECT id, name, body, created_at, notes
|
||||
FROM prompts
|
||||
WHERE role = <cfqueryparam value="#url.role#" cfsqltype="cf_sql_varchar">
|
||||
AND is_active = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
</cfquery>
|
||||
<cfif q.recordCount GT 0>
|
||||
<cfset result = {ok: true, id: q.id, name: q.name, body: q.body, created_at: q.created_at, notes: q.notes}>
|
||||
<cfelse>
|
||||
<cfset result = {ok: true, id: "", name: "", body: "", created_at: "", notes: ""}>
|
||||
</cfif>
|
||||
|
||||
<cfelseif url.action EQ "list" AND structKeyExists(url, "role")>
|
||||
<!--- Все версии для роли (история) --->
|
||||
<cfquery name="q" datasource="baza">
|
||||
SELECT id, name, is_active, created_at, created_by, notes
|
||||
FROM prompts
|
||||
WHERE role = <cfqueryparam value="#url.role#" cfsqltype="cf_sql_varchar">
|
||||
ORDER BY created_at DESC
|
||||
</cfquery>
|
||||
<cfset versions = []>
|
||||
<cfloop query="q">
|
||||
<cfset arrayAppend(versions, {
|
||||
id: q.id, name: q.name, is_active: q.is_active,
|
||||
created_at: q.created_at, created_by: q.created_by, notes: q.notes
|
||||
})>
|
||||
</cfloop>
|
||||
<cfset result = {ok: true, versions: versions}>
|
||||
|
||||
<cfelseif url.action EQ "get" AND structKeyExists(url, "id")>
|
||||
<!--- Получить конкретную версию --->
|
||||
<cfquery name="q" datasource="baza">
|
||||
SELECT id, role, name, body, parent_id, is_active, created_at, created_by, notes
|
||||
FROM prompts
|
||||
WHERE id = <cfqueryparam value="#url.id#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfif q.recordCount GT 0>
|
||||
<cfset result = {ok: true, id: q.id, role: q.role, name: q.name, body: q.body,
|
||||
parent_id: q.parent_id, is_active: q.is_active, created_at: q.created_at,
|
||||
created_by: q.created_by, notes: q.notes}>
|
||||
<cfelse>
|
||||
<cfset result.error = "prompt not found">
|
||||
</cfif>
|
||||
|
||||
<cfelseif url.action EQ "save" AND cgi.request_method EQ "POST">
|
||||
<!--- Создать новую версию (старая остаётся в истории) --->
|
||||
<cfset body = toString(getHttpRequestData().content)>
|
||||
<cfset data = deserializeJSON(body)>
|
||||
<cfset prompt = structKeyExists(data, "prompt") ? data.prompt : "">
|
||||
|
||||
<cfquery datasource="baza">
|
||||
DELETE FROM prompts WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfif len(trim(prompt))>
|
||||
<cfquery datasource="baza">
|
||||
INSERT INTO prompts (contract_id, prompt_text) VALUES (
|
||||
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
||||
<cfqueryparam value="#prompt#" cfsqltype="cf_sql_varchar">
|
||||
)
|
||||
</cfquery>
|
||||
<cfset role = structKeyExists(data, "role") ? data.role : "diff">
|
||||
<cfset pname = structKeyExists(data, "name") ? data.name : "unnamed">
|
||||
<cfset pbody = structKeyExists(data, "body") ? data.body : "">
|
||||
<cfset parentId = structKeyExists(data, "parent_id") AND len(data.parent_id) ? data.parent_id : "">
|
||||
<cfset pnotes = structKeyExists(data, "notes") ? data.notes : "">
|
||||
<cfset makeActive = structKeyExists(data, "is_active") ? data.is_active : true>
|
||||
|
||||
<cfif NOT len(trim(pbody))>
|
||||
<cfset result.error = "body required">
|
||||
<cfelse>
|
||||
<cftransaction>
|
||||
<!--- Если делаем активным — сбросить флаг у всех той же роли --->
|
||||
<cfif makeActive>
|
||||
<cfquery datasource="baza">
|
||||
UPDATE prompts SET is_active = false
|
||||
WHERE role = <cfqueryparam value="#role#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
</cfif>
|
||||
<cfquery datasource="baza" result="ins">
|
||||
INSERT INTO prompts (role, name, body, parent_id, is_active, created_by, notes)
|
||||
VALUES (
|
||||
<cfqueryparam value="#role#" cfsqltype="cf_sql_varchar">,
|
||||
<cfqueryparam value="#pname#" cfsqltype="cf_sql_varchar">,
|
||||
<cfqueryparam value="#pbody#" cfsqltype="cf_sql_varchar">,
|
||||
<cfif len(parentId)><cfqueryparam value="#parentId#" cfsqltype="cf_sql_varchar"><cfelse>NULL</cfif>,
|
||||
<cfqueryparam value="#makeActive#" cfsqltype="cf_sql_bit">,
|
||||
<cfqueryparam value="user" cfsqltype="cf_sql_varchar">,
|
||||
<cfif len(pnotes)><cfqueryparam value="#pnotes#" cfsqltype="cf_sql_varchar"><cfelse>NULL</cfif>
|
||||
)
|
||||
</cfquery>
|
||||
</cftransaction>
|
||||
<cfset result = {ok: true, id: ins.generatedKey}>
|
||||
</cfif>
|
||||
<cfset result = {ok: true, saved: len(trim(prompt)) GT 0}>
|
||||
|
||||
<cfelseif url.action EQ "activate" AND cgi.request_method EQ "POST">
|
||||
<!--- Активировать конкретную версию --->
|
||||
<cfset body = toString(getHttpRequestData().content)>
|
||||
<cfset data = deserializeJSON(body)>
|
||||
<cfset pid = structKeyExists(data, "id") ? data.id : "">
|
||||
|
||||
<cfif NOT len(pid)>
|
||||
<cfset result.error = "id required">
|
||||
<cfelse>
|
||||
<cfquery name="chk" datasource="baza">
|
||||
SELECT role FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfif chk.recordCount EQ 0>
|
||||
<cfset result.error = "prompt not found">
|
||||
<cfelse>
|
||||
<cftransaction>
|
||||
<cfquery datasource="baza">
|
||||
UPDATE prompts SET is_active = false
|
||||
WHERE role = <cfqueryparam value="#chk.role#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfquery datasource="baza">
|
||||
UPDATE prompts SET is_active = true
|
||||
WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
</cftransaction>
|
||||
<cfset result = {ok: true}>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfelseif url.action EQ "delete" AND cgi.request_method EQ "POST">
|
||||
<!--- Удалить неактивную версию (активную нельзя) --->
|
||||
<cfset body = toString(getHttpRequestData().content)>
|
||||
<cfset data = deserializeJSON(body)>
|
||||
<cfset pid = structKeyExists(data, "id") ? data.id : "">
|
||||
|
||||
<cfif NOT len(pid)>
|
||||
<cfset result.error = "id required">
|
||||
<cfelse>
|
||||
<cfquery name="chk" datasource="baza">
|
||||
SELECT is_active FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfif chk.recordCount EQ 0>
|
||||
<cfset result.error = "prompt not found">
|
||||
<cfelseif chk.is_active>
|
||||
<cfset result.error = "cannot delete active prompt">
|
||||
<cfelse>
|
||||
<cfquery datasource="baza">
|
||||
DELETE FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfset result = {ok: true}>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfelse>
|
||||
<!--- Получить промпт --->
|
||||
<cfquery name="q" datasource="baza">
|
||||
SELECT prompt_text FROM prompts
|
||||
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfset result = {ok: true, prompt: q.recordCount GT 0 ? q.prompt_text : ""}>
|
||||
<cfset result.error = "unknown action (use: get_active, list, get, save, activate, delete)">
|
||||
</cfif>
|
||||
|
||||
<cfcatch>
|
||||
<cfset result = {ok: false, error: cfcatch.message}>
|
||||
</cfcatch>
|
||||
|
||||
Reference in New Issue
Block a user