From fe14df5fdab663f8c89270d7b6f43099da97695b Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 25 Apr 2026 01:40:34 -0700 Subject: [PATCH] =?UTF-8?q?chore(backend):=20Ruff=20lint=20pass=20?= =?UTF-8?q?=E2=80=94=20deprecated=20APIs,=20exception=20leaks,=20dead=20pa?= =?UTF-8?q?tterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical sweep of items called out in the PR review: - qwen_llm_backend: AutoModelForCausalLM.from_pretrained(torch_dtype=…) is deprecated in transformers ≥4.41 in favor of dtype=. Renamed. - routes/llm: try/except around backend.generate() raised HTTPException(500, detail=str(e)) which leaks stack traces / paths to clients and trips Ruff B904. Now logs the original exception server-side and hands the client a generic message; chained via `from e` to preserve traceback context. - mcp_bindings + mcp_server/context: datetime.utcnow() is deprecated since 3.12. Switched the two assignment sites to datetime.now(timezone.utc). The schema-level `default=datetime.utcnow` defaults in database/models.py are left for a later schema-aware pass. - routes/generations: `logger = …` sat between two import blocks (Ruff E402). Moved below imports. - mcp_server/server + tests/test_refinement_samples: typing.Callable / typing.Iterable have been preferred-via collections.abc since 3.9 (Ruff UP035). - routes/events: `except asyncio.TimeoutError` aliases plain `TimeoutError` since 3.11 (UP041). - services/captures: hoisted WHISPER_NATIVE_FORMATS to module scope (was a function-local UPPER_SNAKE that tripped N806) and replaced the raw_path.unlink try/except OSError-pass with contextlib.suppress (SIM105). Semantic equivalence preserved — written_files.remove(raw_path) still only runs when unlink succeeds because it sits inside the suppressed block after the unlink call. - database/migrations: hoisted the duplicate `import sqlite3` from inside two helper bodies to a single module-level import. --- backend/backends/qwen_llm_backend.py | 2 +- backend/database/migrations.py | 5 +---- backend/mcp_server/context.py | 4 ++-- backend/mcp_server/server.py | 2 +- backend/routes/events.py | 2 +- backend/routes/generations.py | 4 ++-- backend/routes/llm.py | 9 ++++++++- backend/routes/mcp_bindings.py | 4 ++-- backend/services/captures.py | 14 +++++++------- backend/tests/test_refinement_samples.py | 3 ++- 10 files changed, 27 insertions(+), 22 deletions(-) diff --git a/backend/backends/qwen_llm_backend.py b/backend/backends/qwen_llm_backend.py index a3c59354..e77a3540 100644 --- a/backend/backends/qwen_llm_backend.py +++ b/backend/backends/qwen_llm_backend.py @@ -108,7 +108,7 @@ class PyTorchQwenLLMBackend: dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32 self.model = AutoModelForCausalLM.from_pretrained( repo, - torch_dtype=dtype, + dtype=dtype, ) self.model.to(self.device) self.model.eval() diff --git a/backend/database/migrations.py b/backend/database/migrations.py index a82e451a..75cd1a59 100644 --- a/backend/database/migrations.py +++ b/backend/database/migrations.py @@ -18,6 +18,7 @@ Adding a new migration: """ import logging +import sqlite3 from sqlalchemy import inspect, text @@ -266,8 +267,6 @@ def _migrate_mcp_bindings(engine, inspector, tables: set[str]) -> None: # Leaving the unused column in place is harmless — the ORM # only maps declared columns, so a stray one does no work # and gets no reads or writes. - import sqlite3 - logger.warning( "SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.", sqlite3.sqlite_version, @@ -280,8 +279,6 @@ def _supports_drop_column(engine) -> bool: decades; SQLite only gained the feature in 3.35.""" if engine.dialect.name != "sqlite": return True - import sqlite3 - return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0) diff --git a/backend/mcp_server/context.py b/backend/mcp_server/context.py index 9795fa21..ecf1801a 100644 --- a/backend/mcp_server/context.py +++ b/backend/mcp_server/context.py @@ -11,7 +11,7 @@ import asyncio import ipaddress import logging from contextvars import ContextVar -from datetime import datetime +from datetime import datetime, timezone from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request @@ -141,7 +141,7 @@ def _stamp_last_seen(client_id: str) -> None: if row is None: row = MCPClientBinding(client_id=client_id) db.add(row) - row.last_seen_at = datetime.utcnow() + row.last_seen_at = datetime.now(timezone.utc) db.commit() except Exception: logger.debug( diff --git a/backend/mcp_server/server.py b/backend/mcp_server/server.py index 3434f7c4..3a408df9 100644 --- a/backend/mcp_server/server.py +++ b/backend/mcp_server/server.py @@ -10,7 +10,7 @@ from __future__ import annotations import logging from contextlib import AsyncExitStack, asynccontextmanager -from typing import Callable +from collections.abc import Callable from fastapi import FastAPI from fastmcp import FastMCP diff --git a/backend/routes/events.py b/backend/routes/events.py index 0330eb43..8a8fb33e 100644 --- a/backend/routes/events.py +++ b/backend/routes/events.py @@ -34,7 +34,7 @@ async def speak_events(request: Request): return try: event = await asyncio.wait_for(queue.get(), timeout=15.0) - except asyncio.TimeoutError: + except TimeoutError: # Heartbeat so proxies don't reap idle streams. yield {"event": "ping", "data": "{}"} continue diff --git a/backend/routes/generations.py b/backend/routes/generations.py index 5b837905..936337f2 100644 --- a/backend/routes/generations.py +++ b/backend/routes/generations.py @@ -8,8 +8,6 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session -logger = logging.getLogger(__name__) - from .. import models from ..services import history, personality, profiles, tts from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db @@ -17,6 +15,8 @@ from ..services.generation import run_generation from ..services.task_queue import cancel_generation as cancel_generation_job, enqueue_generation from ..utils.tasks import get_task_manager +logger = logging.getLogger(__name__) + router = APIRouter() diff --git a/backend/routes/llm.py b/backend/routes/llm.py index 36f4c174..0b394dcf 100644 --- a/backend/routes/llm.py +++ b/backend/routes/llm.py @@ -1,5 +1,7 @@ """LLM inference endpoints.""" +import logging + from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse @@ -9,6 +11,8 @@ from ..services import llm from ..services.task_queue import create_background_task from ..utils.tasks import get_task_manager +logger = logging.getLogger(__name__) + router = APIRouter() @@ -70,4 +74,7 @@ async def llm_generate(request: models.LLMGenerateRequest): ) return models.LLMGenerateResponse(text=text, model_size=model_size) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + # The backend exception text can include filesystem paths and stack + # frames — log it server-side and hand the client a generic message. + logger.exception("LLM generate failed") + raise HTTPException(status_code=500, detail="LLM generation failed") from e diff --git a/backend/routes/mcp_bindings.py b/backend/routes/mcp_bindings.py index c4bd67e9..1beb2c40 100644 --- a/backend/routes/mcp_bindings.py +++ b/backend/routes/mcp_bindings.py @@ -6,7 +6,7 @@ column is the same value the MCP client sends in ``X-Voicebox-Client-Id`` (or the stdio shim pulls from ``VOICEBOX_CLIENT_ID``). """ -from datetime import datetime +from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session @@ -56,7 +56,7 @@ async def upsert_mcp_binding( row.profile_id = data.profile_id row.default_engine = data.default_engine row.default_personality = data.default_personality - row.updated_at = datetime.utcnow() + row.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(row) return models.MCPClientBindingResponse.model_validate(row) diff --git a/backend/services/captures.py b/backend/services/captures.py index 73e2979b..d806e9ae 100644 --- a/backend/services/captures.py +++ b/backend/services/captures.py @@ -7,6 +7,7 @@ uploaded file). Storage mirrors the generations flow: audio lives under ``data/captures/.wav`` and rows live in the ``captures`` table. """ +import contextlib import json import logging import uuid @@ -27,6 +28,10 @@ logger = logging.getLogger(__name__) VALID_SOURCES = {"dictation", "recording", "file"} +# Suffixes whisper's miniaudio loader can read directly. Anything outside +# this set has to go through librosa for decode + a soundfile transcode +# before whisper sees it. +WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg") def _to_response(row: DBCapture) -> CaptureResponse: @@ -91,13 +96,11 @@ async def create_capture( audio, sr = None, None duration_ms = None - _WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg") - if audio is None or sr is None: # Decode failed. Only pass the file straight to whisper if the # source is a format its miniaudio loader can still read — webm, # m4a, etc. would just 500 later. Surface a clean error instead. - if suffix not in _WHISPER_NATIVE_FORMATS: + if suffix not in WHISPER_NATIVE_FORMATS: raise ValueError( f"Could not decode {suffix} audio — the recording may be empty or corrupt" ) @@ -110,11 +113,8 @@ async def create_capture( audio_path = config.get_captures_dir() / f"{capture_id}.wav" sf.write(str(audio_path), audio, sr, format="WAV") written_files.append(audio_path) - try: + with contextlib.suppress(OSError): raw_path.unlink() - except OSError: - pass - else: written_files.remove(raw_path) whisper = get_whisper_model() diff --git a/backend/tests/test_refinement_samples.py b/backend/tests/test_refinement_samples.py index 1e91caca..70fb3840 100644 --- a/backend/tests/test_refinement_samples.py +++ b/backend/tests/test_refinement_samples.py @@ -34,7 +34,8 @@ import sys import time from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Iterable, Optional +from collections.abc import Iterable +from typing import Optional import httpx