v1.0.76: фикс uppercase ключей Lucee, заголовки столбцов, красная корзина, Инфо+, столбец Текст с 2-панельной модалкой
This commit is contained in:
+74
-48
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HTTP-сервер: конвертация .doc → .docx + LLM-анализ ДС (/llm-ops) + SSE v2 (/process-v2)"""
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
"""Многопоточный HTTP-сервер."""
|
||||
daemon_threads = True
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import subprocess, tempfile, os, sys, json, time
|
||||
|
||||
@@ -13,6 +18,30 @@ LLM_MODEL = "gpt-oss-120b"
|
||||
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)
|
||||
payload = {
|
||||
"model": LLM_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 8000,
|
||||
"temperature": 0.1,
|
||||
}
|
||||
with httpx.Client(http2=True, timeout=120, verify=False) as client:
|
||||
resp = client.post(LLM_URL, json=payload,
|
||||
headers={"Authorization": f"Bearer {LLM_KEY}", "Content-Type": "application/json"})
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
raw_text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
json_text = raw_text
|
||||
if "```json" in json_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())
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
@@ -21,6 +50,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/llm-ops":
|
||||
self._handle_llm_ops()
|
||||
@@ -57,11 +93,12 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(b": ok\n\n")
|
||||
self.wfile.flush()
|
||||
|
||||
t0 = time.time()
|
||||
|
||||
@@ -102,7 +139,12 @@ class Handler(BaseHTTPRequestHandler):
|
||||
)
|
||||
if not docs:
|
||||
continue
|
||||
elements = json.loads(docs[0]["elements_json"])
|
||||
ej = docs[0]["elements_json"]
|
||||
if isinstance(ej, dict) and "Value" in ej:
|
||||
ej = ej["Value"]
|
||||
elements = json.loads(ej)
|
||||
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
|
||||
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
|
||||
lines = []
|
||||
for el in elements:
|
||||
if el.get("type") == "paragraph":
|
||||
@@ -121,25 +163,37 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
self._sse({"type": "extract_start", "supplement_id": sid, "filename": s["filename"]})
|
||||
|
||||
# LLM
|
||||
# LLM (прямой вызов, не HTTP self-call)
|
||||
import threading
|
||||
t1 = time.time()
|
||||
resp = httpx.post(f"http://127.0.0.1:8766/llm-ops", json={
|
||||
"contract_id": cid, "supplement_id": sid,
|
||||
"current_spec": current_spec, "doc_text": doc_text
|
||||
}, timeout=130)
|
||||
done = threading.Event()
|
||||
result = [None]
|
||||
error = [None]
|
||||
|
||||
def do_llm():
|
||||
try:
|
||||
result[0] = call_llm(current_spec, doc_text)
|
||||
except Exception as e:
|
||||
error[0] = str(e)
|
||||
finally:
|
||||
done.set()
|
||||
|
||||
t = threading.Thread(target=do_llm)
|
||||
t.start()
|
||||
|
||||
# Keepalive каждые 15с пока LLM думает
|
||||
while not done.wait(15):
|
||||
self._sse({"type": "keepalive"})
|
||||
|
||||
t.join()
|
||||
elapsed = round(time.time() - t1, 1)
|
||||
|
||||
if resp.status_code != 200:
|
||||
if error[0]:
|
||||
self._sse({"type": "extract_error", "supplement_id": sid, "filename": s["filename"],
|
||||
"error": f"LLM HTTP {resp.status_code}", "time_s": elapsed})
|
||||
continue
|
||||
|
||||
llm_result = resp.json()
|
||||
if "error" in llm_result:
|
||||
self._sse({"type": "extract_error", "supplement_id": sid, "filename": s["filename"],
|
||||
"error": llm_result["error"], "time_s": elapsed})
|
||||
"error": error[0], "time_s": elapsed})
|
||||
continue
|
||||
|
||||
llm_result = result[0]
|
||||
ops = llm_result.get("ops", [])
|
||||
mode = llm_result.get("mode", "partial")
|
||||
self._sse({"type": "llm_done", "supplement_id": sid, "filename": s["filename"],
|
||||
@@ -152,7 +206,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
)
|
||||
if apply_resp.status_code == 200:
|
||||
ar = apply_resp.json()
|
||||
summary = ar.get("summary", {})
|
||||
# Lucee serializeJSON → UPPERCASE keys
|
||||
summary = {k.lower(): v for k, v in ar.get("SUMMARY", {}).items()}
|
||||
self._sse({"type": "applied", "supplement_id": sid, "summary": summary})
|
||||
else:
|
||||
self._sse({"type": "apply_error", "supplement_id": sid,
|
||||
@@ -167,41 +222,12 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def _handle_llm_ops(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length))
|
||||
|
||||
prompt = build_prompt(
|
||||
body.get("current_spec", []),
|
||||
body.get("doc_text", ""),
|
||||
)
|
||||
payload = {
|
||||
"model": LLM_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 8000,
|
||||
"temperature": 0.1,
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(http2=True, timeout=120, verify=False) as client:
|
||||
resp = client.post(LLM_URL, json=payload,
|
||||
headers={"Authorization": f"Bearer {LLM_KEY}", "Content-Type": "application/json"})
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
raw_text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
|
||||
# Вытащить JSON из ответа
|
||||
json_text = raw_text
|
||||
if "```json" in json_text:
|
||||
json_text = json_text.split("```json")[1].split("```")[0]
|
||||
elif "```" in json_text:
|
||||
json_text = json_text.split("```")[1].split("```")[0]
|
||||
|
||||
result = json.loads(json_text.strip())
|
||||
|
||||
result = call_llm(body.get("current_spec", []), body.get("doc_text", ""))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(result, ensure_ascii=False).encode())
|
||||
|
||||
except Exception as e:
|
||||
self.send_response(502)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
@@ -234,4 +260,4 @@ class Handler(BaseHTTPRequestHandler):
|
||||
os.rmdir(tmpdir)
|
||||
def log_message(self, *a): pass
|
||||
|
||||
HTTPServer(("127.0.0.1", 8766), Handler).serve_forever()
|
||||
ThreadingHTTPServer(("127.0.0.1", 8766), Handler).serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user