From 084af6848b2cfbfb20739dfb269136db83d7ee89 Mon Sep 17 00:00:00 2001 From: alexeybe1kin <210597588+alexeybe1kin@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:21:01 +0300 Subject: [PATCH] Lint in CI with the shared Conker policy Only Pi linted in CI. Adds Pi's ruff.toml policy (per-file allowances for model registration imports and intentional Russian vocabulary), runs pinned ruff in CI, and fixes the findings: import order and unused imports, UTC alias, HTTP errors raised `from None`, zip(strict=True), iterable unpacking, and try/except/pass blocks as contextlib.suppress with their explanations kept as comments. Refs Conker-AI/conker#52 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 ++ ruff.toml | 40 +++++++++++++ services/api/app/core/auth.py | 10 +++- services/api/app/core/db.py | 2 +- services/api/app/core/migrations.py | 1 + services/api/app/main.py | 58 +++++++++---------- services/api/app/models/__init__.py | 20 +++---- services/api/app/models/agent_config.py | 4 +- services/api/app/models/ai_runtime_setting.py | 3 +- services/api/app/models/analysis_object.py | 7 ++- services/api/app/models/audit.py | 8 ++- services/api/app/models/auth_setting.py | 2 +- services/api/app/models/deletion_receipt.py | 7 +-- services/api/app/models/entity.py | 8 ++- services/api/app/models/episode_object.py | 3 +- services/api/app/models/evidence_object.py | 7 ++- services/api/app/models/evidence_source.py | 7 ++- services/api/app/models/memory.py | 10 ++-- services/api/app/models/memory_conflict.py | 3 +- services/api/app/models/memory_revision.py | 3 +- services/api/app/models/object_link.py | 3 +- services/api/app/models/observation.py | 8 ++- services/api/app/models/pattern.py | 8 ++- services/api/app/models/processing_job.py | 3 +- services/api/app/models/session_transcript.py | 8 ++- services/api/app/routes/agent_config.py | 4 +- services/api/app/routes/audit.py | 8 +-- services/api/app/routes/auth_settings.py | 12 ++-- services/api/app/routes/briefing.py | 2 +- services/api/app/routes/entity.py | 34 ++++++----- services/api/app/routes/evidence.py | 11 ++-- services/api/app/routes/lineage.py | 5 +- services/api/app/routes/memory.py | 57 +++++++++--------- services/api/app/routes/observation.py | 38 ++++++------ services/api/app/routes/pattern.py | 33 ++++++----- services/api/app/routes/runtime.py | 31 +++++----- services/api/app/routes/skills.py | 9 +-- services/api/app/routes/system.py | 17 ++++-- services/api/app/routes/transcript.py | 7 ++- services/api/app/schemas/entity.py | 4 +- services/api/app/schemas/explorer.py | 3 +- services/api/app/schemas/memory.py | 1 + services/api/app/schemas/observation.py | 1 + services/api/app/schemas/pattern.py | 4 +- services/api/app/schemas/runtime.py | 1 + services/api/app/schemas/transcript.py | 1 + .../api/app/services/ai_runtime_service.py | 8 +-- .../api/app/services/auth_settings_service.py | 7 ++- services/api/app/services/backup_service.py | 7 ++- services/api/app/services/briefing.py | 9 +-- .../api/app/services/conversation_memory.py | 6 +- .../api/app/services/deletion_recovery.py | 12 ++-- services/api/app/services/entity_dedup.py | 15 +++-- services/api/app/services/memory_explorer.py | 5 +- .../api/app/services/memory_forgetting.py | 2 +- .../api/app/services/memory_reset_service.py | 7 +-- services/api/app/services/memory_truth.py | 7 ++- .../api/app/services/observation_lifecycle.py | 9 +-- services/api/app/services/ollama_service.py | 1 + .../api/app/services/pattern_promotion.py | 11 ++-- .../api/app/services/processing_worker.py | 12 ++-- services/api/app/services/qdrant_store.py | 5 +- services/api/app/services/runtime_pipeline.py | 15 ++--- services/api/app/services/signal_filter.py | 4 +- services/api/tests/test_audit_metrics.py | 9 ++- services/api/tests/test_audit_safety.py | 11 ++-- .../test_auth_and_destructive_actions.py | 11 ++-- .../api/tests/test_conversation_memory.py | 4 +- services/api/tests/test_degraded_retrieval.py | 9 ++- services/api/tests/test_deletion_receipts.py | 2 +- services/api/tests/test_deletion_recovery.py | 15 ++--- services/api/tests/test_memory_explorer.py | 16 ++--- services/api/tests/test_memory_revisions.py | 13 +++-- services/api/tests/test_scoped_context.py | 9 +-- .../test_security_and_runtime_contracts.py | 11 +++- services/api/tests/test_skills.py | 10 ++-- services/mcp/memorygate_mcp.py | 2 - 77 files changed, 456 insertions(+), 330 deletions(-) create mode 100644 ruff.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcbce24..5ca1c87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: pip install --quiet -r services/api/requirements.txt pip install --quiet pytest httpx + - name: Lint + # Same pinned ruff and policy as Pi (ruff.toml). + run: | + pip install --quiet ruff==0.16.6 + python -m ruff check . + - name: The module contract requires these files to exist run: | missing="" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..c5d3676 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,40 @@ +# Lint settings for MemoryGate; the same policy as Pi's, shared across Conker modules. +# +# Explicit rather than default, because the defaults move between ruff releases +# and a lint set that changes under you produces diffs nobody asked for. Pinned +# in requirements-dev.txt for the same reason. + +line-length = 100 +target-version = "py311" + +[lint] +select = [ + "E", "W", # pycodestyle + "F", # pyflakes + "I", # import order + "UP", # modern syntax for the target version + "B", # bugbear + "SIM", # obvious simplifications + "ISC", # implicit string concatenation, which hides missing commas + "RUF", +] +ignore = [ + # Health checks and status endpoints catch broadly on purpose. This service + # reports `unavailable` with a reason when a dependency misbehaves; a check + # that raised instead would take down the very endpoint the owner uses to + # find out what is wrong. See the truthful-status rule in CLAUDE.md. + "BLE001", + # Line length is left to review until this module adopts `ruff format`. + "E501", +] + +[lint.per-file-ignores] +# Test doubles subclass stdlib handlers whose method names are fixed by the +# stdlib, and hold shared state as class attributes on purpose. +"tests/*" = ["N802", "RUF012", "F811"] # F811: pytest fixtures are imported, then requested by name +# Test doubles, fixture imports, and multilingual (Russian) sample text on purpose. +"services/api/tests/*" = ["N802", "RUF012", "F811", "RUF001"] +# Importing every model registers its table for create_all. +"services/api/app/models/__init__.py" = ["F401"] +# Russian vocabulary for multilingual signal detection (ADR-0004), not look-alike letters. +"services/api/app/services/signal_filter.py" = ["RUF001"] diff --git a/services/api/app/core/auth.py b/services/api/app/core/auth.py index 029df19..193aebe 100644 --- a/services/api/app/core/auth.py +++ b/services/api/app/core/auth.py @@ -1,6 +1,12 @@ -from fastapi import Header, HTTPException, Request from app.core.db import SessionLocal -from app.services.auth_settings_service import clear_failed_attempts, get_lockout_status, register_failed_attempt, verify_admin_key, verify_agent_access_key +from app.services.auth_settings_service import ( + clear_failed_attempts, + get_lockout_status, + register_failed_attempt, + verify_admin_key, + verify_agent_access_key, +) +from fastapi import Header, HTTPException, Request def require_key(request: Request, x_memorygate_key: str | None = Header(None, alias="X-MemoryGate-Key")) -> str: diff --git a/services/api/app/core/db.py b/services/api/app/core/db.py index 717a08b..88dd0d5 100644 --- a/services/api/app/core/db.py +++ b/services/api/app/core/db.py @@ -1,6 +1,6 @@ +from app.core.config import DATABASE_URL from sqlalchemy import create_engine, text from sqlalchemy.orm import DeclarativeBase, sessionmaker -from app.core.config import DATABASE_URL engine = create_engine(DATABASE_URL, echo=False, pool_pre_ping=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) diff --git a/services/api/app/core/migrations.py b/services/api/app/core/migrations.py index 8c324e0..837d1de 100644 --- a/services/api/app/core/migrations.py +++ b/services/api/app/core/migrations.py @@ -9,6 +9,7 @@ """ import json import uuid + from sqlalchemy import text from sqlalchemy.engine import Engine diff --git a/services/api/app/main.py b/services/api/app/main.py index 2de6f53..db0ee0c 100644 --- a/services/api/app/main.py +++ b/services/api/app/main.py @@ -1,46 +1,46 @@ import logging import os import time -from datetime import datetime, timezone +from datetime import UTC, datetime -from fastapi import FastAPI, Depends -from fastapi.middleware.cors import CORSMiddleware +from app.core.auth import require_key from app.core.config import CORS_ANY_ORIGIN, CORS_ORIGINS from app.core.db import Base, database_health, engine from app.core.migrations import run_migrations -from app.core.auth import require_key -from app.routes.memory import router as memory_router -from app.routes.audit import router as audit_router -from app.routes.entity import router as entity_router -from app.routes.observation import router as observation_router -from app.routes.pattern import router as pattern_router +from app.models import ( + memory_forget, # noqa: F401 - registers the table for create_all + ) from app.routes.agent_config import router as agent_config_router -from app.routes.briefing import router as briefing_router -from app.routes.transcript import router as transcript_router +from app.routes.audit import router as audit_router from app.routes.auth_settings import router as auth_settings_router +from app.routes.briefing import router as briefing_router +from app.routes.conversation import router as conversation_router +from app.routes.corrections import router as corrections_router +from app.routes.entity import router as entity_router from app.routes.evidence import router as evidence_router from app.routes.lineage import router as lineage_router +from app.routes.memory import router as memory_router +from app.routes.observation import router as observation_router +from app.routes.pattern import router as pattern_router from app.routes.runtime import router as runtime_router -from app.routes.conversation import router as conversation_router -from app.routes.corrections import router as corrections_router -from app.routes.system import router as system_router from app.routes.skills import context_router as skills_context_router from app.routes.skills import router as skills_router -from app.models import memory, audit, agent_config -from app.models import memory_forget # noqa: F401 - registers the table for create_all -from app.models import auth_setting -from app.models import evidence_source, evidence_object, analysis_object -from app.models import episode_object, object_link -from app.models import processing_job -from app.models import entity -from app.models import observation -from app.models import pattern -from app.models import session_transcript -from app.models import ai_runtime_setting -from app.services.qdrant_store import ensure_qdrant_collection, ensure_observation_collection, ensure_entity_collection, qdrant_health +from app.routes.system import router as system_router +from app.routes.transcript import router as transcript_router +from app.services.auth_settings_service import ( + assert_admin_key_configured, + ensure_bootstrap_agent_access_key, +) from app.services.embeddings import embedding_health from app.services.processing_worker import start_worker, stop_worker -from app.services.auth_settings_service import assert_admin_key_configured, ensure_bootstrap_agent_access_key +from app.services.qdrant_store import ( + ensure_entity_collection, + ensure_observation_collection, + ensure_qdrant_collection, + qdrant_health, +) +from fastapi import Depends, FastAPI +from fastapi.middleware.cors import CORSMiddleware log = logging.getLogger("memorygate") @@ -70,8 +70,8 @@ @app.on_event("startup") def startup(): Base.metadata.create_all(bind=engine) - from app.services.deletion_recovery import assert_not_held from app.core.db import SessionLocal + from app.services.deletion_recovery import assert_not_held with SessionLocal() as recovery_db: assert_not_held(recovery_db) run_migrations(engine) @@ -138,7 +138,7 @@ def health(): "status": "degraded" if degraded else "ok", "degraded": degraded, "checks": checks, - "checked_at": datetime.now(timezone.utc).isoformat(), + "checked_at": datetime.now(UTC).isoformat(), } _health_cache["result"] = result _health_cache["checked_at"] = now diff --git a/services/api/app/models/__init__.py b/services/api/app/models/__init__.py index 136ad4a..3108c6a 100644 --- a/services/api/app/models/__init__.py +++ b/services/api/app/models/__init__.py @@ -1,15 +1,15 @@ -from .memory import Memory -from .deletion_receipt import DeletionReceipt -from .audit import MemoryAudit +from .agent_access_key import AgentAccessKey from .agent_config import AgentConfig -from .auth_setting import AuthSetting -from .evidence_source import EvidenceSource -from .evidence_object import EvidenceObject +from .ai_runtime_setting import AiRuntimeSetting from .analysis_object import AnalysisObject +from .audit import MemoryAudit +from .auth_setting import AuthSetting +from .deletion_receipt import DeletionReceipt from .episode_object import EpisodeObject +from .evidence_object import EvidenceObject +from .evidence_source import EvidenceSource +from .memory import Memory +from .memory_conflict import MemoryConflict +from .memory_revision import MemoryRevision from .object_link import ObjectLink from .processing_job import ProcessingJob -from .agent_access_key import AgentAccessKey -from .memory_revision import MemoryRevision -from .memory_conflict import MemoryConflict -from .ai_runtime_setting import AiRuntimeSetting diff --git a/services/api/app/models/agent_config.py b/services/api/app/models/agent_config.py index e3ec3dd..b66282e 100644 --- a/services/api/app/models/agent_config.py +++ b/services/api/app/models/agent_config.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, Float, Integer, Boolean, DateTime, func -from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base +from sqlalchemy import Boolean, DateTime, Float, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column class AgentConfig(Base): diff --git a/services/api/app/models/ai_runtime_setting.py b/services/api/app/models/ai_runtime_setting.py index 3ab2d4b..ca78317 100644 --- a/services/api/app/models/ai_runtime_setting.py +++ b/services/api/app/models/ai_runtime_setting.py @@ -1,10 +1,9 @@ from datetime import datetime +from app.core.db import Base from sqlalchemy import DateTime, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base - class AiRuntimeSetting(Base): """Singleton model-provider configuration; API keys remain encrypted at rest.""" diff --git a/services/api/app/models/analysis_object.py b/services/api/app/models/analysis_object.py index ea5b350..b7dbf11 100644 --- a/services/api/app/models/analysis_object.py +++ b/services/api/app/models/analysis_object.py @@ -1,8 +1,9 @@ -from sqlalchemy import String, Text, DateTime, Float, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import DateTime, Float, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + class AnalysisObject(Base): __tablename__ = "analysis_objects" diff --git a/services/api/app/models/audit.py b/services/api/app/models/audit.py index f05c6f7..fff0b6a 100644 --- a/services/api/app/models/audit.py +++ b/services/api/app/models/audit.py @@ -1,8 +1,10 @@ -from sqlalchemy import String, Text, DateTime, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import DateTime, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class MemoryAudit(Base): __tablename__ = "memory_audit" diff --git a/services/api/app/models/auth_setting.py b/services/api/app/models/auth_setting.py index f4185fe..46762e1 100644 --- a/services/api/app/models/auth_setting.py +++ b/services/api/app/models/auth_setting.py @@ -1,6 +1,6 @@ +from app.core.db import Base from sqlalchemy import String, Text from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class AuthSetting(Base): diff --git a/services/api/app/models/deletion_receipt.py b/services/api/app/models/deletion_receipt.py index f19be10..e16af82 100644 --- a/services/api/app/models/deletion_receipt.py +++ b/services/api/app/models/deletion_receipt.py @@ -1,11 +1,10 @@ """Content-free deletion identities retained for recovery replay.""" -from datetime import datetime, timezone +from datetime import UTC, datetime +from app.core.db import Base from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base - class DeletionReceipt(Base): __tablename__ = "deletion_receipts" @@ -13,7 +12,7 @@ class DeletionReceipt(Base): object_kind: Mapped[str] = mapped_column(String, primary_key=True) object_id: Mapped[str] = mapped_column(String, primary_key=True) deleted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), - default=lambda: datetime.now(timezone.utc)) + default=lambda: datetime.now(UTC)) class RecoveryHold(Base): diff --git a/services/api/app/models/entity.py b/services/api/app/models/entity.py index c8092ce..c0664be 100644 --- a/services/api/app/models/entity.py +++ b/services/api/app/models/entity.py @@ -1,8 +1,10 @@ -from sqlalchemy import String, Text, DateTime, Float, Boolean, Integer, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import DateTime, Float, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class Entity(Base): __tablename__ = "entities" diff --git a/services/api/app/models/episode_object.py b/services/api/app/models/episode_object.py index 4f976dd..86a3e34 100644 --- a/services/api/app/models/episode_object.py +++ b/services/api/app/models/episode_object.py @@ -1,7 +1,8 @@ import uuid + +from app.core.db import Base from sqlalchemy import DateTime, Float, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class EpisodeObject(Base): diff --git a/services/api/app/models/evidence_object.py b/services/api/app/models/evidence_object.py index fe7a5f1..2c6a8ab 100644 --- a/services/api/app/models/evidence_object.py +++ b/services/api/app/models/evidence_object.py @@ -1,8 +1,9 @@ -from sqlalchemy import String, Text, DateTime, Float, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import DateTime, Float, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + class EvidenceObject(Base): __tablename__ = "evidence_objects" diff --git a/services/api/app/models/evidence_source.py b/services/api/app/models/evidence_source.py index 4a839c4..56f669d 100644 --- a/services/api/app/models/evidence_source.py +++ b/services/api/app/models/evidence_source.py @@ -1,8 +1,9 @@ -from sqlalchemy import String, Text, DateTime, Boolean, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import Boolean, DateTime, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + class EvidenceSource(Base): __tablename__ = "evidence_sources" diff --git a/services/api/app/models/memory.py b/services/api/app/models/memory.py index d5cd7fd..7bb2066 100644 --- a/services/api/app/models/memory.py +++ b/services/api/app/models/memory.py @@ -1,15 +1,17 @@ -from sqlalchemy import String, Text, DateTime, Boolean, Integer, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import Boolean, DateTime, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class Memory(Base): __tablename__ = "memories" id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4())) agent_id: Mapped[str] = mapped_column(String, default="default", index=True) revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1") - __mapper_args__ = {"version_id_col": revision} + __mapper_args__ = {"version_id_col": revision} # noqa: RUF012 - SQLAlchemy reads this class attribute text: Mapped[str] = mapped_column(Text) summary: Mapped[str] = mapped_column(Text, default="") diff --git a/services/api/app/models/memory_conflict.py b/services/api/app/models/memory_conflict.py index f3958bb..1029a7e 100644 --- a/services/api/app/models/memory_conflict.py +++ b/services/api/app/models/memory_conflict.py @@ -1,7 +1,8 @@ import uuid + +from app.core.db import Base from sqlalchemy import DateTime, Float, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class MemoryConflict(Base): diff --git a/services/api/app/models/memory_revision.py b/services/api/app/models/memory_revision.py index cbfc987..2964d3d 100644 --- a/services/api/app/models/memory_revision.py +++ b/services/api/app/models/memory_revision.py @@ -1,7 +1,8 @@ import uuid + +from app.core.db import Base from sqlalchemy import DateTime, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class MemoryRevision(Base): diff --git a/services/api/app/models/object_link.py b/services/api/app/models/object_link.py index 3b25d10..f9eeabb 100644 --- a/services/api/app/models/object_link.py +++ b/services/api/app/models/object_link.py @@ -1,7 +1,8 @@ import uuid + +from app.core.db import Base from sqlalchemy import DateTime, Float, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class ObjectLink(Base): diff --git a/services/api/app/models/observation.py b/services/api/app/models/observation.py index 83bb1d0..2aea047 100644 --- a/services/api/app/models/observation.py +++ b/services/api/app/models/observation.py @@ -1,8 +1,10 @@ -from sqlalchemy import String, Text, DateTime, Float, Integer, Boolean, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class Observation(Base): __tablename__ = "observations" diff --git a/services/api/app/models/pattern.py b/services/api/app/models/pattern.py index 41e2553..00bc6b0 100644 --- a/services/api/app/models/pattern.py +++ b/services/api/app/models/pattern.py @@ -1,8 +1,10 @@ -from sqlalchemy import String, Text, DateTime, Float, Integer, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import DateTime, Float, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class Pattern(Base): __tablename__ = "patterns" diff --git a/services/api/app/models/processing_job.py b/services/api/app/models/processing_job.py index 2e69bd2..4c1715c 100644 --- a/services/api/app/models/processing_job.py +++ b/services/api/app/models/processing_job.py @@ -1,7 +1,8 @@ import uuid + +from app.core.db import Base from sqlalchemy import DateTime, Integer, String, Text, func from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base class ProcessingJob(Base): diff --git a/services/api/app/models/session_transcript.py b/services/api/app/models/session_transcript.py index 284abb7..8ee54e6 100644 --- a/services/api/app/models/session_transcript.py +++ b/services/api/app/models/session_transcript.py @@ -1,8 +1,10 @@ -from sqlalchemy import String, Text, DateTime, Integer, Boolean, func -from sqlalchemy.orm import Mapped, mapped_column -from app.core.db import Base import uuid +from app.core.db import Base +from sqlalchemy import Boolean, DateTime, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + + class SessionTranscript(Base): """The 'remember everything' layer - full session transcripts, stored verbatim and never touched by the signal filter or deleted by anything diff --git a/services/api/app/routes/agent_config.py b/services/api/app/routes/agent_config.py index 7192bb6..7de622c 100644 --- a/services/api/app/routes/agent_config.py +++ b/services/api/app/routes/agent_config.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter from app.core.db import SessionLocal from app.schemas.agent_config import AgentConfigUpdateRequest -from app.services.agent_config_service import get_or_create_config, config_to_dict +from app.services.agent_config_service import config_to_dict, get_or_create_config +from fastapi import APIRouter router = APIRouter(prefix="/config", tags=["config"]) diff --git a/services/api/app/routes/audit.py b/services/api/app/routes/audit.py index 412cb7e..eb841e1 100644 --- a/services/api/app/routes/audit.py +++ b/services/api/app/routes/audit.py @@ -1,9 +1,9 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta -from fastapi import APIRouter -from sqlalchemy import select from app.core.db import SessionLocal from app.models.audit import MemoryAudit +from fastapi import APIRouter +from sqlalchemy import select router = APIRouter(prefix="/audit", tags=["audit"]) @@ -35,7 +35,7 @@ def list_audit(): @router.get("/metrics") def audit_metrics(hours: int = 24): bounded_hours = max(1, min(int(hours or 24), 168)) - cutoff = datetime.now(timezone.utc) - timedelta(hours=bounded_hours) + cutoff = datetime.now(UTC) - timedelta(hours=bounded_hours) db = SessionLocal() try: rows = db.execute( diff --git a/services/api/app/routes/auth_settings.py b/services/api/app/routes/auth_settings.py index 8b149a2..9f80200 100644 --- a/services/api/app/routes/auth_settings.py +++ b/services/api/app/routes/auth_settings.py @@ -1,8 +1,13 @@ -from fastapi import APIRouter, HTTPException, Request from app.core.db import SessionLocal -from app.schemas.auth_settings import AdminKeyRotateRequest, AdminKeyUpdateRequest, AgentKeyCreateRequest +from app.models.agent_access_key import AgentAccessKey +from app.schemas.auth_settings import ( + AdminKeyRotateRequest, + AdminKeyUpdateRequest, + AgentKeyCreateRequest, +) from app.services.auth_settings_service import ( clear_failed_attempts, + create_agent_access_key, generate_admin_key, get_auth_state, get_lockout_status, @@ -10,9 +15,8 @@ set_admin_key, validate_new_admin_key, verify_admin_key, - create_agent_access_key, ) -from app.models.agent_access_key import AgentAccessKey +from fastapi import APIRouter, HTTPException, Request router = APIRouter(prefix="/auth", tags=["auth"]) diff --git a/services/api/app/routes/briefing.py b/services/api/app/routes/briefing.py index 6dc38ee..9e4e4b3 100644 --- a/services/api/app/routes/briefing.py +++ b/services/api/app/routes/briefing.py @@ -1,6 +1,6 @@ -from fastapi import APIRouter from app.core.db import SessionLocal from app.services.briefing import build_briefing +from fastapi import APIRouter router = APIRouter(prefix="/briefing", tags=["briefing"]) diff --git a/services/api/app/routes/entity.py b/services/api/app/routes/entity.py index 9c0ab37..8c53e92 100644 --- a/services/api/app/routes/entity.py +++ b/services/api/app/routes/entity.py @@ -1,20 +1,26 @@ +import contextlib import json -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy import select, or_ -from app.core.db import SessionLocal + from app.core.agent import get_agent_id, resolve_agent_id +from app.core.db import SessionLocal from app.models.entity import Entity, EntityEdge, EntityEvent, EntityHistory from app.schemas.entity import ( EntityCreateRequest, - EntityUpdateRequest, - EntitySearchRequest, - EntityLinkRequest, EntityEventCreateRequest, - EntityUpdateByIdRequest, + EntityLinkRequest, EntityMergeRequest, + EntitySearchRequest, + EntityUpdateByIdRequest, + EntityUpdateRequest, ) from app.services.entity_dedup import find_duplicate, merge_entities -from app.services.qdrant_store import index_after_commit, upsert_entity_embedding, delete_entity_embedding +from app.services.qdrant_store import ( + delete_entity_embedding, + index_after_commit, + upsert_entity_embedding, +) +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import or_, select router = APIRouter(prefix="/entity", tags=["entity"]) @@ -154,13 +160,11 @@ def delete_entity(entity_id: str, agent_id: str = Depends(get_agent_id)): record(db, agent_id, "entity", row.id) db.delete(row) db.commit() - try: + # Postgres (the source of truth) already committed the delete - + # a stale/malformed Qdrant point shouldn't turn a successful + # delete into a 500. + with contextlib.suppress(Exception): delete_entity_embedding(entity_id) - except Exception: - # Postgres (the source of truth) already committed the delete - - # a stale/malformed Qdrant point shouldn't turn a successful - # delete into a 500. - pass return {"status": "ok"} finally: db.close() @@ -256,7 +260,7 @@ def merge_entity(payload: EntityMergeRequest, agent_id: str = Depends(get_agent_ try: keep = merge_entities(db, agent_id, payload.keep_entity_id, payload.merge_entity_id) except ValueError as exc: - raise HTTPException(404, str(exc)) + raise HTTPException(404, str(exc)) from None db.add(EntityHistory( entity_id=keep.id, diff --git a/services/api/app/routes/evidence.py b/services/api/app/routes/evidence.py index 6778997..aefae65 100644 --- a/services/api/app/routes/evidence.py +++ b/services/api/app/routes/evidence.py @@ -1,14 +1,15 @@ import json import secrets from datetime import datetime -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy import select + +from app.core.agent import get_agent_id from app.core.db import SessionLocal -from app.models.evidence_source import EvidenceSource -from app.models.evidence_object import EvidenceObject from app.models.analysis_object import AnalysisObject +from app.models.evidence_object import EvidenceObject +from app.models.evidence_source import EvidenceSource from app.schemas.evidence import EvidenceObjectCreateRequest, EvidenceSourceUpsertRequest -from app.core.agent import get_agent_id +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select router = APIRouter(prefix="/evidence", tags=["evidence"]) diff --git a/services/api/app/routes/lineage.py b/services/api/app/routes/lineage.py index 7ec2178..3dac3e1 100644 --- a/services/api/app/routes/lineage.py +++ b/services/api/app/routes/lineage.py @@ -1,7 +1,6 @@ import json from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy import or_, select + from app.core.agent import get_agent_id, resolve_agent_id from app.core.db import SessionLocal from app.models.analysis_object import AnalysisObject @@ -14,6 +13,8 @@ from app.models.pattern import Pattern from app.models.session_transcript import SessionTranscript from app.schemas.lineage import EpisodeCreateRequest, EpisodeUpdateRequest, ObjectLinkCreateRequest +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import or_, select router = APIRouter(prefix="/lineage", tags=["lineage"]) diff --git a/services/api/app/routes/memory.py b/services/api/app/routes/memory.py index 4edbd13..bc0b1a6 100644 --- a/services/api/app/routes/memory.py +++ b/services/api/app/routes/memory.py @@ -1,29 +1,36 @@ +import contextlib import json -from datetime import datetime, timedelta, timezone -from fastapi import APIRouter, HTTPException, Depends, Query -from sqlalchemy import select -from sqlalchemy.orm.exc import StaleDataError -from app.core.db import SessionLocal +from datetime import UTC, datetime, timedelta + from app.core.agent import get_agent_id, resolve_agent_id -from app.models.memory import Memory +from app.core.db import SessionLocal from app.models.audit import MemoryAudit -from app.models.memory_revision import MemoryRevision +from app.models.memory import Memory from app.models.memory_conflict import MemoryConflict -from app.schemas.memory import MemoryWriteRequest, MemorySearchRequest, MemoryPatchRequest, ConflictResolveRequest -from app.services.classifier import classify_memory, normalize_memory_type, CURRENT_MEMORY_TYPES -from app.services.signal_filter import score_value, novelty_bucket, NOVELTY_DUPLICATE, NOVELTY_LOW +from app.models.memory_revision import MemoryRevision +from app.schemas.memory import ( + ConflictResolveRequest, + MemoryPatchRequest, + MemorySearchRequest, + MemoryWriteRequest, +) from app.services.agent_config_service import get_or_create_config +from app.services.classifier import CURRENT_MEMORY_TYPES, classify_memory, normalize_memory_type +from app.services.memory_truth import add_revision, detect_conflicts from app.services.qdrant_store import ( INDEX_UNREACHABLE, + delete_memory_embedding, + find_near_duplicate, index_after_commit, - upsert_memory_embedding, search_memory_embeddings, - find_near_duplicate, - delete_memory_embedding, semantic_status, + upsert_memory_embedding, ) from app.services.scoring import memory_rank_bonus, memory_strength -from app.services.memory_truth import add_revision, detect_conflicts +from app.services.signal_filter import NOVELTY_DUPLICATE, NOVELTY_LOW, novelty_bucket, score_value +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import select +from sqlalchemy.orm.exc import StaleDataError router = APIRouter(prefix="/memory", tags=["memory"]) @@ -38,7 +45,7 @@ def _default_review_by(memory_type: str, review_by: datetime | None) -> datetime if review_by is not None: return review_by if memory_type == "phase": - return datetime.now(timezone.utc) + PHASE_REVIEW_WINDOW + return datetime.now(UTC) + PHASE_REVIEW_WINDOW return None def _row_to_dict(row, score=None): @@ -120,11 +127,11 @@ def resolve_conflict(conflict_id: str, payload: ConflictResolveRequest, agent_id add_revision(db, winner, "selected during conflict resolution", "user") if loser: loser.status = "needs_review" - loser.valid_until = datetime.now(timezone.utc) + loser.valid_until = datetime.now(UTC) add_revision(db, loser, "not selected during conflict resolution", "user") conflict.status = "resolved" conflict.resolved_by = "user" - conflict.resolved_at = datetime.now(timezone.utc) + conflict.resolved_at = datetime.now(UTC) db.commit() return {"status": "ok", "winner_memory_id": payload.winner_memory_id} finally: @@ -285,7 +292,7 @@ def write_memory(payload: MemoryWriteRequest, header_agent_id: str = Depends(get do_not_generalize=final_do_not_generalize, review_by=final_review_by, tags_json=json.dumps(payload.tags), - valid_from=datetime.now(timezone.utc), + valid_from=datetime.now(UTC), ) db.add(memory) db.commit() @@ -484,7 +491,7 @@ def patch_memory(memory_id: str, payload: MemoryPatchRequest, agent_id: str = De return result except StaleDataError: db.rollback() - raise HTTPException(409, {"code": "revision_conflict", "message": "Memory changed; reload before editing."}) + raise HTTPException(409, {"code": "revision_conflict", "message": "Memory changed; reload before editing."}) from None finally: db.close() @@ -503,17 +510,15 @@ def delete_memory(memory_id: str, agent_id: str = Depends(get_agent_id), erase(db, agent_id, row, "delete", {"revision": row.revision}) db.commit() - try: + # Postgres (the source of truth) already committed the delete - + # a stale/malformed Qdrant point shouldn't turn a successful + # delete into a 500. + with contextlib.suppress(Exception): delete_memory_embedding(memory_id) - except Exception: - # Postgres (the source of truth) already committed the delete - - # a stale/malformed Qdrant point shouldn't turn a successful - # delete into a 500. - pass return {"status": "ok"} except StaleDataError: db.rollback() - raise HTTPException(409, {"code": "revision_conflict", "message": "Memory changed; reload before deleting."}) + raise HTTPException(409, {"code": "revision_conflict", "message": "Memory changed; reload before deleting."}) from None finally: db.close() diff --git a/services/api/app/routes/observation.py b/services/api/app/routes/observation.py index 9af201c..7132b9d 100644 --- a/services/api/app/routes/observation.py +++ b/services/api/app/routes/observation.py @@ -1,23 +1,29 @@ +import contextlib import json -from datetime import datetime, timezone -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy import select, and_ -from app.core.db import SessionLocal +from datetime import UTC, datetime + from app.core.agent import get_agent_id, resolve_agent_id +from app.core.db import SessionLocal from app.models.observation import Observation from app.schemas.observation import ( - ObservationCreateRequest, - ObservationSearchRequest, - ObservationUpdateRequest, + ObservationArchiveRequest, ObservationConfirmRequest, ObservationContradictRequest, - ObservationArchiveRequest, + ObservationCreateRequest, + ObservationSearchRequest, ObservationSessionContextRequest, + ObservationUpdateRequest, ) from app.services.agent_config_service import get_or_create_config -from app.services.observation_lifecycle import find_duplicate, enforce_budget, apply_session_context +from app.services.observation_lifecycle import apply_session_context, enforce_budget, find_duplicate from app.services.pattern_promotion import promote_from_observations -from app.services.qdrant_store import index_after_commit, upsert_observation_embedding, delete_observation_embedding +from app.services.qdrant_store import ( + delete_observation_embedding, + index_after_commit, + upsert_observation_embedding, +) +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import and_, select router = APIRouter(prefix="/observation", tags=["observation"]) @@ -182,13 +188,11 @@ def delete_observation(observation_id: str, agent_id: str = Depends(get_agent_id record(db, agent_id, "observation", row.id) db.delete(row) db.commit() - try: + # Postgres (the source of truth) already committed the delete - + # a stale/malformed Qdrant point shouldn't turn a successful + # delete into a 500. + with contextlib.suppress(Exception): delete_observation_embedding(observation_id) - except Exception: - # Postgres (the source of truth) already committed the delete - - # a stale/malformed Qdrant point shouldn't turn a successful - # delete into a 500. - pass return {"status": "ok"} finally: db.close() @@ -267,7 +271,7 @@ def archive_observation(observation_id: str, payload: ObservationArchiveRequest, try: row = _get_owned_observation(db, observation_id, agent_id) row.status = "archived" - row.archived_at = datetime.now(timezone.utc) + row.archived_at = datetime.now(UTC) row.archive_reason = payload.reason db.commit() db.refresh(row) diff --git a/services/api/app/routes/pattern.py b/services/api/app/routes/pattern.py index 125f2c6..4c89e50 100644 --- a/services/api/app/routes/pattern.py +++ b/services/api/app/routes/pattern.py @@ -1,20 +1,25 @@ import json -from datetime import datetime, timezone -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy import select -from app.core.db import SessionLocal +from datetime import UTC, datetime + from app.core.agent import get_agent_id, resolve_agent_id -from app.models.pattern import Pattern +from app.core.db import SessionLocal from app.models.observation import Observation +from app.models.pattern import Pattern from app.schemas.pattern import ( + PatternConfirmRequest, + PatternContradictRequest, PatternCreateRequest, + PatternPromoteRequest, PatternSearchRequest, PatternUpdateRequest, - PatternPromoteRequest, - PatternConfirmRequest, - PatternContradictRequest, ) -from app.services.pattern_promotion import maybe_promote, clamp_confidence, DEPRECATE_AT_CONTRADICTIONS +from app.services.pattern_promotion import ( + DEPRECATE_AT_CONTRADICTIONS, + clamp_confidence, + maybe_promote, +) +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select router = APIRouter(prefix="/pattern", tags=["pattern"]) @@ -65,7 +70,7 @@ def create_pattern(payload: PatternCreateRequest, header_agent_id: str = Depends applies_to_entity_ids_json=json.dumps(payload.applies_to_entity_ids), context_conditions_json=json.dumps(payload.context_conditions), status=payload.status, - promoted_at=datetime.now(timezone.utc) if payload.status == "active" else None, + promoted_at=datetime.now(UTC) if payload.status == "active" else None, ) db.add(row) db.commit() @@ -169,7 +174,7 @@ def update_pattern(pattern_id: str, payload: PatternUpdateRequest, agent_id: str if payload.status is not None: row.status = payload.status if payload.status == "active" and row.promoted_at is None: - row.promoted_at = datetime.now(timezone.utc) + row.promoted_at = datetime.now(UTC) db.commit() db.refresh(row) @@ -183,7 +188,7 @@ def confirm_pattern(pattern_id: str, payload: PatternConfirmRequest, agent_id: s try: row = _get_owned_pattern(db, pattern_id, agent_id) row.confirmation_count += 1 - row.last_confirmed_at = datetime.now(timezone.utc) + row.last_confirmed_at = datetime.now(UTC) maybe_promote(row) db.commit() db.refresh(row) @@ -264,8 +269,8 @@ def promote_pattern(payload: PatternPromoteRequest, header_agent_id: str = Depen applies_to_entity_ids_json=json.dumps(applies_to_entity_ids), context_conditions_json=json.dumps({}), status="candidate" if confidence < 0.85 else "active", - promoted_at=datetime.now(timezone.utc) if confidence >= 0.85 else None, - last_confirmed_at=datetime.now(timezone.utc), + promoted_at=datetime.now(UTC) if confidence >= 0.85 else None, + last_confirmed_at=datetime.now(UTC), ) db.add(pattern) db.commit() diff --git a/services/api/app/routes/runtime.py b/services/api/app/routes/runtime.py index 6c3704b..4d5ad8d 100644 --- a/services/api/app/routes/runtime.py +++ b/services/api/app/routes/runtime.py @@ -1,35 +1,40 @@ -import json import hmac -from datetime import datetime, timezone -from fastapi import APIRouter, Depends, HTTPException, Header, Request -from sqlalchemy import func, or_, select +import json +from datetime import UTC, datetime + from app.core.agent import get_agent_id, resolve_agent_id from app.core.auth import require_key, require_read_key -from app.services.auth_settings_service import clear_failed_attempts, get_lockout_status, register_failed_attempt from app.core.db import SessionLocal +from app.models.audit import MemoryAudit +from app.models.conversation_receipt import ConversationReceipt from app.models.entity import Entity from app.models.episode_object import EpisodeObject from app.models.evidence_object import EvidenceObject from app.models.evidence_source import EvidenceSource -from app.models.audit import MemoryAudit from app.models.memory import Memory -from app.models.conversation_receipt import ConversationReceipt from app.models.object_link import ObjectLink from app.models.processing_job import ProcessingJob -from app.schemas.runtime import AgentContextRequest, IngestEventRequest, MemoryQuestionRequest from app.schemas.explorer import ExploreRequest, LibraryRequest +from app.schemas.runtime import AgentContextRequest, IngestEventRequest, MemoryQuestionRequest +from app.services.auth_settings_service import ( + clear_failed_attempts, + get_lockout_status, + register_failed_attempt, +) from app.services.briefing import build_briefing -from app.services.qdrant_store import INDEX_UNREACHABLE, search_memory_embeddings, semantic_status +from app.services.conversation_memory import citations +from app.services.memory_truth import mark_unsupported from app.services.ollama_service import answer_with_context, ollama_health +from app.services.qdrant_store import INDEX_UNREACHABLE, search_memory_embeddings, semantic_status from app.services.scoring import memory_rank_bonus -from app.services.memory_truth import mark_unsupported -from app.services.conversation_memory import citations +from fastapi import APIRouter, Depends, Header, HTTPException, Request +from sqlalchemy import func, or_, select router = APIRouter(prefix="/runtime", tags=["runtime"]) def _parse_dt(value: str | None): - return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else datetime.now(timezone.utc) + return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else datetime.now(UTC) def _enqueue(db, source: EvidenceSource, payload: IngestEventRequest, agent_id: str) -> dict: @@ -359,7 +364,7 @@ def invalidate_evidence(evidence_id: str, reason: str, agent_id: str = Depends(g evidence = db.get(EvidenceObject, evidence_id) if not evidence or evidence.agent_id != agent_id: raise HTTPException(404, "Evidence not found") - evidence.invalidated_at = datetime.now(timezone.utc) + evidence.invalidated_at = datetime.now(UTC) evidence.invalidation_reason = reason evidence.processing_state = "invalidated" links = db.execute(select(ObjectLink).where(ObjectLink.source_type == "evidence", ObjectLink.source_id == evidence_id)).scalars().all() diff --git a/services/api/app/routes/skills.py b/services/api/app/routes/skills.py index 39e4573..d03b664 100644 --- a/services/api/app/routes/skills.py +++ b/services/api/app/routes/skills.py @@ -1,12 +1,13 @@ import json -from datetime import datetime, timezone -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy import select +from datetime import UTC, datetime + from app.core.agent import get_agent_id, resolve_agent_id from app.core.auth import require_read_key from app.core.db import SessionLocal from app.models.memory import Memory from app.schemas.skill import SkillPatchRequest, SkillWriteRequest +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import select router = APIRouter(prefix="/skills", tags=["skills"]) context_router = APIRouter(prefix="/context", tags=["context"]) @@ -87,7 +88,7 @@ def create_skill(payload: SkillWriteRequest, header_agent_id: str = Depends(get_ confidence="high", tags_json=_skill_tags(payload.linked_tools, payload.version, payload.active), status="active" if payload.active else "inactive", - valid_from=datetime.now(timezone.utc), + valid_from=datetime.now(UTC), ) db.add(row) db.commit() diff --git a/services/api/app/routes/system.py b/services/api/app/routes/system.py index bf05182..10567c6 100644 --- a/services/api/app/routes/system.py +++ b/services/api/app/routes/system.py @@ -1,14 +1,19 @@ import hmac -from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import FileResponse from app.core.db import SessionLocal -from app.services.backup_service import create_backup, list_backups, resolve_backup +from app.schemas.auth_settings import AiRuntimeUpdateRequest, MemoryResetRequest from app.services.ai_runtime_service import get_runtime_status, update_runtime_config -from app.services.auth_settings_service import clear_failed_attempts, get_lockout_status, register_failed_attempt, verify_admin_key +from app.services.auth_settings_service import ( + clear_failed_attempts, + get_lockout_status, + register_failed_attempt, + verify_admin_key, +) +from app.services.backup_service import create_backup, list_backups, resolve_backup from app.services.memory_reset_service import reset_memory from app.services.ollama_service import ollama_health -from app.schemas.auth_settings import AiRuntimeUpdateRequest, MemoryResetRequest +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import FileResponse router = APIRouter(prefix="/system", tags=["system"]) @@ -42,7 +47,7 @@ def download_backup(filename: str): try: path = resolve_backup(filename) except FileNotFoundError: - raise HTTPException(404, "Backup not found") + raise HTTPException(404, "Backup not found") from None return FileResponse(path, media_type="application/json", filename=path.name) diff --git a/services/api/app/routes/transcript.py b/services/api/app/routes/transcript.py index de1c385..7225b7c 100644 --- a/services/api/app/routes/transcript.py +++ b/services/api/app/routes/transcript.py @@ -1,12 +1,13 @@ import json from datetime import datetime -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy import select -from app.core.db import SessionLocal + from app.core.agent import get_agent_id, resolve_agent_id +from app.core.db import SessionLocal from app.models.audit import MemoryAudit from app.models.session_transcript import SessionTranscript from app.schemas.transcript import TranscriptCreateRequest +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select router = APIRouter(prefix="/transcripts", tags=["transcripts"]) diff --git a/services/api/app/schemas/entity.py b/services/api/app/schemas/entity.py index 6e5bd0f..1745a30 100644 --- a/services/api/app/schemas/entity.py +++ b/services/api/app/schemas/entity.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel, Field from typing import Any +from pydantic import BaseModel, Field + + class EntityCreateRequest(BaseModel): agent_id: str | None = None entity_type: str diff --git a/services/api/app/schemas/explorer.py b/services/api/app/schemas/explorer.py index 6d397ff..c631818 100644 --- a/services/api/app/schemas/explorer.py +++ b/services/api/app/schemas/explorer.py @@ -1,6 +1,7 @@ from typing import Literal -from pydantic import Field + from app.schemas.runtime import AgentContextRequest +from pydantic import Field class LibraryRequest(AgentContextRequest): diff --git a/services/api/app/schemas/memory.py b/services/api/app/schemas/memory.py index 5a52ddb..64a8587 100644 --- a/services/api/app/schemas/memory.py +++ b/services/api/app/schemas/memory.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field + class MemoryWriteRequest(BaseModel): agent_id: str | None = None text: str diff --git a/services/api/app/schemas/observation.py b/services/api/app/schemas/observation.py index ee576cc..5b9508a 100644 --- a/services/api/app/schemas/observation.py +++ b/services/api/app/schemas/observation.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field + class ObservationCreateRequest(BaseModel): agent_id: str | None = None session_id: str = "" diff --git a/services/api/app/schemas/pattern.py b/services/api/app/schemas/pattern.py index 9c615bc..85ba3ea 100644 --- a/services/api/app/schemas/pattern.py +++ b/services/api/app/schemas/pattern.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel, Field from typing import Any +from pydantic import BaseModel, Field + + class PatternCreateRequest(BaseModel): agent_id: str | None = None pattern_name: str diff --git a/services/api/app/schemas/runtime.py b/services/api/app/schemas/runtime.py index 1bd491d..4327b19 100644 --- a/services/api/app/schemas/runtime.py +++ b/services/api/app/schemas/runtime.py @@ -1,4 +1,5 @@ from typing import Literal + from pydantic import BaseModel, ConfigDict, Field, model_validator diff --git a/services/api/app/schemas/transcript.py b/services/api/app/schemas/transcript.py index c1e35cf..01e5e3e 100644 --- a/services/api/app/schemas/transcript.py +++ b/services/api/app/schemas/transcript.py @@ -1,5 +1,6 @@ from pydantic import BaseModel + class TranscriptCreateRequest(BaseModel): agent_id: str | None = None session_id: str = "" diff --git a/services/api/app/services/ai_runtime_service.py b/services/api/app/services/ai_runtime_service.py index 17cb42a..1175b7a 100644 --- a/services/api/app/services/ai_runtime_service.py +++ b/services/api/app/services/ai_runtime_service.py @@ -1,11 +1,11 @@ """Provider configuration and encrypted credentials for MemoryGate's bounded AI calls.""" +import contextlib from pathlib import Path from threading import Lock -from cryptography.fernet import Fernet, InvalidToken - from app.core.config import OLLAMA_MODEL, RUNTIME_SECRET_PATH from app.models.ai_runtime_setting import AiRuntimeSetting +from cryptography.fernet import Fernet, InvalidToken _SINGLETON_ID = "singleton" _secret_lock = Lock() @@ -20,10 +20,8 @@ def _fernet() -> Fernet: path.parent.mkdir(parents=True, exist_ok=True) key = Fernet.generate_key() path.write_bytes(key) - try: + with contextlib.suppress(OSError): path.chmod(0o600) - except OSError: - pass return Fernet(key) diff --git a/services/api/app/services/auth_settings_service.py b/services/api/app/services/auth_settings_service.py index 0338089..3e33fcf 100644 --- a/services/api/app/services/auth_settings_service.py +++ b/services/api/app/services/auth_settings_service.py @@ -4,10 +4,11 @@ import re import secrets import time -from datetime import datetime, timezone +from datetime import UTC, datetime + from app.core.config import MEMORYGATE_ADMIN_KEY -from app.models.auth_setting import AuthSetting from app.models.agent_access_key import AgentAccessKey, BootstrapAuthority +from app.models.auth_setting import AuthSetting from sqlalchemy.exc import IntegrityError _PBKDF2_ROUNDS = 200_000 @@ -193,7 +194,7 @@ def verify_agent_access_key(db, key: str | None, agent_id: str) -> bool: rows = db.query(AgentAccessKey).filter(AgentAccessKey.agent_id == agent_id, AgentAccessKey.revoked.is_(False)).all() for row in rows: if _verify_key(key, row.key_hash): - row.last_used_at = datetime.now(timezone.utc) + row.last_used_at = datetime.now(UTC) db.commit() return True return False diff --git a/services/api/app/services/backup_service.py b/services/api/app/services/backup_service.py index df10331..84968da 100644 --- a/services/api/app/services/backup_service.py +++ b/services/api/app/services/backup_service.py @@ -3,16 +3,16 @@ import json from datetime import date, datetime from pathlib import Path -from sqlalchemy import select + from app.core.config import BACKUP_DIR from app.models.analysis_object import AnalysisObject +from app.models.conversation_receipt import ConversationReceipt +from app.models.deletion_receipt import DeletionReceipt, RecoveryHold from app.models.entity import Entity, EntityEdge, EntityEvent, EntityHistory from app.models.episode_object import EpisodeObject from app.models.evidence_object import EvidenceObject from app.models.evidence_source import EvidenceSource from app.models.memory import Memory -from app.models.deletion_receipt import DeletionReceipt, RecoveryHold -from app.models.conversation_receipt import ConversationReceipt from app.models.memory_conflict import MemoryConflict from app.models.memory_revision import MemoryRevision from app.models.object_link import ObjectLink @@ -20,6 +20,7 @@ from app.models.pattern import Pattern from app.models.processing_job import ProcessingJob from app.models.session_transcript import SessionTranscript +from sqlalchemy import select _MODELS = (Memory, Entity, EntityEdge, EntityEvent, EntityHistory, Observation, Pattern, SessionTranscript, EvidenceSource, EvidenceObject, EpisodeObject, AnalysisObject, diff --git a/services/api/app/services/briefing.py b/services/api/app/services/briefing.py index ecff304..2b22f6d 100644 --- a/services/api/app/services/briefing.py +++ b/services/api/app/services/briefing.py @@ -6,11 +6,12 @@ for, so no new columns are added for it. """ import json -from datetime import datetime, timedelta, timezone -from sqlalchemy import select -from app.models.observation import Observation +from datetime import UTC, datetime, timedelta + from app.models.entity import Entity, EntityEvent from app.models.memory import Memory +from app.models.observation import Observation +from sqlalchemy import select RECENT_WINDOW_DAYS = 7 TASK_TOUCHED_WINDOW_DAYS = 21 @@ -21,7 +22,7 @@ def _now(): - return datetime.now(timezone.utc) + return datetime.now(UTC) def _attrs(entity: Entity) -> dict: diff --git a/services/api/app/services/conversation_memory.py b/services/api/app/services/conversation_memory.py index be193a8..2d38884 100644 --- a/services/api/app/services/conversation_memory.py +++ b/services/api/app/services/conversation_memory.py @@ -7,7 +7,7 @@ import hashlib import json import time -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import NAMESPACE_URL, uuid5 from app.core.db import SessionLocal @@ -92,7 +92,7 @@ def ingest(agent_id: str, message_id: str, payload: dict) -> dict: db.commit() return _response(receipt) content = payload["content"] - occurred = datetime.fromtimestamp(payload["created_at"], timezone.utc) + occurred = datetime.fromtimestamp(payload["created_at"], UTC) score = score_value(content) receipt.session_id = payload["session_id"] receipt.fingerprint = fingerprint @@ -183,7 +183,7 @@ def forget(agent_id: str, message_id: str, *, sessions=None) -> dict: {"message_id": message_id, "content_status": "forgotten"} ) evidence.normalized_payload_json = "{}" - evidence.invalidated_at = datetime.now(timezone.utc) + evidence.invalidated_at = datetime.now(UTC) evidence.invalidation_reason = "Pi owner forgetting" evidence.processing_state = "invalidated" analysis_id = str(uuid5(NAMESPACE_URL, receipt.id + ":analysis")) diff --git a/services/api/app/services/deletion_recovery.py b/services/api/app/services/deletion_recovery.py index 4f6a871..b20f9ee 100644 --- a/services/api/app/services/deletion_recovery.py +++ b/services/api/app/services/deletion_recovery.py @@ -2,17 +2,17 @@ import hashlib import json -from datetime import datetime, timezone -from sqlalchemy import select +from datetime import UTC, datetime +from app.models.conversation_receipt import ConversationReceipt from app.models.deletion_receipt import DeletionReceipt, RecoveryHold, RecoveryVerification, record -from app.models.memory import Memory from app.models.entity import Entity -from app.models.observation import Observation from app.models.episode_object import EpisodeObject +from app.models.memory import Memory from app.models.object_link import ObjectLink -from app.models.conversation_receipt import ConversationReceipt +from app.models.observation import Observation from app.services import conversation_memory +from sqlalchemy import select MODELS = {"memory": Memory, "entity": Entity, "observation": Observation, "episode": EpisodeObject, "link": ObjectLink} @@ -67,7 +67,7 @@ def reconcile_indexes(target_sessions, client, collections): if row is None: row = RecoveryVerification(id="vector-deletions") db.add(row) - row.evidence_digest, row.verified_at = digest, datetime.now(timezone.utc) + row.evidence_digest, row.verified_at = digest, datetime.now(UTC) db.commit() return {"indexCleanupVerified": True, "evidenceDigest": digest, "pointCount": len(indexes), "recoveryHeld": True, "promotesRecovery": False} diff --git a/services/api/app/services/entity_dedup.py b/services/api/app/services/entity_dedup.py index 3f30c5f..8d8fae1 100644 --- a/services/api/app/services/entity_dedup.py +++ b/services/api/app/services/entity_dedup.py @@ -1,10 +1,17 @@ """Dedup check for entity creation - exact name match first, then embedding similarity, scoped to (agent_id, entity_type) so e.g. a person and a project that happen to share a name never collide.""" +import contextlib import json -from sqlalchemy import select, func, text + from app.models.entity import Entity -from app.services.qdrant_store import INDEX_UNREACHABLE, find_similar_entities, delete_entity_embedding, semantic_status +from app.services.qdrant_store import ( + INDEX_UNREACHABLE, + delete_entity_embedding, + find_similar_entities, + semantic_status, +) +from sqlalchemy import func, select, text DEDUP_SIMILARITY_THRESHOLD = 0.9 @@ -98,9 +105,7 @@ def merge_entities(db, agent_id: str, keep_id: str, merge_id: str) -> Entity: db.commit() db.refresh(keep) - try: + with contextlib.suppress(Exception): delete_entity_embedding(merge_id) - except Exception: - pass return keep diff --git a/services/api/app/services/memory_explorer.py b/services/api/app/services/memory_explorer.py index a66c847..be13c8d 100644 --- a/services/api/app/services/memory_explorer.py +++ b/services/api/app/services/memory_explorer.py @@ -2,13 +2,12 @@ import json -from fastapi import HTTPException -from sqlalchemy import and_, func, literal, or_, select, union_all - from app.models.conversation_receipt import ConversationReceipt from app.models.entity import EntityEdge from app.models.object_link import ObjectLink from app.routes.lineage import OBJECT_MODELS +from fastapi import HTTPException +from sqlalchemy import and_, func, literal, or_, select, union_all # Explicit projection: never return ORM internals, connector configuration or keys. FIELDS = { diff --git a/services/api/app/services/memory_forgetting.py b/services/api/app/services/memory_forgetting.py index bcf3cac..363cf18 100644 --- a/services/api/app/services/memory_forgetting.py +++ b/services/api/app/services/memory_forgetting.py @@ -13,8 +13,8 @@ import json from app.core.db import SessionLocal -from app.models.audit import MemoryAudit from app.models import deletion_receipt +from app.models.audit import MemoryAudit from app.models.memory import Memory from app.models.memory_conflict import MemoryConflict from app.models.memory_forget import MemoryForget diff --git a/services/api/app/services/memory_reset_service.py b/services/api/app/services/memory_reset_service.py index aea9bc5..584163b 100644 --- a/services/api/app/services/memory_reset_service.py +++ b/services/api/app/services/memory_reset_service.py @@ -1,4 +1,5 @@ """Admin-only destructive reset operations for the single MemoryGate workspace.""" +import contextlib from datetime import datetime from app.models.analysis_object import AnalysisObject @@ -66,10 +67,8 @@ def reset_memory(db, reset_from: datetime | None = None) -> dict: for row in rows: db.delete(row) db.commit() - try: + # Postgres remains the source of truth. A later reset or startup can repair vector state. + with contextlib.suppress(Exception): delete_embeddings(memory_ids, observation_ids, entity_ids) - except Exception: - # Postgres remains the source of truth. A later reset or startup can repair vector state. - pass removed = {name: len(rows) for name, rows in selected.items()} return {"backup": backup, "removed": removed, "reset_from": reset_from.isoformat() if reset_from else None} diff --git a/services/api/app/services/memory_truth.py b/services/api/app/services/memory_truth.py index 1d8b8d5..ce7c49a 100644 --- a/services/api/app/services/memory_truth.py +++ b/services/api/app/services/memory_truth.py @@ -1,9 +1,10 @@ import json -from datetime import datetime, timezone -from sqlalchemy import select +from datetime import UTC, datetime + from app.models.memory import Memory from app.models.memory_conflict import MemoryConflict from app.models.memory_revision import MemoryRevision +from sqlalchemy import select def snapshot(memory: Memory) -> dict: @@ -56,5 +57,5 @@ def detect_conflicts(db, memory: Memory) -> list[MemoryConflict]: def mark_unsupported(db, memory: Memory, reason: str) -> None: if memory.status == "active": memory.status = "needs_review" - memory.valid_until = datetime.now(timezone.utc) + memory.valid_until = datetime.now(UTC) add_revision(db, memory, reason, "support_graph") diff --git a/services/api/app/services/observation_lifecycle.py b/services/api/app/services/observation_lifecycle.py index 1d911fb..fb70b1d 100644 --- a/services/api/app/services/observation_lifecycle.py +++ b/services/api/app/services/observation_lifecycle.py @@ -1,8 +1,9 @@ """Dedup, exposure tracking, and active-observation budget enforcement (FIX 3).""" -from datetime import datetime, timezone -from sqlalchemy import select +from datetime import UTC, datetime + from app.models.observation import Observation from app.services.qdrant_store import INDEX_UNREACHABLE, find_similar_observations, semantic_status +from sqlalchemy import select DEDUP_SIMILARITY_THRESHOLD = 0.85 @@ -39,7 +40,7 @@ def enforce_budget(db, agent_id: str, max_observations: int) -> None: victim = max(active, key=lambda o: o.exposure_count) victim.status = "archived" - victim.archived_at = datetime.now(timezone.utc) + victim.archived_at = datetime.now(UTC) victim.archive_reason = "max active observations reached; archived to make room" db.commit() @@ -71,7 +72,7 @@ def apply_session_context(db, agent_id: str, session_context: str) -> dict: if row.exposure_count >= row.max_exposures and row.status == "unconfirmed": row.status = "archived" - row.archived_at = datetime.now(timezone.utc) + row.archived_at = datetime.now(UTC) row.archive_reason = "max exposures without confirmation" archived_ids.append(row.id) diff --git a/services/api/app/services/ollama_service.py b/services/api/app/services/ollama_service.py index 431b425..6126e3e 100644 --- a/services/api/app/services/ollama_service.py +++ b/services/api/app/services/ollama_service.py @@ -1,4 +1,5 @@ import json + import httpx from app.core.config import OLLAMA_ENABLED, OLLAMA_MODEL, OLLAMA_URL from app.core.db import SessionLocal diff --git a/services/api/app/services/pattern_promotion.py b/services/api/app/services/pattern_promotion.py index e717bcb..a6955a5 100644 --- a/services/api/app/services/pattern_promotion.py +++ b/services/api/app/services/pattern_promotion.py @@ -9,10 +9,11 @@ pattern; candidates promote to active at confirmation_count >= 5. """ import json -from datetime import datetime, timezone -from sqlalchemy import select +from datetime import UTC, datetime + from app.models.observation import Observation from app.models.pattern import Pattern +from sqlalchemy import select MIN_CLUSTER_SIZE = 3 PROMOTE_AT_CONFIRMATIONS = 5 @@ -40,7 +41,7 @@ def _pattern_name_for(signal_type: str, hypothesis_norm: str) -> str: def maybe_promote(pattern: Pattern) -> None: if pattern.status == "candidate" and pattern.confirmation_count >= PROMOTE_AT_CONFIRMATIONS: pattern.status = "active" - pattern.promoted_at = datetime.now(timezone.utc) + pattern.promoted_at = datetime.now(UTC) if pattern.status == "active" and pattern.contradiction_count >= DEPRECATE_AT_CONTRADICTIONS: pattern.status = "deprecated" @@ -85,7 +86,7 @@ def promote_from_observations(db, agent_id: str, signal_type: str | None = None) existing.confirmation_count += 1 existing.instance_count = new_size existing.observation_ids_json = json.dumps(observation_ids) - existing.last_confirmed_at = datetime.now(timezone.utc) + existing.last_confirmed_at = datetime.now(UTC) maybe_promote(existing) touched.append(existing) else: @@ -110,7 +111,7 @@ def promote_from_observations(db, agent_id: str, signal_type: str | None = None) applies_to_entity_ids_json=json.dumps(applies_to), context_conditions_json=json.dumps({}), status="candidate", - last_confirmed_at=datetime.now(timezone.utc), + last_confirmed_at=datetime.now(UTC), ) db.add(pattern) touched.append(pattern) diff --git a/services/api/app/services/processing_worker.py b/services/api/app/services/processing_worker.py index 88febe8..cdba573 100644 --- a/services/api/app/services/processing_worker.py +++ b/services/api/app/services/processing_worker.py @@ -1,12 +1,14 @@ +import contextlib import json import threading -from sqlalchemy import select + from app.core.config import PROCESSING_POLL_SECONDS from app.core.db import SessionLocal from app.models.evidence_object import EvidenceObject from app.models.processing_job import ProcessingJob -from app.services.runtime_pipeline import process_evidence from app.services.conversation_memory import index_pending +from app.services.runtime_pipeline import process_evidence +from sqlalchemy import select _stop = threading.Event() _thread: threading.Thread | None = None @@ -30,11 +32,9 @@ def _run() -> None: db.rollback() finally: db.close() - try: + # Pending rows survive worker/database failure and are retried next poll. + with contextlib.suppress(Exception): index_pending() - except Exception: - # Pending rows survive worker/database failure and are retried next poll. - pass _stop.wait(PROCESSING_POLL_SECONDS) diff --git a/services/api/app/services/qdrant_store.py b/services/api/app/services/qdrant_store.py index 769dc05..49d4cf1 100644 --- a/services/api/app/services/qdrant_store.py +++ b/services/api/app/services/qdrant_store.py @@ -2,6 +2,8 @@ from functools import lru_cache from typing import Any +from app.core.config import EMBED_DIMENSION, QDRANT_COLLECTION, QDRANT_URL +from app.services.embeddings import EmbeddingUnavailable, embed_text, embedding_health from qdrant_client import QdrantClient from qdrant_client.models import ( Distance, @@ -12,9 +14,6 @@ VectorParams, ) -from app.core.config import EMBED_DIMENSION, QDRANT_COLLECTION, QDRANT_URL -from app.services.embeddings import EmbeddingUnavailable, embed_text, embedding_health - OBSERVATION_COLLECTION = f"{QDRANT_COLLECTION}_observations" ENTITY_COLLECTION = f"{QDRANT_COLLECTION}_entities" diff --git a/services/api/app/services/runtime_pipeline.py b/services/api/app/services/runtime_pipeline.py index 243ad46..1dcb57e 100644 --- a/services/api/app/services/runtime_pipeline.py +++ b/services/api/app/services/runtime_pipeline.py @@ -1,6 +1,6 @@ import json -from datetime import datetime, timedelta, timezone -from sqlalchemy import select +from datetime import UTC, datetime, timedelta + from app.models.analysis_object import AnalysisObject from app.models.episode_object import EpisodeObject from app.models.evidence_object import EvidenceObject @@ -8,10 +8,11 @@ from app.models.object_link import ObjectLink from app.models.processing_job import ProcessingJob from app.services.classifier import classify_memory -from app.services.signal_filter import score_value -from app.services.qdrant_store import index_after_commit, upsert_memory_embedding -from app.services.ollama_service import analyze_evidence from app.services.memory_truth import add_revision, detect_conflicts +from app.services.ollama_service import analyze_evidence +from app.services.qdrant_store import index_after_commit, upsert_memory_embedding +from app.services.signal_filter import score_value +from sqlalchemy import select def _link(db, source_type, source_id, target_type, target_id, relationship, confidence=1.0): @@ -39,7 +40,7 @@ def process_evidence(db, evidence: EvidenceObject, content: str, job: Processing job.stage = "episode" db.commit() try: - cutoff = (evidence.occurred_at or datetime.now(timezone.utc)) - timedelta(minutes=30) + cutoff = (evidence.occurred_at or datetime.now(UTC)) - timedelta(minutes=30) episode = db.execute(select(EpisodeObject).where( EpisodeObject.agent_id == evidence.agent_id, EpisodeObject.status == "open", @@ -96,7 +97,7 @@ def process_evidence(db, evidence: EvidenceObject, content: str, job: Processing agent_id=evidence.agent_id, text=content.strip(), summary=classification["summary"], memory_type=classification["memory_type"], source_type="automatic_listener", confidence=classification["confidence"], tags_json=evidence.tags_json, - valid_from=datetime.now(timezone.utc), + valid_from=datetime.now(UTC), ) db.add(memory) db.flush() diff --git a/services/api/app/services/signal_filter.py b/services/api/app/services/signal_filter.py index 4553fce..92abc0f 100644 --- a/services/api/app/services/signal_filter.py +++ b/services/api/app/services/signal_filter.py @@ -54,9 +54,9 @@ def score_value(text: str, existing_similar_text: str | None = None) -> float: if lower.rstrip(".!? ") in ACKNOWLEDGMENTS: return 0.0 groups = (PREFERENCE_WORDS, BEHAVIORAL_WORDS, RELATIONSHIP_WORDS, GOAL_WORDS) - score = sum(HIGH_VALUE_WEIGHT for phrases, russian in zip(groups, RUSSIAN_SIGNALS) + score = sum(HIGH_VALUE_WEIGHT for phrases, russian in zip(groups, RUSSIAN_SIGNALS, strict=True) if _contains(lower, phrases) or re.search(russian, lower)) - negations = NEGATION_WORDS + ["не", "больше не", "перестал", "перестала"] + negations = [*NEGATION_WORDS, "не", "больше не", "перестал", "перестала"] if existing_similar_text and _contains(lower, negations) and not _contains(_normalized(existing_similar_text), negations): score += HIGH_VALUE_WEIGHT return min(score, 1.0) diff --git a/services/api/tests/test_audit_metrics.py b/services/api/tests/test_audit_metrics.py index 419e2b0..cd8ba21 100644 --- a/services/api/tests/test_audit_metrics.py +++ b/services/api/tests/test_audit_metrics.py @@ -1,13 +1,12 @@ -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - from app.core import auth from app.core.db import Base from app.models.memory import Memory from app.routes import audit, runtime, transcript from app.services.auth_settings_service import create_agent_access_key, set_admin_key +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker def make_client(tmp_path, monkeypatch): diff --git a/services/api/tests/test_audit_safety.py b/services/api/tests/test_audit_safety.py index acae620..e6755e6 100644 --- a/services/api/tests/test_audit_safety.py +++ b/services/api/tests/test_audit_safety.py @@ -2,12 +2,11 @@ import time import pytest -from sqlalchemy import create_engine, select -from sqlalchemy.orm import sessionmaker - from app.core.db import Base from app.models.audit import MemoryAudit from app.services import ollama_service +from sqlalchemy import create_engine, select +from sqlalchemy.orm import sessionmaker @pytest.fixture @@ -51,6 +50,7 @@ def test_cost_quote_is_recorded_but_does_not_authorize_spending(sessions, monkey @pytest.mark.parametrize("mode", ["exception", "unknown_dimension"]) def test_failed_collection_inspection_is_never_healthy(monkeypatch, mode): from types import SimpleNamespace as NS + from app.services import qdrant_store as store class Client: @@ -69,9 +69,9 @@ def get_collection(self, name): def test_oversized_ingestion_is_permanent_without_echoing_content(monkeypatch): + from app.routes import conversation from fastapi import FastAPI from fastapi.testclient import TestClient - from app.routes import conversation monkeypatch.setenv("MEMORYGATE_CONVERSATION_KEY", "test-conversation-secret") calls = [] @@ -98,8 +98,9 @@ def test_oversized_ingestion_is_permanent_without_echoing_content(monkeypatch): def test_cryptography_runtime_and_manifest_cover_both_advisories(): from importlib.metadata import version from pathlib import Path - from packaging.version import Version + from cryptography.fernet import Fernet, InvalidToken + from packaging.version import Version requirements = Path(__file__).parents[1] / "requirements.txt" pin = next(line.split("==")[1] for line in requirements.read_text().splitlines() diff --git a/services/api/tests/test_auth_and_destructive_actions.py b/services/api/tests/test_auth_and_destructive_actions.py index c42d72b..922f153 100644 --- a/services/api/tests/test_auth_and_destructive_actions.py +++ b/services/api/tests/test_auth_and_destructive_actions.py @@ -7,12 +7,6 @@ import socket import pytest -from fastapi import Depends, FastAPI -from fastapi.middleware.cors import CORSMiddleware -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - from app.core import auth, config from app.core import db as core_db from app.core.db import Base @@ -27,6 +21,11 @@ set_admin_key, verify_admin_key, ) +from fastapi import Depends, FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker ADMIN_KEY = "Admin-key-123!-long-enough" diff --git a/services/api/tests/test_conversation_memory.py b/services/api/tests/test_conversation_memory.py index 8541841..557eb0e 100644 --- a/services/api/tests/test_conversation_memory.py +++ b/services/api/tests/test_conversation_memory.py @@ -51,8 +51,8 @@ def payload(text): @pytest.mark.parametrize( "text", - TEXTS - + [ + [ + *TEXTS, "My preference is training before school", "I preferred morning training", "Люблю плавать", diff --git a/services/api/tests/test_degraded_retrieval.py b/services/api/tests/test_degraded_retrieval.py index aee25fe..da8403c 100644 --- a/services/api/tests/test_degraded_retrieval.py +++ b/services/api/tests/test_degraded_retrieval.py @@ -9,11 +9,6 @@ import socket import pytest -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - from app.core import auth from app.core import db as core_db from app.core.db import Base @@ -26,6 +21,10 @@ set_admin_key, ) from app.services.embeddings import EmbeddingUnavailable +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker ADMIN_KEY = "Admin-key-123!-long-enough" diff --git a/services/api/tests/test_deletion_receipts.py b/services/api/tests/test_deletion_receipts.py index 90e4d53..48a1c75 100644 --- a/services/api/tests/test_deletion_receipts.py +++ b/services/api/tests/test_deletion_receipts.py @@ -1,9 +1,9 @@ import json -from sqlalchemy import select from app.models.deletion_receipt import DeletionReceipt from app.models.memory import Memory from app.services import backup_service +from sqlalchemy import select from test_memory_revisions import setup # noqa: F401 diff --git a/services/api/tests/test_deletion_recovery.py b/services/api/tests/test_deletion_recovery.py index b30faab..86290ca 100644 --- a/services/api/tests/test_deletion_recovery.py +++ b/services/api/tests/test_deletion_recovery.py @@ -1,21 +1,21 @@ import pytest -from sqlalchemy import create_engine, select -from sqlalchemy.orm import sessionmaker - from app.core.db import Base +from app.models.conversation_receipt import ConversationReceipt from app.models.deletion_receipt import DeletionReceipt, RecoveryHold -from app.models.memory import Memory from app.models.entity import Entity +from app.models.memory import Memory from app.models.observation import Observation -from app.models.conversation_receipt import ConversationReceipt -from app.services import deletion_recovery as recovery from app.services import conversation_memory +from app.services import deletion_recovery as recovery +from sqlalchemy import create_engine, select +from sqlalchemy.orm import sessionmaker def test_real_local_vectors_are_deleted_and_read_back(target): from uuid import uuid4 - from qdrant_client import QdrantClient, models + from app.models.deletion_receipt import RecoveryVerification + from qdrant_client import QdrantClient, models identity, keep = str(uuid4()), str(uuid4()) client = QdrantClient(":memory:") collections = {kind: "recovery_" + kind for kind in ("memory", "entity", "observation")} @@ -39,6 +39,7 @@ def test_real_local_vectors_are_deleted_and_read_back(target): def test_index_failure_never_records_success(target): from types import SimpleNamespace + from app.models.deletion_receipt import RecoveryVerification collections = {kind: kind for kind in ("memory", "entity", "observation")} class FailingClient: diff --git a/services/api/tests/test_memory_explorer.py b/services/api/tests/test_memory_explorer.py index bd9b1dd..f3b012e 100644 --- a/services/api/tests/test_memory_explorer.py +++ b/services/api/tests/test_memory_explorer.py @@ -1,18 +1,18 @@ import json +from datetime import UTC import pytest -from fastapi import HTTPException -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - from app.core.db import Base -from app.models.memory import Memory from app.models.entity import Entity, EntityEdge from app.models.evidence_object import EvidenceObject +from app.models.memory import Memory from app.models.object_link import ObjectLink from app.schemas.explorer import ExploreRequest from app.schemas.runtime import AgentContextRequest from app.services.memory_explorer import compact, explore +from fastapi import HTTPException +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker @pytest.fixture @@ -30,9 +30,9 @@ def db(tmp_path): EvidenceObject(id="invalid", agent_id="owner", source_id="connector", source_key="call", source_type="audio", title="old"), EntityEdge(id="ee", from_entity_id="e", to_entity_id="e2", relationship_type="works_on"), ]) - from datetime import datetime, timezone + from datetime import datetime db.flush() - db.get(EvidenceObject, "invalid").invalidated_at = datetime.now(timezone.utc) + db.get(EvidenceObject, "invalid").invalidated_at = datetime.now(UTC) for identity, kind, target, relation in [("1", "entity", "e", "about"), ("2", "evidence", "s", "derived_from"), ("3", "memory", "foreign", "related"), ("4", "memory", "inactive", "related"), ("5", "evidence", "invalid", "derived_from")]: db.add(ObjectLink(id=identity, source_type="memory", source_id="m", target_type=kind, target_id=target, relationship=relation)) @@ -108,8 +108,8 @@ def test_read_tier_cannot_switch_namespace_in_body(): def test_mcp_context_and_followup_use_same_operator_scope(db, monkeypatch): import importlib.util - from pathlib import Path from io import BytesIO + from pathlib import Path path = Path(__file__).resolve().parents[2] / "mcp" / "memorygate_mcp.py" spec = importlib.util.spec_from_file_location("explorer_mcp", path) module = importlib.util.module_from_spec(spec) diff --git a/services/api/tests/test_memory_revisions.py b/services/api/tests/test_memory_revisions.py index 3a89000..e1afcf1 100644 --- a/services/api/tests/test_memory_revisions.py +++ b/services/api/tests/test_memory_revisions.py @@ -1,15 +1,16 @@ """Revision-aware editing and atomic audit on an isolated database.""" import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from sqlalchemy import create_engine, select, func -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm.exc import StaleDataError from app.core.db import Base -from app.models.memory import Memory from app.models.audit import MemoryAudit +from app.models.memory import Memory from app.models.memory_revision import MemoryRevision from app.routes import memory +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlalchemy import create_engine, func, select +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.exc import StaleDataError + @pytest.fixture def setup(tmp_path, monkeypatch): diff --git a/services/api/tests/test_scoped_context.py b/services/api/tests/test_scoped_context.py index 8ab0c25..f0e6142 100644 --- a/services/api/tests/test_scoped_context.py +++ b/services/api/tests/test_scoped_context.py @@ -1,12 +1,13 @@ import pytest -from pydantic import ValidationError -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker from app.core.db import Base -from app.models.memory import Memory from app.models.conversation_receipt import ConversationReceipt +from app.models.memory import Memory from app.routes import runtime from app.schemas.runtime import AgentContextRequest +from pydantic import ValidationError +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + @pytest.fixture def db(tmp_path, monkeypatch): diff --git a/services/api/tests/test_security_and_runtime_contracts.py b/services/api/tests/test_security_and_runtime_contracts.py index 125f2a3..02e3060 100644 --- a/services/api/tests/test_security_and_runtime_contracts.py +++ b/services/api/tests/test_security_and_runtime_contracts.py @@ -2,10 +2,15 @@ import unittest from pathlib import Path -from app.schemas.runtime import IngestEventRequest from app.schemas.memory import MemoryWriteRequest +from app.schemas.runtime import IngestEventRequest from app.services import backup_service -from app.services.auth_settings_service import _hash_key, _verify_key, ensure_bootstrap_agent_access_key, validate_new_admin_key +from app.services.auth_settings_service import ( + _hash_key, + _verify_key, + ensure_bootstrap_agent_access_key, + validate_new_admin_key, +) class SecurityAndRuntimeContractTests(unittest.TestCase): @@ -37,9 +42,9 @@ def test_memory_write_contract_accepts_evidence_metadata(self): def test_bootstrap_agent_read_key_is_hashed_and_scoped(self): from app.core.db import Base + from app.services.auth_settings_service import verify_agent_access_key from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker - from app.services.auth_settings_service import verify_agent_access_key with tempfile.TemporaryDirectory() as directory: engine = create_engine(f"sqlite:///{directory}/memorygate-test.db") diff --git a/services/api/tests/test_skills.py b/services/api/tests/test_skills.py index 6b78e67..36d80ff 100644 --- a/services/api/tests/test_skills.py +++ b/services/api/tests/test_skills.py @@ -2,17 +2,15 @@ os.environ.setdefault("DATABASE_URL", "sqlite:///./test_skills_import.db") -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from app.core.db import Base from app.core import auth -from app.models.memory import Memory +from app.core.db import Base from app.routes import skills from app.routes.skills import context_router, router from app.services.auth_settings_service import create_agent_access_key, set_admin_key from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker def make_client(tmp_path, monkeypatch): diff --git a/services/mcp/memorygate_mcp.py b/services/mcp/memorygate_mcp.py index 25dcef3..d8f993f 100644 --- a/services/mcp/memorygate_mcp.py +++ b/services/mcp/memorygate_mcp.py @@ -3,10 +3,8 @@ import json import os import sys -from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen - TOOL = { "name": "memorygate_context", "description": "Retrieve bounded, evidence-aware personal memory context. Read-only; use it before answering personal questions.",