Compare commits

...

1 Commits

Author SHA1 Message Date
Alexander Whitestone
c58093dccc WIP: Claude Code progress on #1285
Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
2026-03-23 22:02:09 -04:00
11 changed files with 50 additions and 28 deletions

View File

@@ -18,9 +18,17 @@ jobs:
- name: Lint (ruff via tox) - name: Lint (ruff via tox)
run: tox -e lint run: tox -e lint
test: typecheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: lint needs: lint
steps:
- uses: actions/checkout@v4
- name: Type-check (mypy via tox)
run: tox -e typecheck
test:
runs-on: ubuntu-latest
needs: typecheck
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Run tests (via tox) - name: Run tests (via tox)

View File

@@ -164,3 +164,7 @@ directory = "htmlcov"
[tool.coverage.xml] [tool.coverage.xml]
output = "coverage.xml" output = "coverage.xml"
[tool.mypy]
ignore_missing_imports = true
no_error_summary = true

View File

View File

@@ -6,6 +6,8 @@ import sqlite3
from contextlib import closing from contextlib import closing
from pathlib import Path from pathlib import Path
from typing import Any
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
@@ -36,9 +38,9 @@ def _discover_databases() -> list[dict]:
return dbs return dbs
def _query_database(db_path: str) -> dict: def _query_database(db_path: str) -> dict[str, Any]:
"""Open a database read-only and return all tables with their rows.""" """Open a database read-only and return all tables with their rows."""
result = {"tables": {}, "error": None} result: dict[str, Any] = {"tables": {}, "error": None}
try: try:
with closing(sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)) as conn: with closing(sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)) as conn:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row

View File

@@ -137,7 +137,7 @@ class HermesMonitor:
message=f"Check error: {r}", message=f"Check error: {r}",
) )
) )
else: elif isinstance(r, CheckResult):
checks.append(r) checks.append(r)
# Compute overall level # Compute overall level

View File

