From ce4871227cdff75de1b8bf6adf6864fe9d35f290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 18 Jun 2026 09:32:58 +0400 Subject: [PATCH] =?UTF-8?q?v3.3.1:=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=D0=BF=D1=80=D0=BE=D0=BC=D0=BF=D1=82=D0=B0=20LLM?= =?UTF-8?q?=20=E2=80=94=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D1=8C/=D1=81=D0=B1=D1=80=D0=BE=D1=81=D0=B8=D1=82=D1=8C/=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/app.py | 4 ++- site/extractor.py | 43 ++++++++++++++++++---- site/routes/llm.py | 24 ++++++++++++- site/templates/llm.html | 79 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 8 deletions(-) diff --git a/site/app.py b/site/app.py index 9db4b42..1389604 100644 --- a/site/app.py +++ b/site/app.py @@ -13,7 +13,7 @@ from api import api_bp from routes.main import index from routes.parse import parse from routes.misc import health, logs -from routes.llm import process as llm_process, chat as llm_chat +from routes.llm import process as llm_process, chat as llm_chat, save_prompt, get_prompt load_dotenv() @@ -48,6 +48,8 @@ class ContractsApp: self.app.add_url_rule("/llm", "llm_page", lambda: __import__('flask').render_template('llm.html')) self.app.add_url_rule("/llm/process/", "llm_process", llm_process) self.app.add_url_rule("/llm/chat/", "llm_chat", llm_chat, methods=["POST"]) + self.app.add_url_rule("/llm/prompt/", "llm_prompt_get", get_prompt) + self.app.add_url_rule("/llm/prompt/", "llm_prompt_save", save_prompt, methods=["POST"]) # Служебные self.app.add_url_rule("/health", "health", health) diff --git a/site/extractor.py b/site/extractor.py index e5c8100..28c5629 100644 --- a/site/extractor.py +++ b/site/extractor.py @@ -12,21 +12,37 @@ import json import llm_client -def extract(parsed_text: str) -> dict: +def extract(parsed_text: str, custom_prompt: str = None) -> dict: """ Извлечь строки спецификации из текста документа через LLM. Args: parsed_text: текст документа (выдача textify.py) + custom_prompt: свой промпт (если None — используется DEFAULT_PROMPT) Returns: - {"rows": [{row_num, name, price, qty, sum, date_start}, ...], "unresolved": [...]} - или - {"error": "...", "raw_response": "..."} + {"rows": [...], "unresolved": [...]} или {"error": "..."} """ - # ── Промпт ────────────────────────────────────────────────── - prompt = f"""Ты — анализатор договоров. Ниже текст спецификации услуг из договора облачного провайдера. + prompt = custom_prompt or DEFAULT_PROMPT + prompt = prompt.replace("{parsed_text}", parsed_text) +""" +extractor.py — Извлечение строк спецификации из текста документа. + +Берёт распарсенный текст (parsed_text из documents), отправляет LLM, +получает структурированный список строк спецификации. + +Не зависит от parser.py, textify.py, upload.py. +Зависит только от llm_client.py. +""" + +import json +import llm_client + +# ── Промпт по умолчанию ───────────────────────────────────────── +# {parsed_text} будет заменён на текст документа + +DEFAULT_PROMPT = """Ты — анализатор договоров. Ниже текст спецификации услуг из договора облачного провайдера. Найди ВСЕ таблицы со списком услуг. Извлеки каждую строку в JSON-массив. @@ -53,6 +69,21 @@ def extract(parsed_text: str) -> dict: --- """ + +def extract(parsed_text: str, custom_prompt: str = None) -> dict: + """ + Извлечь строки спецификации из текста документа через LLM. + + Args: + parsed_text: текст документа (выдача textify.py) + custom_prompt: свой промпт (если None — DEFAULT_PROMPT). + Должен содержать {parsed_text} для подстановки текста. + + Returns: + {"rows": [...], "unresolved": [...]} или {"error": "..."} + """ + prompt = (custom_prompt or DEFAULT_PROMPT).replace("{parsed_text}", parsed_text) + # ── Вызов LLM ──────────────────────────────────────────────── result = llm_client.ask(prompt, max_tokens=8000) diff --git a/site/routes/llm.py b/site/routes/llm.py index 8d51be5..6f60886 100644 --- a/site/routes/llm.py +++ b/site/routes/llm.py @@ -59,7 +59,8 @@ def process(cid): yield f"data: {_json.dumps({'type': 'extract_start', 'name': s['filename']})}\n\n" t0 = _time.time() - result = extractor.extract(s["parsed_text"]) + custom_prompt = _prompts.get(cid) or None + result = extractor.extract(s["parsed_text"], custom_prompt=custom_prompt) if "error" in result: yield f"data: {_json.dumps({'type': 'extract_error', 'name': s['filename'], 'error': result['error']})}\n\n" @@ -140,6 +141,27 @@ def process(cid): return Response(generate(), mimetype="text/event-stream") +# ── Хранилище промптов (в памяти) ────────────────────────────── + +_prompts = {} # cid → prompt + + +def save_prompt(cid): + """POST /llm/prompt/ — сохранить промпт. Тело: {"prompt": "..."}""" + data = request.get_json(silent=True) or {} + prompt = data.get("prompt", "").strip() + if prompt: + _prompts[cid] = prompt + return jsonify({"ok": True}) + return jsonify({"error": "пустой промпт"}), 400 + + +def get_prompt(cid): + """GET /llm/prompt/ — получить сохранённый промпт (или DEFAULT_PROMPT)""" + import extractor + return jsonify({"prompt": _prompts.get(cid, "")}) + + def chat(cid): """ POST /llm/chat/ — задать вопрос по договору. diff --git a/site/templates/llm.html b/site/templates/llm.html index 87216ea..bc1f6bf 100644 --- a/site/templates/llm.html +++ b/site/templates/llm.html @@ -94,6 +94,16 @@ + +
+ ⚙ Промпт LLM + +
+ + + +
+
@@ -135,6 +145,32 @@