elmAI rebrand: визуальные названия Elmer → elmAI

This commit is contained in:
Repinoid
2026-05-28 18:25:14 +03:00
parent f40918d07b
commit 0d1cf026dc
4 changed files with 170 additions and 19 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
llm: llm:
api_key: "sk-ucI5YvOticoOQ9Kuj5K9mQ" api_key: "sk-ucI5YvOticoOQ9Kuj5K9mQ"
model: "gpt-oss-120b" model: "qwen3-6-27b-fp8"
base_url: "https://api.aillm.ru/v1" base_url: "https://api.aillm.ru/v1"
elm327: elm327:
+122
View File
@@ -6,8 +6,10 @@
llm_messages — token_id (FK), role, content, timestamp llm_messages — token_id (FK), role, content, timestamp
ecu_parameters — token_id (FK), pid_code, value, unit, timestamp ecu_parameters — token_id (FK), pid_code, value, unit, timestamp
dtc_codes — token_id (FK), code, description, status dtc_codes — token_id (FK), code, description, status
sessions — сводная таблица всех сессий (клиент, ELM, авто, LLM)
""" """
import json
import sqlite3 import sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -64,9 +66,129 @@ class Database:
status TEXT, -- 'stored' | 'pending' status TEXT, -- 'stored' | 'pending'
created_at TEXT NOT NULL DEFAULT (datetime('now')) 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() 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 ────────────────────────────────────────────── # ── cars ──────────────────────────────────────────────
def get_or_create_car(self, vin: str) -> int: def get_or_create_car(self, vin: str) -> int:
+42 -13
View File
@@ -5,6 +5,7 @@ POST /api/v1/session/upload — приём батча, LLM-анализ, воз
""" """
import logging import logging
import time
from web.script_builder import build_default_script, build_full_script from web.script_builder import build_default_script, build_full_script
from web.script_parser import parse_batch, format_no_llm from web.script_parser import parse_batch, format_no_llm
@@ -63,6 +64,7 @@ def register(app):
def upload_session(): def upload_session():
from flask import request, jsonify from flask import request, jsonify
from elmer.config import load from elmer.config import load
from elmer.db import Database
from elmer.diagnose import Diagnoser from elmer.diagnose import Diagnoser
from elmer.prompts import SYSTEM_PROMPT from elmer.prompts import SYSTEM_PROMPT
@@ -72,30 +74,57 @@ def register(app):
responses = data["responses"] responses = data["responses"]
logger.info(f"Upload: {len(responses)} 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) parsed = parse_batch(responses)
cfg = load() cfg = load()
api_key = cfg["llm"]["api_key"] 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: if not api_key:
return jsonify({ diagnosis = format_no_llm(parsed)
"diagnosis": format_no_llm(parsed), else:
"parsed": _summary(parsed), 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( llm_duration_ms = int((time.time() - llm_start) * 1000)
api_key=api_key,
model=cfg["llm"].get("model", "gpt-oss-120b"), # ── Сохранение в БД ───────────────────────────
base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"),
)
try: 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: except Exception as e:
logger.warning(f"LLM failed: {e}") logger.error(f"DB save failed: {e}")
answer = format_no_llm(parsed) + f"\n\n(LLM недоступен: {e})"
return jsonify({ return jsonify({
"diagnosis": answer, "diagnosis": diagnosis,
"parsed": _summary(parsed), "parsed": _summary(parsed),
}) })
+5 -5
View File
@@ -4,22 +4,22 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elmer — Диагностика авто</title> <title>elmAI — Диагностика авто</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🔧 Elmer</h1> <h1>🔧 elmAI</h1>
<p class="subtitle">Диагностика авто через ELM327 + ИИ</p> <p class="subtitle">Диагностика авто через ELM327 + ИИ</p>
<p class="subtitle" style="font-size:12px;opacity:0.7;">v0.16.0-dev — 28 мая 2026</p> <p class="subtitle" style="font-size:12px;opacity:0.7;">v0.18.0-dev — 28 мая 2026</p>
<div class="card" style="text-align:center;margin-bottom:20px;"> <div class="card" style="text-align:center;margin-bottom:20px;">
<p style="margin:0 0 10px 0;">📱 Скачай приложение на телефон:</p> <p style="margin:0 0 10px 0;">📱 Скачай приложение на телефон:</p>
<a href="/static/app-debug.apk" style="color:#ff6b35;font-size:18px;font-weight:bold;text-decoration:none;"> <a href="/static/app-debug.apk" style="color:#ff6b35;font-size:18px;font-weight:bold;text-decoration:none;">
⬇️ Скачать Elmer APK ⬇️ Скачать elmAI APK
</a> </a>
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.16.0-dev • нажмите чтобы скачать</p> <p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.18.0-dev • нажмите чтобы скачать</p>
</div> </div>
<!-- Кнопка десктоп-диагностики скрыта — только для разработчика с прямым ELM327 --> <!-- Кнопка десктоп-диагностики скрыта — только для разработчика с прямым ELM327 -->