"""Эндпоинты для толстого клиента: скрипты и батчевая загрузка. GET /api/v1/script — выдача скрипта диагностики POST /api/v1/session/upload — приём батча, LLM-анализ, возврат диагноза """ import logging import time from web.script_builder import build_default_script, build_full_script from web.script_parser import parse_batch, format_no_llm logger = logging.getLogger("elmer.script") def _build_diagnosis_prompt(data: dict) -> str: """Строит промпт для LLM из распарсенных данных.""" parts = ["## Данные диагностики\n"] if data["vin"]: parts.append(f"**VIN:** {data['vin']}") if data["dtc_stored"]: parts.append(f"\n**Сохранённые ошибки (mode 03):** {', '.join(data['dtc_stored'])}") if data["dtc_pending"]: parts.append(f"**Ожидающие ошибки (mode 07):** {', '.join(data['dtc_pending'])}") if data["parameters"]: parts.append("\n**Параметры в реальном времени:**") for p in data["parameters"]: parts.append(f"- {p['name']}: {p['value']}") if not data["vin"] and not data["dtc_stored"] and not data["parameters"]: parts.append("\n(данные не распознаны)") parts.append("\n**Сырые ответы ЭБУ:**") parts.extend(data["raw_log"]) parts.append("\n---") parts.append("## Запрос на анализ") parts.append( "Дай ГЛУБОКИЙ, РАЗВЁРНУТЫЙ анализ на основе этих данных. " "Не ограничивайся кратким резюме — мне нужен полный технический разбор.\n" "1. Разбери КАЖДУЮ ошибку: все возможные причины, от частых к редким.\n" "2. Проанализируй КАЖДЫЙ параметр: норма/отклонение, на что влияет.\n" "3. Найди ВЗАИМОСВЯЗИ между ошибками и параметрами.\n" "4. Предложи КОНКРЕТНЫЙ план проверок: что сначала, что потом, как проверять.\n" "5. Если данных мало — скажи какие PID'ы досчитать и зачем.\n" "6. Дай оценки уверенности в ПРОЦЕНТАХ для каждой версии." ) return "\n".join(parts) def register(app): """Регистрирует эндпоинты скриптов на Flask-приложении.""" @app.route("/api/v1/script", methods=["GET"]) def get_script(): from flask import jsonify, request mode = request.args.get("mode", "full") script = build_full_script() if mode == "full" else build_default_script() return jsonify(script) @app.route("/api/v1/session/upload", methods=["POST"]) def upload_session(): from flask import request, jsonify from elmer.config import load from api.db import Database from elmer.diagnose import Diagnoser from elmer.prompts import SYSTEM_PROMPT data = request.get_json(silent=True) if not data or "responses" not in data: return jsonify({"error": "missing 'responses'"}), 400 responses = data["responses"] logger.info(f"Upload: {len(responses)} responses") # ── Информация о клиенте ────────────────────── client_info = data.get("client_info", {}) client_info["client_ip"] = request.remote_addr client_info["real_ip"] = request.headers.get("X-Real-IP", "") client_info["user_agent"] = request.headers.get("User-Agent", "") client_info["content_length"] = request.content_length parsed = parse_batch(responses) cfg = load() api_key = cfg["llm"]["api_key"] model = cfg["llm"].get("model", "gpt-oss-120b") llm_available = bool(api_key) llm_start = time.time() llm_success = False diagnosis = "" if not api_key: diagnosis = format_no_llm(parsed) else: diagnoser = Diagnoser( api_key=api_key, model=model, base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"), ) try: diagnosis = diagnoser.diagnose(SYSTEM_PROMPT, _build_diagnosis_prompt(parsed)) llm_success = True except Exception as e: logger.warning(f"LLM failed: {e}") diagnosis = format_no_llm(parsed) + f"\n\n(LLM недоступен: {e})" llm_duration_ms = int((time.time() - llm_start) * 1000) # ── Сохранение в БД ─────────────────────────── try: db = Database() db.save_session( client_info=client_info, responses=responses, diagnosis=diagnosis, llm_model=model, llm_duration_ms=llm_duration_ms, llm_success=llm_success, ) except Exception as e: logger.error(f"DB save failed: {e}") return jsonify({ "diagnosis": diagnosis, "parsed": _summary(parsed), "llm_available": llm_available, "llm_success": llm_success, }) @app.route("/api/v1/chat", methods=["POST"]) def chat(): """Свободный вопрос к LLM (без ELM).""" from flask import request, jsonify from elmer.config import load from elmer.diagnose import Diagnoser data = request.get_json(silent=True) if not data or "question" not in data: return jsonify({"error": "missing 'question'"}), 400 question = data["question"].strip() if not question: return jsonify({"answer": "Пустой вопрос."}) # История диалога history = data.get("history", []) history_text = "" if history: history_text = "## История диалога\n" for m in history[-10:]: # последние 10 сообщений role = "Водитель" if m.get("role") == "user" else "Автоэксперт" history_text += f"{role}: {m.get('content', '')}\n" history_text += "\n" cfg = load() api_key = cfg["llm"]["api_key"] if not api_key: return jsonify({"answer": "LLM не настроен."}) prompt = ( f"{history_text}" f"Ты — автоэксперт. Помни контекст диалога выше. " f"Отвечай КРАТКО, не более 20 строк. Без воды, только по делу.\n\n" f"Вопрос: {question}" ) try: diagnoser = Diagnoser( api_key=api_key, model=cfg["llm"].get("model", "gpt-oss-120b"), base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"), ) answer = diagnoser.diagnose( "Ты — лаконичный автоэксперт. Помни контекст диалога. Отвечай кратко, максимум 20 строк.", prompt, ) except Exception as e: answer = f"LLM недоступен: {e}" return jsonify({"answer": answer}) @app.route("/api/v1/ping", methods=["GET"]) def ping(): """Быстрая проверка доступности сервера.""" return {"ok": True} @app.route("/api/v1/ping-llm", methods=["GET"]) def ping_llm(): """Быстрая проверка доступности LLM.""" from flask import jsonify from elmer.config import load from elmer.diagnose import Diagnoser cfg = load() api_key = cfg["llm"]["api_key"] if not api_key: return jsonify({"ok": False, "error": "no API key"}) t0 = time.time() try: diagnoser = Diagnoser( api_key=api_key, model=cfg["llm"].get("model", "gpt-oss-120b"), base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"), ) diagnoser.diagnose("Отвечай одним словом.", "OK") ms = int((time.time() - t0) * 1000) return jsonify({"ok": True, "ms": ms}) except Exception as e: ms = int((time.time() - t0) * 1000) return jsonify({"ok": False, "ms": ms, "error": str(e)[:100]}) def _summary(p: dict) -> dict: return { "vin": p["vin"], "dtc_stored": p["dtc_stored"], "dtc_pending": p["dtc_pending"], "parameters": p["parameters"], }