@@ -203,7 +203,7 @@ async def reload_config(
@router.get("/history") @router.get("/history")
async def get_history( async def get_history(
hours: int = 24, hours: int = 24,
store: Annotated[HealthHistoryStore, Depends(get_history_store)] = None, store: Annotated[HealthHistoryStore | None, Depends(get_history_store)] = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Get provider health history for the last N hours.""" """Get provider health history for the last N hours."""
if store is None: if store is None:

View File

@@ -744,19 +744,20 @@ class CascadeRouter:
self, self,
provider: Provider, provider: Provider,
messages: list[dict], messages: list[dict],
model: str, model: str | None,
temperature: float, temperature: float,
max_tokens: int | None, max_tokens: int | None,
content_type: ContentType = ContentType.TEXT, content_type: ContentType = ContentType.TEXT,
) -> dict: ) -> dict:
"""Try a single provider request.""" """Try a single provider request."""
start_time = time.time() start_time = time.time()
effective_model: str = model or provider.get_default_model() or ""
if provider.type == "ollama": if provider.type == "ollama":
result = await self._call_ollama( result = await self._call_ollama(
provider=provider, provider=provider,
messages=messages, messages=messages,
model=model or provider.get_default_model(), model=effective_model,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
content_type=content_type, content_type=content_type,
@@ -765,7 +766,7 @@ class CascadeRouter:
result = await self._call_openai( result = await self._call_openai(
provider=provider, provider=provider,
messages=messages, messages=messages,
model=model or provider.get_default_model(), model=effective_model,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
) )
@@ -773,7 +774,7 @@ class CascadeRouter:
result = await self._call_anthropic( result = await self._call_anthropic(
provider=provider, provider=provider,
messages=messages, messages=messages,
model=model or provider.get_default_model(), model=effective_model,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
) )
@@ -781,7 +782,7 @@ class CascadeRouter:
result = await self._call_grok( result = await self._call_grok(
provider=provider, provider=provider,
messages=messages, messages=messages,
model=model or provider.get_default_model(), model=effective_model,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
) )
@@ -789,7 +790,7 @@ class CascadeRouter:
result = await self._call_vllm_mlx( result = await self._call_vllm_mlx(
provider=provider, provider=provider,
messages=messages, messages=messages,
model=model or provider.get_default_model(), model=effective_model,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
) )

View File

@@ -474,7 +474,7 @@ class DiscordVendor(ChatPlatform):
async def _run_client(self, token: str) -> None: async def _run_client(self, token: str) -> None:
"""Run the discord.py client (blocking call in a task).""" """Run the discord.py client (blocking call in a task)."""
try: try:
await self._client.start(token) await self._client.start(token) # type: ignore[union-attr]
except Exception as exc: except Exception as exc:
logger.error("Discord client error: %s", exc) logger.error("Discord client error: %s", exc)
self._state = PlatformState.ERROR self._state = PlatformState.ERROR
@@ -482,32 +482,32 @@ class DiscordVendor(ChatPlatform):
def _register_handlers(self) -> None: def _register_handlers(self) -> None:
"""Register Discord event handlers on the client.""" """Register Discord event handlers on the client."""
@self._client.event @self._client.event # type: ignore[union-attr]
async def on_ready(): async def on_ready():
self._guild_count = len(self._client.guilds) self._guild_count = len(self._client.guilds) # type: ignore[union-attr]
self._state = PlatformState.CONNECTED self._state = PlatformState.CONNECTED
logger.info( logger.info(
"Discord ready: %s in %d guild(s)", "Discord ready: %s in %d guild(s)",
self._client.user, self._client.user, # type: ignore[union-attr]
self._guild_count, self._guild_count,
) )
@self._client.event @self._client.event # type: ignore[union-attr]
async def on_message(message): async def on_message(message):
# Ignore our own messages # Ignore our own messages
if message.author == self._client.user: if message.author == self._client.user: # type: ignore[union-attr]
return return
# Only respond to mentions or DMs # Only respond to mentions or DMs
is_dm = not hasattr(message.channel, "guild") or message.channel.guild is None is_dm = not hasattr(message.channel, "guild") or message.channel.guild is None
is_mention = self._client.user in message.mentions is_mention = self._client.user in message.mentions # type: ignore[union-attr]
if not is_dm and not is_mention: if not is_dm and not is_mention:
return return
await self._handle_message(message) await self._handle_message(message)
@self._client.event @self._client.event # type: ignore[union-attr]
async def on_disconnect(): async def on_disconnect():
if self._state != PlatformState.DISCONNECTED: if self._state != PlatformState.DISCONNECTED:
self._state = PlatformState.CONNECTING self._state = PlatformState.CONNECTING
@@ -535,8 +535,8 @@ class DiscordVendor(ChatPlatform):
def _extract_content(self, message) -> str: def _extract_content(self, message) -> str:
"""Strip the bot mention and return clean message text.""" """Strip the bot mention and return clean message text."""
content = message.content content = message.content
if self._client.user: if self._client.user: # type: ignore[union-attr]
content = content.replace(f"<@{self._client.user.id}>", "").strip() content = content.replace(f"<@{self._client.user.id}>", "").strip() # type: ignore[union-attr]
return content return content
async def _invoke_agent(self, content: str, session_id: str, target): async def _invoke_agent(self, content: str, session_id: str, target):

View File

@@ -102,14 +102,14 @@ class TelegramBot:
self._token = tok self._token = tok
self._app = Application.builder().token(tok).build() self._app = Application.builder().token(tok).build()
self._app.add_handler(CommandHandler("start", self._cmd_start)) self._app.add_handler(CommandHandler("start", self._cmd_start)) # type: ignore[union-attr]
self._app.add_handler( self._app.add_handler( # type: ignore[union-attr]
MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message) MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message)
) )
await self._app.initialize() await self._app.initialize() # type: ignore[union-attr]
await self._app.start() await self._app.start() # type: ignore[union-attr]
await self._app.updater.start_polling(allowed_updates=Update.ALL_TYPES) await self._app.updater.start_polling(allowed_updates=Update.ALL_TYPES) # type: ignore[union-attr]
self._running = True self._running = True
logger.info("Telegram bot started.") logger.info("Telegram bot started.")

View File

@@ -245,6 +245,7 @@ class VoiceLoop:
def _transcribe(self, audio: np.ndarray) -> str: def _transcribe(self, audio: np.ndarray) -> str:
"""Transcribe audio using local Whisper model.""" """Transcribe audio using local Whisper model."""
self._load_whisper() self._load_whisper()
assert self._whisper_model is not None, "Whisper model failed to load"
sys.stdout.write(" 🧠 Transcribing...\r") sys.stdout.write(" 🧠 Transcribing...\r")
sys.stdout.flush() sys.stdout.flush()

10
tox.ini
View File

@@ -41,8 +41,10 @@ description = Static type checking with mypy
commands_pre = commands_pre =
deps = deps =
mypy>=1.0.0 mypy>=1.0.0
types-PyYAML
types-requests
commands = commands =
mypy src --ignore-missing-imports --no-error-summary mypy src
# ── Test Environments ──────────────────────────────────────────────────────── # ── Test Environments ────────────────────────────────────────────────────────
@@ -130,13 +132,17 @@ commands =
# ── Pre-push (mirrors CI exactly) ──────────────────────────────────────────── # ── Pre-push (mirrors CI exactly) ────────────────────────────────────────────
[testenv:pre-push] [testenv:pre-push]
description = Local gate — lint + full CI suite (same as Gitea Actions) description = Local gate — lint + typecheck + full CI suite (same as Gitea Actions)
deps = deps =
ruff>=0.8.0 ruff>=0.8.0
mypy>=1.0.0
types-PyYAML
types-requests
commands = commands =
ruff check src/ tests/ ruff check src/ tests/
ruff format --check src/ tests/ ruff format --check src/ tests/
bash -c 'files=$(grep -rl "<style" src/dashboard/templates/ --include="*.html" 2>/dev/null); if [ -n "$files" ]; then echo "ERROR: inline <style> blocks found — move CSS to static/css/mission-control.css:"; echo "$files"; exit 1; fi; echo "No inline CSS — OK"' bash -c 'files=$(grep -rl "<style" src/dashboard/templates/ --include="*.html" 2>/dev/null); if [ -n "$files" ]; then echo "ERROR: inline <style> blocks found — move CSS to static/css/mission-control.css:"; echo "$files"; exit 1; fi; echo "No inline CSS — OK"'
mypy src
mkdir -p reports mkdir -p reports
pytest tests/ \ pytest tests/ \
--cov=src \ --cov=src \