from __future__ import annotations import json import re from typing import Any import httpx from app.core.config import get_settings SYSTEM_PROMPT = """Ты помощник инженера по редактированию ГОСТ-таблиц (перечень, спецификация, ведомость). Пользователь даёт команды на русском. Ты отвечаешь ТОЛЬКО валидным JSON без markdown: { "reply": "краткий ответ пользователю", "edits": [ {"op": "update_row", "row_id": 123, "fields": {"note": "..."}}, {"op": "update_row", "row_index": 5, "fields": {"designation": "..."}}, {"op": "add_row", "row_index": 10, "fields": {...}}, {"op": "delete_row", "row_id": 123} ] } Правила: - Меняй только то, о чём просят. - Используй row_id из снимка таблицы, если есть. - Не выдумывай поля вне списка колонок. - Если правок нет — edits: []. """ def _extract_json(text: str) -> dict[str, Any]: text = text.strip() if text.startswith("```"): text = re.sub(r"^```(?:json)?\s*", "", text) text = re.sub(r"\s*```$", "", text) try: return json.loads(text) except json.JSONDecodeError: m = re.search(r"\{.*\}", text, re.S) if m: return json.loads(m.group(0)) return {"reply": text, "edits": []} def compress_rows(rows: list[dict[str, Any]], max_rows: int) -> list[dict[str, Any]]: slim = [] for r in rows[:max_rows]: item = {"row_id": r.get("id"), "row_index": r.get("row_index")} for k, v in r.items(): if k in ("id", "project_id", "is_auto_generated", "stretch"): continue if v not in ("", None, False, 0) or k in ("is_header", "is_empty"): item[k] = v slim.append(item) return slim async def chat_edit_table( message: str, table_type: str, rows: list[dict[str, Any]], ) -> dict[str, Any]: settings = get_settings() if not settings.openrouter_api_key: return { "reply": "OpenRouter API key не настроен (OPENROUTER_API_KEY).", "edits": [], } snapshot = compress_rows(rows, settings.llm_max_context_rows) user_content = ( f"Тип таблицы: {table_type}\n" f"Снимок строк (до {settings.llm_max_context_rows}):\n" f"{json.dumps(snapshot, ensure_ascii=False)}\n\n" f"Команда пользователя: {message}" ) payload = { "model": settings.openrouter_model, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_content}, ], "temperature": 0.2, "max_tokens": 2000, } headers = { "Authorization": f"Bearer {settings.openrouter_api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://gostgenerator.local", "X-Title": "GostGenerator Web", } url = settings.openrouter_base_url.rstrip("/") + "/chat/completions" proxy = (settings.openrouter_proxy or "").strip() or None if proxy and "://" not in proxy: proxy = f"socks5://{proxy}" client_kwargs: dict[str, Any] = {"timeout": 90.0} if proxy: client_kwargs["proxy"] = proxy async with httpx.AsyncClient(**client_kwargs) as client: resp = await client.post(url, headers=headers, json=payload) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"] parsed = _extract_json(content) edits = parsed.get("edits") or [] # normalize norm_edits = [] for e in edits: if not isinstance(e, dict): continue norm_edits.append( { "op": e.get("op", "update_row"), "row_id": e.get("row_id"), "row_index": e.get("row_index"), "fields": e.get("fields") or {}, } ) return {"reply": parsed.get("reply") or "", "edits": norm_edits}