chore(backend): Ruff lint pass — deprecated APIs, exception leaks, dead patterns

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.
This commit is contained in:
Jamie Pine
2026-04-25 01:40:34 -07:00
parent 6cf8da2698
commit fe14df5fda
10 changed files with 27 additions and 22 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ class PyTorchQwenLLMBackend:
dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32 dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32
self.model = AutoModelForCausalLM.from_pretrained( self.model = AutoModelForCausalLM.from_pretrained(
repo, repo,
torch_dtype=dtype, dtype=dtype,
) )
self.model.to(self.device) self.model.to(self.device)
self.model.eval() self.model.eval()
+1 -4
View File
@@ -18,6 +18,7 @@ Adding a new migration:
""" """
import logging import logging
import sqlite3
from sqlalchemy import inspect, text 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 # Leaving the unused column in place is harmless — the ORM
# only maps declared columns, so a stray one does no work # only maps declared columns, so a stray one does no work
# and gets no reads or writes. # and gets no reads or writes.
import sqlite3
logger.warning( logger.warning(
"SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.", "SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.",
sqlite3.sqlite_version, sqlite3.sqlite_version,
@@ -280,8 +279,6 @@ def _supports_drop_column(engine) -> bool:
decades; SQLite only gained the feature in 3.35.""" decades; SQLite only gained the feature in 3.35."""
if engine.dialect.name != "sqlite": if engine.dialect.name != "sqlite":
return True return True
import sqlite3
return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0) return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0)
+2 -2
View File
@@ -11,7 +11,7 @@ import asyncio
import ipaddress import ipaddress
import logging import logging
from contextvars import ContextVar from contextvars import ContextVar
from datetime import datetime from datetime import datetime, timezone
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request from starlette.requests import Request
@@ -141,7 +141,7 @@ def _stamp_last_seen(client_id: str) -> None:
if row is None: if row is None:
row = MCPClientBinding(client_id=client_id) row = MCPClientBinding(client_id=client_id)
db.add(row) db.add(row)
row.last_seen_at = datetime.utcnow() row.last_seen_at = datetime.now(timezone.utc)
db.commit() db.commit()
except Exception: except Exception:
logger.debug( logger.debug(
+1 -1
View File
@@ -10,7 +10,7 @@ from __future__ import annotations
import logging import logging
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from typing import Callable from collections.abc import Callable
from fastapi import FastAPI from fastapi import FastAPI
from fastmcp import FastMCP from fastmcp import FastMCP
+1 -1
View File
@@ -34,7 +34,7 @@ async def speak_events(request: Request):
return return
try: try:
event = await asyncio.wait_for(queue.get(), timeout=15.0) event = await asyncio.wait_for(queue.get(), timeout=15.0)
except asyncio.TimeoutError: except TimeoutError:
# Heartbeat so proxies don't reap idle streams. # Heartbeat so proxies don't reap idle streams.
yield {"event": "ping", "data": "{}"} yield {"event": "ping", "data": "{}"}
continue continue
+2 -2
View File
@@ -8,8 +8,6 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
from .. import models from .. import models
from ..services import history, personality, profiles, tts from ..services import history, personality, profiles, tts
from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db 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 ..services.task_queue import cancel_generation as cancel_generation_job, enqueue_generation
from ..utils.tasks import get_task_manager from ..utils.tasks import get_task_manager
logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
+8 -1
View File
@@ -1,5 +1,7 @@
"""LLM inference endpoints.""" """LLM inference endpoints."""
import logging
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@@ -9,6 +11,8 @@ from ..services import llm
from ..services.task_queue import create_background_task from ..services.task_queue import create_background_task
from ..utils.tasks import get_task_manager from ..utils.tasks import get_task_manager
logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@@ -70,4 +74,7 @@ async def llm_generate(request: models.LLMGenerateRequest):
) )
return models.LLMGenerateResponse(text=text, model_size=model_size) return models.LLMGenerateResponse(text=text, model_size=model_size)
except Exception as e: 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
+2 -2
View File
@@ -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``). (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 fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -56,7 +56,7 @@ async def upsert_mcp_binding(
row.profile_id = data.profile_id row.profile_id = data.profile_id
row.default_engine = data.default_engine row.default_engine = data.default_engine
row.default_personality = data.default_personality row.default_personality = data.default_personality
row.updated_at = datetime.utcnow() row.updated_at = datetime.now(timezone.utc)
db.commit() db.commit()
db.refresh(row) db.refresh(row)
return models.MCPClientBindingResponse.model_validate(row) return models.MCPClientBindingResponse.model_validate(row)
+7 -7
View File
@@ -7,6 +7,7 @@ uploaded file). Storage mirrors the generations flow: audio lives under
``data/captures/<id>.wav`` and rows live in the ``captures`` table. ``data/captures/<id>.wav`` and rows live in the ``captures`` table.
""" """
import contextlib
import json import json
import logging import logging
import uuid import uuid
@@ -27,6 +28,10 @@ logger = logging.getLogger(__name__)
VALID_SOURCES = {"dictation", "recording", "file"} 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: def _to_response(row: DBCapture) -> CaptureResponse:
@@ -91,13 +96,11 @@ async def create_capture(
audio, sr = None, None audio, sr = None, None
duration_ms = None duration_ms = None
_WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg")
if audio is None or sr is None: if audio is None or sr is None:
# Decode failed. Only pass the file straight to whisper if the # Decode failed. Only pass the file straight to whisper if the
# source is a format its miniaudio loader can still read — webm, # source is a format its miniaudio loader can still read — webm,
# m4a, etc. would just 500 later. Surface a clean error instead. # 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( raise ValueError(
f"Could not decode {suffix} audio — the recording may be empty or corrupt" 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" audio_path = config.get_captures_dir() / f"{capture_id}.wav"
sf.write(str(audio_path), audio, sr, format="WAV") sf.write(str(audio_path), audio, sr, format="WAV")
written_files.append(audio_path) written_files.append(audio_path)
try: with contextlib.suppress(OSError):
raw_path.unlink() raw_path.unlink()
except OSError:
pass
else:
written_files.remove(raw_path) written_files.remove(raw_path)
whisper = get_whisper_model() whisper = get_whisper_model()
+2 -1
View File
@@ -34,7 +34,8 @@ import sys
import time import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from pathlib import Path from pathlib import Path
from typing import Iterable, Optional from collections.abc import Iterable
from typing import Optional
import httpx import httpx