v1.0.8: extractor.cfm — LLM-извлечение spec_rows через cfhttp
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
|||||||
|
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
|
||||||
|
<cfheader name="Content-Type" value="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<cfparam name="url.supplement_id" default="">
|
||||||
|
|
||||||
|
<cfset result = {ok: false, error: ""}>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfif NOT len(url.supplement_id)>
|
||||||
|
<cfset result.error = "supplement_id required">
|
||||||
|
<cfelse>
|
||||||
|
<!--- Читаем supplement + document --->
|
||||||
|
<cfquery name="sup" datasource="baza">
|
||||||
|
SELECT s.id, s.contract_id, d.elements_json, d.parsed_text
|
||||||
|
FROM supplements s
|
||||||
|
JOIN documents d ON s.document_id = d.id
|
||||||
|
WHERE s.id = <cfqueryparam value="#url.supplement_id#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfif sup.recordCount EQ 0>
|
||||||
|
<cfset result.error = "supplement not found">
|
||||||
|
<cfelse>
|
||||||
|
<!--- textify: elements → текст --->
|
||||||
|
<cfset elements = deserializeJSON(sup.elements_json)>
|
||||||
|
<cfset textLines = []>
|
||||||
|
<cfloop array="#elements#" index="el">
|
||||||
|
<cfif el.type EQ "paragraph">
|
||||||
|
<cfset prefix = len(el.style) ? "[#el.style#] " : "">
|
||||||
|
<cfset arrayAppend(textLines, prefix & el.text)>
|
||||||
|
<cfelseif el.type EQ "table">
|
||||||
|
<cfset rows = el.rows>
|
||||||
|
<cfif arrayLen(rows) GT 0>
|
||||||
|
<cfset ncols = arrayLen(rows[1])>
|
||||||
|
<cfset arrayAppend(textLines, "--- Таблица (" & arrayLen(rows) & "×" & ncols & ") ---")>
|
||||||
|
<cfloop array="#rows#" index="row">
|
||||||
|
<cfset cells = []>
|
||||||
|
<cfloop from="1" to="#ncols#" index="ci">
|
||||||
|
<cfset val = "">
|
||||||
|
<cfif ci LTE arrayLen(row)>
|
||||||
|
<cfset val = replace(replace(toString(row[ci]), chr(10), " ", "ALL"), "|", "\|", "ALL")>
|
||||||
|
</cfif>
|
||||||
|
<cfset arrayAppend(cells, val)>
|
||||||
|
</cfloop>
|
||||||
|
<cfset arrayAppend(textLines, "| " & arrayToList(cells, " | ") & " |")>
|
||||||
|
</cfloop>
|
||||||
|
<cfset arrayAppend(textLines, "")>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
<cfset parsedText = arrayToList(textLines, chr(10))>
|
||||||
|
|
||||||
|
<!--- Промпт --->
|
||||||
|
<cfset prompt = 'Ты — анализатор договоров. Ниже текст спецификации услуг из договора облачного провайдера.
|
||||||
|
|
||||||
|
Найди ВСЕ таблицы со списком услуг. Извлеки каждую строку в JSON-массив.
|
||||||
|
|
||||||
|
Для каждой строки верни:
|
||||||
|
- row_num: номер по порядку (целое число)
|
||||||
|
- name: полное наименование услуги (весь текст из ячейки)
|
||||||
|
- price: цена за единицу в рублях (число, из колонки "Цена")
|
||||||
|
- qty: количество/объём (число, из колонки "Объем")
|
||||||
|
- sum: итоговая сумма (число, из колонки "Сумма")
|
||||||
|
- date_start: дата начала оказания (строка YYYY-MM-DD)
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
1. Пропускай итоговые строки ("Итого...") и строки с подписями.
|
||||||
|
2. Если ячейка пустая — ставь null.
|
||||||
|
3. Если не можешь определить значение — ставь null и добавь в unresolved.
|
||||||
|
4. Верни ТОЛЬКО JSON, без пояснений.
|
||||||
|
|
||||||
|
Пример ответа:
|
||||||
|
{"rows":[{"row_num":1,"name":"Аренда стойко-места","price":213905.44,"qty":3,"sum":641716.32,"date_start":"2026-04-01"}],"unresolved":[]}
|
||||||
|
|
||||||
|
Текст документа:
|
||||||
|
---
|
||||||
|
#parsedText#
|
||||||
|
---'>
|
||||||
|
|
||||||
|
<!--- Вызов LLM --->
|
||||||
|
<cfset apiKey = "sk-ucI5YvOticoOQ9Kuj5K9mQ">
|
||||||
|
<cfset llmPayload = {
|
||||||
|
model: "gpt-oss-120b",
|
||||||
|
messages: [{role: "user", content: prompt}],
|
||||||
|
max_tokens: 8000,
|
||||||
|
temperature: 0.1
|
||||||
|
}>
|
||||||
|
|
||||||
|
<cfhttp url="https://api.aillm.ru/v1/chat/completions" method="POST" timeout="120">
|
||||||
|
<cfhttpparam type="header" name="Authorization" value="Bearer #apiKey#">
|
||||||
|
<cfhttpparam type="header" name="Content-Type" value="application/json">
|
||||||
|
<cfhttpparam type="body" value="#serializeJSON(llmPayload)#">
|
||||||
|
</cfhttp>
|
||||||
|
|
||||||
|
<cfif cfhttp.statusCode EQ 200>
|
||||||
|
<cfset llmResp = deserializeJSON(cfhttp.fileContent)>
|
||||||
|
<cfset llmText = llmResp.choices[1].message.content>
|
||||||
|
|
||||||
|
<!--- Вытаскиваем JSON из ответа --->
|
||||||
|
<cfset jsonText = llmText>
|
||||||
|
<cfif find("```json", jsonText)>
|
||||||
|
<cfset jsonText = listLast(listFirst(jsonText, "```"), "```json")>
|
||||||
|
</cfif>
|
||||||
|
<cfset jsonText = trim(jsonText)>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfset extracted = deserializeJSON(jsonText)>
|
||||||
|
<cfset rows = []>
|
||||||
|
<cfif structKeyExists(extracted, "rows") AND isArray(extracted.rows)>
|
||||||
|
<cfset rows = extracted.rows>
|
||||||
|
</cfif>
|
||||||
|
<cfset unresolved = []>
|
||||||
|
<cfif structKeyExists(extracted, "unresolved") AND isArray(extracted.unresolved)>
|
||||||
|
<cfset unresolved = extracted.unresolved>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Сохраняем spec_rows --->
|
||||||
|
<cfset savedRows = []>
|
||||||
|
<cfloop array="#rows#" index="row">
|
||||||
|
<cftry>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
INSERT INTO spec_rows (supplement_id, row_num, name, price, qty, sum, date_start)
|
||||||
|
VALUES (
|
||||||
|
<cfqueryparam value="#sup.id#" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'row_num') ? row.row_num : 0#" cfsqltype="cf_sql_integer">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'name') ? row.name : ''#" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'price') AND NOT isNull(row.price) ? row.price : ''#" cfsqltype="cf_sql_float" null="#NOT structKeyExists(row, 'price') OR isNull(row.price)#">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'qty') AND NOT isNull(row.qty) ? row.qty : ''#" cfsqltype="cf_sql_float" null="#NOT structKeyExists(row, 'qty') OR isNull(row.qty)#">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'sum') AND NOT isNull(row.sum) ? row.sum : ''#" cfsqltype="cf_sql_float" null="#NOT structKeyExists(row, 'sum') OR isNull(row.sum)#">,
|
||||||
|
<cfqueryparam value="#structKeyExists(row, 'date_start') AND NOT isNull(row.date_start) ? row.date_start : ''#" cfsqltype="cf_sql_varchar" null="#NOT structKeyExists(row, 'date_start') OR isNull(row.date_start)#">
|
||||||
|
)
|
||||||
|
</cfquery>
|
||||||
|
<cfset arrayAppend(savedRows, row)>
|
||||||
|
<cfcatch>
|
||||||
|
<cfset arrayAppend(unresolved, {row: row, error: cfcatch.message})>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
|
<cfset result = {
|
||||||
|
ok: true,
|
||||||
|
supplement_id: sup.id,
|
||||||
|
contract_id: sup.contract_id,
|
||||||
|
rows_saved: arrayLen(savedRows),
|
||||||
|
unresolved: arrayLen(unresolved),
|
||||||
|
model: structKeyExists(llmResp, "model") ? llmResp.model : "gpt-oss-120b"
|
||||||
|
}>
|
||||||
|
|
||||||
|
<cfcatch>
|
||||||
|
<cfset result = {ok: false, error: "JSON parse: " & cfcatch.message, llm_text: left(llmText, 500)}>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
|
<cfelse>
|
||||||
|
<cfset result = {ok: false, error: "LLM HTTP #cfhttp.statusCode#: #left(cfhttp.fileContent, 500)#"}>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfcatch>
|
||||||
|
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON(result)#</cfoutput>
|
||||||
Reference in New Issue
Block a user