diff --git a/config.yaml b/config.yaml index 2b16bcf..b029642 100644 --- a/config.yaml +++ b/config.yaml @@ -3,7 +3,7 @@ llm: api_key: "sk-ucI5YvOticoOQ9Kuj5K9mQ" - model: "gpt-oss-120b" + model: "qwen3-6-27b-fp8" base_url: "https://api.aillm.ru/v1" elm327: diff --git a/elmer/db.py b/elmer/db.py index e4f90ae..de1e4cd 100644 --- a/elmer/db.py +++ b/elmer/db.py @@ -6,8 +6,10 @@ llm_messages — token_id (FK), role, content, timestamp ecu_parameters — token_id (FK), pid_code, value, unit, timestamp dtc_codes — token_id (FK), code, description, status + sessions — сводная таблица всех сессий (клиент, ELM, авто, LLM) """ +import json import sqlite3 from datetime import datetime, timezone from pathlib import Path @@ -64,9 +66,129 @@ class Database: status TEXT, -- 'stored' | 'pending' created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + + CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + -- Сервер + client_ip TEXT, + real_ip TEXT, + user_agent TEXT, + content_length INTEGER, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + + -- Телефон + phone_model TEXT, + phone_maker TEXT, + android_version TEXT, + android_sdk INTEGER, + app_version TEXT, + android_id TEXT, + + -- ELM327 + elm_mac TEXT, + elm_bt_name TEXT, + obd_protocol TEXT, + + -- Авто + vin TEXT, + dtc_count INTEGER DEFAULT 0, + pid_count INTEGER DEFAULT 0, + + -- Сессия + duration_ms INTEGER, + response_count INTEGER DEFAULT 0, + error_count INTEGER DEFAULT 0, + retry_count INTEGER DEFAULT 0, + timeout_count INTEGER DEFAULT 0, + script_mode TEXT, + transport TEXT, -- 'bt' | 'tcp' + mock_mode INTEGER DEFAULT 0, + + -- LLM + diagnosis_text TEXT, + diagnosis_len INTEGER, + llm_model TEXT, + llm_duration_ms INTEGER, + llm_success INTEGER DEFAULT 0, + + -- Сырые данные (JSON) + raw_responses TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at); + CREATE INDEX IF NOT EXISTS idx_sessions_vin ON sessions(vin); + CREATE INDEX IF NOT EXISTS idx_sessions_mac ON sessions(elm_mac); + CREATE INDEX IF NOT EXISTS idx_sessions_aid ON sessions(android_id); """) self.conn.commit() + # ── sessions ────────────────────────────────────────── + + def save_session(self, client_info: dict, responses: list[dict], + diagnosis: str = "", llm_model: str = "", + llm_duration_ms: int = 0, llm_success: bool = False): + """Сохраняет сводную запись о сессии.""" + ci = client_info + + # Подсчёт DTC/PID из ответов + dtc_count = 0 + pid_count = 0 + for r in responses: + dec = (r.get("decoded") or "").lower() + if dec.startswith("dtc"): + dtc_count += 1 + elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")): + pid_count += 1 + + # VIN из ответов + vin = None + for r in responses: + dec = (r.get("decoded") or "") + if dec.startswith("VIN:"): + vin = dec[4:].strip() + if len(vin) != 17: + vin = None + break + + self.conn.execute(""" + INSERT INTO sessions ( + client_ip, real_ip, user_agent, content_length, + phone_model, phone_maker, android_version, android_sdk, + app_version, android_id, + elm_mac, elm_bt_name, obd_protocol, + vin, dtc_count, pid_count, + duration_ms, response_count, error_count, + retry_count, timeout_count, script_mode, + transport, mock_mode, + diagnosis_text, diagnosis_len, llm_model, + llm_duration_ms, llm_success, + raw_responses + ) VALUES (?,?,?,?, ?,?,?,?, ?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?, + ?,?,?, ?,?, ?) + """, ( + ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"), + ci.get("content_length"), + ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"), + ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"), + ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"), + vin, dtc_count, pid_count, + ci.get("duration_ms"), len(responses), ci.get("error_count", 0), + ci.get("retry_count", 0), ci.get("timeout_count", 0), + ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0), + diagnosis, len(diagnosis), llm_model, + llm_duration_ms, 1 if llm_success else 0, + json.dumps(responses, ensure_ascii=False) if responses else None, + )) + self.conn.commit() + + def get_recent_sessions(self, limit: int = 50) -> list[dict]: + """Последние N сессий.""" + rows = self.conn.execute( + "SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?", (limit,) + ).fetchall() + return [dict(r) for r in rows] + # ── cars ────────────────────────────────────────────── def get_or_create_car(self, vin: str) -> int: diff --git a/web/script_endpoint.py b/web/script_endpoint.py index f85cb83..7ea0933 100644 --- a/web/script_endpoint.py +++ b/web/script_endpoint.py @@ -5,6 +5,7 @@ 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 @@ -63,6 +64,7 @@ def register(app): def upload_session(): from flask import request, jsonify from elmer.config import load + from elmer.db import Database from elmer.diagnose import Diagnoser from elmer.prompts import SYSTEM_PROMPT @@ -72,30 +74,57 @@ def register(app): 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_start = time.time() + llm_success = False + diagnosis = "" if not api_key: - return jsonify({ - "diagnosis": format_no_llm(parsed), - "parsed": _summary(parsed), - }) + 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})" - 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"), - ) + llm_duration_ms = int((time.time() - llm_start) * 1000) + + # ── Сохранение в БД ─────────────────────────── try: - answer = diagnoser.diagnose(SYSTEM_PROMPT, _build_diagnosis_prompt(parsed)) + 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.warning(f"LLM failed: {e}") - answer = format_no_llm(parsed) + f"\n\n(LLM недоступен: {e})" + logger.error(f"DB save failed: {e}") return jsonify({ - "diagnosis": answer, + "diagnosis": diagnosis, "parsed": _summary(parsed), }) diff --git a/web/templates/index.html b/web/templates/index.html index 32bb228..d58c138 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -4,22 +4,22 @@
-Диагностика авто через ELM327 + ИИ
-v0.16.0-dev — 28 мая 2026
+v0.18.0-dev — 28 мая 2026
📱 Скачай приложение на телефон:
- ⬇️ Скачать Elmer APK + ⬇️ Скачать elmAI APK -v0.16.0-dev • нажмите чтобы скачать
+v0.18.0-dev • нажмите чтобы скачать