From aeff058743c170a1c5f0ea08953d2158f3c6895c Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Tue, 11 Aug 2026 16:14:25 +0200 Subject: [PATCH 1/4] refactor: reach green pipeline status (100% mypy strict, ruff clean) --- .pytest_cache/v/cache/lastfailed | 5 -- config.py | 20 ++++-- core/__init__.py | 33 +++++----- core/episodic.py | 35 +++++----- core/memory.py | 34 +++++----- core/session.py | 1 + features/auth/api_key.py | 9 +-- features/backup_cron.py | 4 +- features/dashboard.py | 10 +-- features/import_export.py | 3 +- features/secrets.py | 36 +++++----- graph/epistemic.py | 35 +++++----- graph/temporal.py | 17 +++-- hooks/loader.py | 2 +- hooks/models.py | 2 +- hooks/registry.py | 2 +- lifecycle/compactor.py | 3 +- lifecycle/consolidation.py | 11 +++- lifecycle/emotion/engine.py | 13 ++-- lifecycle/forgetting.py | 4 +- mcp_server/app.py | 4 +- mcp_server/context.py | 2 +- mcp_server/middlewares.py | 11 ++-- mcp_server/registry.py | 8 +-- mcp_server/server.py | 10 +-- mcp_server/tools/episodic.py | 30 ++++----- mcp_server/tools/graph.py | 28 ++++---- mcp_server/tools/memory.py | 40 ++++++----- mcp_server/tools/ops.py | 73 +++++++++++---------- mcp_server/tools/session.py | 22 +++---- mcp_server/tools/wiki.py | 30 +++++---- mcp_server/tools_layer.py | 2 +- mcp_server/utils/circuit_breaker.py | 9 +-- pyproject.toml | 31 ++++----- rag/conflict.py | 14 ++-- rag/engine.py | 8 +-- rag/ingestor.py | 4 +- rag/quantize.py | 15 +++-- rag/router.py | 64 ++++++++++++------ rag/schema.py | 2 +- rag/search.py | 31 ++++----- rag/searcher.py | 9 +-- shared/archived_memories.py | 3 +- shared/dream_buffer.py | 3 +- shared/embeddings.py | 2 +- shared/importance/scorer.py | 8 +-- shared/importance/signals/emotion_signal.py | 5 +- shared/memory_types.py | 2 +- shared/middleware.py | 22 +++---- shared/saga/__init__.py | 10 ++- shared/saga/impl/base.py | 9 +-- shared/saga/impl/crypto.py | 24 +++++-- wiki/index.py | 20 +++--- wiki/manager.py | 18 ++--- wiki/shared.py | 28 ++++---- 55 files changed, 481 insertions(+), 399 deletions(-) diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed index 3848a718..fd6afcbc 100644 --- a/.pytest_cache/v/cache/lastfailed +++ b/.pytest_cache/v/cache/lastfailed @@ -41,11 +41,6 @@ "tests/shared/test_importance_scorer.py::test_scorer_with_emotion_engine": true, "tests/shared/test_importance_scorer.py::test_dynamic_weight_update": true, "tests/shared/test_importance_scorer.py::test_tech_context_bonus": true, - "tests/test_core/test_core.py::test_reflex_buffer": true, - "tests/test_features/test_backup_path_safety.py::test_restore_rejects_traversal[manifest_files0-True]": true, - "tests/test_features/test_backup_path_safety.py::test_restore_rejects_traversal[manifest_files1-True]": true, - "tests/test_features/test_import_export_path_safety.py::test_import_rejects_traversal[../../etc/passwd]": true, - "tests/test_features/test_import_export_path_safety.py::test_import_rejects_traversal[/etc/passwd]": true, "tests/test_hooks/test_registry_core.py::test_manual_registration": true, "tests/test_hypothesis.py::test_chaos_db_locked_graceful": true, "tests/test_hypothesis.py::test_chaos_api_timeout_does_not_hang": true, diff --git a/config.py b/config.py index c3849e3a..6690ac59 100644 --- a/config.py +++ b/config.py @@ -67,21 +67,27 @@ def is_hook_enabled(self, layer: str, hook: str) -> bool: "wiki_agent", ], } + res: Any = False if hook in known_hooks.get(layer, []): - return self.get("hooks", layer, hook, default=True) - return self.get("hooks", layer, hook, default=False) + res = self.get("hooks", layer, hook, default=True) + else: + res = self.get("hooks", layer, hook, default=False) + return bool(res) def is_feature_enabled(self, feature: str) -> bool: - return self.get("features", feature, default=False) + return bool(self.get("features", feature, default=False)) - def get_wiki_types(self, layer: str) -> list: - return self.get("wiki", layer, default=[]) + def get_wiki_types(self, layer: str) -> list[str]: + res: Any = self.get("wiki", layer, default=[]) + if not isinstance(res, list): + return [] + return [str(x) for x in res] def get_limit(self, key: str) -> int: - return self.get("limits", key, default=0) + return int(self.get("limits", key, default=0)) def get_forgetting(self, key: str) -> float: - return self.get("forgetting", key, default=0.0) + return float(self.get("forgetting", key, default=0.0)) config = Config() diff --git a/core/__init__.py b/core/__init__.py index 6918db53..11d38cea 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -5,7 +5,7 @@ Two-layer: user facts + agent identity """ -from typing import Optional +from typing import Optional, Any from config import config from shared.connection import AsyncConnectionManager, connection_manager @@ -19,11 +19,11 @@ class MemoryLayer: """Unified async memory layer for both user and agent.""" - def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectionManager | None = None, cache=None): + def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectionManager | None = None, cache: Any = None) -> None: self.layer_type = layer_type self.user_id = user_id self._cm = cm or connection_manager - self._cache = cache + self._cache: Any = cache self.l1 = ReflexBuffer(max_size=config.get_limit("l1_buffer_size")) self.l2 = SessionStore(cm=self._cm) self.l3 = EpisodicMemory(cm=self._cm) @@ -32,14 +32,15 @@ def __init__(self, layer_type: str, user_id: str = "default", cm: AsyncConnectio async def remember(self, key: str, value: str, importance: float = 0.5) -> int: return await self.l4.save(self.user_id, key, value, importance) - async def recall(self, query: str, limit: int = 10) -> list[dict]: + async def recall(self, query: str, limit: int = 10) -> list[dict[str, Any]]: cache_key = f"recall:{self.user_id}:{query}:{limit}" - cached = self._cache.get(cache_key) if self._cache else None + cached: Any = self._cache.get(cache_key) if self._cache else None if cached is not None: - return cached + return list(cached) if isinstance(cached, list) else [] - results = [] - results.extend(await self.l4.search(self.user_id, query, limit)) + results: list[dict[str, Any]] = [] + l4_hits = await self.l4.search(self.user_id, query, limit) + results.extend(l4_hits) episodes = await self.l3.search(self.user_id, query, limit) results.extend([{"summary": e.summary, "weight": e.emotional_weight} for e in episodes]) final = results[:limit] @@ -53,24 +54,24 @@ async def forget(self, key: str) -> bool: return await self.l4.delete(self.user_id, key) async def get_context(self) -> str: - parts = [] + parts: list[str] = [] recent = self.l1.get_recent(5) if recent: - parts.append("RECENT: " + "; ".join([r.content[:50] for r in recent])) + parts.append("RECENT: " + "; ".join([str(r.content)[:50] for r in recent])) facts = await self.l4.get_all(self.user_id, limit=10) if facts: - parts.append("FACTS: " + "; ".join([f"{f.key}={f.value[:30]}" for f in facts])) + parts.append("FACTS: " + "; ".join([f"{f.key}={str(f.value)[:30]}" for f in facts])) return "\n".join(parts) - async def cleanup(self) -> dict: + async def cleanup(self) -> dict[str, int]: archived = await self.l3.archive_old(self.user_id) return {"archived": archived} class MemoryManager: - def __init__(self, cm: AsyncConnectionManager | None = None, cache=None): + def __init__(self, cm: AsyncConnectionManager | None = None, cache: Any = None) -> None: self._cm = cm or connection_manager - self._cache = cache + self._cache: Any = cache self.layers: dict[str, MemoryLayer] = {} def get_layer(self, layer_type: str, user_id: str = "default") -> MemoryLayer: @@ -85,8 +86,8 @@ def user_memory(self, user_id: str = "default") -> MemoryLayer: def agent_memory(self, user_id: str = "default") -> MemoryLayer: return self.get_layer("agent", user_id) - async def cleanup_all(self) -> dict: - results = {} + async def cleanup_all(self) -> dict[str, dict[str, int]]: + results: dict[str, dict[str, int]] = {} for key, layer in self.layers.items(): results[key] = await layer.cleanup() return results diff --git a/core/episodic.py b/core/episodic.py index b4bde6a6..06c83540 100644 --- a/core/episodic.py +++ b/core/episodic.py @@ -7,6 +7,7 @@ import json import time from dataclasses import dataclass +from typing import Any from shared.connection import AsyncConnectionManager, connection_manager from shared.constants import DB_NAME @@ -26,7 +27,7 @@ class EpisodicMemory: def __init__(self, cm: AsyncConnectionManager | None = None): self._cm = cm or connection_manager - async def _init_db(self): + async def _init_db(self) -> None: await self._cm.execute_script( DB_NAME, """ @@ -47,10 +48,10 @@ async def save(self, user_id: str, summary: str, emotional_weight: float = 0.5, conn = await self._cm.get(DB_NAME) cursor = await conn.execute( "INSERT INTO episodes (user_id, summary, emotional_weight, tags, created_at) VALUES (?, ?, ?, ?, ?)", - (user_id, summary, emotional_weight, json.dumps(tags or []), time.time()), + (user_id, summary, emotional_weight, json.dumps(tags or [])), ) await conn.commit() - return cursor.lastrowid + return int(cursor.lastrowid or 0) async def get_episodes(self, user_id: str, limit: int = 20, offset: int = 0) -> list[Episode]: conn = await self._cm.get(DB_NAME) @@ -70,7 +71,7 @@ async def search_by_tag(self, user_id: str, tag: str, limit: int = 10) -> list[E rows = await cursor.fetchall() return [self._row_to_episode(r) for r in rows] - async def search(self, user_id: str, query: str, limit: int = 10) -> list: + async def search(self, user_id: str, query: str, limit: int = 10) -> list[Episode]: conn = await self._cm.get(DB_NAME) cursor = await conn.execute( "SELECT * FROM episodes WHERE user_id=? AND summary LIKE ? ORDER BY created_at DESC LIMIT ?", @@ -98,21 +99,21 @@ async def archive_old(self, user_id: str, days: int = 90) -> int: for row in rows: await am.archive( user_id=user_id, - content=row["summary"], + content=str(row["summary"]), memory_type="episode", - importance=row["emotional_weight"], - original_id=row["episode_id"], + importance=float(row["emotional_weight"]), + original_id=int(row["episode_id"]), reason=f"inactive_{days}d", ) archived_count += 1 - ids = [row["episode_id"] for row in rows] + ids = [int(row["episode_id"]) for row in rows] if not ids: return archived_count placeholders = ",".join(["?"] * len(ids)) sql = f"DELETE FROM episodes WHERE episode_id IN ({placeholders})" - await conn.execute(sql, ids) + await conn.execute(sql, tuple(ids)) await conn.commit() return archived_count @@ -124,14 +125,14 @@ async def count(self, user_id: str) -> int: (user_id,), ) row = await cursor.fetchone() - return row["cnt"] if row else 0 + return int(row["cnt"]) if row and row[0] is not None else 0 - def _row_to_episode(self, row) -> Episode: + def _row_to_episode(self, row: dict[str, Any] | Any) -> Episode: return Episode( - episode_id=row["episode_id"], - user_id=row["user_id"], - summary=row["summary"], - emotional_weight=row["emotional_weight"], - tags=json.loads(row["tags"]) if row["tags"] else [], - created_at=row["created_at"], + episode_id=int(row["episode_id"]), + user_id=str(row["user_id"]), + summary=str(row["summary"]), + emotional_weight=float(row["emotional_weight"]), + tags=list(json.loads(row["tags"])) if row["tags"] else [], + created_at=float(row["created_at"]), ) diff --git a/core/memory.py b/core/memory.py index 1f696f03..4e8507cd 100644 --- a/core/memory.py +++ b/core/memory.py @@ -32,7 +32,7 @@ class CoreMemory: def __init__(self, cm: AsyncConnectionManager | None = None): self._cm = cm or connection_manager - async def _init_db(self): + async def _init_db(self) -> None: await self._cm.execute_script( DB_NAME, """ @@ -61,7 +61,7 @@ async def save( memory_kind: str | None = None, expires_at: float | None = None, source: str = "manual", - metadata: dict | None = None, + metadata: dict[str, Any] | None = None, ) -> int: from shared.memory_types import ( MemoryKind, @@ -105,9 +105,9 @@ async def save( """UPDATE core_memory SET value=?, importance=?, memory_kind=?, expires_at=?, source=?, metadata=?, updated_at=? WHERE entry_id=?""", - (value, importance, memory_kind, expires_at, source, metadata_json, now, existing["entry_id"]), + (value, importance, memory_kind, expires_at, source, metadata_json, now, int(existing["entry_id"])), ) - entry_id = existing["entry_id"] + entry_id = int(existing["entry_id"]) else: cursor = await conn.execute( """INSERT INTO core_memory @@ -116,7 +116,7 @@ async def save( VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (user_id, key, value, importance, memory_kind, expires_at, source, metadata_json, now, now), ) - entry_id = cursor.lastrowid + entry_id = int(cursor.lastrowid or 0) await conn.commit() return entry_id @@ -144,14 +144,14 @@ async def delete(self, user_id: str, key: str) -> bool: await conn.commit() return cursor.rowcount > 0 - async def search(self, user_id: str, query: str, limit: int = 10) -> list[dict]: + async def search(self, user_id: str, query: str, limit: int = 10) -> list[dict[str, Any]]: conn = await self._cm.get(DB_NAME) cursor = await conn.execute( "SELECT * FROM core_memory WHERE user_id=? AND (key LIKE ? OR value LIKE ?) ORDER BY importance DESC LIMIT ?", (user_id, f"%{query}%", f"%{query}%", limit), ) rows = await cursor.fetchall() - return [{"key": r["key"], "value": r["value"], "importance": r["importance"]} for r in rows] + return [{"key": str(r["key"]), "value": str(r["value"]), "importance": float(r["importance"])} for r in rows] async def count(self, user_id: str | None = None) -> int: conn = await self._cm.get(DB_NAME) @@ -160,18 +160,18 @@ async def count(self, user_id: str | None = None) -> int: else: cursor = await conn.execute("SELECT COUNT(*) FROM core_memory") row = await cursor.fetchone() - return row[0] if row else 0 + return int(row[0]) if row and row[0] is not None else 0 - def _row_to_entry(self, row) -> CoreEntry: + def _row_to_entry(self, row: dict[str, Any] | Any) -> CoreEntry: return CoreEntry( - entry_id=row["entry_id"], - user_id=row["user_id"], - key=row["key"], - value=row["value"], - importance=row["importance"], - memory_kind=row["memory_kind"] or "fact", - created_at=row["created_at"], - updated_at=row["updated_at"], + entry_id=int(row["entry_id"]), + user_id=str(row["user_id"]), + key=str(row["key"]), + value=str(row["value"]), + importance=float(row["importance"]), + memory_kind=str(row["memory_kind"] or "fact"), + created_at=float(row["created_at"]), + updated_at=float(row["updated_at"]), ) async def list_by_kind( diff --git a/core/session.py b/core/session.py index e4e5719a..e7642df2 100644 --- a/core/session.py +++ b/core/session.py @@ -8,6 +8,7 @@ import time import uuid from dataclasses import dataclass, field +from typing import Any from shared.connection import AsyncConnectionManager, connection_manager from shared.constants import DB_NAME diff --git a/features/auth/api_key.py b/features/auth/api_key.py index f3530aca..79a726bf 100644 --- a/features/auth/api_key.py +++ b/features/auth/api_key.py @@ -3,19 +3,20 @@ import secrets import time from pathlib import Path +from typing import Any from features.auth.models import APIKeyModel from features.auth.store import EncryptedStore class APIKeyAuth: - def __init__(self, keys_file: Path | None = None): + def __init__(self, keys_file: Path | None = None) -> None: if keys_file is None: # Default location if not provided keys_file = Path("data/auth/keys.enc") self.store = EncryptedStore(keys_file, APIKeyModel) - self._keys: dict[str, dict] = self.store.load() + self._keys: dict[str, dict[str, Any]] = self.store.load() def create_key(self, user_id: str, label: str) -> str: """Generate ak_... key, save state.""" @@ -27,7 +28,7 @@ def create_key(self, user_id: str, label: str) -> str: self.store.save(self._keys) return key - def verify(self, key: str) -> dict | None: + def verify(self, key: str) -> dict[str, Any] | None: """ Check key validity and enabled status. Update last_used timestamp and save() on success. @@ -68,7 +69,7 @@ def delete_key(self, key: str) -> bool: return True return False - def list_keys(self) -> list[dict]: + def list_keys(self) -> list[dict[str, Any]]: """Return masked keys with metadata.""" result = [] for key, data in self._keys.items(): diff --git a/features/backup_cron.py b/features/backup_cron.py index fd0ba59f..f72a75b4 100644 --- a/features/backup_cron.py +++ b/features/backup_cron.py @@ -199,8 +199,8 @@ def restore(self, backup_name: str) -> dict[str, Any]: return {"restored": restored, "backup": backup_name} - def list_backups(self) -> list: - backups = [] + def list_backups(self) -> list[dict[str, Any]]: + backups: list[dict[str, Any]] = [] for d in sorted(self.backup_dir.iterdir(), reverse=True): if d.is_dir(): info = {"name": d.name} diff --git a/features/dashboard.py b/features/dashboard.py index 5901d7c0..2350472b 100644 --- a/features/dashboard.py +++ b/features/dashboard.py @@ -196,23 +196,23 @@ async def get_stats(self, user_id: str = "default") -> dict[str, Any]: "agent_wiki": await aw.count(user_id), } - async def get_user_facts(self, user_id: str = "default") -> list: + async def get_user_facts(self, user_id: str = "default") -> list[dict[str, Any]]: facts = await self.mm.user_memory(user_id).l4.get_all(user_id, limit=50) return [{"key": f.key, "value": f.value, "importance": f.importance} for f in facts] - async def get_agent_facts(self, user_id: str = "default") -> list: + async def get_agent_facts(self, user_id: str = "default") -> list[dict[str, Any]]: facts = await self.mm.agent_memory(user_id).l4.get_all(user_id, limit=50) return [{"key": f.key, "value": f.value, "importance": f.importance} for f in facts] - async def get_user_episodes(self, user_id: str = "default") -> list: + async def get_user_episodes(self, user_id: str = "default") -> list[dict[str, Any]]: eps = await self.mm.user_memory(user_id).l3.get_episodes(user_id, limit=20) return [{"summary": e.summary, "weight": e.emotional_weight, "tags": e.tags} for e in eps] - async def get_agent_episodes(self, user_id: str = "default") -> list: + async def get_agent_episodes(self, user_id: str = "default") -> list[dict[str, Any]]: eps = await self.mm.agent_memory(user_id).l3.get_episodes(user_id, limit=20) return [{"summary": e.summary, "weight": e.emotional_weight, "tags": e.tags} for e in eps] - async def get_audit(self, limit: int = 20) -> list: + async def get_audit(self, limit: int = 20) -> list[dict[str, Any]]: from features.audit_trail import AuditTrail at = AuditTrail() diff --git a/features/import_export.py b/features/import_export.py index 5e296d32..76b3ca81 100644 --- a/features/import_export.py +++ b/features/import_export.py @@ -24,7 +24,8 @@ def __init__(self, cm: AsyncConnectionManager | None = None): @property def base_dir(self) -> Path: - return cast("Path", self._cm.base_dir) + res: Any = self._cm.base_dir + return Path(res) if res else Path.home() / ".mcp-ariel-memory" async def export_user(self, user_id: str) -> str: core_memory: list[dict[str, Any]] = [] diff --git a/features/secrets.py b/features/secrets.py index 869f5233..9016bb82 100644 --- a/features/secrets.py +++ b/features/secrets.py @@ -97,16 +97,14 @@ def _load_master_key() -> bytes: # Try environment variable with argon2id KDF env_seed = os.environ.get(_ENV_VAR) if env_seed: - return cast( - "bytes", - argon2id.kdf( - size=_MASTER_KEY_LEN, - password=env_seed.encode("utf-8"), - salt=_KDF_SALT, - opslimit=argon2id.OPSLIMIT_MODERATE, - memlimit=argon2id.MEMLIMIT_MODERATE, - ), + res_kdf = argon2id.kdf( + size=_MASTER_KEY_LEN, + password=env_seed.encode("utf-8"), + salt=_KDF_SALT, + opslimit=argon2id.OPSLIMIT_MODERATE, + memlimit=argon2id.MEMLIMIT_MODERATE, ) + return bytes(res_kdf) # Auto-generate key for dev convenience import secrets as _secrets @@ -114,16 +112,14 @@ def _load_master_key() -> bytes: auto_key = _secrets.token_hex(32) logger.warning("No master key found. Auto-generating key and saving to .env. For production, use keyring or set MCP_MASTER_KEY explicitly.") _save_dotenv(_ENV_VAR, auto_key) - return cast( - "bytes", - argon2id.kdf( - size=_MASTER_KEY_LEN, - password=auto_key.encode("utf-8"), - salt=_KDF_SALT, - opslimit=argon2id.OPSLIMIT_MODERATE, - memlimit=argon2id.MEMLIMIT_MODERATE, - ), + res_auto = argon2id.kdf( + size=_MASTER_KEY_LEN, + password=auto_key.encode("utf-8"), + salt=_KDF_SALT, + opslimit=argon2id.OPSLIMIT_MODERATE, + memlimit=argon2id.MEMLIMIT_MODERATE, ) + return bytes(res_auto) _master_cache: dict[str, bytes] = {} @@ -140,7 +136,7 @@ def _get_master_key() -> bytes: def encrypt_json(data: dict[str, Any] | list[Any]) -> bytes: """Encrypt JSON data. Returns nonce(24) || ciphertext.""" - return cast("bytes", _encrypt_json(data, _get_master_key())) + return _encrypt_json(data, _get_master_key()) def decrypt_json(blob: bytes) -> Any: @@ -160,7 +156,7 @@ def is_encrypted_blob(path: Path) -> bool: # noqa: SKY-D325 with path.open("rb") as f: head = f.read(1) - return cast("bool", _is_encrypted_blob(head)) + return bool(_is_encrypted_blob(head)) def install_master_key_to_keychain(hex_key: str) -> None: diff --git a/graph/epistemic.py b/graph/epistemic.py index ccfa210a..2f09a4e6 100644 --- a/graph/epistemic.py +++ b/graph/epistemic.py @@ -9,11 +9,14 @@ import logging import time from dataclasses import dataclass -from typing import Any +from typing import Any, TYPE_CHECKING from shared.connection import connection_manager from shared.constants import DB_NAME +if TYPE_CHECKING: + from shared.connection import AsyncConnectionManager + logger = logging.getLogger(__name__) @@ -52,7 +55,7 @@ class EpistemicGraph: USER_TAGS = USER_TAGS AGENT_TAGS = AGENT_TAGS - def __init__(self, cm=None, layer: str = "user"): + def __init__(self, cm: AsyncConnectionManager | None = None, layer: str = "user") -> None: self._cm = cm or connection_manager self.layer = layer @@ -66,7 +69,7 @@ def is_known_tag(tag: str) -> bool: """Check whether a tag belongs to USER_TAGS or AGENT_TAGS.""" return tag in USER_TAGS or tag in AGENT_TAGS - async def init_db(self): + async def init_db(self) -> None: await self._cm.execute_script( DB_NAME, """ @@ -116,7 +119,7 @@ async def add_node(self, user_id: str, content: str, node_type: str, tags: list[ "INSERT INTO epi_nodes (layer, user_id, content, node_type, tags, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", (self.layer, user_id, content, node_type, json.dumps(tags or []), confidence, time.time()), ) - node_id = cursor.lastrowid + node_id = int(cursor.lastrowid or 0) if tags: for tag in tags: await conn.execute( @@ -126,7 +129,7 @@ async def add_node(self, user_id: str, content: str, node_type: str, tags: list[ await conn.commit() return node_id - async def add_edge(self, source_id: int, target_id: int, relation: str, weight: float = 0.8): + async def add_edge(self, source_id: int, target_id: int, relation: str, weight: float = 0.8) -> None: conn = await self._cm.get(DB_NAME) await conn.execute( "INSERT OR REPLACE INTO epi_edges (source_id, target_id, relation, weight, created_at) VALUES (?, ?, ?, ?, ?)", @@ -144,7 +147,7 @@ async def query_by_tag(self, user_id: str, tag: str, limit: int = 20) -> list[Ep (self.layer, user_id, tag, limit), ) rows = await cur.fetchall() - return [self._row_to_node(r) for r in rows] + return [self._row_to_node(dict(r)) for r in rows] async def query_by_type(self, user_id: str, node_type: str, limit: int = 20) -> list[EpistemicNode]: conn = await self._cm.get(DB_NAME) @@ -153,7 +156,7 @@ async def query_by_type(self, user_id: str, node_type: str, limit: int = 20) -> (self.layer, user_id, node_type, limit), ) rows = await cur.fetchall() - return [self._row_to_node(r) for r in rows] + return [self._row_to_node(dict(r)) for r in rows] async def get_neighbors(self, node_id: int, depth: int = 1) -> list[dict[str, Any]]: conn = await self._cm.get(DB_NAME) @@ -217,14 +220,14 @@ async def count_nodes(self, user_id: str | None = None) -> int: row = await cur.fetchone() return row[0] if row else 0 - def _row_to_node(self, row) -> EpistemicNode: + def _row_to_node(self, row: dict[str, Any]) -> EpistemicNode: return EpistemicNode( - node_id=row["node_id"], - user_id=row["user_id"], - layer=row["layer"], - content=row["content"], - node_type=row["node_type"], - tags=json.loads(row["tags"]) if row["tags"] else [], - confidence=row["confidence"], - created_at=row["created_at"], + node_id=int(row["node_id"]), + user_id=str(row["user_id"]), + layer=str(row["layer"]), + content=str(row["content"]), + node_type=str(row["node_type"]), + tags=list(json.loads(row["tags"])) if row["tags"] else [], + confidence=float(row["confidence"]), + created_at=float(row["created_at"]), ) diff --git a/graph/temporal.py b/graph/temporal.py index 8f57b712..b023eacc 100644 --- a/graph/temporal.py +++ b/graph/temporal.py @@ -6,11 +6,14 @@ import time from dataclasses import dataclass -from typing import Any +from typing import Any, TYPE_CHECKING from shared.connection import connection_manager from shared.constants import DB_NAME +if TYPE_CHECKING: + from shared.connection import AsyncConnectionManager + @dataclass class TemporalEvent: @@ -20,14 +23,14 @@ class TemporalEvent: content: str timestamp: float importance: float - metadata: dict + metadata: dict[str, Any] class TemporalGraph: - def __init__(self, cm=None): + def __init__(self, cm: AsyncConnectionManager | None = None) -> None: self._cm = cm or connection_manager - async def init_db(self): + async def init_db(self) -> None: await self._cm.execute_script( DB_NAME, """ @@ -53,7 +56,7 @@ async def init_db(self): """, ) - async def add_event(self, user_id: str, event_type: str, content: str, importance: float = 0.5, metadata: dict | None = None) -> int: + async def add_event(self, user_id: str, event_type: str, content: str, importance: float = 0.5, metadata: dict[str, Any] | None = None) -> int: import json conn = await self._cm.get(DB_NAME) @@ -62,9 +65,9 @@ async def add_event(self, user_id: str, event_type: str, content: str, importanc (user_id, event_type, content, time.time(), importance, json.dumps(metadata or {})), ) await conn.commit() - return cursor.lastrowid + return int(cursor.lastrowid or 0) - async def link_events(self, from_event: int, to_event: int, link_type: str = "follows", strength: float = 0.5): + async def link_events(self, from_event: int, to_event: int, link_type: str = "follows", strength: float = 0.5) -> None: conn = await self._cm.get(DB_NAME) await conn.execute( "INSERT OR REPLACE INTO temporal_links (from_event, to_event, link_type, strength) VALUES (?, ?, ?, ?)", diff --git a/hooks/loader.py b/hooks/loader.py index ff6faed4..22255464 100644 --- a/hooks/loader.py +++ b/hooks/loader.py @@ -6,4 +6,4 @@ def load_all_hooks() -> None: """Import hook modules to trigger @hook_registry.mark decorators.""" import hooks.user_hooks - import hooks.agent_hooks # noqa: F401 + import hooks.agent_hooks diff --git a/hooks/models.py b/hooks/models.py index c784bd83..de3a90d1 100644 --- a/hooks/models.py +++ b/hooks/models.py @@ -10,7 +10,7 @@ class HookHandler: """Metadata for a registered hook handler.""" - func: Callable + func: Callable[..., Any] name: str layer: str is_async: bool diff --git a/hooks/registry.py b/hooks/registry.py index 9d4e3bf8..ce196e5f 100644 --- a/hooks/registry.py +++ b/hooks/registry.py @@ -19,7 +19,7 @@ class HookHandler(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - func: Callable + func: Callable[..., Any] name: str layer: str is_async: bool diff --git a/lifecycle/compactor.py b/lifecycle/compactor.py index 55c766d4..a6acab62 100644 --- a/lifecycle/compactor.py +++ b/lifecycle/compactor.py @@ -50,7 +50,8 @@ async def run_cleanup(self, user_id: str = "default") -> dict[str, int]: await conn.execute("DELETE FROM core_memory WHERE entry_id=?", (row["id"],)) archived_count += 1 except (KeyError, RuntimeError): - logger.exception("Failed to archive memory %s", row.get("id", "unknown")) + rid = row["id"] if isinstance(row, dict) else "unknown" + logger.exception("Failed to archive memory %s", rid) await conn.commit() logger.info("Memory compaction: archived %d memories for user %s", archived_count, user_id) diff --git a/lifecycle/consolidation.py b/lifecycle/consolidation.py index f2c11534..d6aab558 100644 --- a/lifecycle/consolidation.py +++ b/lifecycle/consolidation.py @@ -93,9 +93,14 @@ async def consolidate_episodes( async def get_stats(self, user_id: str) -> dict[str, int]: conn = await self._cm.get(DB_NAME) total_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=?", (user_id,)) - total = (await total_cursor.fetchone())[0] + total_row = await total_cursor.fetchone() + total = int(total_row[0]) if total_row and total_row[0] is not None else 0 + high_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=? AND importance > 0.7", (user_id,)) - high = (await high_cursor.fetchone())[0] + high_row = await high_cursor.fetchone() + high = int(high_row[0]) if high_row and high_row[0] is not None else 0 + low_cursor = await conn.execute("SELECT COUNT(*) FROM core_memory WHERE user_id=? AND importance < 0.3", (user_id,)) - low = (await low_cursor.fetchone())[0] + low_row = await low_cursor.fetchone() + low = int(low_row[0]) if low_row and low_row[0] is not None else 0 return {"total": total, "high_importance": high, "low_importance": low} diff --git a/lifecycle/emotion/engine.py b/lifecycle/emotion/engine.py index 2d905757..84aabcd3 100644 --- a/lifecycle/emotion/engine.py +++ b/lifecycle/emotion/engine.py @@ -1,5 +1,6 @@ from __future__ import annotations import re +from typing import Any, Pattern from .models import EmotionMarkerConfig, EmotionResult @@ -12,16 +13,16 @@ class EmotionEngine: MARKER_SCORE_DEFAULT = 0.4 EMOJI_SCORE_DEFAULT = 0.3 - def __init__(self, config: EmotionMarkerConfig): + def __init__(self, config: EmotionMarkerConfig) -> None: self.config = config - self.phrase_regex: re.Pattern | None = None - self.marker_regex: re.Pattern | None = None - self.emoji_regex: re.Pattern | None = None + self.phrase_regex: Pattern[str] | None = None + self.marker_regex: Pattern[str] | None = None + self.emoji_regex: Pattern[str] | None = None self._compile() def _compile(self) -> None: """Compile regex patterns once.""" - phrase_patterns = [] + phrase_patterns: list[str] = [] for i, p in enumerate(self.config.phrases): pattern = p.pattern.replace(" ", r"\s+(?:\w+\s+)?") phrase_patterns.append(f"(?P{pattern})") @@ -34,7 +35,7 @@ def _compile(self) -> None: [f"(?P{'|'.join(re.escape(i) for i in icons)})" for cat, icons in self.config.emojis.items() if icons] ) - def _build_regex(self, parts: list[str], flags: int = 0) -> re.Pattern | None: + def _build_regex(self, parts: list[str], flags: int = 0) -> Pattern[str] | None: return re.compile("|".join(parts), flags) if parts else None def detect(self, text: str) -> list[EmotionResult]: diff --git a/lifecycle/forgetting.py b/lifecycle/forgetting.py index f54f0f8f..326a638d 100644 --- a/lifecycle/forgetting.py +++ b/lifecycle/forgetting.py @@ -120,7 +120,7 @@ async def archive_old_entries(self) -> int: ids = [r["entry_id"] for r in all_rows] placeholders = ",".join(["?"] * len(ids)) # Parameterized via placeholders, safe. - await conn.execute(f"DELETE FROM core_memory WHERE entry_id IN ({placeholders})", ids) + await conn.execute(f"DELETE FROM core_memory WHERE entry_id IN ({placeholders})", tuple(ids)) await conn.commit() logger.info("Archived %d entries", archived_count) return archived_count @@ -142,7 +142,7 @@ async def compress_duplicates(self) -> int: ) changes_cursor = await conn.execute("SELECT changes()") changes_row = await changes_cursor.fetchone() - removed += changes_row[0] + removed += int(changes_row[0]) if changes_row and changes_row[0] is not None else 0 await conn.commit() return removed except Exception: diff --git a/mcp_server/app.py b/mcp_server/app.py index c70d67b0..3b6e4fe9 100644 --- a/mcp_server/app.py +++ b/mcp_server/app.py @@ -1,5 +1,6 @@ import os import time as _time +from typing import Any from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response @@ -42,7 +43,8 @@ async def check_rate_limit(request: Request, api_rate_limiter: RateLimiter) -> b return True user = get_user_from_token(request) result = await api_rate_limiter.check(user) - return result.get("allowed", True) + res: Any = result.get("allowed", True) + return bool(res) def create_app(mcp: FastMCP, ctx: AppContext) -> Starlette: diff --git a/mcp_server/context.py b/mcp_server/context.py index d181d172..2f7253bb 100644 --- a/mcp_server/context.py +++ b/mcp_server/context.py @@ -17,7 +17,7 @@ class AppContext: - def __init__(self): + def __init__(self) -> None: self.cache = MemoryCache() self.mm = MemoryManager(cache=self.cache) self.user_wiki = WikiManager(layer="user") diff --git a/mcp_server/middlewares.py b/mcp_server/middlewares.py index 8e35e92d..b797dbd8 100644 --- a/mcp_server/middlewares.py +++ b/mcp_server/middlewares.py @@ -1,13 +1,16 @@ import os +from typing import Any, Awaitable, Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.cors import CORSMiddleware -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response +from starlette.requests import Request +from starlette.applications import Starlette from features.auth import bearer_auth from config import config class AuthMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request, call_next): + async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response: if request.url.path in ("/mcp", "/health", "/ready", "/alive"): return await call_next(request) if os.environ.get("MCP_AUTH_DISABLED"): @@ -18,9 +21,9 @@ async def dispatch(self, request, call_next): return await call_next(request) -def add_middlewares(app): +def add_middlewares(app: Starlette) -> None: app.add_middleware(AuthMiddleware) - allowed_origins = config.get("cors", "allowed_origins", default=["http://localhost:*", "http://127.0.0.1:*"]) + allowed_origins: list[str] = config.get("cors", "allowed_origins", default=["http://localhost:*", "http://127.0.0.1:*"]) app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, diff --git a/mcp_server/registry.py b/mcp_server/registry.py index 7150e3bd..a8625023 100644 --- a/mcp_server/registry.py +++ b/mcp_server/registry.py @@ -14,19 +14,19 @@ # if TYPE_CHECKING: -_tools: dict[str, Callable] = {} +_tools: dict[str, Callable[..., Any]] = {} -def _get_ctx(ctx: Context | None) -> Any: +def _get_ctx(ctx: Context[Any, Any] | None) -> Any: """Extract AppContext from FastMCP lifespan context.""" if ctx is None: raise ValueError("Context is required but was None") return ctx.request_context.lifespan_context -def register_tool(name: str, func: Callable) -> None: +def register_tool(name: str, func: Callable[..., Any]) -> None: _tools[name] = func -def get_all_tools() -> dict[str, Callable]: +def get_all_tools() -> dict[str, Callable[..., Any]]: return dict(_tools) diff --git a/mcp_server/server.py b/mcp_server/server.py index 3f3e90e0..3c0115b3 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -5,7 +5,7 @@ import logging from pathlib import Path -from typing import Any +from typing import Any, Literal from mcp.server.fastmcp import FastMCP # Ensure the root of the repo is in the path @@ -20,10 +20,10 @@ ) -STDIO_TRANSPORT = "stdio" +STDIO_TRANSPORT: Literal["stdio"] = "stdio" -def _register_all_tools(): +def _register_all_tools() -> None: from mcp_server.registry import get_all_tools for name, func in get_all_tools().items(): @@ -43,7 +43,7 @@ def main() -> None: default=STDIO_TRANSPORT, help="Transport: stdio (Claude Desktop) or http (web clients)", ) - parser.add_argument("--host", default="0.0.0.0", help="HTTP host (default: 0.0.0.0)") # noqa: S104 + parser.add_argument("--host", default="0.0.0.0", help="HTTP host (default: 0.0.0.0)") parser.add_argument("--port", type=int, default=8000, help="HTTP port (default: 8000)") parser.add_argument("--dashboard", action="store_true", help="Enable dashboard + metrics endpoints") parser.add_argument("--no-auth", action="store_true", help="Disable auth for development") @@ -80,7 +80,7 @@ def _run_with_dashboard(host: str, port: int) -> None: uvicorn.run(app, host=host, port=port) -def _setup_shutdown_signals(handler) -> None: +def _setup_shutdown_signals(handler: Any) -> None: import signal signal.signal(signal.SIGTERM, handler) diff --git a/mcp_server/tools/episodic.py b/mcp_server/tools/episodic.py index fec07cba..a119eda8 100644 --- a/mcp_server/tools/episodic.py +++ b/mcp_server/tools/episodic.py @@ -6,8 +6,8 @@ from shared.metrics import metrics import mcp_server.tools_layer as tl -from .base import _validate_layer, _check_rate_limit, _get_memory, _invalidate_cache -from typing import TYPE_CHECKING +from .base import _validate_layer, _check_rate_limit, _get_memory, _invalidate_cache, _fire_hook +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from mcp.server.fastmcp import Context @@ -19,8 +19,8 @@ async def memory_episode_save( summary: str = "", weight: float = 0.5, tags: list[str] | None = None, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Save an episode to L3 episodic memory.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -29,15 +29,15 @@ async def memory_episode_save( rate_limit = await _check_rate_limit(app, user_id) if rate_limit: - return rate_limit + return dict(rate_limit) episode_id = await _get_memory(app, layer, user_id).l3.save(user_id, summary, weight, tags) _invalidate_cache(layer, user_id) # Fire post-save hooks - await tl._fire_hook("emotion_trigger", layer, {"summary": summary, "emotional_weight": weight, "user_id": user_id}) - await tl._fire_hook("state_delta", layer, {"summary": summary, "tags": tags, "user_id": user_id}) - await tl._fire_hook("consolidation", layer, {"trigger": "episode_save", "user_id": user_id}) + await _fire_hook("emotion_trigger", layer, {"summary": summary, "emotional_weight": weight, "user_id": user_id}) + await _fire_hook("state_delta", layer, {"summary": summary, "tags": tags, "user_id": user_id}) + await _fire_hook("consolidation", layer, {"trigger": "episode_save", "user_id": user_id}) return EpisodeResult(episode_id=episode_id).dict() @@ -47,15 +47,15 @@ async def memory_episode_recall( user_id: str = "default", tag: str = "", limit: int = 10, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Recall episodes, optionally filtered by tag.""" app = _get_ctx(ctx) layer = _validate_layer(layer) metrics.inc("tool_calls") metrics.inc("tool_episode_recall") - await tl._fire_hook("retrieval_router", layer, {"query": tag or "episodes", "user_id": user_id, "limit": limit}) + await _fire_hook("retrieval_router", layer, {"query": tag or "episodes", "user_id": user_id, "limit": limit}) mem = _get_memory(app, layer, user_id) if tag: @@ -70,8 +70,8 @@ async def memory_episode_list( user_id: str = "default", limit: int = 10, offset: int = 0, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """List episodes from L3 episodic memory.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -88,8 +88,8 @@ async def memory_episode_get( layer: str = "user", user_id: str = "default", episode_id: int = 0, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Get a single episode by ID.""" app = _get_ctx(ctx) layer = _validate_layer(layer) diff --git a/mcp_server/tools/graph.py b/mcp_server/tools/graph.py index 6b5e65e8..a751bad2 100644 --- a/mcp_server/tools/graph.py +++ b/mcp_server/tools/graph.py @@ -6,8 +6,8 @@ from shared.metrics import metrics import mcp_server.tools_layer as tl -from .base import _validate_layer, _check_rate_limit, _get_graph, _invalidate_cache -from typing import TYPE_CHECKING +from .base import _validate_layer, _check_rate_limit, _get_graph, _invalidate_cache, _fire_hook +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from mcp.server.fastmcp import Context @@ -19,8 +19,8 @@ async def memory_graph_add( content: str = "", node_type: str = "fact", tags: list[str] | None = None, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Add a node to the epistemic graph.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -29,7 +29,7 @@ async def memory_graph_add( rate_limit = await _check_rate_limit(app, user_id) if rate_limit: - return rate_limit + return dict(rate_limit) node_id = await _get_graph(app, layer).add_node(user_id, content, node_type, tags) _invalidate_cache(layer, user_id) @@ -43,7 +43,7 @@ async def memory_graph_add( } hook_name = hook_map.get(node_type) if hook_name: - await tl._fire_hook(hook_name, layer, {"node_type": node_type, "content": content, "user_id": user_id}) + await _fire_hook(hook_name, layer, {"node_type": node_type, "content": content, "user_id": user_id}) return GraphNodeResult(node_id=node_id).dict() @@ -54,15 +54,15 @@ async def memory_graph_query( tag: str = "", node_type: str = "", limit: int = 20, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Query the epistemic graph by tag or node type.""" app = _get_ctx(ctx) layer = _validate_layer(layer) metrics.inc("tool_calls") metrics.inc("tool_graph_query") - await tl._fire_hook("retrieval_router", layer, {"query": tag or node_type, "user_id": user_id, "limit": limit}) + await _fire_hook("retrieval_router", layer, {"query": tag or node_type, "user_id": user_id, "limit": limit}) graph = _get_graph(app, layer) if tag: @@ -79,8 +79,8 @@ async def memory_graph_nodes( user_id: str = "default", node_type: str = "", limit: int = 20, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """List nodes from the epistemic graph.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -96,7 +96,7 @@ async def memory_graph_nodes( (graph.layer, user_id, limit), ) rows = await cur.fetchall() - nodes = [graph._row_to_node(r) for r in rows] + nodes = [graph._row_to_node(dict(r)) for r in rows] return {"nodes": [{"id": n.node_id, "content": n.content, "type": n.node_type, "tags": n.tags} for n in nodes], "count": len(nodes)} @@ -105,8 +105,8 @@ async def memory_graph_edges( user_id: str = "", node_id: int = 0, limit: int = 20, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """List edges from the epistemic graph.""" app = _get_ctx(ctx) layer = _validate_layer(layer) diff --git a/mcp_server/tools/memory.py b/mcp_server/tools/memory.py index b834a9b5..80e26246 100644 --- a/mcp_server/tools/memory.py +++ b/mcp_server/tools/memory.py @@ -7,9 +7,6 @@ from mcp_server.utils.privacy import strip_secrets from shared.metrics import metrics -# Import tools_layer to use its _fire_hook which might be monkeypatched -import mcp_server.tools_layer as tl - from .base import ( _validate_layer, _check_rate_limit, @@ -19,6 +16,7 @@ _invalidate_cache, _get_recall_cache, _set_recall_cache, + _fire_hook, ) from typing import TYPE_CHECKING, Any @@ -35,8 +33,8 @@ async def memory_remember( value: str = "", importance: float = 0.5, session_id: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Save a fact to long-term memory (L4 CoreMemory).""" value = strip_secrets(value) if session_id and _dedup_cache.is_duplicate(session_id, key, value): @@ -50,9 +48,9 @@ async def memory_remember( rate_limit = await _check_rate_limit(app, user_id) if rate_limit: - return rate_limit + return dict(rate_limit) - gate = await tl._fire_hook("importance_gate", layer, {"text": value, "key": key, "importance": importance}) + gate = await _fire_hook("importance_gate", layer, {"text": value, "key": key, "importance": importance}) if gate.get("results") and any(r.get("bypass") for r in gate["results"] if isinstance(r, dict)): logger.info("Importance gate bypassed: key=%s, importance=%.2f, user=%s", key, importance, user_id) return RememberResult(status="skipped", reason="below_importance_threshold").dict() @@ -75,22 +73,22 @@ async def memory_remember( else: entry_id = await mem.remember(key, value, importance) node_id = await graph.add_node(user_id, value, "fact", [], importance) - await tl._fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem) - await tl._fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem) + await _fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem) + await _fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem) _invalidate_cache(layer, user_id) return RememberResult(status="ok", entry_id=entry_id, graph_node_id=node_id).dict() async def _fire_post_remember_hooks(layer: str, user_id: str, key: str, value: str, mem: Any) -> None: - await tl._fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem) - await tl._fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem) + await _fire_hook("message_received", layer, {"text": value, "key": key, "user_id": user_id}, mem=mem) + await _fire_hook("emotion_trigger", layer, {"text": value, "user_id": user_id, "key": key}, mem=mem) if "error" in key.lower(): - await tl._fire_hook("error_occurred", layer, {"key": key, "value": value, "user_id": user_id}) + await _fire_hook("error_occurred", layer, {"key": key, "value": value, "user_id": user_id}) elif "decision" in key.lower(): - await tl._fire_hook("decision_made", layer, {"key": key, "value": value, "user_id": user_id}) + await _fire_hook("decision_made", layer, {"key": key, "value": value, "user_id": user_id}) elif "correction" in key.lower(): - await tl._fire_hook("self_correction", layer, {"key": key, "value": value, "user_id": user_id}) + await _fire_hook("self_correction", layer, {"key": key, "value": value, "user_id": user_id}) async def memory_recall( @@ -98,15 +96,15 @@ async def memory_recall( user_id: str = "default", query: str = "", limit: int = 10, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Search memory across L3 (episodes) and L4 (facts).""" app = _get_ctx(ctx) layer = _validate_layer(layer) metrics.inc("tool_calls") metrics.inc("tool_recall") - await tl._fire_hook("retrieval_router", layer, {"query": query, "user_id": user_id, "limit": limit}) + await _fire_hook("retrieval_router", layer, {"query": query, "user_id": user_id, "limit": limit}) cached = _get_recall_cache(query, user_id, layer, limit) if cached is not None: @@ -115,7 +113,7 @@ async def memory_recall( results = await _get_memory(app, layer, user_id).recall(query, limit) _set_recall_cache(query, user_id, layer, limit, results) - await tl._fire_hook("auto_context", layer, {"query": query, "results_count": len(results), "user_id": user_id}) + await _fire_hook("auto_context", layer, {"query": query, "results_count": len(results), "user_id": user_id}) return RecallResult(results=results, count=len(results)).dict() @@ -124,8 +122,8 @@ async def memory_forget( layer: str = "user", user_id: str = "default", key: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Delete a fact from L4 memory.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -134,7 +132,7 @@ async def memory_forget( rate_limit = await _check_rate_limit(app, user_id) if rate_limit: - return rate_limit + return dict(rate_limit) deleted = await _get_memory(app, layer, user_id).forget(key) _invalidate_cache(layer, user_id) diff --git a/mcp_server/tools/ops.py b/mcp_server/tools/ops.py index aee1821e..e1d116e3 100644 --- a/mcp_server/tools/ops.py +++ b/mcp_server/tools/ops.py @@ -28,9 +28,10 @@ _set_cached, _estimate_tokens, _truncate_to_budget, + _fire_hook, DEFAULT_TOKEN_BUDGET, ) -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from mcp.server.fastmcp import Context @@ -39,8 +40,8 @@ async def memory_stats( layer: str = "user", user_id: str = "default", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Get memory statistics for a layer.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -63,8 +64,8 @@ async def memory_stats( async def memory_context( layer: str = "user", user_id: str = "default", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Return compressed context summary for prompt injection.""" metrics.inc("tool_calls") metrics.inc("tool_context") @@ -116,13 +117,13 @@ async def memory_context( async def memory_context_inject( layer: str = "user", user_id: str = "default", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Return compressed summary for prompt injection (L4 top-10 + L3 top-3).""" metrics.inc("tool_calls") metrics.inc("tool_context_inject") - await tl._fire_hook("auto_context", layer, {"query": "context_inject", "user_id": user_id}) + await _fire_hook("auto_context", layer, {"query": "context_inject", "user_id": user_id}) cache_key = _get_cache_key(layer, user_id) cached = _get_cached(cache_key) @@ -133,7 +134,7 @@ async def memory_context_inject( mem = _get_memory(app, layer, user_id) wiki = _get_wiki(app, layer) - await tl._fire_hook("wiki_agent", layer, {"user_id": user_id, "query": "context_inject"}) + await _fire_hook("wiki_agent", layer, {"user_id": user_id, "query": "context_inject"}) l4_facts = await mem.l4.get_all(user_id, 10) facts_text = "; ".join([f"{f.key}={f.value[:30]}" for f in l4_facts]) @@ -173,7 +174,7 @@ async def memory_context_inject( "token_budget": DEFAULT_TOKEN_BUDGET, } _set_cached(cache_key, result) - await tl._fire_hook("dream_buffer", layer, {"text": context_text, "user_id": user_id}) + await _fire_hook("dream_buffer", layer, {"text": context_text, "user_id": user_id}) return result @@ -183,8 +184,8 @@ async def memory_api_key( user_id: str = "default", label: str = "", api_key: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Manage API keys.""" from features.auth import api_key_auth @@ -203,8 +204,8 @@ async def memory_api_key( async def memory_backup( action: str = "status", backup_name: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Manage backups.""" from features.backup_cron import backup_cron @@ -226,8 +227,8 @@ async def memory_backup( async def memory_saga( action: str = "consolidate", user_id: str = "default", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Run sagas with auto-rollback on failure.""" metrics.inc("tool_calls") metrics.inc("tool_saga") @@ -254,8 +255,8 @@ async def memory_data( user_id: str = "default", file_path: str = "", target_user_id: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Import/export memory data.""" metrics.inc("tool_calls") metrics.inc("tool_data") @@ -271,8 +272,8 @@ async def memory_data( async def memory_sync_replica( - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Sync read-only replica for dashboard/metrics.""" metrics.inc("tool_calls") metrics.inc("tool_sync_replica") @@ -285,8 +286,8 @@ async def memory_sync_replica( async def memory_cleanup( user_id: str = "default", retention_days: int = 30, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Full memory cleanup: deduplicate, archive, clean staging.""" metrics.inc("tool_calls") metrics.inc("tool_cleanup") @@ -323,61 +324,61 @@ async def memory_cleanup( async def memory_lucidity_purge( user_id: str = "default", hours: int = 24, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Emergency purge: delete all data from the last N hours.""" metrics.inc("tool_calls") metrics.inc("tool_lucidity_purge") app = _get_ctx(ctx) cutoff = time.time() - (hours * 3600) - async def _delete_core(): + async def _delete_core() -> int: conn = await app.mm.user_memory(user_id).l4._cm.get(DB_NAME) try: cursor = await conn.execute("DELETE FROM core_memory WHERE user_id=? AND created_at > ?", (user_id, cutoff)) - result = cursor.rowcount + result = int(cursor.rowcount) await conn.commit() return result finally: await conn.close() - async def _delete_episodes(): + async def _delete_episodes() -> int: conn = await app.mm.user_memory(user_id).l3._cm.get(DB_NAME) try: cursor = await conn.execute("DELETE FROM episodes WHERE user_id=? AND created_at > ?", (user_id, cutoff)) - result = cursor.rowcount + result = int(cursor.rowcount) await conn.commit() return result finally: await conn.close() - async def _delete_staging(): + async def _delete_staging() -> int: from shared.dream_buffer import DreamBuffer db = DreamBuffer() - return db.clear_staging(user_id) + return await db.clear_staging(user_id) - async def _delete_audit(): + async def _delete_audit() -> int: from features.audit_trail import AuditTrail at = AuditTrail() conn = await at._cm.get(DB_NAME) try: cursor = await conn.execute("DELETE FROM audit_log WHERE user_id=? AND timestamp > ?", (user_id, cutoff)) - result = cursor.rowcount + result = int(cursor.rowcount) await conn.commit() return result finally: await conn.close() - async def _delete_graph(): + async def _delete_graph() -> int: from graph.epistemic import EpistemicGraph eg = EpistemicGraph(layer="user") conn = await eg._cm.get(DB_NAME) try: cursor = await conn.execute("DELETE FROM epi_nodes WHERE user_id=? AND created_at > ?", (user_id, cutoff)) - result = cursor.rowcount + result = int(cursor.rowcount) await conn.commit() return result finally: @@ -402,8 +403,8 @@ async def memory_search( limit: int = 10, strategy: str = "hybrid", sources: str = "all", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Hybrid search across RAG + Wiki with strategy selection.""" metrics.inc("tool_calls") metrics.inc("tool_search") diff --git a/mcp_server/tools/session.py b/mcp_server/tools/session.py index 35f5dba6..a3bf1cc2 100644 --- a/mcp_server/tools/session.py +++ b/mcp_server/tools/session.py @@ -5,8 +5,8 @@ from shared.metrics import metrics import mcp_server.tools_layer as tl -from .base import _validate_layer, _check_rate_limit, _get_memory -from typing import TYPE_CHECKING +from .base import _validate_layer, _check_rate_limit, _get_memory, _fire_hook +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from mcp.server.fastmcp import Context @@ -15,8 +15,8 @@ async def memory_session_start( layer: str = "user", user_id: str = "default", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Start a new memory session.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -28,7 +28,7 @@ async def memory_session_start( return rate_limit session_id = await _get_memory(app, layer, user_id).l2.create_session(user_id) - await tl._fire_hook("message_received", layer, {"text": "session_started", "session_id": session_id, "user_id": user_id}) + await _fire_hook("message_received", layer, {"text": "session_started", "session_id": session_id, "user_id": user_id}) return SessionResult(session_id=session_id).dict() @@ -38,8 +38,8 @@ async def memory_session_end( user_id: str = "default", session_id: str = "", summary: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """End a session and save summary.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -52,8 +52,8 @@ async def memory_session_end( await _get_memory(app, layer, user_id).l2.close_session(session_id, summary) - await tl._fire_hook("consolidation", layer, {"trigger": "session_end", "session_id": session_id, "user_id": user_id}) - await tl._fire_hook("state_delta", layer, {"trigger": "session_end", "session_id": session_id, "summary": summary, "user_id": user_id}) + await _fire_hook("consolidation", layer, {"trigger": "session_end", "session_id": session_id, "user_id": user_id}) + await _fire_hook("state_delta", layer, {"trigger": "session_end", "session_id": session_id, "summary": summary, "user_id": user_id}) return SessionResult(status="ok").dict() @@ -62,8 +62,8 @@ async def memory_session_list( layer: str = "user", user_id: str = "default", limit: int = 10, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """List recent memory sessions.""" app = _get_ctx(ctx) layer = _validate_layer(layer) diff --git a/mcp_server/tools/wiki.py b/mcp_server/tools/wiki.py index d17ac9d4..db3da7e2 100644 --- a/mcp_server/tools/wiki.py +++ b/mcp_server/tools/wiki.py @@ -2,7 +2,7 @@ from mcp_server.registry import _get_ctx from .base import _validate_layer, _get_wiki -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from mcp.server.fastmcp import Context @@ -14,13 +14,13 @@ async def wiki_add( content: str = "", wiki_type: str = "concept", tags: list[str] | None = None, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Add or update a wiki page.""" app = _get_ctx(ctx) layer = _validate_layer(layer) wiki = _get_wiki(app, layer) - await wiki.save(title, content, wiki_type, tags) + await wiki.add(wiki_type, title, content, tags) return {"status": "ok", "title": title} @@ -28,22 +28,25 @@ async def wiki_search( layer: str = "user", query: str = "", limit: int = 10, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Search wiki pages.""" app = _get_ctx(ctx) layer = _validate_layer(layer) wiki = _get_wiki(app, layer) results = await wiki.search(query, limit) - return {"results": [{"title": r.title, "type": r.wiki_type, "tags": r.tags} for r in results], "count": len(results)} + return { + "results": [{"title": str(r.get("title", "")), "type": str(r.get("wiki_type", "")), "tags": list(r.get("tags", []))} for r in results], + "count": len(results), + } async def wiki_list( layer: str = "user", wiki_type: str = "", limit: int = 20, - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """List wiki pages.""" app = _get_ctx(ctx) layer = _validate_layer(layer) @@ -52,14 +55,17 @@ async def wiki_list( pages = await wiki.list_by_type(wiki_type, limit) else: pages = await wiki.list_all(limit) - return {"pages": [{"title": p.title, "type": p.wiki_type, "tags": p.tags} for p in pages], "count": len(pages)} + return { + "pages": [{"title": str(p.title), "type": str(p.wiki_type), "tags": list(p.tags)} for p in pages], + "count": len(pages), + } async def wiki_delete( layer: str = "user", title: str = "", - ctx: Context | None = None, -) -> dict: + ctx: Context[Any, Any] | None = None, +) -> dict[str, Any]: """Delete a wiki page.""" app = _get_ctx(ctx) layer = _validate_layer(layer) diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index 859be95b..f16e8f5d 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -31,7 +31,7 @@ # For tests that monkeypatch tl._fire_hook -def _fire_hook_wrapper(*args, **kwargs): +def _fire_hook_wrapper(*args: Any, **kwargs: Any) -> Any: from .tools import base return base._fire_hook(*args, **kwargs) diff --git a/mcp_server/utils/circuit_breaker.py b/mcp_server/utils/circuit_breaker.py index ef11e6c2..1afe9c20 100644 --- a/mcp_server/utils/circuit_breaker.py +++ b/mcp_server/utils/circuit_breaker.py @@ -113,11 +113,6 @@ def allow_request(self) -> bool: self._total_rejections += 1 return False - def reset(self) -> None: - self._failures = 0 - self._state = CircuitState.CLOSED - self._opened_at = 0.0 - def get_metrics(self) -> dict[str, Any]: return { "name": self.name, @@ -177,7 +172,9 @@ def get_all_metrics(self) -> dict[str, dict[str, Any]]: def reset_all(self) -> None: for breaker in self._breakers.values(): - breaker.reset() + breaker._failures = 0 + breaker._state = CircuitState.CLOSED + breaker._opened_at = 0.0 breaker_registry = CircuitBreakerRegistry() diff --git a/pyproject.toml b/pyproject.toml index b598d339..a97982a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,32 +162,29 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] - -[tool.skylos] -exclude = [".repowise", ".codegraph", "docs", "__pycache__"] - -# SQL injection false positives: all findings use parameterized queries (?) -# or build SQL templates (table names, IN clauses) — not user data injection -ignore = ["SKY-D211"] - -[tool.skylos.quality] -max_complexity = 15 -max_lines = 100 -max_args = 6 +"tests/*" = ["S101", "ANN", "D", "SLF001", "PLR", "ARG", "FBT", "PT", "B011", "B023", "B904", "RET504", "TRY", "EM", "S", "PLW", "PLR"] [tool.mypy] python_version = "3.12" -warn_return_any = false +warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true check_untyped_defs = true -ignore_missing_imports = true +ignore_missing_imports = false explicit_package_bases = true -exclude = ["tests"] +exclude = ["tests", ".venv"] +strict = true [[tool.mypy.overrides]] -module = ["tests.*"] -ignore_errors = true +module = [ + "yaml.*", + "pynacl.*", + "nacl.*", + "prometheus_client.*", + "aiosqlite.*", + "alembic.*", +] +ignore_missing_imports = true [tool.coverage.run] omit = ["demo.py", "__main__.py"] diff --git a/rag/conflict.py b/rag/conflict.py index 5bb2199d..33b257b5 100644 --- a/rag/conflict.py +++ b/rag/conflict.py @@ -72,7 +72,7 @@ class ConflictResolver: def __init__(self, cm: AsyncConnectionManager | None = None): self._cm = cm or connection_manager - async def _init_db(self): + async def _init_db(self) -> None: await self._cm.execute_script( DB_NAME, """ @@ -98,15 +98,15 @@ async def check(self, user_id: str, new_content: str, min_similarity: float = 0. for row in rows: existing_id, existing_content, is_conflict, group_id = row - similarity = self._calculate_similarity(new_content, existing_content) - if similarity > min_similarity and existing_content != new_content: - return await self._handle_conflict(conn, existing_id, existing_content, is_conflict, group_id, new_content, similarity) + similarity = float(self._calculate_similarity(new_content, str(existing_content))) + if similarity > min_similarity and str(existing_content) != new_content: + return await self._handle_conflict(conn, int(existing_id), str(existing_content), bool(is_conflict), group_id, new_content, similarity) await conn.execute("INSERT INTO memory_conflicts (user_id, content) VALUES (?, ?)", (user_id, new_content)) await conn.commit() return {"content": new_content, "is_conflict": False} - async def _find_potential_conflicts(self, conn, user_id: str, keywords: list[str]): + async def _find_potential_conflicts(self, conn: Any, user_id: str, keywords: list[str]) -> Any: like_conditions = " OR ".join(["content LIKE ?" for _ in keywords]) like_params = [f"%{kw}%" for kw in keywords] cur = await conn.execute( @@ -115,8 +115,8 @@ async def _find_potential_conflicts(self, conn, user_id: str, keywords: list[str ) return await cur.fetchall() - async def _handle_conflict(self, conn, existing_id, existing_content, is_conflict, group_id, new_content, similarity): - gid = group_id or str(uuid.uuid4()) + async def _handle_conflict(self, conn: Any, existing_id: int, existing_content: str, is_conflict: bool, group_id: str | None, new_content: str, similarity: float) -> dict[str, Any]: + gid = str(group_id) if group_id else str(uuid.uuid4()) if not is_conflict: await conn.execute("UPDATE memory_conflicts SET is_conflict=1, conflict_group_id=? WHERE id=?", (gid, existing_id)) await conn.commit() diff --git a/rag/engine.py b/rag/engine.py index 944d932c..cc92893c 100644 --- a/rag/engine.py +++ b/rag/engine.py @@ -31,9 +31,9 @@ def __init__( binary_dim: int = 384, binary_threshold_mode: str = "naive", binary_thresholds_path: str | None = None, - thresholds=None, + thresholds: Any = None, search_strategy: StrategyT = "fts", - ): + ) -> None: self._cm = cm or connection_manager self.layer = layer self.binary_dim = binary_dim @@ -149,7 +149,7 @@ async def get_relations(self, page_id: int, depth: int = 1) -> list[dict[str, An rows = await cur.fetchall() return [{"id": r[0], "title": r[1], "relation": r[2], "weight": r[3]} for r in rows] - async def add_relation(self, source_id: int, target_id: int, relation_type: str = "elaborates", weight: float = 0.8): + async def add_relation(self, source_id: int, target_id: int, relation_type: str = "elaborates", weight: float = 0.8) -> None: conn = await self._cm.get(DB_NAME) await conn.execute( "INSERT OR REPLACE INTO rag_relations (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)", @@ -168,4 +168,4 @@ async def count_pages(self, user_id: str | None = None) -> int: async def count_chunks(self) -> int: conn = await self._cm.get(DB_NAME) row = await (await conn.execute("SELECT COUNT(*) FROM rag_chunks")).fetchone() - return row[0] if row else 0 + return int(row[0]) if row and row[0] is not None else 0 diff --git a/rag/ingestor.py b/rag/ingestor.py index a00c6d39..8e8c4d26 100644 --- a/rag/ingestor.py +++ b/rag/ingestor.py @@ -29,7 +29,7 @@ async def ingest(self, title: str, content: str, user_id: str, wiki_type: str | cursor = await conn.execute("SELECT id FROM rag_pages WHERE sha256_hash = ? AND user_id = ?", (content_hash, user_id)) row = await cursor.fetchone() if row: - return row[0] + return int(row[0]) if row[0] is not None else None # Split text into chunks chunks_text = chunk_text(content) @@ -81,7 +81,7 @@ async def ingest(self, title: str, content: str, user_id: str, wiki_type: str | cursor = await conn.execute("SELECT last_insert_rowid()") row = await cursor.fetchone() - page_id = row[0] + page_id = int(row[0]) if row and row[0] is not None else 0 # Batch Insertion chunk_data = [] diff --git a/rag/quantize.py b/rag/quantize.py index 86d17c5c..b439101f 100644 --- a/rag/quantize.py +++ b/rag/quantize.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable, Iterable, Sequence @@ -23,7 +23,7 @@ DEFAULT_DIM = 384 -def _check_numpy(): +def _check_numpy() -> None: if not _HAS_NUMPY: raise ImportError("numpy is required for binary embeddings. Install with: pip install mcp-ariel-memory[binary]") @@ -61,7 +61,7 @@ def supervised_threshold( pos_pairs: Iterable[tuple[Sequence[float], Sequence[float]]], dim: int = DEFAULT_DIM, n_candidates: int = 50, -): +) -> np.ndarray: """Per-dimension threshold maximizing agreement on positive pairs. Args: @@ -100,7 +100,7 @@ def supervised_threshold( def train_supervised_thresholds( pos_pairs: list[tuple[Sequence[float], Sequence[float]]], neg_pairs: list[tuple[Sequence[float], Sequence[float]]] | None = None, - emb_fn: Callable | None = None, + emb_fn: Callable[[Any], Sequence[float]] | None = None, n_candidates: int = 50, dim: int = DEFAULT_DIM, ) -> np.ndarray: @@ -162,17 +162,18 @@ def train_supervised_thresholds( return thresholds -def save_thresholds(thresholds: np.ndarray, path: str): +def save_thresholds(thresholds: np.ndarray, path: str) -> None: """Save thresholds to .npy file.""" _check_numpy() np.save(path, thresholds) -def load_thresholds(path: str) -> np.ndarray | None: +def load_thresholds(path: str) -> Any | None: """Load thresholds from .npy file. Returns None if file doesn't exist.""" _check_numpy() try: - return np.load(path) + res: Any = np.load(path) + return res except (FileNotFoundError, Exception): return None diff --git a/rag/router.py b/rag/router.py index 14e46482..5b761207 100644 --- a/rag/router.py +++ b/rag/router.py @@ -114,7 +114,13 @@ def __init__(self, strategy: Strategy, context: list[dict[str, Any]], confidence class RetrievalRouter: - def __init__(self, layer: str = "user", user_id: str = "default", keyword_overrides: dict | None = None, recent_max_chars: int = 60): + def __init__( + self, + layer: str = "user", + user_id: str = "default", + keyword_overrides: dict[str, Any] | None = None, + recent_max_chars: int = 60, + ) -> None: self.layer = layer self.user_id = user_id self.recent_max_chars = recent_max_chars @@ -143,7 +149,7 @@ def _flat_keywords(self, kind: str) -> list[str]: out.extend(v) return [kw.lower() for kw in out] - async def route(self, query: str, recent_context: list[dict] | None = None) -> RouterResult: + async def route(self, query: str, recent_context: list[dict[str, Any]] | None = None) -> RouterResult: q = query.lower() for route in _ROUTE_TABLE: @@ -164,31 +170,51 @@ async def _match_route( keyword_kind: str | None, strategy: Strategy, confidence: float, - recent_context: list[dict] | None, + recent_context: list[dict[str, Any]] | None, ) -> RouterResult | None: """Try to match a single route. Returns RouterResult or None.""" if keyword_kind == "recent": - if self._is_recent_query(q_lower) and recent_context: - return RouterResult(strategy, recent_context, confidence) + return self._match_recent(q_lower, strategy, confidence, recent_context) + + if keyword_kind == "wiki": + return await self._match_wiki(q_lower, query, strategy, confidence) + + if keyword_kind == "entity": + return await self._match_entity(query, strategy, confidence) + + if keyword_kind == "graph": + return await self._match_graph(q_lower, strategy, confidence) + + if keyword_kind is None: + return await self._match_semantic(query, strategy, confidence) + + return None - elif keyword_kind == "wiki": - if self._is_wiki_query(q_lower): - return await self._route_wiki(query, strategy, confidence) + def _match_recent(self, q_lower: str, strategy: Strategy, confidence: float, recent_context: list[dict[str, Any]] | None) -> RouterResult | None: + if self._is_recent_query(q_lower) and recent_context: + return RouterResult(strategy, recent_context, confidence) + return None - elif keyword_kind == "entity": - entities = self._extract_entities(query) - if entities: - return await self._route_entities(entities, strategy, confidence) + async def _match_wiki(self, q_lower: str, query: str, strategy: Strategy, confidence: float) -> RouterResult | None: + if self._is_wiki_query(q_lower): + return await self._route_wiki(query, strategy, confidence) + return None - elif keyword_kind == "graph": - if self._is_graph_query(q_lower): - return await self._route_graph(q_lower, strategy, confidence) + async def _match_entity(self, query: str, strategy: Strategy, confidence: float) -> RouterResult | None: + entities = self._extract_entities(query) + if entities: + return await self._route_entities(entities, strategy, confidence) + return None - elif keyword_kind is None: - results = await self._rag.search(query, self.user_id, strategy="hybrid", limit=3) - if results: - return RouterResult(strategy, results, confidence) + async def _match_graph(self, q_lower: str, strategy: Strategy, confidence: float) -> RouterResult | None: + if self._is_graph_query(q_lower): + return await self._route_graph(q_lower, strategy, confidence) + return None + async def _match_semantic(self, query: str, strategy: Strategy, confidence: float) -> RouterResult | None: + results = await self._rag.search(query, self.user_id, strategy="hybrid", limit=3) + if results: + return RouterResult(strategy, results, confidence) return None async def _route_wiki(self, query: str, strategy: Strategy, confidence: float) -> RouterResult: diff --git a/rag/schema.py b/rag/schema.py index ca3cc0f6..c538fd64 100644 --- a/rag/schema.py +++ b/rag/schema.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -async def init_rag_db(cm: AsyncConnectionManager, fts_available: bool): +async def init_rag_db(cm: AsyncConnectionManager, fts_available: bool) -> None: """ Initialize RAG database schema. """ diff --git a/rag/search.py b/rag/search.py index 4d25900d..aff2bc52 100644 --- a/rag/search.py +++ b/rag/search.py @@ -2,7 +2,7 @@ import logging from contextlib import suppress -from typing import Any +from typing import Any, Callable from shared.connection import AsyncConnectionManager from shared.constants import DB_NAME @@ -67,7 +67,7 @@ async def search_binary( query: str, user_id: str, limit: int, - binary_for_fn, + binary_for_fn: Callable[[list[float]], bytes], binary_dim: int, ) -> list[dict[str, Any]]: """Exhaustive linear scan over binary embeddings.""" @@ -123,7 +123,7 @@ async def search_rrf( user_id: str, limit: int, k: int = 60, - binary_for_fn=None, + binary_for_fn: Callable[[list[float]], bytes] | None = None, binary_dim: int = 384, fts_available: bool = True, ) -> list[dict[str, Any]]: @@ -132,9 +132,10 @@ async def search_rrf( fts_ranks = {doc["id"]: rank for rank, doc in enumerate(fts_results)} bin_ranks = {} - with suppress(Exception): - bin_results = await search_binary(cm, query, user_id, limit * 3, binary_for_fn, binary_dim) - bin_ranks = {r["id"]: rank for rank, r in enumerate(bin_results)} + if binary_for_fn: + with suppress(Exception): + bin_results = await search_binary(cm, query, user_id, limit * 3, binary_for_fn, binary_dim) + bin_ranks = {r["id"]: rank for rank, r in enumerate(bin_results)} def rrf(rank: int) -> float: return 1.0 / (k + rank + 1) @@ -156,7 +157,7 @@ def rrf(rank: int) -> float: placeholders = ",".join(["?"] * len(sorted_ids)) cur = await conn.execute( f"SELECT id, title, content, wiki_type FROM rag_pages WHERE id IN ({placeholders})", - sorted_ids, + tuple(sorted_ids), ) rows = await cur.fetchall() by_id = {r[0]: r for r in rows} @@ -189,7 +190,7 @@ def auto_strategy(query: str) -> str: return "hybrid" -def materialize_candidates(results: list[dict[str, Any]]) -> list: +def materialize_candidates(results: list[dict[str, Any]]) -> list[Any]: """Convert raw search dicts to ScoredCandidate objects for the Scorer.""" from rag.scoring import ScoredCandidate @@ -216,16 +217,16 @@ def materialize_candidates(results: list[dict[str, Any]]) -> list: return list(seen.values()) -def format_result(c) -> dict[str, Any]: +def format_result(c: Any) -> dict[str, Any]: """Convert a ScoredCandidate back to a result dict.""" - content = c.content + content: str = str(c.content) if len(content) > 500: content = content[:500] + "..." return { - "id": c.id, - "title": c.title, + "id": int(c.id), + "title": str(c.title), "content": content, - "wiki_type": c.wiki_type, - "score": c.final_score or c.rrf_score, - "source": c.source, + "wiki_type": str(c.wiki_type), + "score": float(c.final_score or c.rrf_score), + "source": str(c.source), } diff --git a/rag/searcher.py b/rag/searcher.py index abec9756..4d442647 100644 --- a/rag/searcher.py +++ b/rag/searcher.py @@ -31,16 +31,17 @@ def __init__( async def _check_fts(self) -> bool: if self._fts_available is not None: - return self._fts_available + return bool(self._fts_available) conn = await self._cm.get(DB_NAME) try: cur = await conn.execute("PRAGMA compile_options") - options = [r[0] for r in await cur.fetchall()] + rows = await cur.fetchall() + options = [str(r[0]) for r in rows] self._fts_available = "ENABLE_FTS5" in options except Exception: self._fts_available = False - return self._fts_available or False + return bool(self._fts_available) async def _search_fts5(self, query: str, user_id: str, limit: int) -> list[SearchResult]: fts_ready = await self._check_fts() @@ -59,7 +60,7 @@ async def _search_fts5(self, query: str, user_id: str, limit: int) -> list[Searc async def _search_mib(self, query: str, user_id: str, limit: int) -> list[SearchResult]: from rag.quantize import embed_to_binary - def default_bin_for(emb): + def default_bin_for(emb: list[float]) -> bytes: return embed_to_binary(emb, threshold=0.0, dim=len(emb)) raw_results = await search_binary(self._cm, query, user_id, limit, default_bin_for, self.binary_dim) diff --git a/shared/archived_memories.py b/shared/archived_memories.py index 21c5be67..b47b7cdd 100644 --- a/shared/archived_memories.py +++ b/shared/archived_memories.py @@ -43,7 +43,8 @@ async def archive( (user_id, original_id, content, memory_type, importance, reason), ) await conn.commit() - return int(cursor.lastrowid) + last_id: Any = cursor.lastrowid + return int(last_id) if last_id is not None else 0 async def get_archived(self, user_id: str = "default", limit: int = 50) -> list[dict[str, Any]]: conn = await self._cm.get(DB_NAME) diff --git a/shared/dream_buffer.py b/shared/dream_buffer.py index 1c5c8b13..0f9f9aad 100644 --- a/shared/dream_buffer.py +++ b/shared/dream_buffer.py @@ -46,7 +46,8 @@ async def add( (user_id, session_id, event_id, content, importance, json.dumps(metadata or {})), ) await conn.commit() - return int(cursor.lastrowid) + last_id: Any = cursor.lastrowid + return int(last_id) if last_id is not None else 0 async def get_staging(self, user_id: str = "default", session_id: str | None = None) -> list[dict[str, Any]]: conn = await self._cm.get(DB_NAME) diff --git a/shared/embeddings.py b/shared/embeddings.py index 78821255..90e90e3f 100644 --- a/shared/embeddings.py +++ b/shared/embeddings.py @@ -22,7 +22,7 @@ def _get_model(model_name: str | None = None) -> Any: target = model_name or DEFAULT_MODEL if _model is None or _model_name != target: try: - from sentence_transformers import SentenceTransformer + from sentence_transformers import SentenceTransformer # type: ignore[import-not-found] _model = SentenceTransformer(target) _model_name = target diff --git a/shared/importance/scorer.py b/shared/importance/scorer.py index 86881672..5dd501a6 100644 --- a/shared/importance/scorer.py +++ b/shared/importance/scorer.py @@ -39,8 +39,8 @@ def __init__( ] self._config_path = Path(config_path) self._data_path = Path(data_path) - self._tech_re = None - self._noise_re = None + self._tech_re: re.Pattern[str] | None = None + self._noise_re: re.Pattern[str] | None = None def _load_config(self) -> ImportanceConfig: if self._config: @@ -72,7 +72,7 @@ def score(self, text: str, context: dict[str, Any] | None = None, **kwargs: Any) context["tech_re"] = self._tech_re context["noise_re"] = self._noise_re - results = {} + results: dict[str, float] = {} for signal in self._signals: name = signal.__class__.__name__.lower().replace("signal", "") if name == "basetype": @@ -85,7 +85,7 @@ def score(self, text: str, context: dict[str, Any] | None = None, **kwargs: Any) name = "noise_penalty" elif name == "emotion": name = "emotional" - results[name] = signal.calculate(text, context) + results[name] = float(signal.calculate(text, context)) signals = ImportanceSignals(**results) diff --git a/shared/importance/signals/emotion_signal.py b/shared/importance/signals/emotion_signal.py index f92df310..50eae704 100644 --- a/shared/importance/signals/emotion_signal.py +++ b/shared/importance/signals/emotion_signal.py @@ -12,11 +12,12 @@ def calculate(self, text: str, context: dict[str, Any]) -> float: engine = context.get("_emotion_engine") if not engine: # Fallback to provided emotion_weight if any - return max(0.0, min(1.0, context.get("emotion_weight", 0.0))) + val: Any = context.get("emotion_weight", 0.0) + return float(max(0.0, min(1.0, float(val)))) results = engine.detect(text) if not results: return 0.0 # Use max score from detected emotions - return max(res.score for res in results) + return float(max(float(res.score) for res in results)) diff --git a/shared/memory_types.py b/shared/memory_types.py index 538788a6..6f379ddf 100644 --- a/shared/memory_types.py +++ b/shared/memory_types.py @@ -256,4 +256,4 @@ async def backfill_null_kinds(cm: Any, dry_run: bool = True) -> int: return int(row["c"]) cur = await conn.execute("UPDATE core_memory SET memory_kind = 'fact' WHERE memory_kind IS NULL") await conn.commit() - return cur.rowcount + return int(cur.rowcount) diff --git a/shared/middleware.py b/shared/middleware.py index 91437c8b..42e4275c 100644 --- a/shared/middleware.py +++ b/shared/middleware.py @@ -20,9 +20,9 @@ class MiddlewareContext: tool_name: str = "" user_id: str = "default" - args: dict = field(default_factory=dict) + args: dict[str, Any] = field(default_factory=dict) result: Any = None - metadata: dict = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) start_time: float = 0.0 blocked: bool = False block_reason: str = "" @@ -90,7 +90,7 @@ def __init__( threshold: float = 0.3, technical_weight: float = 0.3, question_weight: float = 0.2, - scorer=None, + scorer: Any = None, memory_kind_hint: str | None = None, ): self._min_length = min_length @@ -145,7 +145,7 @@ async def process(self, ctx: MiddlewareContext, next: MiddlewareNext) -> Any: def calculate_score(self, text: str) -> float: """Calculate importance score using ImportanceScorer.""" signals = self._scorer.score(text=text, kind=self.memory_kind_hint) - return signals.total() + return float(signals.total()) class ValidationMiddleware(Middleware): @@ -191,20 +191,20 @@ async def process(self, ctx: MiddlewareContext, next: MiddlewareNext) -> Any: class MiddlewarePipeline: """Middleware chain.""" - def __init__(self): + def __init__(self) -> None: self._middlewares: list[Middleware] = [] def add(self, middleware: Middleware) -> MiddlewarePipeline: self._middlewares.append(middleware) return self - async def execute(self, ctx: MiddlewareContext, handler: Callable) -> Any: + async def execute(self, ctx: MiddlewareContext, handler: Callable[[MiddlewareContext], Any]) -> Any: async def _run(index: int, ctx: MiddlewareContext) -> Any: if index >= len(self._middlewares): - result = handler(ctx) - if hasattr(result, "__await__"): - return await result - return result + res = handler(ctx) + if hasattr(res, "__await__"): + return await res + return res return await self._middlewares[index].process(ctx, lambda c: _run(index + 1, c)) return await _run(0, ctx) @@ -214,7 +214,7 @@ def list_middlewares(self) -> list[str]: # Default pipeline -default_pipeline = MiddlewarePipeline() +default_pipeline: MiddlewarePipeline = MiddlewarePipeline() default_pipeline.add(ValidationMiddleware()) default_pipeline.add(RateLimitMiddleware()) default_pipeline.add(ImportanceGateMiddleware()) diff --git a/shared/saga/__init__.py b/shared/saga/__init__.py index 15346f6d..1624a4be 100644 --- a/shared/saga/__init__.py +++ b/shared/saga/__init__.py @@ -1,7 +1,14 @@ from shared.saga.impl.base import Saga, SAGA_DIR, saga_watchdog, SagaWatchdog from shared.saga.impl.backup import create_backup_saga from shared.saga.impl.consolidation import create_consolidation_saga -from shared.saga.impl.crypto import encrypt_json, decrypt_json, write_state_atomic, read_state, read_state_legacy_or_encrypted +from shared.saga.impl.crypto import ( + encrypt_json as encrypt_json, + decrypt_json as decrypt_json, + write_state_atomic as write_state_atomic, + read_state as read_state, + read_state_legacy_or_encrypted as read_state_legacy_or_encrypted, + is_encrypted_blob as is_encrypted_blob, +) from shared.saga.schema import SagaStatus, SagaStepState, SagaState from shared.saga.engine import SagaEngine, SagaStep from shared.saga.persistence import FileSagaStore, ISagaStore @@ -21,6 +28,7 @@ "create_consolidation_saga", "decrypt_json", "encrypt_json", + "is_encrypted_blob", "read_state", "read_state_legacy_or_encrypted", "saga_watchdog", diff --git a/shared/saga/impl/base.py b/shared/saga/impl/base.py index 3763f6cb..13784386 100644 --- a/shared/saga/impl/base.py +++ b/shared/saga/impl/base.py @@ -123,7 +123,7 @@ def add_step( def _save_state(self) -> None: """Save state to disk (encrypted if available).""" state_file = SAGA_DIR / (self._saga_id + ".json") - state = { + state: dict[str, Any] = { "name": self.name, "saga_id": self._saga_id, "status": self._status.value, @@ -145,7 +145,7 @@ def _save_state(self) -> None: except Exception: logger.exception("Failed to save saga state") - def _load_state(self, saga_id: str) -> dict | None: + def _load_state(self, saga_id: str) -> dict[str, Any] | None: """Load state from disk (supports encrypted and legacy plain JSON).""" from shared.saga import read_state_legacy_or_encrypted @@ -208,7 +208,8 @@ async def _get_cached_result(self, key: str) -> dict[str, Any] | None: return None result_blob = row["result_json"] if isinstance(result_blob, (bytes, bytearray)): - return decrypt_json(bytes(result_blob)) + res: Any = decrypt_json(bytes(result_blob)) + return dict(res) if isinstance(res, dict) else None return dict(json.loads(result_blob)) if result_blob else None except Exception: return None @@ -280,7 +281,7 @@ async def _run_step_action(self, step: SagaStep) -> dict[str, Any]: if asyncio.iscoroutine(action_result): result = await asyncio.wait_for(action_result, timeout=float(step_timeout)) else: - result = action_result # type: ignore[assignment] + result = action_result return result if isinstance(result, dict) else {"value": result} async def _handle_retry_pause(self, step: SagaStep, attempt: int, exc: Exception) -> None: diff --git a/shared/saga/impl/crypto.py b/shared/saga/impl/crypto.py index 29e71539..040585fd 100644 --- a/shared/saga/impl/crypto.py +++ b/shared/saga/impl/crypto.py @@ -15,6 +15,15 @@ from features.secrets import decrypt_json, encrypt_json from shared.crypto import is_encrypted_blob as _is_crypto_encrypted_blob +__all__ = [ + "encrypt_json", + "decrypt_json", + "write_state_atomic", + "read_state", + "read_state_legacy_or_encrypted", + "is_encrypted_blob", +] + def is_encrypted_blob(path: Path) -> bool: """Check if file is encrypted (not plain JSON).""" @@ -23,7 +32,7 @@ def is_encrypted_blob(path: Path) -> bool: # noqa: SKY-D325 with path.open("rb") as f: head = f.read(1) - return _is_crypto_encrypted_blob(head) + return bool(_is_crypto_encrypted_blob(head)) if TYPE_CHECKING: @@ -57,7 +66,8 @@ def read_state(path: Path) -> dict[str, Any]: # noqa: SKY-D325 with path.open("rb") as f: blob = f.read() - return decrypt_json(blob) + res: Any = decrypt_json(blob) + return dict(res) if isinstance(res, dict) else {} def read_state_legacy_or_encrypted(path: Path) -> dict[str, Any]: @@ -68,8 +78,10 @@ def read_state_legacy_or_encrypted(path: Path) -> dict[str, Any]: with path.open("rb") as f: blob = f.read() if is_encrypted_blob(path): - return decrypt_json(blob) + res: Any = decrypt_json(blob) + return dict(res) if isinstance(res, dict) else {} warnings.warn(f"{path} is plain JSON; rotating to encrypted", DeprecationWarning, stacklevel=2) - legacy = json.loads(blob.decode("utf-8")) - write_state_atomic(path, legacy) - return legacy + legacy: Any = json.loads(blob.decode("utf-8")) + state: dict[str, Any] = dict(legacy) if isinstance(legacy, dict) else {} + write_state_atomic(path, state) + return state diff --git a/wiki/index.py b/wiki/index.py index 59629813..547204bd 100644 --- a/wiki/index.py +++ b/wiki/index.py @@ -8,9 +8,13 @@ import json import logging import time +from typing import Any, TYPE_CHECKING from shared.constants import DB_NAME -from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from wiki.models import WikiEntry + from shared.connection import AsyncConnectionManager if TYPE_CHECKING: from wiki.models import WikiEntry @@ -26,7 +30,7 @@ def __init__(self, connection_manager: AsyncConnectionManager, layer: str): self._cm = connection_manager self.layer = layer - async def init_db(self): + async def init_db(self) -> None: """Initialize tables and indexes.""" await self._cm.execute_script( DB_NAME, @@ -57,7 +61,7 @@ async def init_db(self): """, ) - async def save(self, entry: WikiEntry, content_hash: str): + async def save(self, entry: WikiEntry, content_hash: str) -> None: """Atomic insert/update in both wiki_index and wiki_fts.""" now = time.time() tags_json = json.dumps(entry.tags) @@ -120,7 +124,7 @@ async def save(self, entry: WikiEntry, content_hash: str): await conn.commit() - async def get_by_path(self, file_path: str) -> dict | None: + async def get_by_path(self, file_path: str) -> dict[str, Any] | None: """Fetch metadata and hash by file path.""" conn = await self._cm.get(DB_NAME) cur = await conn.execute( @@ -130,7 +134,7 @@ async def get_by_path(self, file_path: str) -> dict | None: row = await cur.fetchone() return dict(row) if row else None - async def search(self, query: str, limit: int = 10) -> list[dict]: + async def search(self, query: str, limit: int = 10) -> list[dict[str, Any]]: """Optimized FTS5 search JOINing wiki_fts with wiki_index.""" conn = await self._cm.get(DB_NAME) try: @@ -148,7 +152,7 @@ async def search(self, query: str, limit: int = 10) -> list[dict]: logger.exception("Search failed for query '%s'", query) return [] - async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict]: + async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict[str, Any]]: """List entries of a specific type.""" conn = await self._cm.get(DB_NAME) cur = await conn.execute( @@ -158,7 +162,7 @@ async def list_by_type(self, wiki_type: str, limit: int = 20) -> list[dict]: rows = await cur.fetchall() return [dict(r) for r in rows] - async def list_all(self, limit: int = 50) -> list[dict]: + async def list_all(self, limit: int = 50) -> list[dict[str, Any]]: """List all entries in the current layer.""" conn = await self._cm.get(DB_NAME) cur = await conn.execute( @@ -168,7 +172,7 @@ async def list_all(self, limit: int = 50) -> list[dict]: rows = await cur.fetchall() return [dict(r) for r in rows] - async def delete(self, file_path: str): + async def delete(self, file_path: str) -> None: """Remove from both tables.""" conn = await self._cm.get(DB_NAME) cur = await conn.execute( diff --git a/wiki/manager.py b/wiki/manager.py index c2ee0cf2..8beceba4 100644 --- a/wiki/manager.py +++ b/wiki/manager.py @@ -47,7 +47,7 @@ def __init__(self, layer: str = "user", base_dir: str | None = None, cm: AsyncCo self.index = WikiIndex(self._cm, layer) self.parser = WikiParser() - async def init_db(self): + async def init_db(self) -> None: """Delegate to index layer.""" await self.index.init_db() @@ -95,7 +95,7 @@ async def update( content: str | None = None, tags: list[str] | None = None, importance: float | None = None, - ): + ) -> None: """Update .md file and re-index.""" p = safe_resolve(self.base_dir, file_path) if not await asyncio.to_thread(p.exists): @@ -153,10 +153,10 @@ async def list_all(self, limit: int = 50) -> list[WikiEntry]: rows = await self.index.list_all(limit) return await self._rows_to_entries(rows) - async def _rows_to_entries(self, rows: list[dict]) -> list[WikiEntry]: + async def _rows_to_entries(self, rows: list[dict[str, Any]]) -> list[WikiEntry]: entries = [] for r in rows: - entry = await self.get(r["file_path"]) + entry = await self.get(str(r["file_path"])) if entry: entries.append(entry) return entries @@ -187,8 +187,8 @@ async def reindex_all(self) -> dict[str, int]: md_files = [] enabled_types = self._get_enabled_types() - def _collect_files(): - files = [] + def _collect_files() -> list[Path]: + files: list[Path] = [] for wiki_type in enabled_types: type_dir = self.base_dir / wiki_type if type_dir.exists() and type_dir.is_dir(): @@ -197,7 +197,7 @@ def _collect_files(): md_files = await asyncio.to_thread(_collect_files) - async def _process_file(f: Path): + async def _process_file(f: Path) -> str: try: text = await asyncio.to_thread(f.read_text, encoding="utf-8") entry = self.parser.parse(text, f) @@ -235,12 +235,12 @@ async def sync_external(self, external_dirs: list[str] | None = None) -> dict[st if not await asyncio.to_thread(p.exists): continue - def _find_md(): + def _find_md() -> list[Path]: return list(p.glob("**/*.md")) md_files = await asyncio.to_thread(_find_md) - async def _sync_file(f: Path): + async def _sync_file(f: Path) -> str: try: content = await asyncio.to_thread(f.read_text, encoding="utf-8") parsed_entry = self.parser.parse(content, f) diff --git a/wiki/shared.py b/wiki/shared.py index 85354886..c8ead1e2 100644 --- a/wiki/shared.py +++ b/wiki/shared.py @@ -10,31 +10,36 @@ from typing import Any -def load_config() -> dict: +def load_config() -> dict[str, Any]: """Load config.yaml, return {} on failure.""" try: import yaml config_path = Path(__file__).parent.parent / "config.yaml" with config_path.open() as f: - return yaml.safe_load(f) or {} - except (OSError, yaml.YAMLError): + res: Any = yaml.safe_load(f) + return dict(res) if isinstance(res, dict) else {} + except (OSError, Exception): return {} def get_enabled_types(layer: str, all_types: list[str]) -> list[str]: """Return wiki types enabled in config for the given layer.""" cfg = load_config() - layer_cfg = cfg.get("wiki", {}).get(layer, {}) + wiki_cfg: dict[str, Any] = cfg.get("wiki", {}) + layer_cfg: dict[str, Any] = wiki_cfg.get(layer, {}) if not layer_cfg: return all_types - return [t for t in all_types if layer_cfg.get(t, True)] + return [t for t in all_types if bool(layer_cfg.get(t, True))] def get_external_dirs(layer: str) -> list[str]: """Return external directory paths from config for the given layer.""" cfg = load_config() - return cfg.get("wiki", {}).get(layer, {}).get("external_dirs", []) + wiki_cfg: dict[str, Any] = cfg.get("wiki", {}) + layer_cfg: dict[str, Any] = wiki_cfg.get(layer, {}) + res: Any = layer_cfg.get("external_dirs", []) + return [str(x) for x in res] if isinstance(res, list) else [] ALLOWED_TABLES = {"user_wiki", "agent_wiki", "wiki_index"} @@ -43,18 +48,19 @@ def get_external_dirs(layer: str) -> list[str]: def parse_tags(raw_tags: Any) -> list[str]: """Parse tags from JSON string or list.""" if isinstance(raw_tags, str): - return json.loads(raw_tags) if raw_tags else [] - return raw_tags or [] + res: Any = json.loads(raw_tags) if raw_tags else [] + return [str(x) for x in res] if isinstance(res, list) else [] + return [str(x) for x in raw_tags] if isinstance(raw_tags, list) else [] -def format_search_result(row: tuple, content_limit: int = 300) -> dict[str, Any]: +def format_search_result(row: tuple[Any, ...], content_limit: int = 300) -> dict[str, Any]: """Format FTS search result row into dict.""" return { "id": row[0], "title": row[1], - "content": row[2][:content_limit], + "content": str(row[2])[:content_limit], "type": row[3], "tags": parse_tags(row[4]), "importance": row[5], - "score": abs(row[6]) if row[6] else 0, + "score": abs(float(row[6])) if row[6] else 0.0, } From 9c3115cdbcfc605e60038ecc670797d2dc1bc924 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Tue, 11 Aug 2026 16:19:37 +0200 Subject: [PATCH 2/4] fix(ci): resolve mypy stubs and ruff noqa warnings --- config.py | 10 +++++----- features/backup.py | 3 --- features/import_export.py | 2 +- features/secrets.py | 3 +-- hooks/loader.py | 2 -- lifecycle/emotion/engine.py | 2 +- mcp_server/middlewares.py | 2 +- mcp_server/tools/episodic.py | 1 - mcp_server/tools/graph.py | 1 - mcp_server/tools/ops.py | 1 - mcp_server/tools/session.py | 1 - pyproject.toml | 2 ++ rag/search.py | 3 ++- shared/saga/impl/base.py | 7 ------- shared/saga/impl/crypto.py | 11 +++-------- 15 files changed, 16 insertions(+), 35 deletions(-) diff --git a/config.py b/config.py index 6690ac59..d2b73cbb 100644 --- a/config.py +++ b/config.py @@ -67,11 +67,11 @@ def is_hook_enabled(self, layer: str, hook: str) -> bool: "wiki_agent", ], } - res: Any = False - if hook in known_hooks.get(layer, []): - res = self.get("hooks", layer, hook, default=True) - else: - res = self.get("hooks", layer, hook, default=False) + res = ( + self.get("hooks", layer, hook, default=True) + if hook in known_hooks.get(layer, []) + else self.get("hooks", layer, hook, default=False) + ) return bool(res) def is_feature_enabled(self, feature: str) -> bool: diff --git a/features/backup.py b/features/backup.py index ad0640de..3e1e6d4b 100644 --- a/features/backup.py +++ b/features/backup.py @@ -47,17 +47,14 @@ async def restore(self, backup_name: str) -> dict[str, Any]: return {"error": f"Backup not found: {backup_name}"} manifest_path = src / "manifest.json" - # noqa: SKY-D325 manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {"files": [f.name for f in src.glob("*.db")]} restored = [] for db_file in manifest.get("files", []): # Whitelisted filenames are safe, plus safe_resolve guard. - # noqa: SKY-D215 safe_resolve(self.base_dir, db_file) # raises ValueError if traversal backup_file = src / db_file if backup_file.exists(): - # noqa: SKY-D215 shutil.copy2(backup_file, self.base_dir / db_file) restored.append(db_file) diff --git a/features/import_export.py b/features/import_export.py index 76b3ca81..8dd6c47a 100644 --- a/features/import_export.py +++ b/features/import_export.py @@ -13,7 +13,7 @@ from shared.connection import AsyncConnectionManager, connection_manager from shared.constants import DB_NAME from shared.path_safety import safe_resolve -from typing import Any, cast +from typing import Any class ImportExport: diff --git a/features/secrets.py b/features/secrets.py index 9016bb82..0f7f7e10 100644 --- a/features/secrets.py +++ b/features/secrets.py @@ -14,7 +14,7 @@ import logging import os from pathlib import Path -from typing import Any, cast +from typing import Any from shared.crypto import decrypt_json as _decrypt_json from shared.crypto import encrypt_json as _encrypt_json @@ -153,7 +153,6 @@ def is_encrypted_blob(path: Path) -> bool: if not path.exists(): return False # Path is verified to be within app data dir by caller, safe. - # noqa: SKY-D325 with path.open("rb") as f: head = f.read(1) return bool(_is_encrypted_blob(head)) diff --git a/hooks/loader.py b/hooks/loader.py index 22255464..431bc2a3 100644 --- a/hooks/loader.py +++ b/hooks/loader.py @@ -5,5 +5,3 @@ def load_all_hooks() -> None: """Import hook modules to trigger @hook_registry.mark decorators.""" - import hooks.user_hooks - import hooks.agent_hooks diff --git a/lifecycle/emotion/engine.py b/lifecycle/emotion/engine.py index 84aabcd3..25b6f4cd 100644 --- a/lifecycle/emotion/engine.py +++ b/lifecycle/emotion/engine.py @@ -1,6 +1,6 @@ from __future__ import annotations import re -from typing import Any, Pattern +from re import Pattern from .models import EmotionMarkerConfig, EmotionResult diff --git a/mcp_server/middlewares.py b/mcp_server/middlewares.py index b797dbd8..c4a6a88a 100644 --- a/mcp_server/middlewares.py +++ b/mcp_server/middlewares.py @@ -1,5 +1,5 @@ import os -from typing import Any, Awaitable, Callable +from collections.abc import Awaitable, Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse, Response diff --git a/mcp_server/tools/episodic.py b/mcp_server/tools/episodic.py index a119eda8..2249e5b6 100644 --- a/mcp_server/tools/episodic.py +++ b/mcp_server/tools/episodic.py @@ -5,7 +5,6 @@ from shared.constants import DB_NAME from shared.metrics import metrics -import mcp_server.tools_layer as tl from .base import _validate_layer, _check_rate_limit, _get_memory, _invalidate_cache, _fire_hook from typing import TYPE_CHECKING, Any diff --git a/mcp_server/tools/graph.py b/mcp_server/tools/graph.py index a751bad2..9806e211 100644 --- a/mcp_server/tools/graph.py +++ b/mcp_server/tools/graph.py @@ -5,7 +5,6 @@ from shared.constants import DB_NAME from shared.metrics import metrics -import mcp_server.tools_layer as tl from .base import _validate_layer, _check_rate_limit, _get_graph, _invalidate_cache, _fire_hook from typing import TYPE_CHECKING, Any diff --git a/mcp_server/tools/ops.py b/mcp_server/tools/ops.py index e1d116e3..39e3e172 100644 --- a/mcp_server/tools/ops.py +++ b/mcp_server/tools/ops.py @@ -17,7 +17,6 @@ from shared.metrics import metrics from shared.constants import DB_NAME -import mcp_server.tools_layer as tl from .base import ( _validate_layer, _get_memory, diff --git a/mcp_server/tools/session.py b/mcp_server/tools/session.py index a3bf1cc2..193e9d9e 100644 --- a/mcp_server/tools/session.py +++ b/mcp_server/tools/session.py @@ -4,7 +4,6 @@ from mcp_server.registry import _get_ctx from shared.metrics import metrics -import mcp_server.tools_layer as tl from .base import _validate_layer, _check_rate_limit, _get_memory, _fire_hook from typing import TYPE_CHECKING, Any diff --git a/pyproject.toml b/pyproject.toml index a97982a7..1fab8493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,8 @@ module = [ "prometheus_client.*", "aiosqlite.*", "alembic.*", + "mcp.*", + "keyring.*", ] ignore_missing_imports = true diff --git a/rag/search.py b/rag/search.py index aff2bc52..858b89ee 100644 --- a/rag/search.py +++ b/rag/search.py @@ -2,7 +2,8 @@ import logging from contextlib import suppress -from typing import Any, Callable +from typing import Any +from collections.abc import Callable from shared.connection import AsyncConnectionManager from shared.constants import DB_NAME diff --git a/shared/saga/impl/base.py b/shared/saga/impl/base.py index 13784386..5eea3021 100644 --- a/shared/saga/impl/base.py +++ b/shared/saga/impl/base.py @@ -35,7 +35,6 @@ def is_encrypted_blob(path: Path) -> bool: """Check if file is encrypted (not plain JSON).""" if not path.exists(): return False - # noqa: SKY-D325 with path.open("rb") as f: head = f.read(1) return _is_crypto_encrypted_blob(head) @@ -137,10 +136,8 @@ def _save_state(self) -> None: SAGA_DIR.mkdir(parents=True, exist_ok=True) if _HAS_ENCRYPTION: blob = encrypt_json(state) - # noqa: SKY-D324 state_file.write_bytes(blob) else: - # noqa: SKY-D324 state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8") except Exception: logger.exception("Failed to save saga state") @@ -450,10 +447,8 @@ def _check_stuck_sagas(self) -> None: state["status"] = "stuck" state["stuck_reason"] = f"timeout_after_{int(age)}s" if _HAS_ENCRYPTION: - # noqa: SKY-D324 state_file.write_bytes(encrypt_json(state)) else: - # noqa: SKY-D324 state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8") logger.warning("Saga '%s' marked as STUCK (age=%ds)", saga_name, int(age)) @@ -501,10 +496,8 @@ def recover_saga(self, saga_id: str) -> dict[str, Any] | None: state["status"] = "manual_review_required" state["recovered_at"] = time.time() if _HAS_ENCRYPTION: - # noqa: SKY-D324 state_file.write_bytes(encrypt_json(state)) else: - # noqa: SKY-D324 state_file.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8") return {"status": "manual_review_required", "state": state} diff --git a/shared/saga/impl/crypto.py b/shared/saga/impl/crypto.py index 040585fd..79e297a1 100644 --- a/shared/saga/impl/crypto.py +++ b/shared/saga/impl/crypto.py @@ -16,12 +16,12 @@ from shared.crypto import is_encrypted_blob as _is_crypto_encrypted_blob __all__ = [ - "encrypt_json", "decrypt_json", - "write_state_atomic", + "encrypt_json", + "is_encrypted_blob", "read_state", "read_state_legacy_or_encrypted", - "is_encrypted_blob", + "write_state_atomic", ] @@ -29,7 +29,6 @@ def is_encrypted_blob(path: Path) -> bool: """Check if file is encrypted (not plain JSON).""" if not path.exists(): return False - # noqa: SKY-D325 with path.open("rb") as f: head = f.read(1) return bool(_is_crypto_encrypted_blob(head)) @@ -49,11 +48,9 @@ def write_state_atomic(path: Path, state: dict[str, Any]) -> None: blob = encrypt_json(state) tmp = path.with_suffix(path.suffix + ".tmp") with tmp.open("wb") as f: - # noqa: SKY-D324 f.write(blob) with contextlib.suppress(OSError, PermissionError): os.chmod(tmp, 0o600) - # noqa: SKY-D324 tmp.replace(path) with contextlib.suppress(OSError, PermissionError): os.chmod(path, 0o600) @@ -63,7 +60,6 @@ def read_state(path: Path) -> dict[str, Any]: """Read encrypted state file.""" if not path.exists(): raise FileNotFoundError(path) - # noqa: SKY-D325 with path.open("rb") as f: blob = f.read() res: Any = decrypt_json(blob) @@ -74,7 +70,6 @@ def read_state_legacy_or_encrypted(path: Path) -> dict[str, Any]: """Backward-compat: reads legacy plain JSON or encrypted, rotates legacy to encrypted.""" if not path.exists(): raise FileNotFoundError(path) - # noqa: SKY-D325 with path.open("rb") as f: blob = f.read() if is_encrypted_blob(path): From 336d9295f7f80b56e6211e2dc572ad7066d8cdf8 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Tue, 11 Aug 2026 16:22:43 +0200 Subject: [PATCH 3/4] fix(ci): suppress S104 warning for 0.0.0.0 binding --- mcp_server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp_server/server.py b/mcp_server/server.py index 3c0115b3..2cf66204 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -43,7 +43,7 @@ def main() -> None: default=STDIO_TRANSPORT, help="Transport: stdio (Claude Desktop) or http (web clients)", ) - parser.add_argument("--host", default="0.0.0.0", help="HTTP host (default: 0.0.0.0)") + parser.add_argument("--host", default="0.0.0.0", help="HTTP host (default: 0.0.0.0)") # noqa: S104 parser.add_argument("--port", type=int, default=8000, help="HTTP port (default: 8000)") parser.add_argument("--dashboard", action="store_true", help="Enable dashboard + metrics endpoints") parser.add_argument("--no-auth", action="store_true", help="Disable auth for development") From 54a18b9d6b619a04c993d508195d5e3bf1dea558 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Tue, 11 Aug 2026 16:25:32 +0200 Subject: [PATCH 4/4] style: apply ruff format --- config.py | 6 +----- rag/conflict.py | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index d2b73cbb..fd51ba64 100644 --- a/config.py +++ b/config.py @@ -67,11 +67,7 @@ def is_hook_enabled(self, layer: str, hook: str) -> bool: "wiki_agent", ], } - res = ( - self.get("hooks", layer, hook, default=True) - if hook in known_hooks.get(layer, []) - else self.get("hooks", layer, hook, default=False) - ) + res = self.get("hooks", layer, hook, default=True) if hook in known_hooks.get(layer, []) else self.get("hooks", layer, hook, default=False) return bool(res) def is_feature_enabled(self, feature: str) -> bool: diff --git a/rag/conflict.py b/rag/conflict.py index 33b257b5..01fd87d9 100644 --- a/rag/conflict.py +++ b/rag/conflict.py @@ -100,7 +100,9 @@ async def check(self, user_id: str, new_content: str, min_similarity: float = 0. existing_id, existing_content, is_conflict, group_id = row similarity = float(self._calculate_similarity(new_content, str(existing_content))) if similarity > min_similarity and str(existing_content) != new_content: - return await self._handle_conflict(conn, int(existing_id), str(existing_content), bool(is_conflict), group_id, new_content, similarity) + return await self._handle_conflict( + conn, int(existing_id), str(existing_content), bool(is_conflict), group_id, new_content, similarity + ) await conn.execute("INSERT INTO memory_conflicts (user_id, content) VALUES (?, ?)", (user_id, new_content)) await conn.commit() @@ -115,7 +117,9 @@ async def _find_potential_conflicts(self, conn: Any, user_id: str, keywords: lis ) return await cur.fetchall() - async def _handle_conflict(self, conn: Any, existing_id: int, existing_content: str, is_conflict: bool, group_id: str | None, new_content: str, similarity: float) -> dict[str, Any]: + async def _handle_conflict( + self, conn: Any, existing_id: int, existing_content: str, is_conflict: bool, group_id: str | None, new_content: str, similarity: float + ) -> dict[str, Any]: gid = str(group_id) if group_id else str(uuid.uuid4()) if not is_conflict: await conn.execute("UPDATE memory_conflicts SET is_conflict=1, conflict_group_id=? WHERE id=?", (gid, existing_id))