From 47f7a85510ac9f9c3d2eccbff3f0ac5a9a6a7317 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 20:53:36 +0200 Subject: [PATCH] =?UTF-8?q?Ruff-Cleanup:=20ganzes=20MC2-Repo=20lint-gr?= =?UTF-8?q?=C3=BCn=20+=20projekt-passende=20ruff.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo auf. Aufgeraeumt: - ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except, S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI- Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports geschuetzt (F401). - ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports, ueberfluessige noqa) auto-behoben. - 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string), UP035 (veraltete typing-Imports). Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung). Co-Authored-By: Claude Opus 4.8 --- backend/app.py | 10 ++++---- backend/gateway_app.py | 5 ++-- backend/migrate_config.py | 3 ++- backend/routers/agent.py | 5 +--- backend/routers/auftragsbuch.py | 1 - backend/routers/chronik.py | 4 +-- backend/routers/connect.py | 1 - backend/routers/console.py | 5 ++-- backend/routers/eigenleben.py | 4 +-- backend/routers/events.py | 3 +-- backend/routers/gateway_forward.py | 3 +-- backend/routers/gateway_proxy.py | 4 +-- backend/routers/health.py | 6 ++--- backend/routers/hermes_ui.py | 3 +-- backend/routers/ideen.py | 1 - backend/routers/maintenance.py | 3 +-- backend/routers/memory.py | 1 - backend/routers/models.py | 6 ++--- backend/routers/reminders.py | 4 +-- backend/routers/routing.py | 1 - backend/routers/system.py | 10 +++----- backend/routers/voice.py | 14 +++++------ backend/routers/wissen.py | 3 +-- backend/routers/zeitmaschine.py | 1 - backend/scripts/parse_mc2_timeout.py | 2 -- backend/services/agent.py | 18 ++++++++------ backend/services/announce.py | 1 - backend/services/backup.py | 2 +- backend/services/connect.py | 7 +++--- backend/services/discover.py | 5 ++-- backend/services/gateway.py | 4 +-- backend/services/ideen.py | 18 +++++++------- backend/services/jobengine.py | 8 +++--- backend/services/ketten_digest.py | 1 - backend/services/llamaswap.py | 23 ++++++++++------- backend/services/maintenance.py | 18 +++++++------- backend/services/memory.py | 1 - backend/services/metrics_history.py | 12 ++++----- backend/services/reminders.py | 1 + backend/services/sentry.py | 4 +-- backend/services/system.py | 1 - backend/services/token_stats.py | 1 - backend/services/warmer.py | 1 - client/hermes-pc/executor.py | 2 +- deploy/agent-hooks/tabu-pfade-guard.py | 5 +++- deploy/ensure-profile-hooks.py | 4 +-- deploy/governor/driver.py | 4 +-- deploy/governor/governor.py | 4 +-- deploy/hermes-patches/apply.py | 4 +-- deploy/hermes-patches/mc2_kanban_reaper.py | 8 +++--- deploy/pruefstand/harness.py | 6 ++--- hermes/plugins/mc2-memory/__init__.py | 21 ++++++++-------- mcp/mcp_web.py | 3 +-- mem0_service/app.py | 9 +++---- mem0_service/smoke_test.py | 2 +- ruff.toml | 29 ++++++++++++++++++++++ scripts/filter_inactive_skills.py | 2 +- voice_service/app.py | 13 ++++++---- 58 files changed, 174 insertions(+), 171 deletions(-) create mode 100644 ruff.toml diff --git a/backend/app.py b/backend/app.py index 1302d0b..0e750dc 100644 --- a/backend/app.py +++ b/backend/app.py @@ -9,17 +9,16 @@ Server (proxyt /api hierher), daher CORS für localhost offen. import asyncio import logging import os +from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager -from typing import Any, AsyncGenerator, Awaitable, Callable +from typing import Any import httpx +from config import FRONTEND_DIST, V1_UPSTREAM, VERSION from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles -from starlette.requests import Request - -from config import FRONTEND_DIST, V1_UPSTREAM, VERSION from routers import ( agent, auftragsbuch, @@ -42,8 +41,9 @@ from routers import ( zeitmaschine, ) from routers import reminders as reminders_router -from services import memory as memory_svc from services import ketten_digest, metrics_history, reminders, sentry, warmer +from services import memory as memory_svc +from starlette.requests import Request # Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration # für alle Module (logging.getLogger(__name__)). diff --git a/backend/gateway_app.py b/backend/gateway_app.py index 8e30f23..22da677 100644 --- a/backend/gateway_app.py +++ b/backend/gateway_app.py @@ -15,13 +15,12 @@ durchreicht (routers/gateway_forward.py, MC_V1_UPSTREAM). import logging import os +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator import httpx -from fastapi import FastAPI, Request - from config import LLAMA_SWAP_URL, VERSION +from fastapi import FastAPI, Request from routers import gateway_proxy logging.basicConfig( diff --git a/backend/migrate_config.py b/backend/migrate_config.py index c5021c6..aabd4a1 100644 --- a/backend/migrate_config.py +++ b/backend/migrate_config.py @@ -4,8 +4,9 @@ from pathlib import Path # Add backend directory to sys.path so we can import services sys.path.append(str(Path(__file__).resolve().parent)) -from services.llamaswap import read_config, write_config, spec_draft_flags, _PATH_RE from config import CONFIG_PATH +from services.llamaswap import _PATH_RE, read_config, spec_draft_flags, write_config + def migrate(): print(f"Reading config from {CONFIG_PATH}...") diff --git a/backend/routers/agent.py b/backend/routers/agent.py index 7abad85..3c4104a 100644 --- a/backend/routers/agent.py +++ b/backend/routers/agent.py @@ -1,10 +1,7 @@ """Agent-Endpoint: Hermes-Status + WebUI-Link (MC verlinkt nur, betreibt nicht).""" -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from pydantic import BaseModel - -from fastapi import HTTPException - from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model router = APIRouter(prefix="/api") diff --git a/backend/routers/auftragsbuch.py b/backend/routers/auftragsbuch.py index 2e163a3..50e1f51 100644 --- a/backend/routers/auftragsbuch.py +++ b/backend/routers/auftragsbuch.py @@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Gate ist der Klick des Commanders.""" from fastapi import APIRouter, HTTPException from pydantic import BaseModel - from services import auftragsbuch router = APIRouter(prefix="/api") diff --git a/backend/routers/chronik.py b/backend/routers/chronik.py index 3fac301..ac672b6 100644 --- a/backend/routers/chronik.py +++ b/backend/routers/chronik.py @@ -3,10 +3,8 @@ Quelle ist der persistente Melde-Briefkasten (services/announce.py): Health-Wäc Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons, Auftragsbuch — alle autonomen Kanäle laufen dort bereits durch. Hier wird nichts Neues erhoben, nur erzählt.""" -from pydantic import BaseModel - from fastapi import APIRouter - +from pydantic import BaseModel from services import announce router = APIRouter(prefix="/api") diff --git a/backend/routers/connect.py b/backend/routers/connect.py index 181a59d..eac9199 100644 --- a/backend/routers/connect.py +++ b/backend/routers/connect.py @@ -1,7 +1,6 @@ """Connect-Endpoint: erzeugt IDE-/Agent-Snippets (auf den Gateway + Memory-MCP).""" from fastapi import APIRouter - from services.connect import DEFAULT_HOST, build_snippets, check_health router = APIRouter(prefix="/api") diff --git a/backend/routers/console.py b/backend/routers/console.py index 1d801ec..f76beac 100644 --- a/backend/routers/console.py +++ b/backend/routers/console.py @@ -14,11 +14,10 @@ import logging import httpx import websockets +from config import BOX_CONSOLE_UPSTREAM from fastapi import APIRouter, Request, WebSocket from starlette.responses import Response -from config import BOX_CONSOLE_UPSTREAM - log = logging.getLogger(__name__) router = APIRouter() @@ -28,7 +27,7 @@ def _register(base: str, upstream: str) -> None: ws_upstream = upstream.replace("http://", "ws://").replace("https://", "wss://") @router.websocket(f"/{base}/ws") - async def _proxy_ws(ws: WebSocket) -> None: # noqa: ANN001 — Closure je base + async def _proxy_ws(ws: WebSocket) -> None: await ws.accept(subprotocol="tty") try: async with websockets.connect( diff --git a/backend/routers/eigenleben.py b/backend/routers/eigenleben.py index 6fb9fd4..4af2f36 100644 --- a/backend/routers/eigenleben.py +++ b/backend/routers/eigenleben.py @@ -1,9 +1,7 @@ """Eigenleben-Endpoints — „Von allein“-Ansicht (Skills + Vorschlags-Bilanz), read-only.""" -from pydantic import BaseModel - from fastapi import APIRouter, HTTPException - +from pydantic import BaseModel from services import eigenleben router = APIRouter(prefix="/api") diff --git a/backend/routers/events.py b/backend/routers/events.py index 25f43b8..dfc9bcd 100644 --- a/backend/routers/events.py +++ b/backend/routers/events.py @@ -27,11 +27,10 @@ import json import logging from pathlib import Path +from config import MODELS_DIR from fastapi import APIRouter, Request from fastapi.responses import StreamingResponse -from config import MODELS_DIR - log = logging.getLogger(__name__) router = APIRouter(prefix="/api") diff --git a/backend/routers/gateway_forward.py b/backend/routers/gateway_forward.py index 352d0fe..27f5604 100644 --- a/backend/routers/gateway_forward.py +++ b/backend/routers/gateway_forward.py @@ -11,11 +11,10 @@ entfernen → app.py bindet wieder den lokalen Gateway ein. import logging +from config import V1_UPSTREAM from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, StreamingResponse -from config import V1_UPSTREAM - log = logging.getLogger(__name__) router = APIRouter(prefix="/v1") diff --git a/backend/routers/gateway_proxy.py b/backend/routers/gateway_proxy.py index 0f0a6e0..77fea05 100644 --- a/backend/routers/gateway_proxy.py +++ b/backend/routers/gateway_proxy.py @@ -2,11 +2,9 @@ import logging import os import httpx - +from config import LLAMA_SWAP_URL from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, StreamingResponse - -from config import LLAMA_SWAP_URL from services.gateway_stream import record_stream_chunk, record_usage, warn_truncation from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image from services.routing_policy import load_policy diff --git a/backend/routers/health.py b/backend/routers/health.py index 36a4a85..644cf25 100644 --- a/backend/routers/health.py +++ b/backend/routers/health.py @@ -1,10 +1,8 @@ """Health-/Status-Endpoint — schlanker Lebenszeichen-Check für MC 2.0.""" -from pydantic import BaseModel - -from fastapi import APIRouter - from config import VERSION +from fastapi import APIRouter +from pydantic import BaseModel from services import gateway, llamaswap router = APIRouter(prefix="/api") diff --git a/backend/routers/hermes_ui.py b/backend/routers/hermes_ui.py index 524cc65..a668ffa 100644 --- a/backend/routers/hermes_ui.py +++ b/backend/routers/hermes_ui.py @@ -22,11 +22,10 @@ import re import httpx import websockets +from config import HERMES_BUILTIN_UI_UPSTREAM from fastapi import APIRouter, Request, WebSocket from starlette.responses import RedirectResponse, Response -from config import HERMES_BUILTIN_UI_UPSTREAM - log = logging.getLogger(__name__) router = APIRouter() diff --git a/backend/routers/ideen.py b/backend/routers/ideen.py index ded8507..9b4e488 100644 --- a/backend/routers/ideen.py +++ b/backend/routers/ideen.py @@ -3,7 +3,6 @@ LAN-only wie alle MC2-Endpoints; das Mensch-Gate bleibt die Karte im Auftragsbuc from fastapi import APIRouter, HTTPException from pydantic import BaseModel - from services import ideen router = APIRouter(prefix="/api") diff --git a/backend/routers/maintenance.py b/backend/routers/maintenance.py index c1f1e68..91b7af9 100644 --- a/backend/routers/maintenance.py +++ b/backend/routers/maintenance.py @@ -1,8 +1,7 @@ """Wartungs-Endpoints: Update-Badge, OS-/Engine-Update, Reboot, Restart, Logs.""" -from fastapi import APIRouter, HTTPException, Header +from fastapi import APIRouter, Header, HTTPException from pydantic import BaseModel - from services import maintenance router = APIRouter(prefix="/api") diff --git a/backend/routers/memory.py b/backend/routers/memory.py index 90ea42c..abfdf06 100644 --- a/backend/routers/memory.py +++ b/backend/routers/memory.py @@ -4,7 +4,6 @@ import time from fastapi import APIRouter, HTTPException from pydantic import BaseModel - from services import memory from services.voice_metrics import park, record_stage diff --git a/backend/routers/models.py b/backend/routers/models.py index 4900f49..5c28297 100644 --- a/backend/routers/models.py +++ b/backend/routers/models.py @@ -1,10 +1,9 @@ """Modelle-Endpoints: Liste (mit Caps), Discover, Fit, Register, Groups.""" import psutil +from config import HF_DOWNLOAD_ENV, MODELS_DIR from fastapi import APIRouter, HTTPException from pydantic import BaseModel - -from config import HF_DOWNLOAD_ENV, MODELS_DIR from services import budget, discover, hf, jobengine, llamaswap from services.fit import evaluate_fit, max_ctx_for @@ -140,8 +139,7 @@ def install(req: InstallReq) -> dict: # Download-Job: alle GGUF-Teile (+ mmproj) per --include holen. args = [hf.hf_bin(), "download", repo] - for f in info["files"]: - args.append(f) + args.extend(info["files"]) if info["mmproj"]: args.append(info["mmproj"]) args += ["--local-dir", str(target)] diff --git a/backend/routers/reminders.py b/backend/routers/reminders.py index 13c0cde..78eac55 100644 --- a/backend/routers/reminders.py +++ b/backend/routers/reminders.py @@ -2,10 +2,8 @@ Konsument ist v.a. der Hermes-Agent via mcp_mc.py (reminder_create/list/delete); LAN-only wie alle MC2-Endpoints.""" -from pydantic import BaseModel - from fastapi import APIRouter, HTTPException - +from pydantic import BaseModel from services import reminders router = APIRouter(prefix="/api") diff --git a/backend/routers/routing.py b/backend/routers/routing.py index 2f3a713..ae41659 100644 --- a/backend/routers/routing.py +++ b/backend/routers/routing.py @@ -2,7 +2,6 @@ from fastapi import APIRouter, HTTPException from pydantic import BaseModel - from services import gateway from services.routing_policy import policy_meta, save_policy diff --git a/backend/routers/system.py b/backend/routers/system.py index 2a07cb8..9b941bd 100644 --- a/backend/routers/system.py +++ b/backend/routers/system.py @@ -9,12 +9,10 @@ import logging import os import subprocess +import httpx +from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL from fastapi import APIRouter from pydantic import BaseModel - -import httpx - -from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL from services import backup as backup_svc from services import maintenance from services.agent import agent_status @@ -67,7 +65,7 @@ def _user_unit_active(unit: str) -> bool: r = subprocess.run(["systemctl", "--user", "is-active", unit], capture_output=True, text=True, timeout=3) return r.stdout.strip() == "active" - except Exception: # noqa: BLE001 + except Exception: return False @@ -135,7 +133,7 @@ def _run(cmd: list[str], cwd: str | None = None) -> dict: p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=180) return {"ok": p.returncode == 0, "code": p.returncode, "out": (p.stdout or "")[-2000:], "err": (p.stderr or "")[-2000:]} - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"ok": False, "code": -1, "out": "", "err": str(exc)} diff --git a/backend/routers/voice.py b/backend/routers/voice.py index a93e1a8..5a5faa8 100644 --- a/backend/routers/voice.py +++ b/backend/routers/voice.py @@ -11,14 +11,16 @@ LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints. import logging import os + +# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv). +import sys as _sys import time import httpx +from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL from fastapi import APIRouter, File, Form, HTTPException, UploadFile from fastapi.responses import Response, StreamingResponse from pydantic import BaseModel - -from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL from services import announce from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (intern) Timer, @@ -29,8 +31,6 @@ from services.voice_metrics import ( # Per-Stage-Latenz + Per-Turn-Trace (inter record_stage, ) -# Injection-Schutz (Stufe 0): guard.py liegt im mcp/-Verzeichnis. Per Pfad laden (eigene MC2-Venv). -import sys as _sys _GUARD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "mcp") if _GUARD_DIR not in _sys.path: _sys.path.insert(0, _GUARD_DIR) @@ -155,7 +155,7 @@ def voice_health() -> dict: r = httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=httpx.Timeout(5.0)) out["sidecar"] = r.status_code == 200 out["detail"] = r.json() if r.status_code == 200 else None - except Exception as exc: # noqa: BLE001 + except Exception as exc: out["error"] = str(exc) return out @@ -180,7 +180,7 @@ def voice_voices() -> dict: r = httpx.get(f"{VOICE_SERVICE_URL}/voices", timeout=httpx.Timeout(10.0)) r.raise_for_status() return r.json() - except Exception as exc: # noqa: BLE001 + except Exception as exc: raise HTTPException(502, f"Voice-Sidecar nicht erreichbar: {exc}") @@ -245,7 +245,7 @@ def voice_get_reference() -> dict: r = httpx.get(f"{VOICE_SERVICE_URL}/reference", timeout=httpx.Timeout(8.0)) r.raise_for_status() return r.json() - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"active": False, "error": str(exc)} diff --git a/backend/routers/wissen.py b/backend/routers/wissen.py index 391af87..2f45495 100644 --- a/backend/routers/wissen.py +++ b/backend/routers/wissen.py @@ -6,9 +6,8 @@ import os import time from pathlib import Path -from pydantic import BaseModel - from fastapi import APIRouter, HTTPException +from pydantic import BaseModel router = APIRouter(prefix="/api") diff --git a/backend/routers/zeitmaschine.py b/backend/routers/zeitmaschine.py index bab90ec..5b5fe33 100644 --- a/backend/routers/zeitmaschine.py +++ b/backend/routers/zeitmaschine.py @@ -13,7 +13,6 @@ from pathlib import Path from fastapi import APIRouter, HTTPException from pydantic import BaseModel - from services import backup as backup_svc router = APIRouter(prefix="/api") diff --git a/backend/scripts/parse_mc2_timeout.py b/backend/scripts/parse_mc2_timeout.py index c9b274d..71d4c85 100644 --- a/backend/scripts/parse_mc2_timeout.py +++ b/backend/scripts/parse_mc2_timeout.py @@ -10,12 +10,10 @@ Permission errors are handled gracefully. Uses user journal (systemctl --user) to query logs. """ -import os import subprocess from datetime import datetime, timezone from pathlib import Path - LOG_DIR = Path.home() / "logs" LOG_FILE = LOG_DIR / "mc2-timeout.log" SERVICE_NAME = "mission-control-2.service" diff --git a/backend/services/agent.py b/backend/services/agent.py index e88badb..bb47bac 100644 --- a/backend/services/agent.py +++ b/backend/services/agent.py @@ -9,11 +9,15 @@ import os import re import httpx -import psutil - -from config import (BOX_CONSOLE_UPSTREAM, - BOX_CONSOLE_PATH, HERMES_BUILTIN_UI_UPSTREAM, HERMES_BUILTIN_UI_PATH, - HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL) +from config import ( + BOX_CONSOLE_PATH, + BOX_CONSOLE_UPSTREAM, + HERMES_API_URL, + HERMES_BUILTIN_UI_PATH, + HERMES_BUILTIN_UI_UPSTREAM, + HERMES_HOME, + PC_EXECUTOR_URL, +) log = logging.getLogger(__name__) @@ -194,7 +198,7 @@ def set_agent_brain(model_id: str) -> dict: new_members = [x for x in brains if x not in (old, model_id)] + [model_id] llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm # 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen. - from services.llamaswap import set_ttl, DEFAULT_TTL + from services.llamaswap import DEFAULT_TTL, set_ttl set_ttl(model_id, 0) if old: set_ttl(old, DEFAULT_TTL) @@ -253,7 +257,7 @@ def update_brain_model(new_model: str) -> bool: # Restart the user-space service to apply changes try: - import services.maintenance as maintenance + from services import maintenance maintenance.restart_service("hermes-gateway") except Exception: log.warning("update_brain_model: hermes-gateway-Restart fehlgeschlagen", exc_info=True) diff --git a/backend/services/announce.py b/backend/services/announce.py index cbb2261..b537dfe 100644 --- a/backend/services/announce.py +++ b/backend/services/announce.py @@ -20,7 +20,6 @@ import time from pathlib import Path import httpx - from config import MODELS_DIR log = logging.getLogger(__name__) diff --git a/backend/services/backup.py b/backend/services/backup.py index a375737..db42a1c 100644 --- a/backend/services/backup.py +++ b/backend/services/backup.py @@ -51,7 +51,7 @@ def backup_now() -> dict: r = subprocess.run(["/bin/bash", str(BACKUP_SH)], capture_output=True, text=True, timeout=180) if r.returncode != 0: return {"ok": False, "snapshot": "", "files": [], "error": (r.stderr or r.stdout).strip()[-300:]} - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"ok": False, "snapshot": "", "files": [], "error": str(exc)} latest = _latest() diff --git a/backend/services/connect.py b/backend/services/connect.py index e0284db..72bf83e 100644 --- a/backend/services/connect.py +++ b/backend/services/connect.py @@ -10,7 +10,6 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit. import json import httpx - from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT, V1_UPSTREAM DEFAULT_HOST = "192.168.178.151" @@ -139,7 +138,7 @@ def check_health() -> dict: gateway = {"ok": True, "detail": f"{n} Modelle verfügbar" if n else "bereit"} else: gateway = {"ok": False, "detail": f"HTTP {r.status_code}"} - except Exception: # noqa: BLE001 + except Exception: pass memory = {"ok": False, "detail": "nicht erreichbar"} @@ -148,7 +147,7 @@ def check_health() -> dict: r = c.get(f"{MEM0_SERVICE_URL}/health") memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200 else {"ok": False, "detail": f"HTTP {r.status_code}"}) - except Exception: # noqa: BLE001 + except Exception: pass desktop_gateway = {"ok": False, "detail": "nicht erreichbar"} @@ -157,7 +156,7 @@ def check_health() -> dict: r = c.get(f"{HERMES_BUILTIN_UI_UPSTREAM}/api/status") desktop_gateway = ({"ok": True, "detail": "bereit"} if r.status_code == 200 else {"ok": False, "detail": f"HTTP {r.status_code}"}) - except Exception: # noqa: BLE001 + except Exception: pass return {"gateway": gateway, "memory": memory, "desktop_gateway": desktop_gateway} diff --git a/backend/services/discover.py b/backend/services/discover.py index 0d4adf4..3041d17 100644 --- a/backend/services/discover.py +++ b/backend/services/discover.py @@ -9,16 +9,15 @@ spätere Auto-Setups nutzen ihn, damit sie nie auseinanderlaufen. """ import json +import logging import math import os import time from datetime import datetime import httpx - -import logging - from config import DISCOVER_CACHE_PATH, DISCOVER_TTL + from services import catalog from services.caps import capabilities from services.fit import evaluate_fit, extract_params_b, max_ctx_for diff --git a/backend/services/gateway.py b/backend/services/gateway.py index 63b3dce..67c6b22 100644 --- a/backend/services/gateway.py +++ b/backend/services/gateway.py @@ -5,8 +5,8 @@ V1_UPSTREAM gilt der alte eingebaute Modus (MC2 serviert /v1 selbst). """ import httpx - from config import PORT, V1_UPSTREAM + from services.llamaswap import engine_reachable from services.routing_policy import load_policy @@ -50,7 +50,7 @@ def gateway_reachable() -> bool: if V1_UPSTREAM: try: return httpx.get(f"{V1_UPSTREAM}/v1/models", timeout=2.0).status_code == 200 - except Exception: # noqa: BLE001 + except Exception: return False # Eingebauter Modus: der Gateway lebt in MC selbst und proxyt llama-swap. return engine_reachable() diff --git a/backend/services/ideen.py b/backend/services/ideen.py index fb74a42..91ea5d9 100644 --- a/backend/services/ideen.py +++ b/backend/services/ideen.py @@ -109,7 +109,7 @@ def _kurz(text: str, max_len: int = 80) -> str: return (schnitt or t[:max_len]) + "…" -_TITEL_PRAEFIX_RX = re.compile(r"^\s*(idee|projekt|bitte)\s*[:\-–]?\s*", re.I) +_TITEL_PRAEFIX_RX = re.compile(r"^\s*(idee|projekt|bitte)\s*[:\-–]?\s*", re.IGNORECASE) def _projekt_titel(titles: list[str]) -> str: @@ -220,7 +220,7 @@ def _ketten_anreichern(items: list[dict]) -> list[dict]: for it in items: familien.setdefault(boss(it["id"]), []).append(it) projekte = [] - for wurzel, mitglieder in familien.items(): + for mitglieder in familien.values(): if len(mitglieder) < 2: continue mitglieder.sort(key=lambda i: i.get("erstellt") or 0) @@ -752,8 +752,8 @@ def _lokales_konzept(text: str, titel: str = "") -> tuple | None: if not kandidaten and titel and _KONZEPT_DIR.is_dir(): worte = {w for w in re.findall(r"[a-z0-9]{3,}", titel.lower())} treffer = [p.name for p in _KONZEPT_DIR.glob("*.md") - if (lambda g: len(g) >= 2 or any(len(w) >= 6 for w in g))( - worte & set(re.findall(r"[a-z0-9]{3,}", p.stem.lower())))] + if len(g := worte & set(re.findall(r"[a-z0-9]{3,}", p.stem.lower()))) >= 2 + or any(len(w) >= 6 for w in g)] if len(treffer) == 1: kandidaten.append(treffer[0]) for name in kandidaten: @@ -935,7 +935,7 @@ def _konzept_und_name(task_id: str) -> tuple: quell_titel = _TITEL_PRAEFIX_RX.sub("", str(t.get("title") or "").strip()) if not quell_titel: return {}, "", "Quell-Karte hat keinen Titel." - h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.M) + h1 = re.search(r"^#\s+(.+)$", konz["konzept"], re.MULTILINE) kurz = h1.group(1).strip() if h1 else re.split(r"(?<=[.!?])\s", quell_titel)[0] return konz, _kurz(kurz, 85), "" @@ -1052,9 +1052,9 @@ def konzept_ueberarbeiten(task_id: str, hinweis: str) -> dict: "als `KONZEPT.md` hinein (klonen, schreiben, pushen, Push beweisen)." if konz.get("repo") else "")) - teile = [f"{_UEBERARBEITEN_KOPF}\n{quelle}\n" - f"· SO SOLL ES ANDERS WERDEN — Wortlaut des Commanders:\n „{hinweis}“\n" - f"{_UEBERARBEITEN_FUSS}", + teile = [(f"{_UEBERARBEITEN_KOPF}\n{quelle}\n" + f"· SO SOLL ES ANDERS WERDEN — Wortlaut des Commanders:\n „{hinweis}“\n" + f"{_UEBERARBEITEN_FUSS}"), _INFRA_LXC] titel = f"Konzept nachschärfen: {kurz}"[:200] args = ["create", titel, "--body", "\n\n".join(teile)[:4000], "--assignee", "projektstart", @@ -1098,7 +1098,7 @@ def log_of(task_id: str) -> dict: try: r = _hermes(["log", task_id]) - except Exception as exc: + except Exception: log.warning("ideen: kanban log fehlgeschlagen", exc_info=True) return {"available": True, "lines": []} if r.returncode != 0: diff --git a/backend/services/jobengine.py b/backend/services/jobengine.py index de2dc96..cfda835 100644 --- a/backend/services/jobengine.py +++ b/backend/services/jobengine.py @@ -90,7 +90,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor log_str = "\n".join(job["log"]) if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str: job["sudo_failed"] = True - except Exception as exc: # noqa: BLE001 + except Exception as exc: _append_log(job, f"[mc] Fehler: {exc}") job["state"] = "failed" job["returncode"] = -1 @@ -101,7 +101,7 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_passwor if cb and job["state"] == "done": try: cb() - except Exception as exc: # noqa: BLE001 + except Exception as exc: _append_log(job, f"[mc] Nachbearbeitung-Fehler: {exc}") @@ -136,7 +136,7 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N j["rate_bps"] = rate j["eta_s"] = int((total_bytes - cur) / rate) prev_t, prev_b = now, cur - except Exception: # noqa: BLE001 + except Exception: pass time.sleep(1.0) j = JOBS.get(job_id) @@ -182,7 +182,7 @@ def cancel_job(job_id: str) -> bool: if proc is not None: try: proc.terminate() - except Exception: # noqa: BLE001 + except Exception: pass else: job["state"] = "canceled" diff --git a/backend/services/ketten_digest.py b/backend/services/ketten_digest.py index 7fcd65a..84e145e 100644 --- a/backend/services/ketten_digest.py +++ b/backend/services/ketten_digest.py @@ -87,7 +87,6 @@ def _tick() -> None: return items = data.get("items") or [] projekte = data.get("projekte") or [] - by_id = {i["id"]: i for i in items if i.get("id")} state = _load_state() gemeldete_fragen: dict = state.setdefault("fragen", {}) projekt_state: dict = state.setdefault("projekte", {}) diff --git a/backend/services/llamaswap.py b/backend/services/llamaswap.py index 358c165..20079fe 100644 --- a/backend/services/llamaswap.py +++ b/backend/services/llamaswap.py @@ -11,12 +11,17 @@ import os import re import httpx -from ruamel.yaml.scalarstring import LiteralScalarString - from config import ( - CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, DRAFTS_DIR, LLAMA_SWAP_URL, - SPEC_DRAFT_MODEL_PATH, SPEC_DRAFT_N_MAX, SPEC_TYPE, + CMD_TEMPLATE, + CONFIG_PATH, + DEFAULT_TTL, + DRAFTS_DIR, + LLAMA_SWAP_URL, + SPEC_DRAFT_MODEL_PATH, + SPEC_DRAFT_N_MAX, + SPEC_TYPE, ) +from ruamel.yaml.scalarstring import LiteralScalarString log = logging.getLogger(__name__) @@ -143,13 +148,13 @@ def model_id_from_path(model_path: str) -> str: Split-GGUFs liegen oft in einem Quant-Unterordner (…/Q4_K_M/file-00001-of-…) → dann eine Ebene höher (Repo-Ordner) nehmen, sonst hieße das Modell 'Q4_K_M'.""" d = os.path.basename(os.path.dirname(model_path)) - if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.I): + if re.fullmatch(r"(I?Q\d[\w]*|UD-Q\d[\w]*|F16|BF16|FP16|F32)", d, flags=re.IGNORECASE): d = os.path.basename(os.path.dirname(os.path.dirname(model_path))) - name = re.sub(r"[-_]?GGUF$", "", d, flags=re.I).strip("-_") + name = re.sub(r"[-_]?GGUF$", "", d, flags=re.IGNORECASE).strip("-_") if not name: - fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.I) + fn = re.sub(r"\.gguf$", "", os.path.basename(model_path), flags=re.IGNORECASE) fn = re.sub(r"-\d+-of-\d+$", "", fn) - name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.I) + name = re.sub(r"[-_](Q\d[\w]*|IQ\d[\w]*|F16|BF16|FP16|F32)$", "", fn, flags=re.IGNORECASE) return name or "modell" @@ -222,7 +227,7 @@ def write_config(cfg: dict) -> None: try: from services import warmer warmer.nudge() - except Exception: # noqa: BLE001 — Vorwärmen ist Komfort, nie ein Schreib-Blocker + except Exception: pass except PermissionError as exc: raise PermissionError( diff --git a/backend/services/maintenance.py b/backend/services/maintenance.py index 7beed07..3409a93 100644 --- a/backend/services/maintenance.py +++ b/backend/services/maintenance.py @@ -35,7 +35,7 @@ _engine_cache = {"ts": 0.0, "avail": False} # manchmal noch keine CI-Assets (0 Assets) → ihr Download-Link 404t. Sowohl der Update-Check # als auch der Download (update-engine.sh) müssen daher die neueste ASSET-tragende Release # nehmen, sonst zeigt das UI „Update verfügbar", das dann beim Einspielen scheitert. -_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.I) +_ENGINE_ASSET_RX = re.compile(r"ubuntu-vulkan-x64\.tar\.gz$", re.IGNORECASE) def _latest_engine_asset_release() -> dict | None: @@ -289,7 +289,7 @@ def model_upgrades() -> list[dict]: continue base = rec.split("/")[-1].lower() - stem = base[:-5] if base.endswith("-gguf") else base + stem = base.removesuffix("-gguf") if base in cmds or (stem and stem in cmds): continue # schon installiert out.append({"role": role, "title": c["title"], "repo": rec}) @@ -343,7 +343,7 @@ def _os_held_back() -> list[dict]: held.append({"name": name, "reason": reason}) elif reason: # nicht eingerückt → Abschnitt zu Ende reason = None - except Exception: # noqa: BLE001 — nur Zusatzinfo, nie ein Blocker + except Exception: pass held.sort(key=lambda p: p["name"]) return held @@ -368,7 +368,7 @@ def os_update_details() -> dict: out_pkgs.sort(key=lambda p: p["name"]) return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs, "held_back": _os_held_back()} - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs, "error": str(exc)} @@ -393,7 +393,7 @@ def engine_update_details() -> dict: ctx = ("Es geht um ein Update der Inferenz-Engine llama.cpp (Vulkan-Build, treibt alle " "Sprachmodelle der Box auf der AMD-Strix-Halo-GPU).") info.update(_summarize_release("engine", tag, ctx, body[:6000])) - except Exception as exc: # noqa: BLE001 + except Exception as exc: info["error"] = str(exc) return info @@ -418,7 +418,7 @@ def swap_update_details() -> dict: ctx = ("Es geht um ein Update von llama-swap (der Router, der Anfragen an die Box " "verteilt und Sprachmodelle heiß nachlädt).") info.update(_summarize_release("swap", tag, ctx, body[:6000])) - except Exception as exc: # noqa: BLE001 + except Exception as exc: info["error"] = str(exc) return info @@ -496,7 +496,7 @@ def _summarize_release(kind: str, key: str, context: str, changes: str) -> dict: if text: cache.update(key=key, data=data) return data - except Exception as exc: # noqa: BLE001 — Zusammenfassung ist Komfort, nie Blocker + except Exception as exc: return {"summary": f"(Zusammenfassung nicht verfügbar: {exc})", "action_needed": None, "action_text": ""} @@ -534,7 +534,7 @@ def hermes_update_details() -> dict: info["behind"] = len(commits) if commits: info.update(_summarize_hermes_commits(commits)) - except Exception as exc: # noqa: BLE001 + except Exception as exc: info["error"] = str(exc) return info @@ -575,7 +575,7 @@ def _run(cmd: list[str], sudo_password: str | None = None) -> dict: return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."} return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]} - except Exception as exc: # noqa: BLE001 + except Exception as exc: return {"ok": False, "out": "", "err": str(exc)} diff --git a/backend/services/memory.py b/backend/services/memory.py index 75d6dc0..9d5176f 100644 --- a/backend/services/memory.py +++ b/backend/services/memory.py @@ -19,7 +19,6 @@ import re from difflib import SequenceMatcher import httpx - from config import MEM0_SERVICE_URL log = logging.getLogger(__name__) diff --git a/backend/services/metrics_history.py b/backend/services/metrics_history.py index eca0e0a..ced3400 100644 --- a/backend/services/metrics_history.py +++ b/backend/services/metrics_history.py @@ -51,7 +51,7 @@ def _load() -> None: log.info("metrics_history: %d Punkte aus %s geladen", len(_points), HISTORY_PATH) except FileNotFoundError: pass - except Exception: # noqa: BLE001 — kaputte Datei = leer starten, nie crashen + except Exception: log.warning("metrics_history: %s nicht lesbar — starte leer", HISTORY_PATH, exc_info=True) @@ -77,19 +77,19 @@ def _sample() -> None: try: du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd()) disk = du.percent - except Exception: # noqa: BLE001 + except Exception: pass gpu = None try: g = _gpu_sysfs() gpu = g.get("busy_percent") if g else None - except Exception: # noqa: BLE001 + except Exception: pass tp = tc = None try: ts = get_stats() tp, tc = ts.get("prompt_tokens"), ts.get("completion_tokens") - except Exception: # noqa: BLE001 + except Exception: pass # interval=None: nicht-blockierende CPU-Messung seit dem letzten Aufruf (10 s her — ideal). _points.append([int(time.time()), psutil.cpu_percent(interval=None), vm.percent, gpu, disk, tp, tc]) @@ -101,12 +101,12 @@ async def sampler_loop() -> None: while True: try: await asyncio.to_thread(_sample) - except Exception: # noqa: BLE001 + except Exception: log.debug("metrics_history: Sample fehlgeschlagen", exc_info=True) if time.time() - _last_flush >= FLUSH_S: try: await asyncio.to_thread(_flush) - except Exception: # noqa: BLE001 + except Exception: pass await asyncio.sleep(SAMPLE_S) diff --git a/backend/services/reminders.py b/backend/services/reminders.py index fcf943f..0fac64b 100644 --- a/backend/services/reminders.py +++ b/backend/services/reminders.py @@ -24,6 +24,7 @@ from pathlib import Path from zoneinfo import ZoneInfo from config import MODELS_DIR + from services import announce log = logging.getLogger(__name__) diff --git a/backend/services/sentry.py b/backend/services/sentry.py index 4b461c8..3321df2 100644 --- a/backend/services/sentry.py +++ b/backend/services/sentry.py @@ -20,8 +20,8 @@ import time import httpx import psutil +from config import HERMES_API_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL -from config import HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL from services import announce, llamaswap log = logging.getLogger(__name__) @@ -100,7 +100,7 @@ if os.environ.get("MC_SENTRY_WATCH_MC2", "") == "1": class _Watch: - __slots__ = ("fails", "alerted", "alert_ts") + __slots__ = ("alert_ts", "alerted", "fails") def __init__(self) -> None: self.fails = 0 # Fehl-Ticks in Folge diff --git a/backend/services/system.py b/backend/services/system.py index bcd0457..c6a2274 100644 --- a/backend/services/system.py +++ b/backend/services/system.py @@ -12,7 +12,6 @@ import subprocess import threading import psutil - from config import MODELS_DIR diff --git a/backend/services/token_stats.py b/backend/services/token_stats.py index e098c39..19c3e01 100644 --- a/backend/services/token_stats.py +++ b/backend/services/token_stats.py @@ -11,7 +11,6 @@ import json import logging import threading import time -from pathlib import Path from config import HERMES_HOME diff --git a/backend/services/warmer.py b/backend/services/warmer.py index 70993ad..1c9f7dd 100644 --- a/backend/services/warmer.py +++ b/backend/services/warmer.py @@ -24,7 +24,6 @@ import os import subprocess import httpx - from config import LLAMA_SWAP_URL log = logging.getLogger(__name__) diff --git a/client/hermes-pc/executor.py b/client/hermes-pc/executor.py index ed2d8f8..528997c 100644 --- a/client/hermes-pc/executor.py +++ b/client/hermes-pc/executor.py @@ -154,7 +154,7 @@ async def volume_control(req: dict): async def open_target(req: dict): target = req.get("target", "") try: - if target.startswith("http://") or target.startswith("https://"): + if target.startswith(("http://", "https://")): webbrowser.open(target) else: os.startfile(target) diff --git a/deploy/agent-hooks/tabu-pfade-guard.py b/deploy/agent-hooks/tabu-pfade-guard.py index 4ed486f..06c0146 100644 --- a/deploy/agent-hooks/tabu-pfade-guard.py +++ b/deploy/agent-hooks/tabu-pfade-guard.py @@ -20,7 +20,10 @@ nur Schreib-Operationen. Ausgabe {"action":"block","message":...} = Tool geblockt; {} = durchlassen. """ -import sys, json, os, re +import json +import os +import re +import sys def out(obj): diff --git a/deploy/ensure-profile-hooks.py b/deploy/ensure-profile-hooks.py index e6af2c5..d9dc3cc 100644 --- a/deploy/ensure-profile-hooks.py +++ b/deploy/ensure-profile-hooks.py @@ -15,10 +15,10 @@ verschlucken und den Tabu-Guard lautlos deaktivieren. Idempotent + validiert + Backup. Anker fuer neue Event-Bloecke ist der bestehende `pre_verify`-Block. Arg: Pfad zur Profil-config.yaml. """ -import sys -import os import datetime +import os import shutil +import sys try: import yaml diff --git a/deploy/governor/driver.py b/deploy/governor/driver.py index 2a4fef8..36e1268 100644 --- a/deploy/governor/driver.py +++ b/deploy/governor/driver.py @@ -44,7 +44,7 @@ def main() -> int: try: coder.summarizer.max_tokens = 10 ** 9 print(f"[driver] summarizer.max_tokens -> {coder.summarizer.max_tokens}", flush=True) - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f"[driver] WARN konnte summarizer nicht abschalten: {exc}", flush=True) for i, msg in enumerate(messages, 1): @@ -52,7 +52,7 @@ def main() -> int: print(f">> {msg[:140]}", flush=True) try: coder.run(with_message=msg) - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f"[driver] TURN {i} Fehler: {exc!r}", flush=True) break sent = getattr(coder, "total_tokens_sent", "?") diff --git a/deploy/governor/governor.py b/deploy/governor/governor.py index 63a4866..5e591e7 100644 --- a/deploy/governor/governor.py +++ b/deploy/governor/governor.py @@ -153,7 +153,7 @@ def _post_announce(est) -> None: resp.read() conn.close() log(f"ANNOUNCE -> Lucy status={resp.status} est={est}") - except Exception as exc: # noqa: BLE001 + except Exception as exc: log(f"ANNOUNCE fehlgeschlagen: {exc!r}") @@ -383,7 +383,7 @@ class Handler(BaseHTTPRequestHandler): return None try: text = tail.decode("utf-8", errors="ignore") - except Exception: # noqa: BLE001 + except Exception: return None matches = _PROMPT_TOKENS_RE.findall(text) if not matches: diff --git a/deploy/hermes-patches/apply.py b/deploy/hermes-patches/apply.py index 5a6e6fc..5f4f548 100644 --- a/deploy/hermes-patches/apply.py +++ b/deploy/hermes-patches/apply.py @@ -234,8 +234,8 @@ def main() -> int: continue n = src.count(p["old"]) if n != 1: - failed.append((p["name"], f"erwartete 1 Vorkommen von old, fand {n} " - "(Update hat den Kontext geaendert?)")) + failed.append((p["name"], (f"erwartete 1 Vorkommen von old, fand {n} " + "(Update hat den Kontext geaendert?)"))) continue f.write_text(src.replace(p["old"], p["new"]), encoding="utf-8") try: diff --git a/deploy/hermes-patches/mc2_kanban_reaper.py b/deploy/hermes-patches/mc2_kanban_reaper.py index 8294891..2244386 100644 --- a/deploy/hermes-patches/mc2_kanban_reaper.py +++ b/deploy/hermes-patches/mc2_kanban_reaper.py @@ -23,7 +23,7 @@ import sys _TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)") -def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]": +def reap_orphaned_workers(connect_closing) -> list[tuple[int, str]]: """Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist. ``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus @@ -32,7 +32,7 @@ def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]": """ if sys.platform != "linux": return [] - candidates: "dict[int, str]" = {} + candidates: dict[int, str] = {} try: entries = os.listdir("/proc") except OSError: @@ -53,7 +53,7 @@ def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]": task_ids = list(set(candidates.values())) placeholders = ",".join("?" * len(task_ids)) - running: "set[str]" = set() + running: set[str] = set() try: with connect_closing() as conn: running = { @@ -67,7 +67,7 @@ def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]": except Exception: return [] - killed: "list[tuple[int, str]]" = [] + killed: list[tuple[int, str]] = [] for pid, task_id in candidates.items(): if task_id in running: continue diff --git a/deploy/pruefstand/harness.py b/deploy/pruefstand/harness.py index 2f4e9b5..058022a 100644 --- a/deploy/pruefstand/harness.py +++ b/deploy/pruefstand/harness.py @@ -84,7 +84,7 @@ def norm(s): def code_block(text, sprache="python"): """Letzten passenden Code-Block extrahieren; ohne Zaun: ganzen Text nehmen.""" - bloecke = re.findall(r"```(?:%s)?\s*\n(.*?)```" % re.escape(sprache), + bloecke = re.findall(rf"```(?:{re.escape(sprache)})?\s*\n(.*?)```", text or "", re.DOTALL | re.IGNORECASE) if bloecke: return bloecke[-1] @@ -469,7 +469,7 @@ def suite_speed(cfg, ergebnisse): # Kurz-Probe: misst tg (Alltags-Turn) — 2 Läufe, erster wärmt den Cache an. for lauf in ("warm", "kurz"): t0 = time.time() - msg, tim, err = api_chat(cfg.endpoint, cfg.model, [ + _msg, tim, err = api_chat(cfg.endpoint, cfg.model, [ {"role": "user", "content": "Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell " "ist."}], max_tokens=200, temperature=0) @@ -487,7 +487,7 @@ def suite_speed(cfg, ergebnisse): return lang = LANG_BAUSTEIN * 550 t0 = time.time() - msg, tim, err = api_chat(cfg.endpoint, cfg.model, [ + _msg, tim, err = api_chat(cfg.endpoint, cfg.model, [ {"role": "user", "content": lang + "\n\nWie oft steht sinngemäß derselbe Satz oben? Antworte in " "einem Satz."}], max_tokens=80, temperature=0, timeout=900) diff --git a/hermes/plugins/mc2-memory/__init__.py b/hermes/plugins/mc2-memory/__init__.py index 83529f0..a34546e 100644 --- a/hermes/plugins/mc2-memory/__init__.py +++ b/hermes/plugins/mc2-memory/__init__.py @@ -27,10 +27,9 @@ import queue import re import threading import time -from typing import Any, Dict, List, Optional +from typing import Any, Optional import httpx - from agent.memory_provider import MemoryProvider log = logging.getLogger(__name__) @@ -79,12 +78,12 @@ class MC2MemoryProvider(MemoryProvider): def __init__(self) -> None: self._session_id = "" self._agent_context = "primary" - self._q: "queue.Queue[list]" = queue.Queue(maxsize=200) - self._worker: Optional[threading.Thread] = None + self._q: queue.Queue[list] = queue.Queue(maxsize=200) + self._worker: threading.Thread | None = None self._stop = threading.Event() - self._recall_cache: Dict[str, str] = {} + self._recall_cache: dict[str, str] = {} # Turn-Puffer der Lern-Buendelung (siehe BATCH_TURNS/BATCH_IDLE_S). - self._buf: List[List[Dict[str, str]]] = [] + self._buf: list[list[dict[str, str]]] = [] self._buf_lock = threading.Lock() self._buf_last_add = 0.0 @@ -146,7 +145,7 @@ class MC2MemoryProvider(MemoryProvider): # -- Lernen (nach dem Turn) ---------------------------------------------- def sync_turn(self, user_content: str, assistant_content: str, *, - session_id: str = "", messages: Optional[List[Dict[str, Any]]] = None) -> None: + session_id: str = "", messages: list[dict[str, Any]] | None = None) -> None: if self._agent_context != "primary": return u = (user_content or "").strip() @@ -158,7 +157,7 @@ class MC2MemoryProvider(MemoryProvider): if SKIP_UNTRUSTED and any(m in u for m in UNTRUSTED_MARKERS): log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen") return - msgs: List[Dict[str, str]] = [{"role": "user", "content": u}] + msgs: list[dict[str, str]] = [{"role": "user", "content": u}] a = (assistant_content or "").strip() # Hermes ≥0.18 liefert optional den vollen Turn-Kontext (`messages`, inkl. Tool-Calls). # Bewusst NUR die Tool-NAMEN mitlernen ("hat X per Tool Y geprüft") — Tool-ERGEBNISSE @@ -188,7 +187,7 @@ class MC2MemoryProvider(MemoryProvider): with self._buf_lock: if not self._buf: return - merged: List[Dict[str, str]] = [] + merged: list[dict[str, str]] = [] for turn in self._buf: merged.extend(turn) self._buf.clear() @@ -237,10 +236,10 @@ class MC2MemoryProvider(MemoryProvider): self._post_learn(msgs) # -- Context-only: keine Agent-Tools → kein Tool-Loop -------------------- - def get_tool_schemas(self) -> List[Dict[str, Any]]: + def get_tool_schemas(self) -> list[dict[str, Any]]: return [] - def on_session_end(self, messages: List[Dict[str, Any]]) -> None: + def on_session_end(self, messages: list[dict[str, Any]]) -> None: self._flush() def shutdown(self) -> None: diff --git a/mcp/mcp_web.py b/mcp/mcp_web.py index c03209b..9dcb8cd 100644 --- a/mcp/mcp_web.py +++ b/mcp/mcp_web.py @@ -7,9 +7,8 @@ Fetcht URLs und extrahiert sauberen Text — kein API-Key nötig. """ import httpx -from mcp.server.fastmcp import FastMCP - from guard import wrap_untrusted # Injection-Schutz für untrusted Web-Inhalt (Stufe 0 Security) +from mcp.server.fastmcp import FastMCP mcp = FastMCP("hermes-web-fetch") diff --git a/mem0_service/app.py b/mem0_service/app.py index bee8123..22beda9 100644 --- a/mem0_service/app.py +++ b/mem0_service/app.py @@ -24,11 +24,10 @@ import re from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException -from pydantic import BaseModel - from mem0 import Memory from mem0.configs.llms.openai import OpenAIConfig from mem0.llms.openai import OpenAILLM +from pydantic import BaseModel log = logging.getLogger("mem0_service") @@ -203,7 +202,7 @@ def health() -> dict: try: mem().get_all(filters={"user_id": USER_ID}, top_k=1) return {"ok": True} - except Exception as exc: # noqa: BLE001 + except Exception as exc: raise HTTPException(503, f"mem0 nicht bereit: {exc}") @@ -230,7 +229,7 @@ def _rerank(query: str, items: list[dict]) -> list[dict]: ranked.append(item) # Nur übernehmen, wenn der Reranker ALLE Kandidaten bewertet hat (sonst Verlustgefahr). return ranked if len(ranked) == len(items) else items - except Exception as exc: # noqa: BLE001 — Gedächtnis darf am Reranker nie scheitern + except Exception as exc: log.debug("rerank übersprungen: %s", exc) return items @@ -383,7 +382,7 @@ def classify_facts(facts: list[tuple[str, str]]) -> dict: {"role": "user", "content": "Fakten:\n" + body}], response_format={"type": "json_object"}, ) - m = _re.search(r"\{.*\}", resp or "", _re.S) + m = _re.search(r"\{.*\}", resp or "", _re.DOTALL) data = _json.loads(m.group(0)) if m else {} out: dict = {} for k, v in data.items(): diff --git a/mem0_service/smoke_test.py b/mem0_service/smoke_test.py index 21d7532..c198dd0 100644 --- a/mem0_service/smoke_test.py +++ b/mem0_service/smoke_test.py @@ -22,7 +22,7 @@ os.environ["MEM0_HISTORY_DB"] = os.path.join(_TMP, "history.db") os.environ["MEM0_COLLECTION"] = f"smoke_{int(time.time())}" sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -import app # noqa: E402 +import app _fails: list[str] = [] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..37a5f0a --- /dev/null +++ b/ruff.toml @@ -0,0 +1,29 @@ +# Lint-Politik fuer MC2 (24.07.2026, mit der Ampel-Nachruestung angelegt). +# Diese Codebasis nutzt BEWUSST best-effort-Fehlerbehandlung (blind-except, +# try/except-pass/continue) fuer Robustheit und FastAPI-Idiome (Depends()/File() +# als Argument-Defaults). Solche absichtlichen Muster sind KEINE Fehler; echte +# Probleme (unsortierte/ungenutzte Imports, veraltete Annotationen, Bug-Risiken) +# werden weiterhin geflaggt. Ruff-Default-Regelmenge minus der Ausnahmen unten. + +target-version = "py310" +line-length = 120 + +[lint] +ignore = [ + "BLE001", # blind-except — bewusstes best-effort-Muster im ganzen Stack + "S110", # try-except-pass — bewusst (best-effort, Fallback) + "S112", # try-except-continue — bewusst + "PLW1510", # subprocess ohne check= — best-effort, Rueckgabe wird selbst geprueft + "B008", # Funktionsaufruf im Argument-Default — FastAPI Depends()/File()-Idiom + "EXE001", # Shebang ohne x-Bit — Skripte laufen ueber den Interpreter + "DTZ005", # datetime.now() ohne tz — lokale Zeit ist hier gewollt + "SIM102", # verschachteltes if — Stil, nicht erzwungen + "SIM115", # open() ohne Kontextmanager — in kurzen Skripten bewusst + "SIM117", # mehrere with — Stil + "ASYNC221", # subprocess in async — bewusst (kurze, seltene Aufrufe) + "N999", # Modulname mit Bindestrich — Plugin-Konvention, nicht aenderbar + "PYI034", # non-self return type — Absicht +] + +[lint.per-file-ignores] +"**/__init__.py" = ["F401"] # Re-Exports sind gewollt "ungenutzt" diff --git a/scripts/filter_inactive_skills.py b/scripts/filter_inactive_skills.py index 8f07dcd..eda5424 100644 --- a/scripts/filter_inactive_skills.py +++ b/scripts/filter_inactive_skills.py @@ -10,8 +10,8 @@ Ausgabe: Liste sortiert nach last_used_at (älteste zuerst), nur Skills mit use_ """ import json -from pathlib import Path from datetime import datetime +from pathlib import Path def load_usage_data(path: Path) -> dict: diff --git a/voice_service/app.py b/voice_service/app.py index 4874ca1..d306747 100644 --- a/voice_service/app.py +++ b/voice_service/app.py @@ -331,7 +331,7 @@ def chatterbox_tts(text: str, language: str, ref_path: str) -> bytes: try: wav = model.generate(text, **kwargs) break - except Exception as exc: # noqa: BLE001 + except Exception as exc: last_err = exc log.warning("Chatterbox-Generation fehlgeschlagen (Versuch %d/3): %s", attempt + 1, exc) if wav is None: @@ -421,7 +421,7 @@ def elevenlabs_list() -> list[dict]: for v in data.get("voices", []) if v.get("category") != "premade" ] - except Exception: # noqa: BLE001 + except Exception: log.warning("ElevenLabs-Stimmenliste fehlgeschlagen", exc_info=True) return [] @@ -431,6 +431,7 @@ def elevenlabs_list() -> list[dict]: # ================================================================================= def edge_tts_synth(text: str, voice: str) -> bytes: import asyncio + import edge_tts v = voice or EDGE_DEFAULT @@ -445,7 +446,7 @@ def edge_tts_synth(text: str, voice: str) -> bytes: try: return asyncio.run(_run()) - except Exception as exc: # noqa: BLE001 + except Exception as exc: raise HTTPException(502, f"Edge-TTS-Fehler: {exc}") @@ -459,6 +460,7 @@ def edge_list() -> list[dict]: return _edge_cache try: import asyncio + import edge_tts voices = asyncio.run(edge_tts.list_voices()) @@ -474,7 +476,7 @@ def edge_list() -> list[dict]: "clonable": False, "_female": v.get("Gender") == "Female"}) out.sort(key=lambda i: (not i.pop("_female"), i["id"])) _edge_cache = out - except Exception: # noqa: BLE001 + except Exception: log.warning("Edge-Stimmenliste fehlgeschlagen", exc_info=True) _edge_cache = [] return _edge_cache @@ -498,6 +500,7 @@ async def lifespan(_app: FastAPI): # /turn-Aufruf kostete sonst ~3,3 s — das traf beim Realtest den ersten gesprochenen Satz. try: import io as _io + import numpy as _np import soundfile as _sf buf = _io.BytesIO() @@ -582,7 +585,7 @@ def _save_reference_as_wav(data: bytes, filename: str) -> str: dest = REFS_DIR / "ref.wav" import soundfile as sf try: - from faster_whisper.audio import decode_audio # PyAV-basiert, kann mp3/m4a/ogg/wav + from faster_whisper.audio import decode_audio # PyAV-basiert, kann mp3/m4a/ogg/wav arr = decode_audio(str(raw), sampling_rate=24000) sf.write(str(dest), arr, 24000, subtype="PCM_16") return str(dest)