diff --git a/docs/superpowers/plans/2026-05-09-drop-quality-score-columns.md b/docs/superpowers/plans/2026-05-09-drop-quality-score-columns.md new file mode 100644 index 0000000..70459f8 --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-drop-quality-score-columns.md @@ -0,0 +1,132 @@ +# Drop importance_score and confidence columns + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to resume this plan from the checkpoint. Check the "Status as of last commit" section first to see what's already done. + +**Goal:** Remove the two broken-by-default `memory` columns (`importance_score` always 1.0; `confidence` always 0.5 for observations) and every code path that reads, writes, or ranks by them. Lore's recall stack already does cosine-similarity + FTS hybrid scoring; importance/confidence multipliers are no-ops on the current data and only confuse the UI. + +**Architecture:** Schema migration drops columns; persistence layer stops carrying the fields; service / MCP / HTTP / CLI / export layers stop accepting and emitting them; `importance.py` was deleted and replaced by `decay.py` with just the still-needed pure functions. `min_confidence` parameters that are actually min-score thresholds get renamed to `min_score`. Recall scoring is now `cosine × time_decay × tier_weight × graph_boost` (no importance multiplier; was always a no-op since every row had identical 1.0). + +**Out of scope (separate "confidence" concepts — DO NOT TOUCH):** +- `recommend/engine.py` / `recommend/types.py` — RecommendationConfidence (`0.6 * magnitude + 0.4 * agreement`) +- `freshness/detector.py` — staleness confidence per commit-count threshold +- `services/graph/`, `graph_extraction.py`, graph relationships — these have their own per-relationship `confidence` and `weight` fields on a *different* table (entities/mentions/relationships) +- `classify/` — classification confidence per axis + +--- + +## Status as of last commit (2af9ead) + +**Done:** +- ✅ Phase 1: schema migrations (`migrations/025_drop_quality_score_columns.sql` + SQLite version) +- ✅ Phase 2: dataclass field removals (NewMemory, StoredMemory, MemoryPatch, ExportedMemory in `persistence/types.py`) +- ✅ Phase 3a: SQLite persistence (INSERT, SELECT, recall scoring, record_memory_access, bump_access_counts, recommendation candidate ordering, search_memories_text, _row_to_memory, _row_to_exported, _MEMORY_COLS, GraphStats.avg_importance) +- ✅ Phase 3b: PostgreSQL persistence (matching shape changes — RETURNING clauses, recall scoring, FTS branch, recall_by_entities, list_memories_without_mentions, list_temporal_buckets, search_memories_text, record_memory_access, list_candidate_memories_for_recommendation, AVG aggregates) +- ✅ Phase 3c: protocol.py (bump_access_counts docstring; import_extracted_memory and upsert_memory_with_embedding signatures lose the confidence parameter) +- ✅ Phase 4: `src/lore/importance.py` deleted; new `src/lore/decay.py` with `decay_factor` + `resolve_half_life`; `lore.py` updated to use `_memory_decay` helper instead of `time_adjusted_importance`; upvote/downvote no longer recompute importance; `cleanup_expired` uses decay-only thresholding; `recalculate_importance` method removed; `tests/test_importance_scoring.py` deleted +- ✅ Phase 5a: 3 services (`services/observations.py`, `services/memories.py`, `services/lessons.py`) — drop `confidence` parameter, drop `min_confidence` (renamed to `min_score`), strip from response dicts and bulk-upsert path + +**Remaining (Phase 5b onward):** + +### Phase 5b — services + adjacent +- `src/lore/services/conversations.py` — drop confidence pass-through to memory creation +- `src/lore/services/snapshots.py` — drop importance_score from response/dataclass mappings +- `src/lore/services/retrieve.py` — line ~524 has `float(memory.importance_score) if ... else 0.5` to drop +- `src/lore/services/graph/graph.py`, `services/graph/review.py` — sweep for memory.importance_score / memory.confidence references; LEAVE per-relationship graph confidence/weight alone +- `src/lore/services/graph_extraction.py` — sweep for memory.importance_score / memory.confidence (NOT the graph extraction's own LLM-emitted confidence per fact, that's a different concept) +- `src/lore/consolidation.py` — drop importance_score from consolidated NewMemory; replace `max(memories, key=lambda m: m.importance_score)` with `max(..., key=lambda m: m.created_at)` +- `src/lore/conversation/extractor.py` — drop `confidence=candidate.get("confidence", 0.8)` from create-memory call (the LLM still emits per-fact confidence, we just stop persisting it on the memory column) +- `src/lore/extract/extractor.py`, `extract/prompts.py`, `extract/resolver.py` — drop confidence from any NewMemory constructors +- `src/lore/ingest/dedup.py` — line 68 `min_confidence=0.0` → rename to `min_score=0.0` + +### Phase 6 — MCP server +- `src/lore/mcp/server.py::remember()` — drop `confidence: float = 1.0` parameter from signature; drop from the `lore.remember(...)` call +- Sweep response dicts for confidence / importance_score + +### Phase 7 — HTTP routes & response models +- `src/lore/server/models.py` — drop confidence/importance_score from any pydantic MemoryResponse / MemoryRow +- `src/lore/server/routes/recent.py` — `RecentMemoryItem.importance_score` field gone; drop from `_to_item()` and the markdown formatter +- `src/lore/server/routes/retention.py` — drop `min_importance_score` query parameter; drop importance_score from response model. Replace with min_age_days/tier or just remove. +- `src/lore/server/routes/lessons.py` — drop confidence from create body; rename `min_confidence` → `min_score`; drop confidence + importance_score from response model +- `src/lore/server/routes/memories.py` — drop both fields from MemoryResponse; replace any min_confidence/min_importance_score with min_score +- `src/lore/server/routes/observations.py` — drop fields from response (they're in `meta`, but verify) +- `src/lore/server/routes/temporal.py` — drop importance_score from temporal response models +- `src/lore/server/routes/review.py` — sweep for the dropped columns (leave graph confidence alone) +- `src/lore/server/routes/recommendations.py` — sweep; leave RecommendationConfidence alone, only touch references to memory column +- `src/lore/server/routes/graph/memories.py`, `graph/models.py`, `graph/stats.py` — drop avg_importance from GraphStats response; leave per-relationship graph confidence + +### Phase 8 — top-level + CLI + export + UI source +- `src/lore/lore.py::remember()` — drop `confidence` parameter from public API; drop `0 <= confidence <= 1.0` guard +- `src/lore/async_lore.py` — same surgery +- `src/lore/types.py` — drop `confidence: float = 1.0` and `importance_score: float = 1.0` from any top-level Memory / ScoredMemory dataclasses; drop avg_importance / avg_confidence from MemoryStats +- `src/lore/temporal.py:181` — drop the `importance: {mem.importance_score:.2f}` formatter line +- `src/lore/recent.py`, `src/lore/retention.py` — drop importance_score from data structures; drop importance threshold parameters +- `src/lore/store/http.py` — drop importance_score / confidence from the request payloads and response parsers +- `src/lore/cli/__init__.py:575` — drop `--min-importance` argument +- `src/lore/cli/commands/manage.py` — drop importance-recompute commands +- `src/lore/cli/commands/misc.py` — sweep references +- `src/lore/cli/commands/remember.py` — drop `--confidence` flag +- `src/lore/export/markdown.py:137` — drop `"confidence": mem.confidence,` +- `src/lore/export/serializers.py` — drop both fields from JSON serializer +- `src/lore/ui/src/panels/detail.js` — drop the rows that render "Importance: 100%" and "Confidence: 50%" +- `src/lore/ui/src/state.js` — sweep +- `src/lore/ui/dist/app.js` — leave (build artifact); note in PR that UI build needs re-run + +### Phase 9 — tests update + add regression +~30 test files reference these fields. Sweep: +```bash +grep -rln "importance_score\|memory\.confidence\|min_confidence\|min_importance" tests/ | grep -v __pycache__ +``` +For each: remove `importance_score=`/`confidence=` from fixture constructors, drop assertions on those fields, rename `min_confidence` → `min_score` parameters. + +Add `tests/persistence/test_quality_columns_dropped.py` regression test: +```python +import pytest +pytest.importorskip("aiosqlite") +pytest.importorskip("sqlite_vec") +from lore.persistence.sqlite import SqliteStore + +@pytest.mark.asyncio +async def test_memories_table_has_no_quality_score_columns(tmp_path): + db = tmp_path / "lore.db" + store = await SqliteStore.create(f"sqlite:///{db}", run_migrations=True) + try: + async with store._pool.acquire() as conn: + cur = await conn.execute("PRAGMA table_info(memories)") + cols = [row[1] async for row in cur] + assert "importance_score" not in cols + assert "confidence" not in cols + finally: + await store.close() + + +def test_importance_module_does_not_exist(): + with pytest.raises(ImportError): + from lore import importance # noqa: F401 +``` + +### Phase 10 — verify, lint, push, PR + +```bash +PYTHONPATH=src python3 -m pytest tests/ -x -q --ignore=tests/integration +ruff check src/ tests/ +git push +gh pr ready # if originally opened as draft +``` + +--- + +## Helpful invariants for the next executor + +- **`m.confidence` is OK to keep** in queries against `entity_mentions` table (graph relationship confidence — different column, different concept). Verify by checking the FROM clause: if it's `FROM entity_mentions` or has a `JOIN entity_mentions m ON ...` with `m.` prefix, that's graph. +- The `_row_to_mention` function in both sqlite.py and postgres.py keeps its `confidence=row["confidence"]` line — that's the graph mention. +- `recommend/engine.py` `_compute_confidence` is a separate computation (signal magnitude × agreement). Don't touch. +- `freshness/detector.py` `confidence` is a per-staleness-status threshold. Don't touch. +- `classify/` axis confidence is unrelated. Don't touch. + +## Done-state self-check + +- `grep -rn "importance_score\|memory\.confidence" src/ | grep -v "graph\|recommend\|freshness\|classify"` returns zero hits +- `python3 -c "from lore import importance"` raises ImportError +- `pytest tests/ --ignore=tests/integration` is fully green +- `ruff check src/ tests/` is clean +- migration runs cleanly on a fresh DB diff --git a/migrations/025_drop_quality_score_columns.sql b/migrations/025_drop_quality_score_columns.sql new file mode 100644 index 0000000..ad5d6d6 --- /dev/null +++ b/migrations/025_drop_quality_score_columns.sql @@ -0,0 +1,86 @@ +-- Migration 025: drop importance_score and confidence columns from memories. +-- +-- Both columns carried mechanical defaults that no caller ever overrode: +-- * importance_score = 1.0 (schema default; INSERT statements never +-- populated it before PR #295038e; the recall-side multiplier was a +-- no-op since every row had identical 1.0). +-- * confidence = 0.5 hardcoded for observations (services/observations.py), +-- uncalibrated user-supplied number for lessons; never used in ranking, +-- only echoed back in API responses. +-- +-- Lore's recall stack already does cosine-similarity + ts_rank/FTS hybrid +-- scoring with optional fts_weight per profile. The dropped columns added +-- noise (UI rendered a flat "Importance 100% / Confidence 50%" on every +-- memory) without changing any actual ranking on existing data. +-- +-- Industry consensus across mem0 / Letta / LangMem / Zep-graphiti / Cognee +-- is to skip per-memory quality scores entirely. +-- +-- Out of scope (DO NOT confuse with these): graph-table per-relationship +-- ``confidence`` and ``weight`` columns live in entities/mentions/ +-- relationships and are unaffected. + +-- Indexes first — keeping them after a column drop would leave dangling +-- references in some DB engines. +DROP INDEX IF EXISTS idx_lessons_importance; +DROP INDEX IF EXISTS idx_memories_importance; +DROP INDEX IF EXISTS idx_memories_confidence; + +-- The legacy ``lessons`` view (created in 009_rename_to_memories.sql) +-- references both columns; CASCADE drops it together with its rewrite +-- rules so the ALTER TABLE below succeeds. We recreate the view at the +-- end without the dropped columns. +DROP VIEW IF EXISTS lessons CASCADE; + +-- Drop the on-access trigger that recomputed importance on read. Names +-- vary — try the variants we've used historically. +DROP TRIGGER IF EXISTS on_update_importance ON memories; +DROP TRIGGER IF EXISTS memories_recompute_importance_on_access ON memories; +DROP FUNCTION IF EXISTS update_importance_score() CASCADE; +DROP FUNCTION IF EXISTS memories_recompute_importance() CASCADE; + +ALTER TABLE memories DROP COLUMN IF EXISTS importance_score; +ALTER TABLE memories DROP COLUMN IF EXISTS confidence; + +-- Recreate the read/write lessons compatibility view (minus the dropped +-- columns) so any legacy /v1/lessons callers that still query it keep +-- working. Mirrors the original CREATE VIEW + rules in migration 009. +CREATE OR REPLACE VIEW lessons AS + SELECT id, org_id, + content AS problem, + context AS resolution, + NULL::text AS context, + tags, source, project, embedding, + created_at, updated_at, expires_at, + upvotes, downvotes, meta, + access_count, last_accessed_at, + reputation_score, quality_signals + FROM memories; + +CREATE OR REPLACE RULE lessons_insert AS ON INSERT TO lessons +DO INSTEAD + INSERT INTO memories (id, org_id, content, context, tags, source, project, + embedding, created_at, updated_at, expires_at, upvotes, downvotes, meta) + VALUES (NEW.id, NEW.org_id, NEW.problem, NEW.resolution, NEW.tags, + NEW.source, NEW.project, NEW.embedding, NEW.created_at, NEW.updated_at, + NEW.expires_at, NEW.upvotes, NEW.downvotes, NEW.meta); + +CREATE OR REPLACE RULE lessons_update AS ON UPDATE TO lessons +DO INSTEAD + UPDATE memories SET + content = NEW.problem, + context = NEW.resolution, + tags = NEW.tags, + source = NEW.source, + project = NEW.project, + embedding = NEW.embedding, + updated_at = NEW.updated_at, + expires_at = NEW.expires_at, + upvotes = NEW.upvotes, + downvotes = NEW.downvotes, + meta = NEW.meta + WHERE id = OLD.id; + +CREATE OR REPLACE RULE lessons_delete AS ON DELETE TO lessons +DO INSTEAD + DELETE FROM memories WHERE id = OLD.id; diff --git a/migrations_sqlite/025_drop_quality_score_columns.sql b/migrations_sqlite/025_drop_quality_score_columns.sql new file mode 100644 index 0000000..1298b24 --- /dev/null +++ b/migrations_sqlite/025_drop_quality_score_columns.sql @@ -0,0 +1,34 @@ +-- Migration 025 (SQLite): drop importance_score and confidence columns. +-- See migrations/025_drop_quality_score_columns.sql for rationale. +-- +-- The memories table got its current name in 009_rename_to_memories.sql +-- (formerly ``lessons``). The migration runner only applies once per file, +-- so we target ``memories`` directly. +-- +-- SQLite 3.35.0+ (Mar 2021) supports native ``ALTER TABLE DROP COLUMN`` +-- but refuses to drop a column referenced by indexes or views. Drop the +-- ``lessons`` compatibility view (recreated below without the dropped +-- columns) and the per-column indexes before the ALTERs. + +DROP VIEW IF EXISTS lessons; +DROP INDEX IF EXISTS idx_lessons_importance; +DROP INDEX IF EXISTS idx_memories_importance; +DROP INDEX IF EXISTS idx_memories_confidence; + +ALTER TABLE memories DROP COLUMN importance_score; +ALTER TABLE memories DROP COLUMN confidence; + +-- Recreate the read-only lessons view minus the dropped columns. Keeps the +-- legacy ``problem``/``resolution``/``context`` shape for any old callers +-- that still hit the view. +CREATE VIEW IF NOT EXISTS lessons AS + SELECT id, org_id, + content AS problem, + context AS resolution, + NULL AS context, + tags, source, project, + created_at, updated_at, expires_at, + upvotes, downvotes, meta, + access_count, last_accessed_at, + reputation_score, quality_signals + FROM memories; diff --git a/src/lore/async_lore.py b/src/lore/async_lore.py index 12cc045..62a1750 100644 --- a/src/lore/async_lore.py +++ b/src/lore/async_lore.py @@ -398,7 +398,6 @@ async def remember( source: Optional[str] = None, embedding: Optional[Sequence[float]] = None, context: Optional[str] = None, - confidence: float = 0.5, meta: Optional[dict[str, Any]] = None, ) -> StoredMemory: """Store a memory. Returns the persisted ``StoredMemory``. @@ -415,7 +414,6 @@ async def remember( embedding=vec, context=context, tags=tuple(tags or ()), - confidence=confidence, source=source, project=project, meta=meta or {}, @@ -852,31 +850,17 @@ async def enrich_memories( ) async def cleanup_expired( - self, importance_threshold: Optional[float] = None # noqa: ARG002 - parity + self, decay_threshold: Optional[float] = None # noqa: ARG002 - parity ) -> int: """Purge expired memories (TTL-based). Returns rowcount. - ``importance_threshold`` is accepted for parity with the sync + ``decay_threshold`` is accepted for parity with the sync ``Lore`` API but is currently ignored — the async layer doesn't - track importance_score outside of recall scoring. Phase 4C will - re-introduce importance-based pruning if it proves needed. + do decay-based pruning at this level. """ store = self._require_store() return await store.expire_memories() - async def recalculate_importance( - self, project: Optional[str] = None # noqa: ARG002 - parity - ) -> int: - """Recompute importance for every memory in scope. - - The async Store recomputes importance on every read/recall/vote - already, so this is currently a no-op that returns 0. Kept for - symmetry with the sync class so callers can switch implementations - without code changes. - """ - self._require_store() - return 0 - # ── Phase 4B: classify + as_prompt ────────────────────────────────── async def classify(self, text: str) -> "Classification": @@ -947,7 +931,6 @@ async def as_prompt( updated_at=h.updated_at.isoformat() if h.updated_at else "", ttl=None, expires_at=h.expires_at.isoformat() if h.expires_at else None, - confidence=h.confidence, ) score = getattr(h, "score", 0.0) results.append(RecallResult(memory=mem, score=score, verbatim=verbatim)) diff --git a/src/lore/cli/__init__.py b/src/lore/cli/__init__.py index 553110a..106fc96 100644 --- a/src/lore/cli/__init__.py +++ b/src/lore/cli/__init__.py @@ -46,7 +46,6 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--context", default=None, help="Additional context for the memory") p.add_argument("--ttl", type=int, default=None, help="Time-to-live in seconds") p.add_argument("--source", default=None) - p.add_argument("--confidence", type=float, default=1.0) p.add_argument("--project", default=None, help="Project namespace") p.add_argument("--metadata", default=None, help="JSON metadata (e.g. '{\"key\": \"val\"}')") @@ -112,10 +111,6 @@ def build_parser() -> argparse.ArgumentParser: help="Filter by memory tier", ) p.add_argument("--limit", type=int, default=None) - p.add_argument( - "--sort", type=str, choices=["created", "importance"], - default="created", help="Sort order (default: created)", - ) # stats sub.add_parser("stats", help="Show memory statistics") @@ -569,11 +564,9 @@ def build_parser() -> argparse.ArgumentParser: p_restore.add_argument("--input", "-i", required=True, dest="input_file", help="Path to backup JSON file") # retention - p_ret = sub.add_parser("retention", help="Apply retention policy to remove old low-importance memories") + p_ret = sub.add_parser("retention", help="Apply retention policy to remove old memories") p_ret.add_argument("--max-age-days", type=int, default=90, dest="max_age_days", help="Delete memories older than N days (default: 90)") - p_ret.add_argument("--min-importance", type=float, default=0.3, dest="min_importance", - help="Only delete memories below this importance score (default: 0.3)") p_ret.add_argument("--archive", action="store_true", default=False, help="Export affected memories to JSON before deleting") p_ret.add_argument("--dry-run", action="store_true", default=False, dest="dry_run", diff --git a/src/lore/cli/commands/manage.py b/src/lore/cli/commands/manage.py index 54fda27..f414988 100644 --- a/src/lore/cli/commands/manage.py +++ b/src/lore/cli/commands/manage.py @@ -25,17 +25,14 @@ def cmd_memories(args: argparse.Namespace) -> None: if not memories: print("No memories.") return - sort_key = getattr(args, "sort", "created") - if sort_key == "importance": - memories.sort(key=lambda m: m.importance_score, reverse=True) - print(f"{'ID':<28} {'Tier':<10} {'Type':<12} {'Importance':<12} {'Created':<22} {'Topics':<30} {'Content':<40}") - print("-" * 154) + print(f"{'ID':<28} {'Tier':<10} {'Type':<12} {'Created':<22} {'Topics':<30} {'Content':<40}") + print("-" * 142) for m in memories: created = m.created_at[:19] if m.created_at else "" enrichment = (m.metadata or {}).get("enrichment", {}) topics = ", ".join(enrichment.get("topics", [])) if enrichment else "-" print( - f"{m.id:<28} {m.tier:<10} {m.type:<12} {m.importance_score:<12.2f} " + f"{m.id:<28} {m.tier:<10} {m.type:<12} " f"{created:<22} {topics:<30} {m.content[:40]:<40}" ) @@ -206,13 +203,12 @@ def cmd_restore(args: argparse.Namespace) -> None: def cmd_retention(args: argparse.Namespace) -> None: - """Apply a retention policy to prune old low-importance memories.""" + """Apply a retention policy to prune old memories.""" from lore.retention import RetentionPolicy, apply_retention lore = _helpers._get_lore(args.db) policy = RetentionPolicy( max_age_days=args.max_age_days, - min_importance_score=args.min_importance, archive_on_expire=args.archive, dry_run=args.dry_run, ) diff --git a/src/lore/cli/commands/misc.py b/src/lore/cli/commands/misc.py index 9fdca3c..6239f05 100644 --- a/src/lore/cli/commands/misc.py +++ b/src/lore/cli/commands/misc.py @@ -405,7 +405,6 @@ def cmd_on_this_day(args: argparse.Namespace) -> None: "content": m.content, "type": m.type, "tier": m.tier, - "importance_score": m.importance_score, "created_at": m.created_at, "project": m.project, "tags": m.tags, diff --git a/src/lore/cli/commands/remember.py b/src/lore/cli/commands/remember.py index 23b9efe..dd05f00 100644 --- a/src/lore/cli/commands/remember.py +++ b/src/lore/cli/commands/remember.py @@ -29,7 +29,6 @@ def cmd_remember(args: argparse.Namespace) -> None: source=args.source, project=args.project, ttl=args.ttl, - confidence=args.confidence, ) except SecretBlockedError as exc: lore.close() diff --git a/src/lore/consolidation.py b/src/lore/consolidation.py index 3fa30b1..024c5a2 100644 --- a/src/lore/consolidation.py +++ b/src/lore/consolidation.py @@ -204,12 +204,12 @@ def _summarize_group( ) -> str: """Summarize a group of memories into consolidated content.""" if strategy == "deduplicate" or self._llm is None: - best = max(memories, key=lambda m: m.importance_score) + best = max(memories, key=lambda m: m.created_at) return best.content # LLM summarization memories_text = "\n---\n".join( - f"[{m.type}, importance: {m.importance_score:.2f}] {m.content}" + f"[{m.type}] {m.content}" for m in memories ) prompt = CONSOLIDATION_PROMPT.format(memories=memories_text) @@ -218,11 +218,11 @@ def _summarize_group( except Exception: logger.warning( "LLM summarization failed for group of %d memories, " - "falling back to highest-importance content", + "falling back to most recent content", len(memories), exc_info=True, ) - best = max(memories, key=lambda m: m.importance_score) + best = max(memories, key=lambda m: m.created_at) return best.content # ------------------------------------------------------------------ @@ -265,8 +265,6 @@ def _create_consolidated_memory( embedding=embedding_bytes, created_at=now, updated_at=now, - confidence=max(m.confidence for m in originals), - importance_score=max(m.importance_score for m in originals), access_count=sum(m.access_count for m in originals), upvotes=sum(m.upvotes for m in originals), downvotes=sum(m.downvotes for m in originals), diff --git a/src/lore/conversation/extractor.py b/src/lore/conversation/extractor.py index 422b839..f193292 100644 --- a/src/lore/conversation/extractor.py +++ b/src/lore/conversation/extractor.py @@ -121,7 +121,6 @@ def extract( metadata=metadata, source="conversation", project=project or self._lore.project, - confidence=candidate.get("confidence", 0.8), ) job.memory_ids.append(memory_id) diff --git a/src/lore/decay.py b/src/lore/decay.py new file mode 100644 index 0000000..21108a5 --- /dev/null +++ b/src/lore/decay.py @@ -0,0 +1,47 @@ +"""Adaptive decay functions for memory recall scoring. + +Replaces the dropped ``lore.importance`` module with the bits we still +need: a pure exponential ``decay_factor`` and a tier-aware +``resolve_half_life``. ``compute_importance`` and +``time_adjusted_importance`` are gone — the importance_score column +they computed against was dropped in 025_drop_quality_score_columns. +""" + +from __future__ import annotations + +from typing import Dict, Optional, Tuple + +from lore.types import DECAY_HALF_LIVES, TIER_DECAY_HALF_LIVES + + +def decay_factor(age_days: float, half_life_days: float) -> float: + """Pure exponential decay multiplier. Returns value in (0, 1].""" + return 0.5 ** (age_days / max(half_life_days, 0.001)) + + +def resolve_half_life( + tier: Optional[str], + memory_type: str, + overrides: Optional[Dict[Tuple[str, str], float]] = None, +) -> float: + """Resolve half-life with fallback chain. + + Resolution order: + 1. overrides[(tier, type)] -- per-project config + 2. TIER_DECAY_HALF_LIVES[tier][type] -- tier+type specific + 3. TIER_DECAY_HALF_LIVES[tier]["default"] -- tier default + 4. DECAY_HALF_LIVES[type] -- legacy flat lookup (= long tier) + 5. 30.0 -- global default + """ + effective_tier = tier or "long" + + if overrides and (effective_tier, memory_type) in overrides: + return overrides[(effective_tier, memory_type)] + + tier_config = TIER_DECAY_HALF_LIVES.get(effective_tier, {}) + if memory_type in tier_config: + return tier_config[memory_type] + if "default" in tier_config: + return tier_config["default"] + + return DECAY_HALF_LIVES.get(memory_type, 30.0) diff --git a/src/lore/export/markdown.py b/src/lore/export/markdown.py index c695e2f..1f77df7 100644 --- a/src/lore/export/markdown.py +++ b/src/lore/export/markdown.py @@ -134,8 +134,6 @@ def render( "tier": mem.tier, "project": mem.project, "tags": mem.tags or [], - "confidence": mem.confidence, - "importance_score": mem.importance_score, "upvotes": mem.upvotes, "downvotes": mem.downvotes, "created_at": mem.created_at, diff --git a/src/lore/export/serializers.py b/src/lore/export/serializers.py index a86ac80..34679e5 100644 --- a/src/lore/export/serializers.py +++ b/src/lore/export/serializers.py @@ -40,10 +40,8 @@ def memory_to_dict(memory: Memory, include_embedding: bool = False) -> Dict[str, "updated_at": memory.updated_at, "ttl": memory.ttl, "expires_at": memory.expires_at, - "confidence": memory.confidence, "upvotes": memory.upvotes, "downvotes": memory.downvotes, - "importance_score": memory.importance_score, "access_count": memory.access_count, "last_accessed_at": memory.last_accessed_at, "archived": memory.archived, @@ -74,10 +72,8 @@ def dict_to_memory(d: Dict[str, Any]) -> Memory: updated_at=d.get("updated_at", ""), ttl=d.get("ttl"), expires_at=d.get("expires_at"), - confidence=d.get("confidence", 1.0), upvotes=d.get("upvotes", 0), downvotes=d.get("downvotes", 0), - importance_score=d.get("importance_score", 1.0), access_count=d.get("access_count", 0), last_accessed_at=d.get("last_accessed_at"), archived=d.get("archived", False), diff --git a/src/lore/importance.py b/src/lore/importance.py deleted file mode 100644 index 65af747..0000000 --- a/src/lore/importance.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Importance scoring and adaptive decay functions. - -All functions are pure (no I/O, no side effects) except that -``time_adjusted_importance`` defaults ``now`` to ``datetime.utcnow()``. -""" - -from __future__ import annotations - -import math -from datetime import datetime -from typing import Dict, Optional, Tuple - -from lore.types import DECAY_HALF_LIVES, TIER_DECAY_HALF_LIVES, Memory - - -def compute_importance(memory: Memory) -> float: - """Compute base importance score from local signals. - - Formula: confidence * vote_factor * access_factor - """ - vote_factor = max(0.1, 1.0 + (memory.upvotes - memory.downvotes) * 0.1) - access_factor = 1.0 + math.log2(1 + memory.access_count) * 0.1 - return memory.confidence * vote_factor * access_factor - - -def decay_factor(age_days: float, half_life_days: float) -> float: - """Pure decay multiplier. Returns value in (0, 1].""" - return 0.5 ** (age_days / max(half_life_days, 0.001)) - - -def time_adjusted_importance( - memory: Memory, - half_life_days: float, - now: Optional[datetime] = None, -) -> float: - """Apply exponential decay to base importance score. - - Uses min(age_since_created, age_since_last_accessed) as the effective - age so that recently-accessed memories decay from their last access. - """ - now = now or datetime.utcnow() - created = datetime.fromisoformat(memory.created_at) - - if memory.last_accessed_at: - last_access = datetime.fromisoformat(memory.last_accessed_at) - age_days = min( - (now - created).total_seconds() / 86400, - (now - last_access).total_seconds() / 86400, - ) - else: - age_days = (now - created).total_seconds() / 86400 - - return memory.importance_score * decay_factor(age_days, half_life_days) - - -def resolve_half_life( - tier: Optional[str], - memory_type: str, - overrides: Optional[Dict[Tuple[str, str], float]] = None, -) -> float: - """Resolve half-life with fallback chain. - - Resolution order: - 1. overrides[(tier, type)] -- per-project config - 2. TIER_DECAY_HALF_LIVES[tier][type] -- tier+type specific - 3. TIER_DECAY_HALF_LIVES[tier]["default"] -- tier default - 4. DECAY_HALF_LIVES[type] -- legacy flat lookup (= long tier) - 5. 30.0 -- global default - """ - effective_tier = tier or "long" - - if overrides and (effective_tier, memory_type) in overrides: - return overrides[(effective_tier, memory_type)] - - tier_config = TIER_DECAY_HALF_LIVES.get(effective_tier, {}) - if memory_type in tier_config: - return tier_config[memory_type] - if "default" in tier_config: - return tier_config["default"] - - return DECAY_HALF_LIVES.get(memory_type, 30.0) diff --git a/src/lore/ingest/dedup.py b/src/lore/ingest/dedup.py index 8d01237..13832c6 100644 --- a/src/lore/ingest/dedup.py +++ b/src/lore/ingest/dedup.py @@ -65,7 +65,7 @@ def check( embedding=embedding, project=project, limit=5, - min_confidence=0.0, + min_score=0.0, ) for result in similar: if result.score >= self.threshold: diff --git a/src/lore/lore.py b/src/lore/lore.py index 6da9818..fca2891 100644 --- a/src/lore/lore.py +++ b/src/lore/lore.py @@ -16,15 +16,11 @@ from lore.classify.llm import LLMClassifier from lore.classify.rules import RuleBasedClassifier from lore.consolidation import ConsolidationResult +from lore.decay import decay_factor, resolve_half_life from lore.embed.base import Embedder from lore.embed.local import LocalEmbedder, make_code_embedder from lore.embed.router import EmbeddingRouter from lore.exceptions import MemoryNotFoundError, SecretBlockedError -from lore.importance import ( - compute_importance, - resolve_half_life, - time_adjusted_importance, -) from lore.recent import group_memories_by_project from lore.redact.pipeline import RedactionPipeline from lore.store.base import Store @@ -81,6 +77,28 @@ def _deserialize_embedding(data: bytes) -> np.ndarray: return np.array(struct.unpack(f"{count}f", data), dtype=np.float32) +def _memory_decay(memory, half_life_days: float, *, now=None) -> float: + """Recency decay multiplier for a memory. + + Replaces the prior ``time_adjusted_importance``: returns just the + decay factor (no importance_score multiplier — that column is gone). + Uses ``min(age_since_created, age_since_last_accessed)`` so + recently-accessed memories decay from their last access. + """ + from datetime import datetime as _dt + now = now or _dt.utcnow() + created = _dt.fromisoformat(memory.created_at) + if memory.last_accessed_at: + last_access = _dt.fromisoformat(memory.last_accessed_at) + age_days = min( + (now - created).total_seconds() / 86400, + (now - last_access).total_seconds() / 86400, + ) + else: + age_days = (now - created).total_seconds() / 86400 + return decay_factor(age_days, half_life_days) + + class _FnEmbedder(Embedder): """Wraps a user-provided embedding function as an Embedder.""" @@ -122,7 +140,7 @@ def __init__( dual_embedding: bool = False, api_url: Optional[str] = None, api_key: Optional[str] = None, - importance_threshold: float = 0.05, + decay_threshold: float = 0.05, decay_config: Optional[Dict[Tuple[str, str], float]] = None, tier_recall_weights: Optional[Dict[str, float]] = None, classify: bool = False, @@ -150,7 +168,7 @@ def __init__( self._half_lives: Dict[str, float] = {**DECAY_HALF_LIVES} if decay_half_lives: self._half_lives.update(decay_half_lives) - self._importance_threshold = importance_threshold + self._decay_threshold = decay_threshold self._decay_config = decay_config self._last_cleanup: float = 0.0 self._last_cleanup_count: int = 0 @@ -161,7 +179,7 @@ def __init__( warnings.warn( "decay_similarity_weight and decay_freshness_weight are deprecated " "and ignored. Scoring now uses multiplicative model: " - "score = cosine_similarity * time_adjusted_importance. " + "score = cosine_similarity * decay * tier_weight * graph_boost. " "Remove these parameters. They will be deleted in v0.7.0.", DeprecationWarning, stacklevel=2, @@ -365,7 +383,6 @@ def remember( source: Optional[str] = None, project: Optional[str] = None, ttl: Optional[int] = None, - confidence: float = 1.0, scope: Optional[str] = None, ) -> str: """Store a memory. Returns the memory ID (ULID). @@ -387,10 +404,6 @@ def remember( f"invalid memory type {type!r}, " f"must be one of: {', '.join(sorted(VALID_MEMORY_TYPES))}" ) - if not (0.0 <= confidence <= 1.0): - raise ValueError( - f"confidence must be between 0.0 and 1.0, got {confidence}" - ) # Security scan and redact before storage if self._redactor is not None: @@ -473,7 +486,6 @@ def remember( updated_at=now, ttl=effective_ttl, expires_at=expires_at, - confidence=confidence, scope=scope, ) self._store.save(memory) @@ -516,7 +528,7 @@ def remember( "Format as a bulleted list. Omit categories with nothing to report. Max 300 words.") def save_snapshot(self, content: str, *, title=None, session_id=None, tags=None): - """Save a session snapshot as a high-importance memory.""" + """Save a session snapshot.""" if not content or not content.strip(): raise ValueError("content must be non-empty") import uuid @@ -537,11 +549,8 @@ def save_snapshot(self, content: str, *, title=None, session_id=None, tags=None) logger.warning("Snapshot LLM extraction failed, saving raw", exc_info=True) all_tags = ["session_snapshot", session_id] + (tags or []) metadata = {"session_id": session_id, "title": title, "extraction_method": extraction_method} - memory_id = self.remember(content=content, type="session_snapshot", tier="long", context=context, tags=all_tags, metadata=metadata, confidence=1.0) + memory_id = self.remember(content=content, type="session_snapshot", tier="long", context=context, tags=all_tags, metadata=metadata) memory = self._store.get(memory_id) - if memory: - memory.importance_score = 0.95 - self._store.update(memory) return memory # ------------------------------------------------------------------ @@ -725,7 +734,7 @@ def recall( tier: Optional[str] = None, limit: int = 5, offset: int = 0, - min_confidence: float = 0.0, + min_score: float = 0.0, check_freshness: bool = False, repo_path: Optional[str] = None, user_id: Optional[str] = None, @@ -814,7 +823,7 @@ def recall( project=self.project, tier=tier, limit=limit, - min_confidence=min_confidence, + min_score=min_score, scope_mode=scope_mode, ) else: @@ -822,7 +831,7 @@ def recall( results = self._recall_local( query_vec, tags=tags, type=type, tier=tier, limit=limit, offset=offset, - min_confidence=min_confidence, + min_score=min_score, query_vecs=query_vecs, user_id=user_id, intent=intent, domain=domain, emotion=emotion, @@ -905,7 +914,7 @@ def _recall_local( tier: Optional[str] = None, limit: int = 5, offset: int = 0, - min_confidence: float = 0.0, + min_score: float = 0.0, query_vecs: Optional[Dict[str, List[float]]] = None, user_id: Optional[str] = None, intent: Optional[str] = None, @@ -964,12 +973,6 @@ def _recall_local( if tag_set.issubset(set(m.tags)) ] - # Filter by min_confidence - if min_confidence > 0.0: - all_memories = [ - m for m in all_memories if m.confidence >= min_confidence - ] - # Filter out memories without embeddings candidates = [m for m in all_memories if m.embedding] if not candidates: @@ -1010,7 +1013,10 @@ def _recall_local( depth=graph_depth, ) - # Multiplicative scoring: cosine_similarity * time_adjusted_importance * graph_boost + # Multiplicative scoring: cosine_similarity * time_decay * tier_weight * graph_boost. + # Per-memory importance was dropped in 025_drop_quality_score_columns; ranking now + # depends purely on (a) semantic similarity, (b) recency decay against tier-typed + # half-life, (c) tier weight, (d) optional graph proximity boost. results: List[RecallResult] = [] for i, memory in enumerate(candidates): # Pick cosine score matching the model that embedded this memory @@ -1027,10 +1033,10 @@ def _recall_local( memory.type, overrides=self._decay_config, ) - tai = time_adjusted_importance(memory, half_life, now=now) + decay = _memory_decay(memory, half_life, now=now) tier_weight = self._tier_weights.get(memory.tier, 1.0) graph_boost = self._compute_graph_boost(memory.id, graph_context) if graph_context else 1.0 - final_score = cosine_score * tai * tier_weight * graph_boost + final_score = cosine_score * decay * tier_weight * graph_boost results.append(RecallResult(memory=memory, score=final_score)) # Add graph-discovered memories not in vector results @@ -1049,14 +1055,18 @@ def _recall_local( mem_norm = mem_vec / max(float(np.linalg.norm(mem_vec)), 1e-9) cosine_score = float(query_norm @ mem_norm) half_life = resolve_half_life(mem.tier, mem.type, overrides=self._decay_config) - tai = time_adjusted_importance(mem, half_life, now=now) + decay = _memory_decay(mem, half_life, now=now) tier_weight = self._tier_weights.get(mem.tier, 1.0) graph_boost = self._compute_graph_boost(mid, graph_context) - final_score = cosine_score * tai * tier_weight * graph_boost + final_score = cosine_score * decay * tier_weight * graph_boost results.append(RecallResult(memory=mem, score=final_score)) results.sort(key=lambda r: r.score, reverse=True) + # Filter by min_score (post-scoring) + if min_score > 0.0: + results = [r for r in results if r.score >= min_score] + # Classification post-filter if intent or domain or emotion: results = [ @@ -1084,7 +1094,6 @@ def _recall_local( memory = r.memory memory.access_count += 1 memory.last_accessed_at = access_now - memory.importance_score = compute_importance(memory) self._store.update(memory) return top_results @@ -1392,7 +1401,7 @@ def get_consolidation_log( return self._store.get_consolidation_log(limit=limit, project=project) def upvote(self, memory_id: str) -> None: - """Increment upvotes for a memory and recompute importance.""" + """Increment upvotes for a memory.""" if hasattr(self._store, 'upvote'): self._store.upvote(memory_id) return @@ -1400,12 +1409,11 @@ def upvote(self, memory_id: str) -> None: if memory is None: raise MemoryNotFoundError(memory_id) memory.upvotes += 1 - memory.importance_score = compute_importance(memory) memory.updated_at = _utc_now_iso() self._store.update(memory) def downvote(self, memory_id: str) -> None: - """Increment downvotes for a memory and recompute importance.""" + """Increment downvotes for a memory.""" if hasattr(self._store, 'downvote'): self._store.downvote(memory_id) return @@ -1413,7 +1421,6 @@ def downvote(self, memory_id: str) -> None: if memory is None: raise MemoryNotFoundError(memory_id) memory.downvotes += 1 - memory.importance_score = compute_importance(memory) memory.updated_at = _utc_now_iso() self._store.update(memory) @@ -1558,16 +1565,19 @@ def reindex( # TTL Cleanup # ------------------------------------------------------------------ - def cleanup_expired(self, importance_threshold: Optional[float] = None) -> int: - """Remove expired memories AND memories below importance threshold.""" - threshold = importance_threshold if importance_threshold is not None else self._importance_threshold + def cleanup_expired(self, decay_threshold: Optional[float] = None) -> int: + """Remove expired memories AND memories whose decay multiplier is below threshold.""" + threshold = decay_threshold if decay_threshold is not None else self._decay_threshold now = datetime.now(timezone.utc) count = 0 # Phase 1: TTL/expiry cleanup count += self._store.cleanup_expired() - # Phase 2: Importance-based cleanup + # Phase 2: decay-based cleanup. Memories whose recency-decay + # multiplier falls below ``threshold`` (computed against the + # tier-typed half-life) are pruned. Replaces the prior + # importance-based cleanup; the importance_score column is gone. all_memories = self._store.list(limit=10000) to_delete = [] for memory in all_memories: @@ -1576,8 +1586,8 @@ def cleanup_expired(self, importance_threshold: Optional[float] = None) -> int: memory.type, overrides=self._decay_config, ) - tai = time_adjusted_importance(memory, half_life, now=now) - if tai < threshold: + decay = _memory_decay(memory, half_life, now=now) + if decay < threshold: to_delete.append(memory.id) for memory_id in to_delete: @@ -1586,18 +1596,6 @@ def cleanup_expired(self, importance_threshold: Optional[float] = None) -> int: return count - def recalculate_importance(self, project: Optional[str] = None) -> int: - """Recompute importance_score for all memories. Returns count updated.""" - memories = self._store.list(project=project, limit=100000) - count = 0 - for memory in memories: - new_score = compute_importance(memory) - if memory.importance_score != new_score: - memory.importance_score = new_score - self._store.update(memory) - count += 1 - return count - def _maybe_cleanup_expired(self) -> None: """Run cleanup_expired at most once per 60 seconds. diff --git a/src/lore/mcp/server.py b/src/lore/mcp/server.py index 7ca2ae3..28be03b 100644 --- a/src/lore/mcp/server.py +++ b/src/lore/mcp/server.py @@ -485,8 +485,7 @@ def recall( f"{cls_data.get('emotion', '?')}]" ) lines.append( - f"Memory {i} (importance: {mem.importance_score:.2f}, " - f"score: {r.score:.2f}, id: {mem.id}, " + f"Memory {i} (score: {r.score:.2f}, id: {mem.id}, " f"type: {mem.type}, tier: {mem.tier}){staleness_badge}{cls_badge}" ) lines.append(f"Content: {mem.content}") @@ -741,8 +740,7 @@ def list_memories( lines: List[str] = [f"Found {len(memories)} memory(ies):\n"] for mem in memories: lines.append( - f"[{mem.id}] ({mem.type}, importance: {mem.importance_score:.2f}) " - f"{mem.content[:100]}" + f"[{mem.id}] ({mem.type}) {mem.content[:100]}" ) if mem.tags: lines.append(f" Tags: {', '.join(mem.tags)}") @@ -1341,7 +1339,7 @@ def ingest( "Retrieve memories from this month+day across all years. " "USE THIS WHEN: you want to reflect on what happened on a specific date " "in past years, find anniversaries, or review historical context. " - "Returns memories grouped by year, sorted by importance. " + "Returns memories grouped by year. " "Defaults to today's date. Supports date window for fuzzy matching." ), ) diff --git a/src/lore/persistence/postgres.py b/src/lore/persistence/postgres.py index fb6622e..807a97b 100644 --- a/src/lore/persistence/postgres.py +++ b/src/lore/persistence/postgres.py @@ -124,7 +124,6 @@ def _row_to_stored(row: "asyncpg.Record") -> StoredMemory: content=row["content"], context=raw_context if raw_context else None, tags=tuple(tags or ()), - confidence=float(row["confidence"]) if row["confidence"] is not None else 0.5, source=row["source"], project=row["project"], created_at=row["created_at"], @@ -133,7 +132,6 @@ def _row_to_stored(row: "asyncpg.Record") -> StoredMemory: upvotes=row["upvotes"] or 0, downvotes=row["downvotes"] or 0, meta=dict(meta or {}), - importance_score=float(row["importance_score"]) if row["importance_score"] is not None else 1.0, access_count=row["access_count"] or 0, last_accessed_at=row["last_accessed_at"], scope=scope_val if scope_val else "project", @@ -381,7 +379,6 @@ def _row_to_exported_memory(row: "asyncpg.Record") -> ExportedMemory: content=row["content"], context=row["context"] if row["context"] else None, tags=tuple(tags or ()), - confidence=float(row["confidence"]), source=row["source"], project=row["project"], embedding=embedding if embedding is not None else None, @@ -552,12 +549,12 @@ async def insert_memory(self, memory: NewMemory) -> StoredMemory: row = await conn.fetchrow( """ INSERT INTO memories - (id, org_id, content, context, tags, confidence, source, - project, embedding, expires_at, meta, scope, importance_score) - VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9::vector, $10, $11::jsonb, $12, $13) - RETURNING id, org_id, content, context, tags, confidence, source, + (id, org_id, content, context, tags, source, + project, embedding, expires_at, meta, scope) + VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8::vector, $9, $10::jsonb, $11) + RETURNING id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope """, memory_id, @@ -565,14 +562,12 @@ async def insert_memory(self, memory: NewMemory) -> StoredMemory: memory.content, memory.context or "", # context is NOT NULL in the schema; coerce None to "" json.dumps(list(memory.tags)), - memory.confidence, memory.source, memory.project, json.dumps(list(memory.embedding)), memory.expires_at, json.dumps(dict(memory.meta)), memory.scope, - memory.importance_score if memory.importance_score is not None else memory.confidence, ) return _row_to_stored(row) @@ -580,9 +575,9 @@ async def get_memory(self, org_id: str, memory_id: str) -> Optional[StoredMemory async with self._acquire() as conn: row = await conn.fetchrow( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE id = $1 @@ -643,9 +638,9 @@ async def update_memory( f"SET {', '.join(sets)} " "WHERE id = $1 AND org_id = $2 " "AND (expires_at IS NULL OR expires_at > now()) " - "RETURNING id, org_id, content, context, tags, confidence, source, " + "RETURNING id, org_id, content, context, tags, source, " "project, created_at, updated_at, expires_at, upvotes, downvotes, " - "meta, importance_score, access_count, last_accessed_at, scope" + "meta, access_count, last_accessed_at, scope" ) async with self._acquire() as conn: row = await conn.fetchrow(sql, *params) @@ -690,9 +685,9 @@ async def list_memories( where.append("(expires_at IS NULL OR expires_at > now())") sql = ( - "SELECT id, org_id, content, context, tags, confidence, source, " + "SELECT id, org_id, content, context, tags, source, " "project, created_at, updated_at, expires_at, upvotes, downvotes, " - "meta, importance_score, access_count, last_accessed_at, scope " + "meta, access_count, last_accessed_at, scope " "FROM memories " f"WHERE {' AND '.join(where)} " "ORDER BY created_at DESC" @@ -753,9 +748,9 @@ async def list_memories_paginated( count_sql = f"SELECT COUNT(*) FROM memories WHERE {where_sql}" select_sql = ( - "SELECT id, org_id, content, context, tags, confidence, source, " + "SELECT id, org_id, content, context, tags, source, " "project, created_at, updated_at, expires_at, upvotes, downvotes, " - "meta, importance_score, access_count, last_accessed_at, scope " + "meta, access_count, last_accessed_at, scope " f"FROM memories WHERE {where_sql} " f"ORDER BY created_at DESC " f"LIMIT ${limit_idx} OFFSET ${offset_idx}" @@ -824,7 +819,6 @@ async def upsert_memory_with_embedding( content: str, context: Optional[str], tags: Sequence[str], - confidence: float, source: Optional[str], project: Optional[str], embedding: Optional[Sequence[float]], @@ -847,16 +841,15 @@ async def upsert_memory_with_embedding( query = """ INSERT INTO memories - (id, org_id, content, context, tags, confidence, source, project, + (id, org_id, content, context, tags, source, project, embedding, created_at, updated_at, expires_at, upvotes, downvotes, meta) - VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9::vector, now(), now(), - $10, $11, $12, $13::jsonb) + VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8::vector, now(), now(), + $9, $10, $11, $12::jsonb) ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, context = EXCLUDED.context, tags = EXCLUDED.tags, - confidence = EXCLUDED.confidence, source = EXCLUDED.source, project = EXCLUDED.project, embedding = EXCLUDED.embedding, @@ -877,7 +870,6 @@ async def upsert_memory_with_embedding( content, safe_context, encoded_tags, - confidence, source, project, encoded_embedding, @@ -922,11 +914,10 @@ async def recall_by_embedding( limit_idx = len(sql_params) sql = f""" - SELECT id, org_id, content, context, tags, confidence, source, project, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, downvotes, meta, - importance_score, access_count, last_accessed_at, scope, + access_count, last_accessed_at, scope, (1 - (embedding <=> ${emb_idx}::vector)) * - COALESCE(importance_score, 1.0) * power(0.5, LEAST( EXTRACT(EPOCH FROM (now() - created_at)) / 86400.0, @@ -955,7 +946,6 @@ async def recall_by_embedding( content=sm.content, context=sm.context, tags=sm.tags, - confidence=sm.confidence, source=sm.source, project=sm.project, created_at=sm.created_at, @@ -964,7 +954,6 @@ async def recall_by_embedding( upvotes=sm.upvotes, downvotes=sm.downvotes, meta=sm.meta, - importance_score=sm.importance_score, access_count=sm.access_count, last_accessed_at=sm.last_accessed_at, scope=sm.scope, @@ -992,10 +981,7 @@ async def bump_access_counts(self, org_id: str, memory_ids: Sequence[str]) -> No """ UPDATE memories SET access_count = COALESCE(access_count, 0) + 1, - last_accessed_at = now(), - importance_score = COALESCE(confidence, 1.0) - * GREATEST(0.1, 1.0 + (COALESCE(upvotes, 0) - COALESCE(downvotes, 0)) * 0.1) - * (1.0 + ln(COALESCE(access_count, 0) + 2) / ln(2) * 0.1) + last_accessed_at = now() WHERE id = ANY($1) AND org_id = $2 """, list(memory_ids), @@ -1040,9 +1026,9 @@ async def vote_memory( SET {column} = COALESCE({column}, 0) + 1, updated_at = now() WHERE id = $1 AND org_id = $2 - RETURNING id, org_id, content, context, tags, confidence, source, + RETURNING id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope """, memory_id, @@ -1062,15 +1048,14 @@ async def import_extracted_memory( tags: "Sequence[str]", source: str, meta: "Mapping[str, Any]", - confidence: float, ) -> bool: async with self._acquire() as conn: result = await conn.execute( """ INSERT INTO memories - (id, org_id, content, context, tags, source, meta, confidence, + (id, org_id, content, context, tags, source, meta, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb, $8, now(), now()) + VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb, now(), now()) ON CONFLICT (id) DO NOTHING """, memory_id, @@ -1080,7 +1065,6 @@ async def import_extracted_memory( json.dumps(list(tags)), source, json.dumps(dict(meta)), - confidence, ) return result.endswith(" 1") @@ -1358,10 +1342,10 @@ async def list_memories_without_mentions( params.append(limit) sql = f""" SELECT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM memories m LEFT JOIN entity_mentions em ON em.memory_id = m.id WHERE {' AND '.join(where)} @@ -1718,10 +1702,6 @@ async def get_graph_stats( "SELECT COUNT(*) FROM memories WHERE project = $1 AND created_at >= $2", project, cutoff_7d, ) - avg_imp = await conn.fetchval( - "SELECT AVG(COALESCE(importance_score, 1.0)) FROM memories WHERE project = $1", - project, - ) oldest = await conn.fetchval( "SELECT MIN(created_at) FROM memories WHERE project = $1", project, ) @@ -1745,9 +1725,6 @@ async def get_graph_stats( recent_7d = await conn.fetchval( "SELECT COUNT(*) FROM memories WHERE created_at >= $1", cutoff_7d, ) - avg_imp = await conn.fetchval( - "SELECT AVG(COALESCE(importance_score, 1.0)) FROM memories" - ) oldest = await conn.fetchval("SELECT MIN(created_at) FROM memories") newest = await conn.fetchval("SELECT MAX(created_at) FROM memories") type_rows = await conn.fetch( @@ -1792,7 +1769,6 @@ async def get_graph_stats( by_project=by_project, by_entity_type=by_entity_type, top_entities=top_entities, - avg_importance=round(float(avg_imp or 0), 3), recent_24h=recent_24h or 0, recent_7d=recent_7d or 0, oldest_memory=oldest, @@ -1848,10 +1824,10 @@ async def get_memories_by_entities( params.append(limit) sql = f""" SELECT DISTINCT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM entity_mentions em JOIN memories m ON m.id = em.memory_id WHERE {' AND '.join(where)} @@ -1872,13 +1848,13 @@ async def search_memories_text( async with self._acquire() as conn: rows = await conn.fetch( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE content ILIKE $1 - ORDER BY importance_score DESC NULLS LAST, created_at DESC + ORDER BY created_at DESC LIMIT $2 """, pattern, @@ -1932,9 +1908,9 @@ async def recall_by_text( limit_idx = len(sql_params) sql = f""" - SELECT id, org_id, content, context, tags, confidence, source, project, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, downvotes, meta, - importance_score, access_count, last_accessed_at, scope, + access_count, last_accessed_at, scope, ts_rank( to_tsvector('english', content || ' ' || COALESCE(context, '')), plainto_tsquery('english', ${q_idx}) @@ -1987,17 +1963,17 @@ async def recall_by_entities( sql_params.append(limit) limit_idx = len(sql_params) sql = f""" - SELECT m.id, m.org_id, m.content, m.context, m.tags, m.confidence, + SELECT m.id, m.org_id, m.content, m.context, m.tags, m.source, m.project, m.created_at, m.updated_at, m.expires_at, - m.upvotes, m.downvotes, m.meta, m.importance_score, + m.upvotes, m.downvotes, m.meta, m.access_count, m.last_accessed_at, m.scope, COUNT(DISTINCT em.entity_id) AS overlap_count FROM entity_mentions em JOIN memories m ON m.id = em.memory_id WHERE {' AND '.join(where)} - GROUP BY m.id, m.org_id, m.content, m.context, m.tags, m.confidence, + GROUP BY m.id, m.org_id, m.content, m.context, m.tags, m.source, m.project, m.created_at, m.updated_at, m.expires_at, - m.upvotes, m.downvotes, m.meta, m.importance_score, + m.upvotes, m.downvotes, m.meta, m.access_count, m.last_accessed_at, m.scope ORDER BY overlap_count DESC, m.created_at DESC LIMIT ${limit_idx} @@ -2484,16 +2460,11 @@ async def record_memory_access( UPDATE memories SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = now(), - importance_score = ( - confidence - * GREATEST(0.1, 1.0 + (upvotes - downvotes) * 0.1) - * (1.0 + ln(COALESCE(access_count, 0) + 2) / ln(2) * 0.1) - ), updated_at = now() WHERE id = $1 AND org_id = $2 - RETURNING id, org_id, content, context, tags, confidence, source, + RETURNING id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope """, memory_id, @@ -2528,9 +2499,9 @@ async def list_recent_session_snapshots( limit_idx = len(params) sql = ( - "SELECT id, org_id, content, context, tags, confidence, source, " + "SELECT id, org_id, content, context, tags, source, " "project, created_at, updated_at, expires_at, upvotes, downvotes, " - "meta, importance_score, access_count, last_accessed_at, scope " + "meta, access_count, last_accessed_at, scope " "FROM memories " f"WHERE {' AND '.join(where)} " f"ORDER BY created_at DESC LIMIT ${limit_idx}" @@ -2638,7 +2609,7 @@ async def list_candidate_memories_for_recommendation( SELECT id, content, embedding, meta, created_at, access_count, last_accessed_at FROM memories WHERE org_id = $1 AND embedding IS NOT NULL - ORDER BY importance_score DESC NULLS LAST + ORDER BY created_at DESC LIMIT $2 """, org_id, @@ -3989,10 +3960,10 @@ async def list_memories_at_time( params.append(limit) sql = f""" SELECT DISTINCT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM memories m {joins} WHERE {' AND '.join(where)} @@ -4030,9 +4001,9 @@ async def list_timeline_around( async with self._acquire() as conn: anchor_row = await conn.fetchrow( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE id = $1 AND org_id = $2 @@ -4052,9 +4023,9 @@ async def list_timeline_around( async def _fetch(predicate_sql: str, order_sql: str, n: int) -> list[StoredMemory]: sql = f""" - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE org_id = $1 diff --git a/src/lore/persistence/protocol.py b/src/lore/persistence/protocol.py index e367031..a6d8fee 100644 --- a/src/lore/persistence/protocol.py +++ b/src/lore/persistence/protocol.py @@ -119,7 +119,7 @@ async def expire_memories(self) -> int: ... async def bump_access_counts(self, org_id: str, memory_ids: Sequence[str]) -> None: - """Increment access_count + last_accessed_at + recompute importance_score.""" + """Increment access_count + last_accessed_at.""" ... async def vote_memory( @@ -142,7 +142,6 @@ async def import_extracted_memory( tags: Sequence[str], source: str, meta: Mapping[str, Any], - confidence: float, ) -> bool: """Insert a pre-extracted memory with a caller-supplied id; returns True if inserted, False if duplicate.""" ... @@ -167,7 +166,6 @@ async def upsert_memory_with_embedding( content: str, context: Optional[str], tags: Sequence[str], - confidence: float, source: Optional[str], project: Optional[str], embedding: Optional[Sequence[float]], diff --git a/src/lore/persistence/sqlite.py b/src/lore/persistence/sqlite.py index 680d51e..5070d32 100644 --- a/src/lore/persistence/sqlite.py +++ b/src/lore/persistence/sqlite.py @@ -306,7 +306,6 @@ def _row_to_exported(row, embedding: Optional[list[float]]) -> ExportedMemory: content=row["content"], context=raw_context if raw_context else None, tags=tuple(tags or ()), - confidence=float(row["confidence"]) if row["confidence"] is not None else 0.5, source=row["source"], project=row["project"], embedding=embedding, @@ -344,7 +343,6 @@ def _row_to_memory(row) -> StoredMemory: content=row["content"], context=raw_context if raw_context else None, tags=tuple(tags or ()), - confidence=float(row["confidence"]) if row["confidence"] is not None else 0.5, source=row["source"], project=row["project"], created_at=_parse_iso(row["created_at"]), @@ -353,7 +351,6 @@ def _row_to_memory(row) -> StoredMemory: upvotes=row["upvotes"] or 0, downvotes=row["downvotes"] or 0, meta=dict(meta or {}), - importance_score=float(row["importance_score"]) if row["importance_score"] is not None else 1.0, access_count=row["access_count"] or 0, last_accessed_at=_parse_iso(row["last_accessed_at"]), scope=scope_val if scope_val else "project", @@ -1082,9 +1079,9 @@ async def insert_memory(self, memory: "NewMemory") -> "StoredMemory": cursor = await tx.execute( """ INSERT INTO memories - (id, org_id, content, context, tags, confidence, source, - project, expires_at, meta, scope, importance_score) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (id, org_id, content, context, tags, source, + project, expires_at, meta, scope) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( memory_id, @@ -1092,13 +1089,11 @@ async def insert_memory(self, memory: "NewMemory") -> "StoredMemory": memory.content, memory.context or "", # NOT NULL in PG schema; mirror json.dumps(list(memory.tags)), - memory.confidence, memory.source, memory.project, memory.expires_at.isoformat() if memory.expires_at else None, json.dumps(dict(memory.meta)), memory.scope, - memory.importance_score if memory.importance_score is not None else memory.confidence, ), ) rowid = cursor.lastrowid @@ -1111,9 +1106,9 @@ async def insert_memory(self, memory: "NewMemory") -> "StoredMemory": async with tx.execute( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE rowid = ? """, @@ -1136,9 +1131,9 @@ async def get_memory(self, org_id: str, memory_id: str) -> Optional["StoredMemor async with self._acquire() as conn: async with conn.execute( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE id = ? @@ -1181,9 +1176,9 @@ async def delete_memory(self, org_id: str, memory_id: str) -> bool: # ── MemoryOps: rest of the slice (Phase 3D) ─────────────────────── _MEMORY_COLS = ( - "id, org_id, content, context, tags, confidence, source, " + "id, org_id, content, context, tags, source, " "project, created_at, updated_at, expires_at, upvotes, " - "downvotes, meta, importance_score, access_count, last_accessed_at, " + "downvotes, meta, access_count, last_accessed_at, " "scope" ) @@ -1396,17 +1391,11 @@ async def bump_access_counts( org_id: str, memory_ids: Sequence[str], ) -> None: - """Atomically bump access_count + last_accessed_at + importance_score. + """Atomically bump access_count + last_accessed_at. Mirrors ``PostgresStore.bump_access_counts``: increments - ``access_count``, sets ``last_accessed_at = now()``, and recomputes - ``importance_score`` from confidence, vote delta, and the (slightly - damped) log of the new access count. - - Translation notes: - * PG ``GREATEST(0.1, x)`` → SQLite ``MAX(0.1, x)``. - * SQLite has ``ln`` since 3.35; the formula matches PG verbatim. - * Cross-org isolation is preserved by the WHERE clause. + ``access_count`` and sets ``last_accessed_at = now()``. The prior + ``importance_score`` recomputation was removed in 025_drop_quality_score_columns. """ if not memory_ids: return @@ -1414,10 +1403,7 @@ async def bump_access_counts( sql = ( "UPDATE memories SET " "access_count = COALESCE(access_count, 0) + 1, " - "last_accessed_at = datetime('now'), " - "importance_score = COALESCE(confidence, 1.0) " - " * MAX(0.1, 1.0 + (COALESCE(upvotes, 0) - COALESCE(downvotes, 0)) * 0.1) " - " * (1.0 + ln(COALESCE(access_count, 0) + 2) / ln(2) * 0.1) " + "last_accessed_at = datetime('now') " f"WHERE id IN ({placeholders}) AND org_id = ?" ) params: list[Any] = list(memory_ids) @@ -1512,7 +1498,7 @@ async def list_memories_with_embeddings( # TEXT" — guard with CASE so LEFT JOIN misses surface as NULL. sql = ( "SELECT m.id, m.org_id, m.content, m.context, m.tags, " - "m.confidence, m.source, m.project, m.created_at, m.updated_at, " + "m.source, m.project, m.created_at, m.updated_at, " "m.expires_at, m.upvotes, m.downvotes, m.meta, " "CASE WHEN v.embedding IS NULL THEN NULL " " ELSE vec_to_json(v.embedding) END AS embedding_json " @@ -1537,7 +1523,7 @@ async def recall_by_embedding( Mirrors PG's ``recall_by_embedding``: - ``score = (1 - cosine_distance) * importance_score + ``score = (1 - cosine_distance) * 0.5 ^ ( min(days_since_created, days_since_last_accessed) / half_life_days )`` @@ -1594,14 +1580,13 @@ async def recall_by_embedding( # virtual constraint syntax. We thread it as a bind param. sql = f""" SELECT - m.id, m.org_id, m.content, m.context, m.tags, m.confidence, + m.id, m.org_id, m.content, m.context, m.tags, m.source, m.project, m.created_at, m.updated_at, m.expires_at, - m.upvotes, m.downvotes, m.meta, m.importance_score, + m.upvotes, m.downvotes, m.meta, m.access_count, m.last_accessed_at, m.scope, v.distance AS distance, ( (1.0 - v.distance) - * COALESCE(m.importance_score, 1.0) * pow( 0.5, MIN( @@ -1643,7 +1628,6 @@ async def recall_by_embedding( content=sm.content, context=sm.context, tags=sm.tags, - confidence=sm.confidence, source=sm.source, project=sm.project, created_at=sm.created_at, @@ -1652,7 +1636,6 @@ async def recall_by_embedding( upvotes=sm.upvotes, downvotes=sm.downvotes, meta=sm.meta, - importance_score=sm.importance_score, access_count=sm.access_count, last_accessed_at=sm.last_accessed_at, scope=sm.scope, @@ -1669,7 +1652,6 @@ async def upsert_memory_with_embedding( content: str, context: Optional[str], tags: Sequence[str], - confidence: float, source: Optional[str], project: Optional[str], embedding: Optional[Sequence[float]], @@ -1714,10 +1696,10 @@ async def upsert_memory_with_embedding( cursor = await tx.execute( """ INSERT INTO memories - (id, org_id, content, context, tags, confidence, + (id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, downvotes, meta) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), + VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ?, ?, ?, ?) """, ( @@ -1726,7 +1708,6 @@ async def upsert_memory_with_embedding( content, safe_context, encoded_tags, - confidence, source, project, expires_iso, @@ -1754,7 +1735,6 @@ async def upsert_memory_with_embedding( content = ?, context = ?, tags = ?, - confidence = ?, source = ?, project = ?, updated_at = datetime('now'), @@ -1768,7 +1748,6 @@ async def upsert_memory_with_embedding( content, safe_context, encoded_tags, - confidence, source, project, expires_iso, @@ -1804,7 +1783,6 @@ async def import_extracted_memory( tags: "Sequence[str]", source: str, meta: "Mapping[str, Any]", - confidence: float, ) -> bool: """INSERT … ON CONFLICT (id) DO NOTHING; returns True if inserted. @@ -1821,8 +1799,8 @@ async def import_extracted_memory( """ INSERT INTO memories (id, org_id, content, context, tags, source, meta, - confidence, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) + created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) ON CONFLICT (id) DO NOTHING """, ( @@ -1833,7 +1811,6 @@ async def import_extracted_memory( encoded_tags, source, encoded_meta, - confidence, ), ) inserted = cursor.rowcount == 1 @@ -1923,10 +1900,8 @@ async def record_memory_access( """Increment access counters and return the updated memory, or None. Mirrors ``PostgresStore.record_memory_access``: bumps - ``access_count`` by 1, sets ``last_accessed_at = now()``, and - recomputes ``importance_score`` from confidence, vote delta, and - the (slightly damped) log of the new access count. Returns the - updated row, or None if (id, org_id) does not match. + ``access_count`` by 1 and sets ``last_accessed_at = now()``. + Returns the updated row, or None if (id, org_id) does not match. SQLite has no ``UPDATE … RETURNING`` (added in 3.35; aiosqlite's wrapper doesn't expose it everywhere), so we issue an UPDATE and @@ -1937,9 +1912,6 @@ async def record_memory_access( "UPDATE memories SET " "access_count = COALESCE(access_count, 0) + 1, " "last_accessed_at = datetime('now'), " - "importance_score = COALESCE(confidence, 1.0) " - " * MAX(0.1, 1.0 + (COALESCE(upvotes, 0) - COALESCE(downvotes, 0)) * 0.1) " - " * (1.0 + ln(COALESCE(access_count, 0) + 2) / ln(2) * 0.1), " "updated_at = datetime('now') " "WHERE id = ? AND org_id = ?" ) @@ -3106,17 +3078,17 @@ async def list_candidate_memories_for_recommendation( self, org_id: str, *, limit: int = 500, ) -> Sequence[RecommendationCandidate]: """List candidate memories (memories with embeddings) for the - recommendation engine, ordered by ``importance_score`` DESC NULLS LAST. + recommendation engine, ordered by ``created_at`` DESC. Translation: PG selects ``embedding`` directly from ``memories``; SQLite stores embeddings in the ``memory_vectors`` vec0 virtual table joined by ``memory_rowid``. Memories without a vec0 row are excluded (mirrors PG's ``embedding IS NOT NULL`` filter). - ``ORDER BY importance_score DESC NULLS LAST``: SQLite's NULL - ordering is opposite to PG's (NULLs sort first by default with - ``DESC``), so we use ``CASE WHEN ... IS NULL THEN 1 ELSE 0 END`` - as a primary sort key to match PG's ``NULLS LAST`` semantics. + Recency-first ordering replaces the prior ``importance_score`` + ordering — that column was dropped in + 025_drop_quality_score_columns.sql. ``RecommendationEngine`` + re-scores candidates anyway via signal weighting. """ sql = ( "SELECT m.id, m.content, m.meta, m.created_at, " @@ -3125,8 +3097,7 @@ async def list_candidate_memories_for_recommendation( "FROM memories m " "INNER JOIN memory_vectors v ON v.memory_rowid = m.rowid " "WHERE m.org_id = ? " - "ORDER BY CASE WHEN m.importance_score IS NULL THEN 1 ELSE 0 END, " - " m.importance_score DESC " + "ORDER BY m.created_at DESC " "LIMIT ?" ) async with self._acquire() as conn: @@ -4699,10 +4670,10 @@ async def list_memories_without_mentions( params.append(limit) sql = f""" SELECT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM memories m LEFT JOIN entity_mentions em ON em.memory_id = m.id WHERE {' AND '.join(where)} @@ -5165,13 +5136,6 @@ async def get_graph_stats( row = await cur.fetchone() recent_7d = int(row["n"]) if row and row["n"] is not None else 0 - async with conn.execute( - "SELECT AVG(COALESCE(importance_score, 1.0)) AS v FROM memories WHERE project = ?", - (project,), - ) as cur: - row = await cur.fetchone() - avg_imp = row["v"] if row else None - async with conn.execute( "SELECT MIN(created_at) AS v FROM memories WHERE project = ?", (project,), @@ -5219,12 +5183,6 @@ async def get_graph_stats( row = await cur.fetchone() recent_7d = int(row["n"]) if row and row["n"] is not None else 0 - async with conn.execute( - "SELECT AVG(COALESCE(importance_score, 1.0)) AS v FROM memories" - ) as cur: - row = await cur.fetchone() - avg_imp = row["v"] if row else None - async with conn.execute( "SELECT MIN(created_at) AS v FROM memories" ) as cur: @@ -5293,7 +5251,6 @@ async def get_graph_stats( by_project=by_project, by_entity_type=by_entity_type, top_entities=top_entities, - avg_importance=round(float(avg_imp or 0), 3), recent_24h=recent_24h, recent_7d=recent_7d, oldest_memory=oldest, @@ -5367,10 +5324,10 @@ async def get_memories_by_entities( sql = f""" SELECT DISTINCT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM entity_mentions em JOIN memories m ON m.id = em.memory_id WHERE {' AND '.join(where)} @@ -5400,13 +5357,13 @@ async def search_memories_text( async with self._acquire() as conn: async with conn.execute( """ - SELECT id, org_id, content, context, tags, confidence, source, + SELECT id, org_id, content, context, tags, source, project, created_at, updated_at, expires_at, upvotes, - downvotes, meta, importance_score, access_count, + downvotes, meta, access_count, last_accessed_at, scope FROM memories WHERE LOWER(content) LIKE ? - ORDER BY (importance_score IS NULL), importance_score DESC, created_at DESC + ORDER BY created_at DESC LIMIT ? """, (like_pattern, limit), @@ -5487,9 +5444,9 @@ async def recall_by_text( sql_params.append(limit) sql = f""" - SELECT m.id, m.org_id, m.content, m.context, m.tags, m.confidence, + SELECT m.id, m.org_id, m.content, m.context, m.tags, m.source, m.project, m.created_at, m.updated_at, m.expires_at, - m.upvotes, m.downvotes, m.meta, m.importance_score, + m.upvotes, m.downvotes, m.meta, m.access_count, m.last_accessed_at, m.scope, -bm25(memories_fts) AS fts_rank FROM memories_fts @@ -5544,9 +5501,9 @@ async def recall_by_entities( where.append("m.project = ?") params_tail.append(project) sql = f""" - SELECT m.id, m.org_id, m.content, m.context, m.tags, m.confidence, + SELECT m.id, m.org_id, m.content, m.context, m.tags, m.source, m.project, m.created_at, m.updated_at, m.expires_at, - m.upvotes, m.downvotes, m.meta, m.importance_score, + m.upvotes, m.downvotes, m.meta, m.access_count, m.last_accessed_at, m.scope, COUNT(DISTINCT em.entity_id) AS overlap_count FROM entity_mentions em @@ -5915,10 +5872,10 @@ async def list_memories_at_time( params.append(limit) sql = f""" SELECT DISTINCT m.id, m.org_id, m.content, m.context, m.tags, - m.confidence, m.source, m.project, + m.source, m.project, m.created_at, m.updated_at, m.expires_at, m.upvotes, m.downvotes, m.meta, - m.importance_score, m.access_count, m.last_accessed_at + m.access_count, m.last_accessed_at FROM memories m {joins} WHERE {' AND '.join(where)} diff --git a/src/lore/persistence/types.py b/src/lore/persistence/types.py index 9a960bd..ea1f467 100644 --- a/src/lore/persistence/types.py +++ b/src/lore/persistence/types.py @@ -14,7 +14,6 @@ class NewMemory: embedding: Sequence[float] context: Optional[str] = None tags: Sequence[str] = () - confidence: float = 0.5 source: Optional[str] = None project: Optional[str] = None expires_at: Optional[datetime] = None @@ -23,7 +22,6 @@ class NewMemory: # default; recall applies (scope='global') OR (scope='project' AND # project = current_project). scope: str = "project" - importance_score: Optional[float] = None @dataclass(frozen=True, slots=True) @@ -57,7 +55,6 @@ class StoredMemory: content: str context: Optional[str] tags: Sequence[str] - confidence: float source: Optional[str] project: Optional[str] created_at: datetime @@ -66,7 +63,6 @@ class StoredMemory: upvotes: int downvotes: int meta: Mapping[str, Any] - importance_score: float access_count: int last_accessed_at: Optional[datetime] # Phase 6G: scope is read from the column with NOT NULL DEFAULT 'project', @@ -104,7 +100,6 @@ class MemoryPatch: content: Optional[str] = None context: Optional[str] = None tags: Optional[Sequence[str]] = None - confidence: Optional[float] = None source: Optional[str] = None project: Optional[str] = None expires_at: Optional[datetime] = None @@ -215,7 +210,6 @@ class GraphStats: by_project: Mapping[str, int] by_entity_type: Mapping[str, int] top_entities: Sequence[Mapping[str, Any]] - avg_importance: float recent_24h: int recent_7d: int oldest_memory: Optional[datetime] @@ -508,7 +502,6 @@ class ExportedMemory: content: str context: Optional[str] tags: Sequence[str] - confidence: float source: Optional[str] project: Optional[str] embedding: Optional[Sequence[float]] diff --git a/src/lore/recent.py b/src/lore/recent.py index 7895bbc..53255f5 100644 --- a/src/lore/recent.py +++ b/src/lore/recent.py @@ -74,7 +74,7 @@ def format_detailed(result: RecentActivityResult) -> str: for m in group.memories: ts = _format_time(m.created_at) prefix = "[Session Snapshot] " if m.type == "session_snapshot" else "" - lines.append(f"**[{ts}] {prefix}{m.type}** (tier: {m.tier}, importance: {m.importance_score:.2f})") + lines.append(f"**[{ts}] {prefix}{m.type}** (tier: {m.tier})") lines.append(m.content) if m.tags: lines.append(f"Tags: {', '.join(m.tags)}") @@ -96,7 +96,6 @@ def format_structured(result: RecentActivityResult) -> Dict[str, Any]: "tier": m.tier, "created_at": m.created_at, "tags": m.tags, - "importance_score": m.importance_score, } for m in g.memories ], diff --git a/src/lore/retention.py b/src/lore/retention.py index a759121..8934344 100644 --- a/src/lore/retention.py +++ b/src/lore/retention.py @@ -1,4 +1,4 @@ -"""Policy-based retention — find and remove stale, low-importance memories.""" +"""Policy-based retention — find and remove stale memories.""" from __future__ import annotations @@ -21,7 +21,6 @@ class RetentionPolicy: """Declarative retention policy.""" max_age_days: int = 90 - min_importance_score: float = 0.3 archive_on_expire: bool = False dry_run: bool = False @@ -37,7 +36,7 @@ class RetentionResult: def _find_expired(lore: "Lore", policy: RetentionPolicy) -> List["Memory"]: - """Return memories that exceed *max_age_days* and fall below *min_importance_score*.""" + """Return memories older than *max_age_days*.""" cutoff = datetime.now(timezone.utc) - timedelta(days=policy.max_age_days) memories = lore.list_memories() expired: List["Memory"] = [] @@ -50,7 +49,7 @@ def _find_expired(lore: "Lore", policy: RetentionPolicy) -> List["Memory"]: created = created.replace(tzinfo=timezone.utc) except (ValueError, TypeError): continue - if created < cutoff and m.importance_score < policy.min_importance_score: + if created < cutoff: expired.append(m) return expired @@ -67,7 +66,6 @@ def _archive_memories(memories: List["Memory"], output_path: str) -> int: "tags": m.tags, "project": m.project, "source": m.source, - "importance_score": m.importance_score, "created_at": m.created_at, "metadata": m.metadata, }) diff --git a/src/lore/server/models.py b/src/lore/server/models.py index 917e77a..f6150b7 100644 --- a/src/lore/server/models.py +++ b/src/lore/server/models.py @@ -21,7 +21,6 @@ class LessonCreateRequest(BaseModel): resolution: str = Field(..., min_length=1) context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float = Field(default=0.5, ge=0.0, le=1.0) source: Optional[str] = None project: Optional[str] = None embedding: Optional[List[float]] = Field(default=None) @@ -56,7 +55,6 @@ class LessonResponse(BaseModel): resolution: str context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float source: Optional[str] = None project: Optional[str] = None created_at: datetime @@ -76,7 +74,6 @@ class LessonUpdateRequest(BaseModel): All fields optional. upvotes/downvotes can be "+1"/"-1" for atomic increment. """ - confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0) tags: Optional[List[str]] = None upvotes: Optional[Union[str, int]] = None downvotes: Optional[Union[str, int]] = None @@ -115,7 +112,6 @@ class LessonExportItem(BaseModel): resolution: str context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float source: Optional[str] = None project: Optional[str] = None embedding: Optional[List[float]] = None @@ -141,7 +137,6 @@ class LessonImportItem(BaseModel): resolution: str = Field(..., min_length=1) context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float = Field(default=0.5, ge=0.0, le=1.0) source: Optional[str] = None project: Optional[str] = None embedding: List[float] = Field(..., min_length=384, max_length=384) @@ -173,7 +168,7 @@ class LessonSearchRequest(BaseModel): tags: Optional[List[str]] = None project: Optional[str] = None limit: int = Field(default=5, ge=1, le=50) - min_confidence: float = Field(default=0.0, ge=0.0, le=1.0) + min_score: float = Field(default=0.0, ge=0.0, le=1.0) # Phase 6G: scope predicate selector. 'default' applies the standard # ``(scope='global') OR (scope='project' AND project=:current)`` # predicate; 'all' skips it entirely. @@ -208,7 +203,6 @@ class MemoryCreateRequest(BaseModel): content: str = Field(..., min_length=1) context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float = Field(default=0.5, ge=0.0, le=1.0) source: Optional[str] = None project: Optional[str] = None embedding: Optional[List[float]] = Field(default=None) @@ -241,7 +235,6 @@ class MemoryResponse(BaseModel): content: str context: Optional[str] = None tags: List[str] = Field(default_factory=list) - confidence: float source: Optional[str] = None project: Optional[str] = None created_at: datetime @@ -257,7 +250,6 @@ class MemoryResponse(BaseModel): class MemoryUpdateRequest(BaseModel): """Request body for PATCH /v1/memories/{id}.""" - confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0) tags: Optional[List[str]] = None upvotes: Optional[Union[str, int]] = None downvotes: Optional[Union[str, int]] = None @@ -289,7 +281,7 @@ class MemorySearchRequest(BaseModel): tags: Optional[List[str]] = None project: Optional[str] = None limit: int = Field(default=5, ge=1, le=50) - min_confidence: float = Field(default=0.0, ge=0.0, le=1.0) + min_score: float = Field(default=0.0, ge=0.0, le=1.0) # Phase 6G: scope predicate selector. 'default' applies the standard # ``(scope='global') OR (scope='project' AND project=:current)`` # predicate; 'all' skips it entirely. diff --git a/src/lore/server/routes/graph/memories.py b/src/lore/server/routes/graph/memories.py index b822066..22dfb22 100644 --- a/src/lore/server/routes/graph/memories.py +++ b/src/lore/server/routes/graph/memories.py @@ -48,8 +48,6 @@ def _node_to_pydantic(n) -> GraphNode: type=n.type, tier=n.tier, project=n.project, - importance=n.importance, - confidence=n.confidence, tags=list(n.tags) if n.tags else None, created_at=n.created_at.isoformat() if n.created_at else None, upvotes=n.upvotes, @@ -77,7 +75,6 @@ async def get_graph( project: Optional[str] = Query(None), type: Optional[str] = Query(None), tier: Optional[str] = Query(None), - min_importance: float = Query(0.0, ge=0.0, le=1.0), since: Optional[str] = Query(None), until: Optional[str] = Query(None), limit: int = Query(1000, ge=1, le=10000), @@ -91,7 +88,6 @@ async def get_graph( project=project, type=type, tier=tier, - min_importance=min_importance, since=since_dt, until=until_dt, limit=limit, @@ -125,7 +121,6 @@ async def search_memories( content=h.content, type=h.type, project=h.project, - score=h.score, created_at=h.created_at.isoformat() if h.created_at else "", ) for h in res.results @@ -151,8 +146,6 @@ async def get_memory_detail( tier=meta.get("tier", "long"), project=m.project, tags=list(m.tags), - importance_score=m.importance_score, - confidence=m.confidence, upvotes=m.upvotes, downvotes=m.downvotes, access_count=m.access_count, diff --git a/src/lore/server/routes/graph/models.py b/src/lore/server/routes/graph/models.py index c1c226a..f2e9666 100644 --- a/src/lore/server/routes/graph/models.py +++ b/src/lore/server/routes/graph/models.py @@ -14,8 +14,6 @@ class GraphNode(BaseModel): type: str tier: Optional[str] = None project: Optional[str] = None - importance: Optional[float] = None - confidence: Optional[float] = None tags: Optional[List[str]] = None created_at: Optional[str] = None upvotes: Optional[int] = None @@ -57,7 +55,6 @@ class StatsResponse(BaseModel): by_project: Dict[str, int] = {} by_tier: Dict[str, int] = {} by_entity_type: Dict[str, int] = {} - avg_importance: float = 0.0 top_entities: List[Dict[str, Any]] = [] recent_24h: int = 0 recent_7d: int = 0 @@ -83,8 +80,6 @@ class MemoryDetailResponse(BaseModel): tier: str = "long" project: Optional[str] = None tags: List[str] = [] - importance_score: float = 1.0 - confidence: float = 1.0 upvotes: int = 0 downvotes: int = 0 access_count: int = 0 @@ -114,7 +109,6 @@ class SearchResult(BaseModel): content: str type: str project: Optional[str] = None - score: float = 0.0 created_at: str = "" diff --git a/src/lore/server/routes/graph/stats.py b/src/lore/server/routes/graph/stats.py index e6d9b34..483064a 100644 --- a/src/lore/server/routes/graph/stats.py +++ b/src/lore/server/routes/graph/stats.py @@ -34,8 +34,6 @@ def _node_to_pydantic(n) -> GraphNode: type=n.type, tier=n.tier, project=n.project, - importance=n.importance, - confidence=n.confidence, tags=list(n.tags) if n.tags else None, created_at=n.created_at.isoformat() if n.created_at else None, upvotes=n.upvotes, @@ -62,7 +60,6 @@ async def get_stats_route( by_project=dict(s.by_project), by_tier={}, by_entity_type=dict(s.by_entity_type), - avg_importance=s.avg_importance, top_entities=list(s.top_entities), recent_24h=s.recent_24h, recent_7d=s.recent_7d, diff --git a/src/lore/server/routes/lessons.py b/src/lore/server/routes/lessons.py index ebc13d5..3584bee 100644 --- a/src/lore/server/routes/lessons.py +++ b/src/lore/server/routes/lessons.py @@ -46,7 +46,6 @@ def _to_lesson_response(m: StoredMemory) -> LessonResponse: resolution=m.context if m.context else "", context=None, # legacy field; not stored tags=list(m.tags), - confidence=m.confidence, source=m.source, project=m.project, created_at=m.created_at, @@ -65,7 +64,6 @@ def _to_export_item(em: ExportedMemory) -> LessonExportItem: resolution=em.context if em.context else "", context=None, tags=list(em.tags), - confidence=em.confidence, source=em.source, project=em.project, embedding=list(em.embedding) if em.embedding else None, @@ -99,7 +97,6 @@ async def create_lesson( resolution=body.resolution, context=body.context, tags=body.tags, - confidence=body.confidence, source=body.source, project=project, embedding=body.embedding, @@ -131,7 +128,7 @@ async def search_lessons( project=project, tags=body.tags, limit=body.limit, - min_confidence=body.min_confidence, + min_score=body.min_score, scope_mode=body.scope, ) @@ -142,7 +139,6 @@ async def search_lessons( resolution=r["context"] or "", context=None, tags=r["tags"], - confidence=r["confidence"], source=r["source"], project=r["project"], created_at=r["created_at"], @@ -167,8 +163,7 @@ async def record_access( auth: AuthContext = Depends(get_auth_context), store: Store = Depends(get_store), ) -> dict: - """Record an access event: increment access_count, set last_accessed_at, - and recompute importance_score server-side.""" + """Record an access event: increment access_count and set last_accessed_at.""" try: result = await lessons_service.record_access( store, @@ -184,7 +179,6 @@ async def record_access( "id": result["id"], "access_count": result["access_count"], "last_accessed_at": last_acc.isoformat() if last_acc else None, - "importance_score": float(result["importance_score"] or 0.0), } @@ -227,7 +221,6 @@ async def update_lesson( org_id=auth.org_id, lesson_id=lesson_id, project=auth.project, - confidence=body.confidence, tags=body.tags, meta=body.meta, upvotes=body.upvotes, diff --git a/src/lore/server/routes/memories.py b/src/lore/server/routes/memories.py index 476e10c..d04f8a3 100644 --- a/src/lore/server/routes/memories.py +++ b/src/lore/server/routes/memories.py @@ -70,7 +70,6 @@ def _row_to_response(row: dict) -> MemoryResponse: content=row["content"], context=row.get("context"), tags=tags, - confidence=row["confidence"], source=row.get("source"), project=row.get("project"), created_at=row["created_at"], @@ -107,7 +106,6 @@ async def create_memory( context=body.context, embedding=embedding, tags=body.tags or [], - confidence=body.confidence if body.confidence is not None else 0.5, source=body.source, project=auth.project or body.project, expires_at=body.expires_at, @@ -155,7 +153,7 @@ async def search_memories( org_id=auth.org_id, query_vec=body.embedding, limit=body.limit, - min_score=body.min_confidence, + min_score=body.min_score, project=auth.project or body.project, scope_mode=body.scope, ) @@ -166,7 +164,6 @@ async def search_memories( content=r.content, context=r.context, tags=list(r.tags), - confidence=r.confidence, source=r.source, project=r.project, created_at=r.created_at, @@ -192,7 +189,7 @@ async def record_access( auth: AuthContext = Depends(get_auth_context), store: Store = Depends(get_store), ) -> dict: - """Record an access event and recompute importance_score.""" + """Record an access event.""" # Enforce project scoping: a project-scoped key must not access memories # outside its project. if auth.project: @@ -209,7 +206,6 @@ async def record_access( "id": updated.id, "access_count": updated.access_count, "last_accessed_at": updated.last_accessed_at.isoformat() if updated.last_accessed_at else None, - "importance_score": updated.importance_score, } @@ -313,7 +309,6 @@ async def get_memory( content=m.content, context=m.context, tags=list(m.tags), - confidence=m.confidence, source=m.source, project=m.project, created_at=m.created_at, @@ -337,8 +332,7 @@ async def update_memory( ) -> MemoryResponse: """Update a memory.""" if ( - body.confidence is None - and body.tags is None + body.tags is None and body.meta is None and body.upvotes is None and body.downvotes is None @@ -351,7 +345,6 @@ async def update_memory( store, org_id=auth.org_id, memory_id=memory_id, - confidence=body.confidence, tags=body.tags, meta=body.meta, ) @@ -411,7 +404,7 @@ async def list_memories( def _stored_to_memory_response(m) -> MemoryResponse: return MemoryResponse( id=m.id, content=m.content, context=m.context, tags=list(m.tags), - confidence=m.confidence, source=m.source, project=m.project, + source=m.source, project=m.project, created_at=m.created_at, updated_at=m.updated_at, expires_at=m.expires_at, upvotes=m.upvotes, downvotes=m.downvotes, meta=dict(m.meta), scope=getattr(m, "scope", "project") or "project", diff --git a/src/lore/server/routes/recent.py b/src/lore/server/routes/recent.py index e1e8a4f..bc71d5c 100644 --- a/src/lore/server/routes/recent.py +++ b/src/lore/server/routes/recent.py @@ -36,7 +36,6 @@ class RecentMemoryItem(BaseModel): tier: str created_at: str tags: List[str] = [] - importance_score: float = 1.0 class RecentProjectGroup(BaseModel): @@ -65,7 +64,6 @@ def _to_item(m: StoredMemory) -> RecentMemoryItem: tier=(m.meta or {}).get("tier", "long"), created_at=created_at, tags=list(m.tags), - importance_score=m.importance_score or 1.0, ) @@ -149,7 +147,7 @@ def _format_text(groups: List[RecentProjectGroup], hours: int, fmt: str) -> str: for m in group.memories: ts = m.created_at[11:16] if len(m.created_at) >= 16 else "??:??" if fmt == "detailed": - lines.append(f"**[{ts}] {m.type}** (tier: {m.tier}, importance: {m.importance_score:.2f})") + lines.append(f"**[{ts}] {m.type}** (tier: {m.tier})") lines.append(m.content) if m.tags: lines.append(f"Tags: {', '.join(m.tags)}") diff --git a/src/lore/server/routes/retention.py b/src/lore/server/routes/retention.py index df8f6d7..6521a18 100644 --- a/src/lore/server/routes/retention.py +++ b/src/lore/server/routes/retention.py @@ -1,4 +1,4 @@ -"""Retention policy endpoints — preview and apply age/importance-based cleanup.""" +"""Retention policy endpoints — preview and apply age-based cleanup.""" from __future__ import annotations @@ -21,7 +21,6 @@ class RetentionRequest(BaseModel): max_age_days: int = Field(90, ge=1, description="Max age in days") - min_importance_score: float = Field(0.3, ge=0.0, le=1.0, description="Importance threshold") archive: bool = Field(False, description="Archive before deleting") dry_run: bool = Field(False, description="Preview only, do not delete") @@ -29,7 +28,6 @@ class RetentionRequest(BaseModel): class AffectedMemory(BaseModel): id: str content_preview: str - importance_score: float created_at: str @@ -57,7 +55,6 @@ def _run_retention(params: RetentionRequest) -> RetentionResponse: lore = _get_lore() policy = RetentionPolicy( max_age_days=params.max_age_days, - min_importance_score=params.min_importance_score, archive_on_expire=params.archive, dry_run=params.dry_run, ) @@ -70,7 +67,6 @@ def _run_retention(params: RetentionRequest) -> RetentionResponse: AffectedMemory( id=m.id, content_preview=m.content[:120], - importance_score=m.importance_score, created_at=m.created_at, ) for m in expired @@ -90,13 +86,11 @@ def _run_retention(params: RetentionRequest) -> RetentionResponse: @router.get("/preview", response_model=RetentionResponse) async def preview_retention( max_age_days: int = 90, - min_importance_score: float = 0.3, auth: AuthContext = Depends(get_auth_context), ) -> RetentionResponse: """Preview which memories would be affected by a retention policy (dry-run).""" params = RetentionRequest( max_age_days=max_age_days, - min_importance_score=min_importance_score, archive=False, dry_run=True, ) diff --git a/src/lore/server/routes/review.py b/src/lore/server/routes/review.py index b2eaf63..24853a0 100644 --- a/src/lore/server/routes/review.py +++ b/src/lore/server/routes/review.py @@ -39,7 +39,6 @@ class RiskScore(BaseModel): """Risk scoring breakdown for a pending connection.""" total: float = 0.0 confidence_risk: float = 0.0 - source_reliability: float = 0.0 entity_importance: float = 0.0 staleness_risk: float = 0.0 @@ -103,7 +102,6 @@ def _to_review_item(p: PendingReview) -> ReviewItemResponse: risk_score=RiskScore( total=p.risk_score.total, confidence_risk=p.risk_score.confidence_risk, - source_reliability=p.risk_score.source_reliability, entity_importance=p.risk_score.entity_importance, staleness_risk=p.risk_score.staleness_risk, ), diff --git a/src/lore/server/routes/temporal.py b/src/lore/server/routes/temporal.py index f51f997..9cffe84 100644 --- a/src/lore/server/routes/temporal.py +++ b/src/lore/server/routes/temporal.py @@ -164,7 +164,6 @@ async def list_at_time( content=m.content, context=m.context, tags=list(m.tags), - confidence=m.confidence, source=m.source, project=m.project, created_at=m.created_at, diff --git a/src/lore/services/conversations.py b/src/lore/services/conversations.py index 7447081..7fb97f4 100644 --- a/src/lore/services/conversations.py +++ b/src/lore/services/conversations.py @@ -128,7 +128,6 @@ async def process_job_async( tags=list(mem.tags or []), source=mem.source or "conversation", meta=meta_dict, - confidence=mem.confidence, ) elapsed_ms = int((time.monotonic() - start) * 1000) diff --git a/src/lore/services/graph/graph.py b/src/lore/services/graph/graph.py index 6247696..2661502 100644 --- a/src/lore/services/graph/graph.py +++ b/src/lore/services/graph/graph.py @@ -26,8 +26,6 @@ class GraphNode: type: str # mtype for memories, entity_type for entities tier: Optional[str] = None project: Optional[str] = None - importance: Optional[float] = None - confidence: Optional[float] = None tags: Optional[Sequence[str]] = None created_at: Optional[datetime] = None upvotes: Optional[int] = None @@ -70,7 +68,6 @@ class SearchHit: content: str # first 200 chars type: str project: Optional[str] - score: float created_at: datetime @@ -161,8 +158,6 @@ def _memory_node(m: StoredMemory) -> GraphNode: type=mtype, tier=mtier, project=m.project, - importance=m.importance_score, - confidence=m.confidence, tags=tuple(m.tags), created_at=m.created_at, upvotes=m.upvotes, @@ -182,7 +177,6 @@ async def get_graph_data( project: Optional[str] = None, type: Optional[str] = None, tier: Optional[str] = None, - min_importance: float = 0.0, since: Optional[datetime] = None, until: Optional[datetime] = None, limit: int = 1000, @@ -214,10 +208,6 @@ async def get_graph_data( ) memories = await store.list_memories(f) - # Filter by min_importance in Python (MemoryFilter has no field for it). - if min_importance > 0.0: - memories = [m for m in memories if m.importance_score >= min_importance] - # 3. Fetch all entities (no limit in legacy SQL). entities = await store.list_entities(limit=10000) @@ -323,7 +313,6 @@ async def search_graph_memories( content=(m.content or "")[:200], type=(m.meta or {}).get("type", "general"), project=m.project, - score=m.importance_score, created_at=m.created_at, ) for m in memories @@ -432,8 +421,6 @@ async def get_clusters( type=mtype, tier=mtier, project=m.project, - importance=m.importance_score, - confidence=m.confidence, tags=tuple(m.tags), created_at=m.created_at, upvotes=m.upvotes, diff --git a/src/lore/services/graph/review.py b/src/lore/services/graph/review.py index 92c3877..84112c6 100644 --- a/src/lore/services/graph/review.py +++ b/src/lore/services/graph/review.py @@ -21,7 +21,6 @@ class RiskScore: total: float confidence_risk: float - source_reliability: float entity_importance: float staleness_risk: float @@ -72,7 +71,6 @@ class BulkReviewResult: def _compute_risk_score( weight: float, - source_importance: Optional[float], source_mention_count: int, target_mention_count: int, age_hours: float, @@ -82,18 +80,15 @@ def _compute_risk_score( Higher score = needs more careful review. """ confidence_risk = round(max(0.0, (1.0 - min(weight, 1.0)) * 40.0), 2) - imp = source_importance if source_importance is not None else 0.5 - source_reliability = round(max(0.0, (1.0 - min(imp, 1.0)) * 25.0), 2) max_mentions = max(source_mention_count, target_mention_count, 1) entity_importance = round(min(25.0, max_mentions * 2.5), 2) staleness_risk = round(min(10.0, age_hours / 168.0 * 10.0), 2) total = round( - confidence_risk + source_reliability + entity_importance + staleness_risk, 2 + confidence_risk + entity_importance + staleness_risk, 2 ) return RiskScore( total=total, confidence_risk=confidence_risk, - source_reliability=source_reliability, entity_importance=entity_importance, staleness_risk=staleness_risk, ) @@ -126,17 +121,14 @@ async def list_pending_reviews( enriched: list[PendingReview] = [] for row in rows: memory_content: Optional[str] = None - source_importance: Optional[float] = None if row.source_memory_id is not None: # TODO: replace hardcoded "solo" org_id with a proper org parameter # once multi-org support lands. Matches legacy route (no org filter). mem = await store.get_memory("solo", row.source_memory_id) if mem is not None: memory_content = (mem.content or "")[:200] - source_importance = mem.importance_score risk = _compute_risk_score( weight=row.weight, - source_importance=source_importance, source_mention_count=row.source_mentions, target_mention_count=row.target_mentions, age_hours=_age_hours(row.created_at), diff --git a/src/lore/services/lessons.py b/src/lore/services/lessons.py index 77304a3..5962484 100644 --- a/src/lore/services/lessons.py +++ b/src/lore/services/lessons.py @@ -67,7 +67,6 @@ async def create( resolution: Optional[str], context: Optional[str], # intentionally unused — wire-compat only tags: Optional[Sequence[str]], - confidence: float, source: Optional[str], project: Optional[str], embedding: Optional[Sequence[float]], @@ -100,7 +99,6 @@ async def create( content=problem, context=resolution if resolution is not None else "", tags=tuple(tags or ()), - confidence=confidence, source=source, project=project, embedding=embedding if embedding else [0.0] * 384, @@ -120,7 +118,7 @@ async def search( project: Optional[str], tags: Optional[Sequence[str]], limit: int, - min_confidence: float, + min_score: float, scope_mode: str = "default", ) -> list[dict]: """Vector recall with time-decay re-ranking. @@ -164,9 +162,9 @@ async def search( effective_age = min(age_created_days, age_accessed_days) half_life = _half_life_for(m.meta) time_decay = 0.5 ** (effective_age / half_life) - final_score = m.score * (m.importance_score or 1.0) * time_decay + final_score = m.score * time_decay - if final_score < min_confidence: + if final_score < min_score: continue results.append({ @@ -174,7 +172,6 @@ async def search( "content": m.content, "context": m.context, "tags": list(m.tags), - "confidence": m.confidence, "source": m.source, "project": m.project, "created_at": m.created_at, @@ -214,7 +211,6 @@ async def record_access( "id": updated.id, "access_count": updated.access_count, "last_accessed_at": updated.last_accessed_at, - "importance_score": updated.importance_score, } @@ -241,7 +237,6 @@ async def update( org_id: str, lesson_id: str, project: Optional[str], - confidence: Optional[float], tags: Optional[Sequence[str]], meta: Optional[Mapping[str, Any]], upvotes: Optional[Union[str, int]], @@ -261,13 +256,12 @@ async def update( raise StoreNotFoundError("memories", lesson_id) patch = MemoryPatch( - confidence=confidence, tags=tuple(tags) if tags is not None else None, meta=dict(meta) if meta is not None else None, ) has_non_vote_patch = any( - getattr(patch, f) is not None for f in ("confidence", "tags", "meta") + getattr(patch, f) is not None for f in ("tags", "meta") ) # Validate vote modes early, before any writes @@ -369,7 +363,7 @@ async def import_lessons( ) -> int: """Upsert a batch of lesson records. - Each lesson must have: id, problem, resolution, tags, confidence, source, + Each lesson must have: id, problem, resolution, tags, source, project, embedding, expires_at, upvotes, downvotes, meta. Returns the count of items processed. @@ -384,7 +378,6 @@ async def import_lessons( content=lesson.problem, context=lesson.resolution if lesson.resolution else "", tags=tuple(lesson.tags or ()), - confidence=lesson.confidence, source=lesson.source, project=project, embedding=list(lesson.embedding) if lesson.embedding else None, diff --git a/src/lore/services/memories.py b/src/lore/services/memories.py index ba20333..032e5c7 100644 --- a/src/lore/services/memories.py +++ b/src/lore/services/memories.py @@ -46,13 +46,11 @@ async def create_memory( embedding: Sequence[float], context: Optional[str] = None, tags: Sequence[str] = (), - confidence: float = 0.5, source: Optional[str] = None, project: Optional[str] = None, expires_at: Optional[datetime] = None, meta: Optional[Mapping[str, Any]] = None, scope: Optional[str] = None, - importance_score: Optional[float] = None, ) -> StoredMemory: """Insert a memory. Tag normalization and meta defaulting happen here. @@ -76,13 +74,11 @@ async def create_memory( embedding=embedding, context=context, tags=normalized_tags, - confidence=confidence, source=source, project=project, expires_at=expires_at, meta=dict(meta or {}), scope=effective_scope, - importance_score=importance_score, ) ) @@ -101,7 +97,6 @@ async def update_memory( content: Optional[str] = None, context: Optional[str] = None, tags: Optional[Sequence[str]] = None, - confidence: Optional[float] = None, source: Optional[str] = None, project: Optional[str] = None, expires_at: Optional[datetime] = None, @@ -111,7 +106,6 @@ async def update_memory( content=content, context=context, tags=tuple(tags) if tags is not None else None, - confidence=confidence, source=source, project=project, expires_at=expires_at, diff --git a/src/lore/services/observations.py b/src/lore/services/observations.py index 7186a30..db4cfd7 100644 --- a/src/lore/services/observations.py +++ b/src/lore/services/observations.py @@ -89,10 +89,8 @@ async def create_observation( context=obs.title, embedding=embedding, tags=tuple(obs.tags), - confidence=0.5, source=obs.source or "observation", project=obs.project, meta=meta, scope=obs.scope, - importance_score=0.5, ) diff --git a/src/lore/services/retrieve.py b/src/lore/services/retrieve.py index 9d207b3..2fee086 100644 --- a/src/lore/services/retrieve.py +++ b/src/lore/services/retrieve.py @@ -516,21 +516,16 @@ def _status(raw: Any) -> str: ) superseded_set = set() - # Annotate with recency + importance, multiplicative. + # Annotate with recency, multiplicative. annotated: list[HybridResult] = [] for memory, base_score, raw_signals in fused[: max(params.limit * 2, params.limit)]: recency = _recency_signal(memory.created_at, profile.recency_bias) - importance = ( - float(memory.importance_score) if memory.importance_score is not None else 0.5 - ) is_superseded = memory.id in superseded_set supersession_multiplier = 0.1 if is_superseded else 1.0 - # multiplicative annotations: recency multiplier ∈ [1.0, 1.5]; - # importance multiplier ∈ [0.75, 1.25] for importance ∈ [0, 1]. + # recency multiplier ∈ [1.0, 1.5]. final = ( base_score * (1.0 + 0.5 * recency) - * (1.0 + 0.5 * (importance - 0.5)) * supersession_multiplier ) signals: Dict[str, float] = { @@ -538,7 +533,6 @@ def _status(raw: Any) -> str: "fts": raw_signals.get("signal_1", 0.0), "graph": raw_signals.get("signal_2", 0.0), "recency": recency, - "importance": importance, # ``superseded`` lands in signals so the route can surface it # alongside the per-signal breakdown. Float (1.0/0.0) keeps # the existing ``Mapping[str, float]`` shape from changing. diff --git a/src/lore/services/snapshots.py b/src/lore/services/snapshots.py index 1b17643..4bf6fd0 100644 --- a/src/lore/services/snapshots.py +++ b/src/lore/services/snapshots.py @@ -47,7 +47,6 @@ async def create_snapshot( content=content, embedding=[0.0] * 384, # snapshots aren't recall targets; placeholder zero-vector tags=all_tags, - confidence=1.0, project=project, meta=meta, ) diff --git a/src/lore/store/http.py b/src/lore/store/http.py index 20868a4..7bbf54d 100644 --- a/src/lore/store/http.py +++ b/src/lore/store/http.py @@ -160,7 +160,6 @@ def _memory_to_lesson(memory: Memory) -> Dict[str, Any]: "resolution": memory.content, "context": memory.context, "tags": memory.tags, - "confidence": memory.confidence, "source": memory.source, "project": memory.project, "meta": meta, @@ -219,10 +218,8 @@ def _to_iso(val: Any) -> str: updated_at=_to_iso(data.get("updated_at")), ttl=None, expires_at=_to_iso(data.get("expires_at")) or None, - confidence=data.get("confidence", 1.0), upvotes=data.get("upvotes", 0), downvotes=data.get("downvotes", 0), - importance_score=data.get("importance_score", 1.0), access_count=data.get("access_count", 0), last_accessed_at=data.get("last_accessed_at"), archived=data.get("archived", False), @@ -281,8 +278,6 @@ def list( def update(self, memory: Memory) -> bool: payload: Dict[str, Any] = {} - if memory.confidence is not None: - payload["confidence"] = memory.confidence if memory.tags: payload["tags"] = memory.tags meta = dict(memory.metadata) if memory.metadata else {} @@ -345,13 +340,13 @@ def search( project: Optional[str] = None, tier: Optional[str] = None, limit: int = 5, - min_confidence: float = 0.0, + min_score: float = 0.0, scope_mode: str = "default", ) -> List[RecallResult]: payload: Dict[str, Any] = { "embedding": embedding, "limit": limit, - "min_confidence": min_confidence, + "min_score": min_score, } if tags: payload["tags"] = tags diff --git a/src/lore/temporal.py b/src/lore/temporal.py index 6c12471..6c3a3e8 100644 --- a/src/lore/temporal.py +++ b/src/lore/temporal.py @@ -61,7 +61,7 @@ def on_this_day( Returns: Dict mapping year (int) to list of Memory objects, sorted by - year DESC, then importance_score DESC within each year. + year DESC, then created_at DESC within each year. Raises: ValueError: If month or day values are out of range. @@ -125,14 +125,13 @@ def on_this_day( matched.append(mem) - # Sort by year DESC, then importance DESC, then created_at DESC + # Sort by year DESC, then created_at DESC matched.sort( key=lambda m: ( - -datetime.fromisoformat(m.created_at).year, - -m.importance_score, + datetime.fromisoformat(m.created_at).year, m.created_at, ), - reverse=False, + reverse=True, ) # Apply offset and limit @@ -178,8 +177,7 @@ def format_results( lines.append(f"--- {year} ---") for mem in memories: lines.append( - f" [{mem.id}] (importance: {mem.importance_score:.2f}, " - f"type: {mem.type}, tier: {mem.tier})" + f" [{mem.id}] (type: {mem.type}, tier: {mem.tier})" ) lines.append(f" {mem.content[:200]}") if include_metadata: diff --git a/src/lore/types.py b/src/lore/types.py index efa2e65..0a00504 100644 --- a/src/lore/types.py +++ b/src/lore/types.py @@ -113,10 +113,8 @@ class Memory: updated_at: str = "" ttl: Optional[int] = None expires_at: Optional[str] = None - confidence: float = 1.0 upvotes: int = 0 downvotes: int = 0 - importance_score: float = 1.0 access_count: int = 0 last_accessed_at: Optional[str] = None archived: bool = False @@ -151,8 +149,6 @@ class MemoryStats: oldest: Optional[str] = None newest: Optional[str] = None expired_cleaned: int = 0 - avg_importance: Optional[float] = None - below_threshold_count: int = 0 archived_count: int = 0 consolidation_count: int = 0 last_consolidation_at: Optional[str] = None diff --git a/src/lore/ui/src/panels/detail.js b/src/lore/ui/src/panels/detail.js index 126c08b..6899eb2 100644 --- a/src/lore/ui/src/panels/detail.js +++ b/src/lore/ui/src/panels/detail.js @@ -124,8 +124,6 @@ export class DetailPanel { const meta = document.createElement('div'); meta.className = 'detail-meta'; const fields = [ - ['Importance', ((data.importance_score || 0) * 100).toFixed(0) + '%'], - ['Confidence', ((data.confidence || 0) * 100).toFixed(0) + '%'], ['Votes', '+' + data.upvotes + ' / -' + data.downvotes], ['Access Count', data.access_count], ['Created', formatDate(data.created_at)], diff --git a/src/lore/ui/src/panels/filters.js b/src/lore/ui/src/panels/filters.js index 4a97878..214d340 100644 --- a/src/lore/ui/src/panels/filters.js +++ b/src/lore/ui/src/panels/filters.js @@ -1,7 +1,6 @@ // Filter sidebar import { MEMORY_COLORS, ENTITY_COLORS } from '../colors.js'; -import { debounce } from '../utils.js'; export class FilterPanel { constructor(container, state) { @@ -107,32 +106,6 @@ export class FilterPanel { ['working', 'short', 'long'], {}, 'tiers', inner ); - // Importance slider - inner.appendChild(this._createLabel('Min Importance')); - const sliderRow = document.createElement('div'); - sliderRow.className = 'slider-row'; - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = '0'; - slider.max = '100'; - slider.value = String(Math.round((this.state.filters.minImportance || 0) * 100)); - slider.className = 'filter-slider'; - const sliderVal = document.createElement('span'); - sliderVal.className = 'slider-value'; - sliderVal.textContent = slider.value + '%'; - const debouncedSlider = debounce((v) => { - this.state.setFilter('minImportance', v / 100); - }, 100); - slider.oninput = () => { - sliderVal.textContent = slider.value + '%'; - debouncedSlider(parseInt(slider.value)); - }; - sliderRow.appendChild(slider); - sliderRow.appendChild(sliderVal); - inner.appendChild(sliderRow); - this._importanceSlider = slider; - this._importanceValue = sliderVal; - // Date range inner.appendChild(this._createLabel('Date Range')); const dateRow = document.createElement('div'); @@ -226,10 +199,6 @@ export class FilterPanel { _refreshControls() { if (this._projectSelect) this._projectSelect.value = ''; - if (this._importanceSlider) { - this._importanceSlider.value = '0'; - this._importanceValue.textContent = '0%'; - } if (this._sinceInput) this._sinceInput.value = ''; if (this._untilInput) this._untilInput.value = ''; if (this._memTypeCheckboxes) { diff --git a/src/lore/ui/src/panels/stats.js b/src/lore/ui/src/panels/stats.js index 33dd63f..555d4b2 100644 --- a/src/lore/ui/src/panels/stats.js +++ b/src/lore/ui/src/panels/stats.js @@ -96,12 +96,6 @@ export class StatsPanel { } this.container.appendChild(totals); - // Average importance - const avgDiv = document.createElement('div'); - avgDiv.className = 'stat-row'; - avgDiv.textContent = 'Avg Importance: ' + ((data.avg_importance || 0) * 100).toFixed(0) + '%'; - this.container.appendChild(avgDiv); - // Recent activity const recentDiv = document.createElement('div'); recentDiv.className = 'stat-row'; diff --git a/src/lore/ui/src/state.js b/src/lore/ui/src/state.js index bc39bba..cbba3b1 100644 --- a/src/lore/ui/src/state.js +++ b/src/lore/ui/src/state.js @@ -20,7 +20,6 @@ export class AppState extends EventTarget { types: new Set(), entityTypes: new Set(), tiers: new Set(), - minImportance: 0, dateRange: [null, null], }; this._nodeMap = new Map(); @@ -83,7 +82,6 @@ export class AppState extends EventTarget { types: new Set(), entityTypes: new Set(), tiers: new Set(), - minImportance: 0, dateRange: [null, null], }; this._recomputeFiltered(); @@ -114,7 +112,6 @@ export class AppState extends EventTarget { if (this.filters.types.size > 0) count++; if (this.filters.entityTypes.size > 0) count++; if (this.filters.tiers.size > 0) count++; - if (this.filters.minImportance > 0) count++; if (this.filters.dateRange[0] || this.filters.dateRange[1]) count++; return count; } @@ -135,7 +132,6 @@ export class AppState extends EventTarget { if (f.project && node.project !== f.project) return false; if (f.types.size > 0 && !f.types.has(node.type)) return false; if (f.tiers.size > 0 && !f.tiers.has(node.tier)) return false; - if (f.minImportance > 0 && (node.importance || 0) < f.minImportance) return false; if (f.dateRange[0] && node.created_at < f.dateRange[0]) return false; if (f.dateRange[1] && node.created_at > f.dateRange[1]) return false; } @@ -151,7 +147,6 @@ export class AppState extends EventTarget { if (this.filters.types.size > 0) params.set('type', [...this.filters.types].join(',')); if (this.filters.entityTypes.size > 0) params.set('entity_type', [...this.filters.entityTypes].join(',')); if (this.filters.tiers.size > 0) params.set('tier', [...this.filters.tiers].join(',')); - if (this.filters.minImportance > 0) params.set('min_importance', this.filters.minImportance); if (this.filters.dateRange[0]) params.set('since', this.filters.dateRange[0]); if (this.filters.dateRange[1]) params.set('until', this.filters.dateRange[1]); if (this.searchQuery) params.set('search', this.searchQuery); @@ -168,7 +163,6 @@ export class AppState extends EventTarget { if (params.has('type')) this.filters.types = new Set(params.get('type').split(',')); if (params.has('entity_type')) this.filters.entityTypes = new Set(params.get('entity_type').split(',')); if (params.has('tier')) this.filters.tiers = new Set(params.get('tier').split(',')); - if (params.has('min_importance')) this.filters.minImportance = parseFloat(params.get('min_importance')); if (params.has('since')) this.filters.dateRange[0] = params.get('since'); if (params.has('until')) this.filters.dateRange[1] = params.get('until'); if (params.has('search')) this.searchQuery = params.get('search'); diff --git a/tests/embedded/test_async_lore.py b/tests/embedded/test_async_lore.py index baced1b..2a659dd 100644 --- a/tests/embedded/test_async_lore.py +++ b/tests/embedded/test_async_lore.py @@ -556,14 +556,6 @@ async def test_cleanup_expired_returns_int(self): count = await lore.cleanup_expired() assert count == 0 - @pytest.mark.asyncio - async def test_recalculate_importance_noop(self): - from lore import AsyncLore - - async with AsyncLore("sqlite:///:memory:", embed=_stub_embed) as lore: - assert await lore.recalculate_importance() == 0 - - class TestAsyncLoreEnrichment: """``enrich_memories`` short-circuits on already-enriched rows.""" diff --git a/tests/integration/test_remote.py b/tests/integration/test_remote.py index d6fa59f..981abf0 100644 --- a/tests/integration/test_remote.py +++ b/tests/integration/test_remote.py @@ -101,7 +101,6 @@ def _lesson_row( "resolution": "test resolution", "context": None, "tags": json.dumps(["test"]), - "confidence": 0.8, "source": None, "project": project, "created_at": NOW, @@ -186,10 +185,10 @@ async def test_full_flow_publish_query_verify(client: AsyncClient) -> None: _stored = StoredMemory( id="lesson-flow-001", org_id=ORG_ID, content="test problem", - context="test resolution", tags=("test",), confidence=0.8, + context="test resolution", tags=("test",), source=None, project=None, created_at=NOW, updated_at=NOW, expires_at=None, upvotes=0, downvotes=0, meta={}, - access_count=0, last_accessed_at=None, importance_score=1.0, + access_count=0, last_accessed_at=None, ) with patch("lore.server.auth.get_store", return_value=auth_store), \ @@ -222,7 +221,6 @@ async def test_full_flow_publish_query_verify(client: AsyncClient) -> None: assert data["problem"] == "test problem" assert data["resolution"] == "test resolution" assert data["tags"] == ["test"] - assert data["confidence"] == 0.8 # ── Integration Test: Project Scoping Isolation ──────────────────── @@ -295,17 +293,17 @@ async def test_upvote_downvote_round_trip(client: AsyncClient) -> None: _after_upvote = StoredMemory( id="lesson-vote-001", org_id=ORG_ID, content="test problem", - context="test resolution", tags=(), confidence=0.8, + context="test resolution", tags=(), source=None, project=None, created_at=NOW, updated_at=NOW, expires_at=None, upvotes=1, downvotes=0, meta={}, - access_count=0, last_accessed_at=None, importance_score=1.0, + access_count=0, last_accessed_at=None, ) _after_downvote = StoredMemory( id="lesson-vote-001", org_id=ORG_ID, content="test problem", - context="test resolution", tags=(), confidence=0.8, + context="test resolution", tags=(), source=None, project=None, created_at=NOW, updated_at=NOW, expires_at=None, upvotes=1, downvotes=1, meta={}, - access_count=0, last_accessed_at=None, importance_score=1.0, + access_count=0, last_accessed_at=None, ) with patch("lore.server.auth.get_store", return_value=auth_store), \ @@ -345,14 +343,14 @@ async def test_export_import_between_contexts(client: AsyncClient) -> None: _exported_mems = [ ExportedMemory( id="lesson-exp-001", org_id=ORG_ID, content="test problem", - context="test resolution", tags=("test",), confidence=0.8, + context="test resolution", tags=("test",), source=None, project=None, created_at=NOW, updated_at=NOW, expires_at=None, upvotes=0, downvotes=0, meta={}, embedding=[0.1] * 384, ), ExportedMemory( id="lesson-exp-002", org_id=ORG_ID, content="test problem", - context="test resolution", tags=("test",), confidence=0.8, + context="test resolution", tags=("test",), source=None, project=None, created_at=NOW, updated_at=NOW, expires_at=None, upvotes=0, downvotes=0, meta={}, embedding=[0.1] * 384, @@ -381,7 +379,6 @@ async def test_export_import_between_contexts(client: AsyncClient) -> None: "resolution": l["resolution"], "embedding": l["embedding"], "tags": l["tags"], - "confidence": l["confidence"], } for l in exported ]}, diff --git a/tests/migrate/test_migrate_round_trip.py b/tests/migrate/test_migrate_round_trip.py index a0b43d0..a916a49 100644 --- a/tests/migrate/test_migrate_round_trip.py +++ b/tests/migrate/test_migrate_round_trip.py @@ -61,7 +61,6 @@ async def _seed_sqlite_with_data(url: str, *, n_memories: int = 5) -> list[str]: content=f"memory content {i}", context=f"context {i}", tags=("alpha", "beta"), - confidence=0.9, source="test", project="test_project", embedding=tuple([0.01 * i] * migrate_mod.EMBED_DIM), diff --git a/tests/persistence/test_contract_analytics.py b/tests/persistence/test_contract_analytics.py index c16e0d1..d4a77cd 100644 --- a/tests/persistence/test_contract_analytics.py +++ b/tests/persistence/test_contract_analytics.py @@ -202,17 +202,6 @@ async def test_bump_access_counts_increments(store: Store): assert after.access_count == 1 -@pytest.mark.asyncio -async def test_bump_access_counts_recomputes_importance_score(store: Store): - memory_id = await _insert_memory(store, org_id="org-bac2") - - await store.bump_access_counts("org-bac2", [memory_id]) - - after = await store.get_memory("org-bac2", memory_id) - assert after is not None - assert after.importance_score > 0.0 - - @pytest.mark.asyncio async def test_bump_access_counts_empty_list_is_noop(store: Store): # Should not raise, should not hit the DB (no table access errors) diff --git a/tests/persistence/test_contract_recommendations.py b/tests/persistence/test_contract_recommendations.py index b24b7f6..5e1f545 100644 --- a/tests/persistence/test_contract_recommendations.py +++ b/tests/persistence/test_contract_recommendations.py @@ -17,9 +17,6 @@ # ── helpers ─────────────────────────────────────────────────────────────────── -_UNSET = object() - - async def _insert_memory_with_embedding( store, *, @@ -27,7 +24,6 @@ async def _insert_memory_with_embedding( org_id="solo", content="x", embedding=None, - importance_score=_UNSET, meta=None, ) -> str: """Insert a memory with an optional embedding. @@ -42,13 +38,11 @@ async def _insert_memory_with_embedding( mid = memory_id or f"mem_{ULID()}" meta_param = json.dumps(dict(meta or {})) - # importance_score=_UNSET means use default 0.5; explicit None inserts NULL - importance = None if importance_score is None else (0.5 if importance_score is _UNSET else importance_score) if _is_sqlite(store): cursor = await store._conn.execute( - """INSERT INTO memories (id, org_id, content, context, tags, confidence, meta, importance_score) - VALUES (?, ?, ?, '', '[]', 0.5, ?, ?)""", - (mid, org_id, content, meta_param, importance), + """INSERT INTO memories (id, org_id, content, context, tags, meta) + VALUES (?, ?, ?, '', '[]', ?)""", + (mid, org_id, content, meta_param), ) rowid = cursor.lastrowid await cursor.close() @@ -61,14 +55,13 @@ async def _insert_memory_with_embedding( else: embedding_param = json.dumps(list(embedding)) if embedding is not None else None await store._conn.execute( - """INSERT INTO memories (id, org_id, content, context, tags, confidence, embedding, meta, importance_score) - VALUES ($1, $2, $3, '', '[]'::jsonb, 0.5, $4::vector, $5::jsonb, $6)""", + """INSERT INTO memories (id, org_id, content, context, tags, embedding, meta) + VALUES ($1, $2, $3, '', '[]'::jsonb, $4::vector, $5::jsonb)""", mid, org_id, content, embedding_param, meta_param, - importance, ) return mid @@ -323,24 +316,6 @@ async def test_list_candidates_org_isolation(store: Store): assert len(results_a) == 1 -@pytest.mark.asyncio -async def test_list_candidates_ordered_by_importance_score_desc_nulls_last(store: Store): - mid_null = await _insert_memory_with_embedding( - store, org_id="solo", content="null-imp", embedding=_EMB, importance_score=None - ) - mid_low = await _insert_memory_with_embedding( - store, org_id="solo", content="low-imp", embedding=_EMB, importance_score=0.5 - ) - mid_high = await _insert_memory_with_embedding( - store, org_id="solo", content="high-imp", embedding=_EMB, importance_score=0.9 - ) - - results = await store.list_candidate_memories_for_recommendation("solo") - ids = [r.id for r in results] - assert ids.index(mid_high) < ids.index(mid_low) - assert ids.index(mid_low) < ids.index(mid_null) - - @pytest.mark.asyncio async def test_list_candidates_respects_limit(store: Store): for i in range(5): diff --git a/tests/persistence/test_quality_columns_dropped.py b/tests/persistence/test_quality_columns_dropped.py new file mode 100644 index 0000000..acf7930 --- /dev/null +++ b/tests/persistence/test_quality_columns_dropped.py @@ -0,0 +1,37 @@ +"""Regression test: importance_score and confidence columns must stay dropped. + +Phase 1 of the drop-quality-score-columns refactor removed two memory columns +(`importance_score` and `confidence`) and the corresponding +``lore.importance`` module. This test asserts that nothing reintroduces them +on a freshly-migrated SQLite database, and that the importance module stays +gone — both are easy to add back by accident in a future migration. +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("aiosqlite") +pytest.importorskip("sqlite_vec") + +from lore.persistence.sqlite import SqliteStore + + +@pytest.mark.asyncio +async def test_memories_table_has_no_quality_score_columns(tmp_path): + db = tmp_path / "lore.db" + store = await SqliteStore.open(f"sqlite:///{db}") + try: + cur = await store._conn.execute("PRAGMA table_info(memories)") + rows = await cur.fetchall() + cols = [row[1] for row in rows] + await cur.close() + assert "importance_score" not in cols + assert "confidence" not in cols + finally: + await store.close() + + +def test_importance_module_does_not_exist(): + with pytest.raises(ImportError): + from lore import importance # noqa: F401 diff --git a/tests/persistence/test_types.py b/tests/persistence/test_types.py index 65716e8..cd10a0a 100644 --- a/tests/persistence/test_types.py +++ b/tests/persistence/test_types.py @@ -87,7 +87,6 @@ def test_stored_memory_round_trip(): content="hello", context=None, tags=("a", "b"), - confidence=0.9, source=None, project="proj", created_at=now, @@ -96,7 +95,6 @@ def test_stored_memory_round_trip(): upvotes=0, downvotes=0, meta={"type": "lesson"}, - importance_score=1.0, access_count=0, last_accessed_at=None, ) @@ -112,7 +110,6 @@ def test_scored_memory_extends_stored(): content="ranked", context=None, tags=(), - confidence=1.0, source=None, project=None, created_at=now, @@ -121,7 +118,6 @@ def test_scored_memory_extends_stored(): upvotes=0, downvotes=0, meta={}, - importance_score=1.0, access_count=0, last_accessed_at=None, score=0.87, @@ -316,7 +312,6 @@ def test_graph_stats_construction(): {"name": "Alice", "type": "person", "mention_count": 10}, {"name": "Bob", "type": "person", "mention_count": 8}, ], - avg_importance=0.65, recent_24h=5, recent_7d=15, oldest_memory=None, @@ -326,7 +321,6 @@ def test_graph_stats_construction(): assert gs.total_entities == 50 assert gs.total_relationships == 75 assert len(gs.top_entities) == 2 - assert gs.avg_importance == 0.65 def test_timeline_bucket_row_construction(): @@ -1395,7 +1389,6 @@ def test_exported_memory_defaults(): content="exported content", context=None, tags=(), - confidence=0.5, source=None, project=None, embedding=None, @@ -1411,7 +1404,6 @@ def test_exported_memory_defaults(): assert em.content == "exported content" assert em.context is None assert em.tags == () - assert em.confidence == 0.5 assert em.source is None assert em.project is None assert em.embedding is None @@ -1432,7 +1424,6 @@ def test_exported_memory_all_fields(): content="full memory", context="some context", tags=("python", "backend"), - confidence=0.9, source="conversation", project="proj_alpha", embedding=[0.1, 0.2, 0.3], @@ -1445,7 +1436,6 @@ def test_exported_memory_all_fields(): ) assert em.context == "some context" assert em.tags == ("python", "backend") - assert em.confidence == 0.9 assert em.source == "conversation" assert em.project == "proj_alpha" assert list(em.embedding) == [0.1, 0.2, 0.3] @@ -1463,7 +1453,6 @@ def test_exported_memory_frozen(): content="frozen test", context=None, tags=(), - confidence=0.5, source=None, project=None, embedding=None, @@ -1486,7 +1475,6 @@ def test_exported_memory_slots(): content="slots test", context=None, tags=(), - confidence=0.5, source=None, project=None, embedding=None, diff --git a/tests/server/test_dashboards_routes.py b/tests/server/test_dashboards_routes.py index 872abbc..97a266b 100644 --- a/tests/server/test_dashboards_routes.py +++ b/tests/server/test_dashboards_routes.py @@ -49,7 +49,6 @@ def _make_stored_memory( content=content, context=context, tags=list(tags), - confidence=0.9, source=None, project=project, created_at=now, @@ -58,7 +57,6 @@ def _make_stored_memory( upvotes=0, downvotes=0, meta={}, - importance_score=1.0, access_count=0, last_accessed_at=None, ) diff --git a/tests/server/test_graph_routes.py b/tests/server/test_graph_routes.py index 6d6ff36..ebc002d 100644 --- a/tests/server/test_graph_routes.py +++ b/tests/server/test_graph_routes.py @@ -38,7 +38,6 @@ def _make_stored_memory(memory_id="mem-001", content="hello world", **kwargs): content=content, context=None, tags=("python",), - confidence=0.9, source=None, project="lore", created_at=now, @@ -47,7 +46,6 @@ def _make_stored_memory(memory_id="mem-001", content="hello world", **kwargs): upvotes=0, downvotes=0, meta={"type": "lesson"}, - importance_score=1.0, access_count=0, last_accessed_at=None, ) @@ -144,7 +142,6 @@ def _make_graph_stats(**kwargs): by_project={}, by_entity_type={}, top_entities=[], - avg_importance=0.0, recent_24h=0, recent_7d=0, oldest_memory=None, diff --git a/tests/server/test_lessons_routes.py b/tests/server/test_lessons_routes.py index f268091..7fa5622 100644 --- a/tests/server/test_lessons_routes.py +++ b/tests/server/test_lessons_routes.py @@ -37,7 +37,6 @@ def _make_stored_memory( content="KeyError when dict key absent", context="Use .get() with a default instead of direct access", tags=("python", "dict"), - confidence=0.9, source=None, project=None, upvotes=0, @@ -54,7 +53,6 @@ def _make_stored_memory( content=content, context=context, tags=list(tags), - confidence=confidence, source=source, project=project, created_at=now, @@ -63,7 +61,6 @@ def _make_stored_memory( upvotes=upvotes, downvotes=downvotes, meta=meta if meta is not None else {}, - importance_score=0.0, access_count=0, last_accessed_at=None, ) @@ -87,7 +84,6 @@ def _make_exported_memory( content=content, context=context, tags=[], - confidence=0.9, source=None, project=None, embedding=embedding, @@ -115,7 +111,6 @@ def _make_search_result_dict( content=content, context=context, tags=[], - confidence=0.9, source=None, project=None, created_at=now, @@ -196,7 +191,6 @@ def test_post_returns_201_with_id(client, monkeypatch): "problem": "KeyError in dict access", "resolution": "Use .get() method", "tags": ["python"], - "confidence": 0.8, }, ) assert resp.status_code == 201 @@ -254,7 +248,6 @@ def test_post_access_returns_dict(client, monkeypatch): "id": "mem-1", "access_count": 5, "last_accessed_at": now, - "importance_score": 0.75, } ), ) @@ -263,7 +256,6 @@ def test_post_access_returns_dict(client, monkeypatch): body = resp.json() assert body["id"] == "mem-1" assert body["access_count"] == 5 - assert body["importance_score"] == 0.75 def test_post_access_404_on_missing(client, monkeypatch): @@ -329,7 +321,7 @@ def test_patch_changes_field(client, monkeypatch): memory_id="mem-1", content="KeyError in dict access", context="Use .get() with a default", - confidence=0.95, + tags=("python", "dict", "new-tag"), ) monkeypatch.setattr( lessons_service, @@ -338,12 +330,11 @@ def test_patch_changes_field(client, monkeypatch): ) resp = test_client.patch( "/v1/lessons/mem-1", - json={"confidence": 0.95}, + json={"tags": ["python", "dict", "new-tag"]}, ) assert resp.status_code == 200 body = resp.json() assert body["id"] == "mem-1" - assert body["confidence"] == 0.95 assert body["problem"] == "KeyError in dict access" assert body["resolution"] == "Use .get() with a default" @@ -387,7 +378,7 @@ def test_patch_404_on_missing(client, monkeypatch): "update", AsyncMock(side_effect=StoreNotFoundError("memories", "mem-gone")), ) - resp = test_client.patch("/v1/lessons/mem-gone", json={"confidence": 0.5}) + resp = test_client.patch("/v1/lessons/mem-gone", json={"tags": ["x"]}) assert resp.status_code == 404 @@ -488,7 +479,6 @@ def test_import_returns_count(client, monkeypatch): "problem": f"Problem {i}", "resolution": f"Resolution {i}", "embedding": [0.1] * 384, - "confidence": 0.8, } for i in range(3) ] diff --git a/tests/server/test_retrieve.py b/tests/server/test_retrieve.py index b77ee24..ef8947e 100644 --- a/tests/server/test_retrieve.py +++ b/tests/server/test_retrieve.py @@ -63,7 +63,6 @@ def _scored_memory( content=content, context=None, tags=tags, - confidence=1.0, source="conversation", project=project, created_at=NOW, @@ -72,7 +71,6 @@ def _scored_memory( upvotes=0, downvotes=0, meta={"type": mem_type, "tier": "long"}, - importance_score=1.0, access_count=0, last_accessed_at=None, score=score, @@ -173,7 +171,7 @@ async def _fake_get_store(): # Phase 6C: per-signal breakdown lands on every hybrid result. for m in data["memories"]: assert isinstance(m["signals"], dict) - for key in ("vector", "fts", "graph", "recency", "importance"): + for key in ("vector", "fts", "graph", "recency"): assert key in m["signals"] assert data["query_time_ms"] >= 0 assert "1, should clamp - source_importance=2.0, # >1, should clamp source_mention_count=1000, target_mention_count=1000, age_hours=10000.0, ) assert rs.confidence_risk == 0.0 - assert rs.source_reliability == 0.0 assert rs.entity_importance == 25.0 assert rs.staleness_risk == 10.0 diff --git a/tests/services/test_lessons.py b/tests/services/test_lessons.py index 296cbd1..3e037a6 100644 --- a/tests/services/test_lessons.py +++ b/tests/services/test_lessons.py @@ -24,11 +24,9 @@ def _make_scored_memory( content: str = "problem text", context: str = "resolution text", score: float = 0.9, - importance_score: float = 1.0, created_at: datetime | None = None, last_accessed_at: datetime | None = None, tags: tuple = (), - confidence: float = 0.8, source: str | None = None, project: str | None = None, upvotes: int = 0, @@ -42,7 +40,6 @@ def _make_scored_memory( content=content, context=context, tags=tags, - confidence=confidence, source=source, project=project, created_at=created_at or now, @@ -51,7 +48,6 @@ def _make_scored_memory( upvotes=upvotes, downvotes=downvotes, meta=meta or {}, - importance_score=importance_score, access_count=0, last_accessed_at=last_accessed_at, score=score, @@ -71,7 +67,6 @@ async def test_create_inserts_with_field_translation(store): resolution="The resolution", context="legacy context field", tags=["a", "b"], - confidence=0.9, source="manual", project=None, embedding=None, @@ -94,7 +89,6 @@ async def test_create_drops_context_field_silently(store): resolution="res", context="this should be ignored", tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -119,7 +113,6 @@ async def test_search_applies_time_decay(store, monkeypatch): recent = _make_scored_memory( memory_id="recent", score=0.8, - importance_score=1.0, created_at=now - timedelta(days=1), meta={"type": "lesson"}, # half_life=30 ) @@ -127,7 +120,6 @@ async def test_search_applies_time_decay(store, monkeypatch): old = _make_scored_memory( memory_id="old", score=0.8, - importance_score=1.0, created_at=now - timedelta(days=60), meta={"type": "lesson"}, # half_life=30 ) @@ -144,7 +136,7 @@ async def fake_recall(params): project=None, tags=[], limit=5, - min_confidence=0.0, + min_score=0.0, ) assert len(results) == 2 @@ -156,8 +148,8 @@ async def fake_recall(params): @pytest.mark.asyncio -async def test_search_filters_below_min_confidence(store, monkeypatch): - """Memories whose final score < min_confidence are excluded.""" +async def test_search_filters_below_min_score(store, monkeypatch): + """Memories whose final score < min_score are excluded.""" now = datetime.now(timezone.utc) high = _make_scored_memory(memory_id="high", score=0.9, created_at=now) low = _make_scored_memory(memory_id="low", score=0.1, created_at=now) @@ -174,7 +166,7 @@ async def fake_recall(params): project=None, tags=[], limit=5, - min_confidence=0.5, + min_score=0.5, ) ids = [r["id"] for r in results] @@ -195,7 +187,6 @@ async def test_record_access_returns_dict(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -208,7 +199,6 @@ async def test_record_access_returns_dict(store): assert result["id"] == lesson_id assert result["access_count"] == 1 assert result["last_accessed_at"] is not None - assert "importance_score" in result @pytest.mark.asyncio @@ -233,7 +223,6 @@ async def test_record_access_404_on_project_mismatch(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project="project-a", embedding=None, @@ -259,7 +248,6 @@ async def test_get_returns_stored_memory(store): resolution="got it", context=None, tags=["x"], - confidence=0.7, source="test", project=None, embedding=None, @@ -284,7 +272,6 @@ async def test_get_404_on_project_mismatch(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project="project-a", embedding=None, @@ -300,37 +287,6 @@ async def test_get_404_on_project_mismatch(store): # ── update ──────────────────────────────────────────────────────────── -@pytest.mark.asyncio -async def test_update_changes_confidence(store): - """update() patches confidence and returns the updated StoredMemory.""" - lesson_id = await lessons.create( - store, - org_id="solo", - problem="update me", - resolution="", - context=None, - tags=[], - confidence=0.3, - source=None, - project=None, - embedding=None, - expires_at=None, - meta={}, - ) - updated = await lessons.update( - store, - org_id="solo", - lesson_id=lesson_id, - project=None, - confidence=0.9, - tags=None, - meta=None, - upvotes=None, - downvotes=None, - ) - assert updated.confidence == pytest.approx(0.9, abs=1e-4) - - @pytest.mark.asyncio async def test_update_with_plus_one_upvote(store): """upvotes='+1' increments upvotes by 1.""" @@ -341,7 +297,6 @@ async def test_update_with_plus_one_upvote(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -353,7 +308,6 @@ async def test_update_with_plus_one_upvote(store): org_id="solo", lesson_id=lesson_id, project=None, - confidence=None, tags=None, meta=None, upvotes="+1", @@ -372,7 +326,6 @@ async def test_update_with_minus_one_vote_raises(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -385,7 +338,6 @@ async def test_update_with_minus_one_vote_raises(store): org_id="solo", lesson_id=lesson_id, project=None, - confidence=None, tags=None, meta=None, upvotes="-1", @@ -403,7 +355,6 @@ async def test_update_with_absolute_vote_raises(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -416,7 +367,6 @@ async def test_update_with_absolute_vote_raises(store): org_id="solo", lesson_id=lesson_id, project=None, - confidence=None, tags=None, meta=None, upvotes=5, @@ -434,7 +384,6 @@ async def test_update_no_fields_raises(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project=None, embedding=None, @@ -447,7 +396,6 @@ async def test_update_no_fields_raises(store): org_id="solo", lesson_id=lesson_id, project=None, - confidence=None, tags=None, meta=None, upvotes=None, @@ -483,7 +431,6 @@ async def test_list_returns_total_and_lessons(store): resolution="", context=None, tags=[], - confidence=0.5, source=None, project="list-proj", embedding=None, @@ -518,7 +465,6 @@ async def test_export_includes_embeddings(store): resolution="exported", context=None, tags=[], - confidence=0.5, source=None, project="export-proj", embedding=[0.1] * 384, @@ -548,7 +494,6 @@ async def test_import_upserts(store): problem="imported problem", resolution="imported resolution", tags=["imported"], - confidence=0.8, source="import", project="import-proj", embedding=[0.0] * 384, @@ -590,7 +535,6 @@ async def test_import_uses_project_override(store): problem="override test", resolution="", tags=[], - confidence=0.5, source=None, project="original-project", embedding=None, diff --git a/tests/services/test_recommendations.py b/tests/services/test_recommendations.py index d02ce3a..b0087c1 100644 --- a/tests/services/test_recommendations.py +++ b/tests/services/test_recommendations.py @@ -70,8 +70,8 @@ async def _insert_memory_with_embedding( await conn.execute( """ INSERT INTO memories - (id, org_id, content, context, embedding, importance_score, access_count) - VALUES ($1, $2, $3, '', $4::vector, 0.5, 0) + (id, org_id, content, context, embedding, access_count) + VALUES ($1, $2, $3, '', $4::vector, 0) """, mem_id, org_id, diff --git a/tests/services/test_temporal_supersession.py b/tests/services/test_temporal_supersession.py index 0b54b63..59ed84c 100644 --- a/tests/services/test_temporal_supersession.py +++ b/tests/services/test_temporal_supersession.py @@ -54,9 +54,9 @@ def _make_stored(mid: str, content: str) -> StoredMemory: now = datetime.now(timezone.utc) return StoredMemory( id=mid, org_id="solo", content=content, context=None, tags=(), - confidence=0.5, source=None, project=None, created_at=now, + source=None, project=None, created_at=now, updated_at=now, expires_at=None, upvotes=0, downvotes=0, - meta={}, importance_score=0.5, access_count=0, last_accessed_at=None, + meta={}, access_count=0, last_accessed_at=None, ) @@ -319,10 +319,10 @@ async def recall_by_embedding(self, params): for m in self._memories: out.append(ScoredMemory( id=m.id, org_id=m.org_id, content=m.content, context=m.context, - tags=m.tags, confidence=m.confidence, source=m.source, + tags=m.tags, source=m.source, project=m.project, created_at=m.created_at, updated_at=m.updated_at, expires_at=m.expires_at, upvotes=m.upvotes, downvotes=m.downvotes, - meta=m.meta, importance_score=m.importance_score, + meta=m.meta, access_count=m.access_count, last_accessed_at=m.last_accessed_at, score=1.0, )) @@ -487,7 +487,6 @@ async def insert_memory(self, nm): id=new_id, org_id=nm.org_id, content=nm.content, context=getattr(nm, "context", None), tags=tuple(getattr(nm, "tags", ())), - confidence=getattr(nm, "confidence", 0.5), source=getattr(nm, "source", None), project=getattr(nm, "project", None), created_at=datetime.now(timezone.utc), @@ -495,7 +494,7 @@ async def insert_memory(self, nm): expires_at=getattr(nm, "expires_at", None), upvotes=0, downvotes=0, meta=dict(getattr(nm, "meta", {}) or {}), - importance_score=0.5, access_count=0, last_accessed_at=None, + access_count=0, last_accessed_at=None, ) self.memories[new_id] = stored return stored @@ -645,12 +644,12 @@ def test_http_provenance_returns_sources_chain_and_meta(http_client): fake.memories["m-merged"] = _make_stored("m-merged", "merged") fake.memories["m-merged"] = StoredMemory( id="m-merged", org_id="solo", content="merged", context=None, - tags=(), confidence=0.5, source="consolidation", project=None, + tags=(), source="consolidation", project=None, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), expires_at=None, upvotes=0, downvotes=0, meta={"type": "lesson", "consolidated_from": ["legacy-x"]}, - importance_score=0.5, access_count=0, last_accessed_at=None, + access_count=0, last_accessed_at=None, ) fake.events.append(("m-old", "m-merged", "merge old", "api")) fake.events.append(("m-new", "m-merged", "merge new", "api")) diff --git a/tests/test_consolidation.py b/tests/test_consolidation.py index d3ee8c6..be1e589 100644 --- a/tests/test_consolidation.py +++ b/tests/test_consolidation.py @@ -66,14 +66,12 @@ def _make_memory( tier: str = "short", project: str = "proj", created_at: str = "", - importance_score: float = 0.5, embedding: bytes | None = _UNSET, tags: list | None = None, type: str = "general", access_count: int = 0, upvotes: int = 0, downvotes: int = 0, - confidence: float = 1.0, archived: bool = False, consolidated_into: str | None = None, ) -> Memory: @@ -91,11 +89,9 @@ def _make_memory( embedding=embedding, created_at=created_at, updated_at=created_at, - importance_score=importance_score, access_count=access_count, upvotes=upvotes, downvotes=downvotes, - confidence=confidence, archived=archived, consolidated_into=consolidated_into, ) @@ -504,12 +500,12 @@ def test_no_mentions_returns_empty(self): # --------------------------------------------------------------------------- class TestLLMSummarization: - def test_dedup_strategy_uses_highest_importance(self): - m1 = _make_memory("m1", importance_score=0.3, content="low") - m2 = _make_memory("m2", importance_score=0.8, content="high") + def test_dedup_strategy_uses_most_recent(self): + m1 = _make_memory("m1", created_at="2024-01-01T00:00:00+00:00", content="older") + m2 = _make_memory("m2", created_at="2024-06-01T00:00:00+00:00", content="newer") engine = _make_engine(llm=FakeLLM()) content = engine._summarize_group([m1, m2], "deduplicate") - assert content == "high" + assert content == "newer" def test_summarize_with_llm(self): m1 = _make_memory("m1", content="first memory") @@ -519,11 +515,11 @@ def test_summarize_with_llm(self): assert content == "Consolidated summary of memories." def test_no_llm_falls_back(self): - m1 = _make_memory("m1", importance_score=0.5, content="best") - m2 = _make_memory("m2", importance_score=0.3, content="other") + m1 = _make_memory("m1", created_at="2024-06-01T00:00:00+00:00", content="latest") + m2 = _make_memory("m2", created_at="2024-01-01T00:00:00+00:00", content="older") engine = _make_engine(llm=None) content = engine._summarize_group([m1, m2], "summarize") - assert content == "best" + assert content == "latest" def test_llm_error_falls_back(self): class FailingLLM: @@ -531,19 +527,19 @@ class FailingLLM: def complete(self, prompt, *, max_tokens=200): raise RuntimeError("LLM unavailable") - m1 = _make_memory("m1", importance_score=0.7, content="fallback content") - m2 = _make_memory("m2", importance_score=0.3, content="other") + m1 = _make_memory("m1", created_at="2024-06-01T00:00:00+00:00", content="fallback content") + m2 = _make_memory("m2", created_at="2024-01-01T00:00:00+00:00", content="other") engine = _make_engine(llm=FailingLLM()) content = engine._summarize_group([m1, m2], "summarize") assert content == "fallback content" def test_create_consolidated_memory(self): - m1 = _make_memory("m1", type="fact", importance_score=0.3, tags=["python"], - access_count=5, upvotes=1, downvotes=0, confidence=0.8) - m2 = _make_memory("m2", type="fact", importance_score=0.8, tags=["testing", "python"], - access_count=3, upvotes=0, downvotes=2, confidence=0.9) - m3 = _make_memory("m3", type="lesson", importance_score=0.5, tags=["ci"], - access_count=2, upvotes=2, downvotes=0, confidence=0.7) + m1 = _make_memory("m1", type="fact", tags=["python"], + access_count=5, upvotes=1, downvotes=0) + m2 = _make_memory("m2", type="fact", tags=["testing", "python"], + access_count=3, upvotes=0, downvotes=2) + m3 = _make_memory("m3", type="lesson", tags=["ci"], + access_count=2, upvotes=2, downvotes=0) engine = _make_engine() consolidated = engine._create_consolidated_memory([m1, m2, m3], "merged content", "deduplicate") @@ -552,11 +548,9 @@ def test_create_consolidated_memory(self): assert consolidated.type == "fact" # most common assert consolidated.tier == "long" assert consolidated.source == "consolidation" - assert consolidated.importance_score == 0.8 assert consolidated.access_count == 10 assert consolidated.upvotes == 3 assert consolidated.downvotes == 2 - assert consolidated.confidence == 0.9 assert set(consolidated.tags) == {"python", "testing", "ci"} assert consolidated.metadata["consolidation_strategy"] == "deduplicate" assert consolidated.metadata["original_count"] == 3 @@ -760,10 +754,12 @@ async def failing_process(group, strategy, result): def test_full_execute_with_dedup(self): store = MemoryStore() vec = [0.5] * 384 - m1 = _make_memory("m1", embedding=_embed(vec), created_at=_old_iso(700000), - importance_score=0.8, content="primary content") + # Both eligible (older than short-tier 7-day threshold). Newer one + # wins the deduplication summary under the most-recent strategy. + m1 = _make_memory("m1", embedding=_embed(vec), created_at=_old_iso(900000), + content="older duplicate") m2 = _make_memory("m2", embedding=_embed(vec), created_at=_old_iso(700000), - importance_score=0.3, content="duplicate content") + content="primary content") store.save(m1) store.save(m2) @@ -778,7 +774,7 @@ def test_full_execute_with_dedup(self): active = store.list() assert len(active) == 1 assert active[0].source == "consolidation" - assert active[0].content == "primary content" # highest importance + assert active[0].content == "primary content" # most recent # Verify log log = store.get_consolidation_log() diff --git a/tests/test_decay_voting.py b/tests/test_decay_voting.py index 4098a56..49be47b 100644 --- a/tests/test_decay_voting.py +++ b/tests/test_decay_voting.py @@ -78,8 +78,8 @@ def test_older_memory_scores_lower(self) -> None: now = datetime.now(timezone.utc) - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) + mid1 = lore.remember("stripe 429 — backoff") + mid2 = lore.remember("stripe 429 — backoff") m1 = store.get(mid1) m2 = store.get(mid2) @@ -94,49 +94,13 @@ def test_older_memory_scores_lower(self) -> None: scores = {r.memory.id: r.score for r in results} assert scores[mid1] > scores[mid2] - def test_upvotes_boost_score(self) -> None: - """A memory with 5 upvotes scores higher than identical with 0.""" - fixed_vec = np.random.RandomState(42).randn(_DIM).astype(np.float32) - fixed_vec = (fixed_vec / np.linalg.norm(fixed_vec)).tolist() - - store = MemoryStore() - lore = Lore(store=store, embedding_fn=lambda _: fixed_vec) - - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) - - for _ in range(5): - lore.upvote(mid1) - - results = lore.recall("stripe rate limit", limit=10) - scores = {r.memory.id: r.score for r in results} - assert scores[mid1] > scores[mid2] - - def test_downvotes_reduce_score(self) -> None: - """More downvotes than upvotes reduces score.""" - fixed_vec = np.random.RandomState(42).randn(_DIM).astype(np.float32) - fixed_vec = (fixed_vec / np.linalg.norm(fixed_vec)).tolist() - - store = MemoryStore() - lore = Lore(store=store, embedding_fn=lambda _: fixed_vec) - - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) - - for _ in range(3): - lore.downvote(mid2) - - results = lore.recall("stripe rate limit", limit=10) - scores = {r.memory.id: r.score for r in results} - assert scores[mid1] > scores[mid2] - def test_configurable_half_life(self) -> None: """Custom half-life affects decay.""" store = MemoryStore() lore_short = Lore(store=store, embedding_fn=_fake_embed, decay_half_life_days=7) now = datetime.now(timezone.utc) - mid = lore_short.remember("test memory", confidence=1.0) + mid = lore_short.remember("test memory") memory = store.get(mid) assert memory is not None memory.created_at = (now - timedelta(days=7)).isoformat() @@ -151,7 +115,7 @@ def test_vote_factor_clamped_at_0_1(self) -> None: store = MemoryStore() lore = Lore(store=store, embedding_fn=_fake_embed) - mid = lore.remember("test memory", confidence=0.9) + mid = lore.remember("test memory") for _ in range(100): lore.downvote(mid) diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 55f46b7..1c95d25 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -124,24 +124,6 @@ def test_tags_with_special_chars(self, lore): assert "c#" in memory.tags -class TestConfidenceBoundaries: - def test_confidence_zero(self, lore): - mid = lore.remember("test", confidence=0.0) - memory = lore.get(mid) - assert memory.confidence == 0.0 - - def test_confidence_one(self, lore): - mid = lore.remember("test", confidence=1.0) - memory = lore.get(mid) - assert memory.confidence == 1.0 - - def test_confidence_negative_raises(self, lore): - with pytest.raises(ValueError): - lore.remember("test", confidence=-0.1) - - def test_confidence_above_one_raises(self, lore): - with pytest.raises(ValueError): - lore.remember("test", confidence=1.1) class TestRedactionEdgeCases: diff --git a/tests/test_enrichment_memories.py b/tests/test_enrichment_memories.py index bc16240..d97b2e6 100644 --- a/tests/test_enrichment_memories.py +++ b/tests/test_enrichment_memories.py @@ -27,7 +27,6 @@ def _make_stored_memory(memory_id: str = "mem-001", content: str = "Test content content=content, context=None, tags=(), - confidence=0.5, source=None, project=None, created_at=now, @@ -36,7 +35,6 @@ def _make_stored_memory(memory_id: str = "mem-001", content: str = "Test content upvotes=0, downvotes=0, meta={}, - importance_score=1.0, access_count=0, last_accessed_at=None, ) diff --git a/tests/test_export_integration.py b/tests/test_export_integration.py index d96064c..c261375 100644 --- a/tests/test_export_integration.py +++ b/tests/test_export_integration.py @@ -31,8 +31,8 @@ def _make_full_dataset(store): created_at="2026-01-10T10:00:00Z", updated_at="2026-01-10T10:30:00Z", ttl=None, expires_at=None, - confidence=0.95, upvotes=3, downvotes=1, - importance_score=0.82, access_count=5, + upvotes=3, downvotes=1, + access_count=5, last_accessed_at="2026-01-10T10:30:00Z", archived=False, consolidated_into=None, ) diff --git a/tests/test_export_json.py b/tests/test_export_json.py index 430e6cd..d1d5ba6 100644 --- a/tests/test_export_json.py +++ b/tests/test_export_json.py @@ -36,7 +36,6 @@ def _seed_data(store): tags=["sqlite"], source="test", created_at="2026-01-10T10:00:00Z", updated_at="2026-01-10T10:00:00Z", - confidence=0.9, importance_score=0.8, ) m2 = Memory( id="m2", content="Docker build fails on M1", diff --git a/tests/test_export_markdown.py b/tests/test_export_markdown.py index be79f3b..b7f7e32 100644 --- a/tests/test_export_markdown.py +++ b/tests/test_export_markdown.py @@ -28,8 +28,7 @@ def _seed_data(store): m1 = Memory( id="m1", content="SQLite WAL mode fixes concurrency", type="code", tier="long", project="lore", - tags=["sqlite"], source="test", confidence=0.9, - importance_score=0.8, + tags=["sqlite"], source="test", created_at="2026-01-10T10:00:00Z", updated_at="2026-01-10T10:00:00Z", ) diff --git a/tests/test_export_serializers.py b/tests/test_export_serializers.py index 4a740df..2f04f5c 100644 --- a/tests/test_export_serializers.py +++ b/tests/test_export_serializers.py @@ -51,10 +51,8 @@ def _make_memory(**overrides) -> Memory: updated_at="2026-01-15T10:30:00Z", ttl=3600, expires_at="2026-01-15T11:00:00Z", - confidence=0.95, upvotes=3, downvotes=1, - importance_score=0.82, access_count=5, last_accessed_at="2026-01-15T10:30:00Z", archived=False, @@ -79,10 +77,8 @@ def test_memory_to_dict_all_fields(self): assert d["project"] == "lore" assert d["embedding"] is not None # base64 assert d["created_at"] == "2026-01-15T10:00:00Z" - assert d["confidence"] == 0.95 assert d["upvotes"] == 3 assert d["downvotes"] == 1 - assert d["importance_score"] == 0.82 assert d["access_count"] == 5 assert d["archived"] is False assert d["consolidated_into"] is None @@ -101,10 +97,8 @@ def test_dict_to_memory_all_fields(self): assert m2.source == m.source assert m2.project == m.project assert m2.embedding == m.embedding - assert m2.confidence == m.confidence assert m2.upvotes == m.upvotes assert m2.downvotes == m.downvotes - assert m2.importance_score == m.importance_score assert m2.access_count == m.access_count assert m2.archived == m.archived diff --git a/tests/test_graph_extraction_wiring.py b/tests/test_graph_extraction_wiring.py index 5792995..87984a1 100644 --- a/tests/test_graph_extraction_wiring.py +++ b/tests/test_graph_extraction_wiring.py @@ -36,10 +36,10 @@ def _stored(memory_id="mem-1", content="Pinecone ships Nexus."): now = datetime.now(timezone.utc) return StoredMemory( id=memory_id, org_id="org-001", content=content, context=None, - tags=(), confidence=0.5, source=None, project=None, + tags=(), source=None, project=None, created_at=now, updated_at=now, expires_at=None, upvotes=0, downvotes=0, meta={}, - importance_score=0.5, access_count=0, last_accessed_at=None, + access_count=0, last_accessed_at=None, ) diff --git a/tests/test_http_store.py b/tests/test_http_store.py index 536d36c..54821fe 100644 --- a/tests/test_http_store.py +++ b/tests/test_http_store.py @@ -288,7 +288,6 @@ def _make_memory(**overrides) -> Memory: updated_at="2026-01-01T00:00:00+00:00", ttl=None, expires_at=None, - confidence=0.9, upvotes=2, downvotes=1, ) @@ -304,7 +303,6 @@ def test_basic_mapping(self): assert lesson["resolution"] == "Always use retries" assert lesson["context"] == "HTTP calls" assert lesson["tags"] == ["http", "retry"] - assert lesson["confidence"] == 0.9 assert lesson["source"] == "test" assert lesson["project"] == "myproject" @@ -350,7 +348,6 @@ def test_basic_mapping(self): "resolution": "Use retries", "context": "HTTP", "tags": ["retry"], - "confidence": 0.8, "source": "test", "project": "proj", "created_at": "2026-01-01T00:00:00+00:00", @@ -365,7 +362,6 @@ def test_basic_mapping(self): assert mem.content == "Use retries" assert mem.type == "lesson" assert mem.tags == ["retry"] - assert mem.confidence == 0.8 assert mem.upvotes == 3 assert mem.embedding is None @@ -459,7 +455,7 @@ def test_get_returns_memory(self): store = _make_store() lesson_data = { "id": "srv-1", "problem": "content", "resolution": "content", - "context": None, "tags": ["a"], "confidence": 0.9, + "context": None, "tags": ["a"], "source": "s", "project": "p", "created_at": "2026-01-01T00:00:00+00:00", "updated_at": "2026-01-01T00:00:00+00:00", @@ -623,13 +619,13 @@ def test_search_with_filters(self): tags=["http"], project="proj", limit=10, - min_confidence=0.5, + min_score=0.5, ) body = store._client.request.call_args[1]["json"] assert body["tags"] == ["http"] assert body["project"] == "proj" assert body["limit"] == 10 - assert body["min_confidence"] == 0.5 + assert body["min_score"] == 0.5 store.close() def test_search_empty_results(self): @@ -663,7 +659,7 @@ def test_recall_delegates_to_search_when_available(self): lore._last_cleanup_count = 0 lore._half_life_days = 30 lore._half_lives = {} - lore._importance_threshold = 0.05 + lore._decay_threshold = 0.05 lore._decay_config = None lore._tier_weights = {"working": 1.0, "short": 1.1, "long": 1.2} @@ -689,7 +685,7 @@ def test_recall_uses_local_for_stores_without_search(self): lore._last_cleanup_count = 0 lore._half_life_days = 30 lore._half_lives = {} - lore._importance_threshold = 0.05 + lore._decay_threshold = 0.05 lore._decay_config = None lore._tier_weights = {"working": 1.0, "short": 1.1, "long": 1.2} @@ -720,7 +716,7 @@ def test_recall_uses_prose_vec_for_search(self): lore._last_cleanup_count = 0 lore._half_life_days = 30 lore._half_lives = {} - lore._importance_threshold = 0.05 + lore._decay_threshold = 0.05 lore._decay_config = None lore._tier_weights = {"working": 1.0, "short": 1.1, "long": 1.2} lore._dual_embedding = True diff --git a/tests/test_hybrid_retrieval.py b/tests/test_hybrid_retrieval.py index 4330cd6..2ff8055 100644 --- a/tests/test_hybrid_retrieval.py +++ b/tests/test_hybrid_retrieval.py @@ -50,8 +50,7 @@ def _vec(seed: int) -> Sequence[float]: return [((seed + i * 7) % 100) / 100.0 for i in range(384)] -def _make_stored(mid: str, content: str, *, age_days: float = 0.0, - importance: float = 0.5) -> StoredMemory: +def _make_stored(mid: str, content: str, *, age_days: float = 0.0) -> StoredMemory: created = NOW - timedelta(days=age_days) return StoredMemory( id=mid, @@ -59,7 +58,6 @@ def _make_stored(mid: str, content: str, *, age_days: float = 0.0, content=content, context=None, tags=(), - confidence=1.0, source=None, project=None, created_at=created, @@ -68,7 +66,6 @@ def _make_stored(mid: str, content: str, *, age_days: float = 0.0, upvotes=0, downvotes=0, meta={}, - importance_score=importance, access_count=0, last_accessed_at=None, ) @@ -197,10 +194,10 @@ async def _vec(_params): for m, s in (vec or []): out.append(ScoredMemory( id=m.id, org_id=m.org_id, content=m.content, context=m.context, - tags=m.tags, confidence=m.confidence, source=m.source, + tags=m.tags, source=m.source, project=m.project, created_at=m.created_at, updated_at=m.updated_at, expires_at=m.expires_at, upvotes=m.upvotes, downvotes=m.downvotes, - meta=m.meta, importance_score=m.importance_score, + meta=m.meta, access_count=m.access_count, last_accessed_at=m.last_accessed_at, score=s, )) @@ -238,8 +235,8 @@ async def _ent_by_name(_n): @pytest.mark.asyncio async def test_hybrid_recall_combines_signals(): - a = _make_stored("a", "alpha", importance=0.8) - b = _make_stored("b", "beta", importance=0.5) + a = _make_stored("a", "alpha") + b = _make_stored("b", "beta") store = _fake_store_with( vec=[(a, 0.9), (b, 0.4)], fts=[(a, 5.0)], @@ -257,7 +254,6 @@ async def test_hybrid_recall_combines_signals(): assert sigs["fts"] == pytest.approx(5.0) assert sigs["graph"] == pytest.approx(2.0) assert 0.0 < sigs["recency"] <= 1.0 - assert sigs["importance"] == pytest.approx(0.8) # Diagnostic plumbing must reflect that all three branches succeeded. assert report.attempted == {"vector": "ok", "fts": "ok", "graph": "ok"} assert report.best_score >= results[0].score @@ -370,7 +366,6 @@ def _scored_memory(memory_id="m1", content="hello", score=0.85, content=content, context=None, tags=tags, - confidence=1.0, source=None, project=project, created_at=NOW, @@ -379,7 +374,6 @@ def _scored_memory(memory_id="m1", content="hello", score=0.85, upvotes=0, downvotes=0, meta=meta or {"type": "note", "tier": "long"}, - importance_score=1.0, access_count=0, last_accessed_at=None, score=score, @@ -421,7 +415,7 @@ def mock_embedder(): @pytest.mark.asyncio async def test_v1_retrieve_returns_signals_breakdown(client): - """Each memory in the response carries vector/fts/graph/recency/importance signals.""" + """Each memory in the response carries vector/fts/graph/recency/superseded signals.""" sm = _scored_memory("mem-001", "kubernetes ingress troubleshooting", 0.85) fake_store = MagicMock() fake_store.recall_by_embedding = AsyncMock(return_value=[sm]) @@ -449,7 +443,7 @@ async def _fake_get_store(): assert data["count"] == 1 sigs = data["memories"][0]["signals"] assert set(sigs.keys()) == { - "vector", "fts", "graph", "recency", "importance", "superseded", + "vector", "fts", "graph", "recency", "superseded", } assert sigs["vector"] > 0 assert sigs["fts"] == 0 diff --git a/tests/test_importance_scoring.py b/tests/test_importance_scoring.py deleted file mode 100644 index 201da9b..0000000 --- a/tests/test_importance_scoring.py +++ /dev/null @@ -1,380 +0,0 @@ -"""Tests for F5 — Importance Scoring + Adaptive Decay. - -Covers: -- compute_importance: default, upvotes, downvotes floor, access log, combined -- time_adjusted_importance: fresh, one half-life, last_accessed recency -- resolve_half_life: tier+type, tier default, no tier, overrides -- decay_factor: boundary conditions -- Integration: access tracking, multiplicative scoring, cleanup, backward compat -""" - -from __future__ import annotations - -import math -import warnings -from datetime import datetime, timedelta, timezone -from typing import List - -import numpy as np - -from lore import Lore -from lore.importance import ( - compute_importance, - decay_factor, - resolve_half_life, - time_adjusted_importance, -) -from lore.store.memory import MemoryStore -from lore.types import DECAY_HALF_LIVES, TIER_DECAY_HALF_LIVES, Memory - -_DIM = 384 - - -def _fixed_embed(text: str) -> List[float]: - rng = np.random.RandomState(42) - vec = rng.randn(_DIM).astype(np.float32) - vec = vec / np.linalg.norm(vec) - return vec.tolist() - - -def _make_memory(**kwargs) -> Memory: - defaults = dict( - id="test-id", - content="test content", - type="lesson", - created_at=datetime.now(timezone.utc).isoformat(), - updated_at=datetime.now(timezone.utc).isoformat(), - confidence=1.0, - ) - defaults.update(kwargs) - return Memory(**defaults) - - -# ────────────────────────────────────────────────────────────── -# Unit tests: compute_importance -# ────────────────────────────────────────────────────────────── - - -class TestComputeImportance: - def test_default(self) -> None: - mem = _make_memory() - assert compute_importance(mem) == 1.0 - - def test_upvotes(self) -> None: - mem = _make_memory(upvotes=5, downvotes=0, access_count=0) - result = compute_importance(mem) - # vote_factor = 1.0 + 5*0.1 = 1.5, access_factor = 1.0 - assert abs(result - 1.5) < 0.01 - - def test_downvotes_floor(self) -> None: - mem = _make_memory(upvotes=0, downvotes=10, access_count=0) - result = compute_importance(mem) - # vote_factor = max(0.1, 1.0 - 10*0.1) = max(0.1, 0.0) = 0.1 - assert abs(result - 0.1) < 0.01 - - def test_access_log(self) -> None: - mem = _make_memory(access_count=10) - result = compute_importance(mem) - # access_factor = 1.0 + log2(11) * 0.1 ≈ 1.346 - expected = 1.0 * 1.0 * (1.0 + math.log2(11) * 0.1) - assert abs(result - expected) < 0.01 - - def test_combined(self) -> None: - mem = _make_memory(upvotes=5, downvotes=0, access_count=10, confidence=1.0) - result = compute_importance(mem) - vote_factor = 1.5 - access_factor = 1.0 + math.log2(11) * 0.1 - expected = 1.0 * vote_factor * access_factor - assert abs(result - expected) < 0.01 - - def test_low_confidence(self) -> None: - mem = _make_memory(confidence=0.5) - result = compute_importance(mem) - assert abs(result - 0.5) < 0.01 - - -# ────────────────────────────────────────────────────────────── -# Unit tests: time_adjusted_importance -# ────────────────────────────────────────────────────────────── - - -class TestTimeAdjustedImportance: - def test_fresh(self) -> None: - now = datetime.utcnow() - mem = _make_memory(created_at=now.isoformat(), importance_score=1.0) - tai = time_adjusted_importance(mem, 30.0, now=now) - assert abs(tai - 1.0) < 0.01 - - def test_one_half_life(self) -> None: - now = datetime.utcnow() - created = now - timedelta(days=30) - mem = _make_memory(created_at=created.isoformat(), importance_score=1.0) - tai = time_adjusted_importance(mem, 30.0, now=now) - assert abs(tai - 0.5) < 0.01 - - def test_last_accessed_recency(self) -> None: - now = datetime.utcnow() - created = now - timedelta(days=30) - last_accessed = now - timedelta(days=1) - mem = _make_memory( - created_at=created.isoformat(), - last_accessed_at=last_accessed.isoformat(), - importance_score=1.0, - ) - tai = time_adjusted_importance(mem, 30.0, now=now) - # Should use age=1 day (min of 30 and 1), so decay ≈ 0.977 - assert tai > 0.95 - - -# ────────────────────────────────────────────────────────────── -# Unit tests: resolve_half_life -# ────────────────────────────────────────────────────────────── - - -class TestResolveHalfLife: - def test_tier_type(self) -> None: - assert resolve_half_life("long", "convention") == 60.0 - - def test_tier_default(self) -> None: - assert resolve_half_life("working", "unknown_type") == 1 - - def test_no_tier(self) -> None: - result = resolve_half_life(None, "lesson") - assert result == 30.0 # Falls back to "long" tier - - def test_overrides(self) -> None: - overrides = {("short", "code"): 3.0} - result = resolve_half_life("short", "code", overrides=overrides) - assert result == 3.0 - - def test_nonexistent_tier(self) -> None: - result = resolve_half_life("nonexistent_tier", "note") - # Falls to DECAY_HALF_LIVES (long tier alias) - assert result == DECAY_HALF_LIVES.get("note", 30.0) - - -# ────────────────────────────────────────────────────────────── -# Unit tests: decay_factor -# ────────────────────────────────────────────────────────────── - - -class TestDecayFactor: - def test_age_zero(self) -> None: - assert decay_factor(0, 30) == 1.0 - - def test_very_large_age(self) -> None: - result = decay_factor(300, 30) - assert result < 0.001 - - def test_one_half_life(self) -> None: - result = decay_factor(30, 30) - assert abs(result - 0.5) < 0.01 - - -# ────────────────────────────────────────────────────────────── -# Integration tests -# ────────────────────────────────────────────────────────────── - - -class TestRecallAccessTracking: - def test_recall_updates_access_count(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - mid = lore.remember("test memory") - mem = store.get(mid) - assert mem is not None - assert mem.access_count == 0 - - lore.recall("anything", limit=1) - mem = store.get(mid) - assert mem is not None - assert mem.access_count == 1 - - def test_recall_sets_last_accessed(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - mid = lore.remember("test memory") - mem = store.get(mid) - assert mem is not None - assert mem.last_accessed_at is None - - lore.recall("anything", limit=1) - mem = store.get(mid) - assert mem is not None - assert mem.last_accessed_at is not None - - def test_recall_recomputes_importance(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - mid = lore.remember("test memory") - - # Upvote to change importance - for _ in range(3): - lore.upvote(mid) - - mem_before = store.get(mid) - assert mem_before is not None - score_before = mem_before.importance_score - - lore.recall("anything", limit=1) - mem_after = store.get(mid) - assert mem_after is not None - # access_count increased, so importance should increase - assert mem_after.importance_score > score_before - - -class TestMultiplicativeScoring: - def test_higher_importance_ranks_higher(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - - mid_a = lore.remember("test A") - mid_b = lore.remember("test B") - - # Upvote A to give it higher importance - for _ in range(5): - lore.upvote(mid_a) - - results = lore.recall("anything", limit=10) - scores = {r.memory.id: r.score for r in results} - assert scores[mid_a] > scores[mid_b] - - def test_working_tier_decays_faster(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - now = datetime.now(timezone.utc) - - mid_working = lore.remember("working tier", type="lesson") - mid_long = lore.remember("long tier", type="lesson") - - m_w = store.get(mid_working) - m_l = store.get(mid_long) - assert m_w is not None and m_l is not None - - m_w.tier = "working" - m_l.tier = "long" - age = (now - timedelta(days=5)).isoformat() - m_w.created_at = age - m_l.created_at = age - store.save(m_w) - store.save(m_l) - - results = lore.recall("anything", limit=10) - scores = {r.memory.id: r.score for r in results} - # Working tier lesson HL=3, long tier lesson HL=30 - # At 5 days: working decay ≈ 0.5^(5/3) ≈ 0.31, long decay ≈ 0.5^(5/30) ≈ 0.89 - assert scores[mid_long] > scores[mid_working] - - -class TestUpvoteUpdatesImportance: - def test_upvote_increases_importance(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - mid = lore.remember("test memory") - - mem = store.get(mid) - assert mem is not None - assert mem.importance_score == 1.0 - - lore.upvote(mid) - mem = store.get(mid) - assert mem is not None - assert mem.importance_score > 1.0 - - def test_downvote_decreases_importance(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - mid = lore.remember("test memory") - - for _ in range(3): - lore.downvote(mid) - - mem = store.get(mid) - assert mem is not None - # vote_factor = max(0.1, 1.0 - 0.3) = 0.7 - assert abs(mem.importance_score - 0.7) < 0.01 - - -class TestCleanupImportance: - def test_removes_low_importance(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - now = datetime.now(timezone.utc) - - mid = lore.remember("old memory", type="lesson") - mem = store.get(mid) - assert mem is not None - # Age it 150 days with HL=30 → TAI ≈ 0.031 - mem.created_at = (now - timedelta(days=150)).isoformat() - store.save(mem) - - count = lore.cleanup_expired(importance_threshold=0.05) - assert count >= 1 - assert store.get(mid) is None - - def test_preserves_important(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - now = datetime.now(timezone.utc) - - mid = lore.remember("important memory", type="lesson") - mem = store.get(mid) - assert mem is not None - # High importance through upvotes - mem.upvotes = 10 - mem.importance_score = compute_importance(mem) - mem.created_at = (now - timedelta(days=150)).isoformat() - store.save(mem) - - lore.cleanup_expired(importance_threshold=0.05) - # TAI ≈ 2.0 * 0.031 = 0.063 > 0.05 - assert store.get(mid) is not None - - -class TestBackwardCompat: - def test_decay_half_lives_alias(self) -> None: - assert DECAY_HALF_LIVES is TIER_DECAY_HALF_LIVES["long"] - assert DECAY_HALF_LIVES["code"] == 14 - assert DECAY_HALF_LIVES["convention"] == 60 - - def test_deprecated_params_warn(self) -> None: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - Lore( - - store=MemoryStore(), - embedding_fn=_fixed_embed, - decay_similarity_weight=0.5, - decay_freshness_weight=0.5, - ) - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "multiplicative" in str(w[0].message) - - def test_no_warning_for_defaults(self) -> None: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - Lore(store=MemoryStore(), embedding_fn=_fixed_embed) - deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] - assert len(deprecation_warnings) == 0 - - -class TestRecalculateImportance: - def test_recalculate(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - - mid = lore.remember("test memory") - # Manually set stale importance - mem = store.get(mid) - assert mem is not None - mem.upvotes = 5 - mem.importance_score = 0.5 # stale - store.save(mem) - - count = lore.recalculate_importance() - assert count == 1 - - mem = store.get(mid) - assert mem is not None - assert mem.importance_score > 1.0 # recomputed correctly diff --git a/tests/test_memories_server.py b/tests/test_memories_server.py index c6e1f0c..21b6eaf 100644 --- a/tests/test_memories_server.py +++ b/tests/test_memories_server.py @@ -23,7 +23,6 @@ def _make_stored_memory( content: str = "Use type hints everywhere", context: Optional[str] = "Python best practices", tags: Sequence[str] = ("python",), - confidence: float = 0.9, source: Optional[str] = "manual", project: Optional[str] = "lore", upvotes: int = 3, @@ -39,7 +38,6 @@ def _make_stored_memory( content=content, context=context, tags=tuple(tags), - confidence=confidence, source=source, project=project, created_at=now, @@ -48,7 +46,6 @@ def _make_stored_memory( upvotes=upvotes, downvotes=downvotes, meta=dict(meta or {}), - importance_score=1.0, access_count=0, last_accessed_at=None, ) @@ -189,7 +186,7 @@ def test_patch_not_found(self, client): from lore.persistence.exceptions import StoreNotFoundError store.update_memory.side_effect = StoreNotFoundError("memory", "nonexistent") resp = test_client.patch("/v1/memories/nonexistent", json={ - "confidence": 0.8, + "tags": ["x"], }) assert resp.status_code == 404 @@ -219,7 +216,6 @@ def test_create_request_fields(self): req = MemoryCreateRequest(content="test memory") assert req.content == "test memory" assert req.context is None - assert req.confidence == 0.5 assert req.tags == [] assert req.enrich is None @@ -230,7 +226,6 @@ def test_response_fields(self): id="mem-001", content="test", context="ctx", - confidence=0.9, created_at=now, updated_at=now, ) diff --git a/tests/test_memory.py b/tests/test_memory.py index 69ae3d2..3e9af98 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -12,7 +12,6 @@ def test_memory_creation_minimal(): assert memory.content == "some knowledge" assert memory.type == "general" assert memory.tags == [] - assert memory.confidence == 1.0 assert memory.upvotes == 0 assert memory.metadata is None assert memory.ttl is None @@ -33,7 +32,6 @@ def test_memory_creation_full(): updated_at="2026-01-01T00:00:00+00:00", ttl=3600, expires_at="2026-01-01T01:00:00+00:00", - confidence=0.9, upvotes=3, downvotes=1, ) @@ -41,12 +39,6 @@ def test_memory_creation_full(): assert memory.tags == ["a", "b"] assert memory.metadata == {"problem": "rate limiting", "resolution": "backoff"} assert memory.ttl == 3600 - assert memory.confidence == 0.9 - - -def test_memory_default_confidence_is_1(): - memory = Memory(id="x", content="test") - assert memory.confidence == 1.0 def test_memory_type_default(): diff --git a/tests/test_observations.py b/tests/test_observations.py index df60598..febff9d 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -215,7 +215,6 @@ def _make_stored(memory_id="mem-1", **overrides): content="narrative body", context="title", tags=["a"], - confidence=0.5, source="observation", project=None, created_at=now, @@ -230,7 +229,6 @@ def _make_stored(memory_id="mem-1", **overrides): "narrative": "narrative body", "captured_by": "auto", }, - importance_score=0.0, access_count=0, last_accessed_at=None, ) diff --git a/tests/test_progressive.py b/tests/test_progressive.py index 0b198d2..8dbab53 100644 --- a/tests/test_progressive.py +++ b/tests/test_progressive.py @@ -59,7 +59,6 @@ def _stored_memory( content=content, context=None, tags=tuple(tags), - confidence=1.0, source="conversation", project=project, created_at=NOW, @@ -68,7 +67,6 @@ def _stored_memory( upvotes=0, downvotes=0, meta=meta or {"type": "preference", "tier": "long"}, - importance_score=1.0, access_count=0, last_accessed_at=None, ) @@ -186,7 +184,6 @@ def _make_hybrid_results(memories_with_score): "fts": 0.5, "graph": 0.0, "recency": 1.0, - "importance": 0.8, }, ) ) @@ -242,7 +239,7 @@ async def _fake_get_store(): assert resp.status_code == 200 sigs = resp.json()["hits"][0]["signals"] - for k in ("vector", "fts", "graph", "recency", "importance"): + for k in ("vector", "fts", "graph", "recency"): assert k in sigs @@ -799,7 +796,7 @@ def test_token_budget_compact_index_is_5x_smaller_than_full_payload(): "title": memory_title(m), "score": 0.85, "signals": {"vector": 0.9, "fts": 0.5, "graph": 0.0, - "recency": 1.0, "importance": 0.8}, + "recency": 1.0}, } for m in memories ], @@ -814,7 +811,6 @@ def test_token_budget_compact_index_is_5x_smaller_than_full_payload(): "content": m.content, "context": m.context, "tags": list(m.tags), - "confidence": m.confidence, "source": m.source, "project": m.project, "created_at": m.created_at.isoformat(), diff --git a/tests/test_recall.py b/tests/test_recall.py index 6f8d85b..741c89d 100644 --- a/tests/test_recall.py +++ b/tests/test_recall.py @@ -63,14 +63,6 @@ def test_recall_with_limit(self) -> None: results = lore.recall("memory", limit=3) assert len(results) == 3 - def test_recall_with_min_confidence(self) -> None: - lore = _make_lore() - lore.remember("low confidence", confidence=0.2) - lore.remember("high confidence", confidence=0.8) - results = lore.recall("test", min_confidence=0.5) - assert len(results) == 1 - assert results[0].memory.confidence >= 0.5 - def test_recall_scores_reasonable(self) -> None: lore = _make_lore() for i in range(5): diff --git a/tests/test_recent.py b/tests/test_recent.py index d95baad..7e6ef93 100644 --- a/tests/test_recent.py +++ b/tests/test_recent.py @@ -21,7 +21,6 @@ def _make_memory( project: str | None = "lore", created_at: str = "2026-03-14T14:30:00+00:00", tags: list | None = None, - importance_score: float = 1.0, ) -> Memory: return Memory( id=id, @@ -32,7 +31,6 @@ def _make_memory( created_at=created_at, updated_at=created_at, tags=tags or [], - importance_score=importance_score, ) @@ -177,14 +175,12 @@ class TestFormatDetailed: def test_metadata_included(self): m = _make_memory( tier="short", - importance_score=0.85, tags=["architecture", "decision"], ) g = ProjectGroup(project="lore", memories=[m], count=1) r = RecentActivityResult(groups=[g], total_count=1, hours=24) text = format_detailed(r) assert "tier: short" in text - assert "importance: 0.85" in text assert "Tags: architecture, decision" in text def test_no_memories(self): @@ -214,7 +210,6 @@ def test_all_fields_present(self): mem = group["memories"][0] assert mem["id"] == "m1" assert mem["tags"] == ["tag1"] - assert "importance_score" in mem class TestFormatCli: diff --git a/tests/test_semantic_decay.py b/tests/test_semantic_decay.py index 8751b98..ea61938 100644 --- a/tests/test_semantic_decay.py +++ b/tests/test_semantic_decay.py @@ -75,7 +75,7 @@ def test_formula_components(self) -> None: store = MemoryStore() lore = Lore(store=store, embedding_fn=_fixed_embed) - lore.remember("test content", type="lesson", confidence=1.0) + lore.remember("test content", type="lesson") # Memory is brand new (age ~0), so TAI ≈ importance (1.0) # Long tier weight = 1.2 results = lore.recall("anything", limit=1) @@ -139,8 +139,8 @@ def _embed(text: str) -> List[float]: lore = Lore(store=store, embedding_fn=_embed) - mid_recent = lore.remember("recent code fix", type="code", confidence=1.0) - mid_old = lore.remember("old code fix", type="code", confidence=1.0) + mid_recent = lore.remember("recent code fix", type="code") + mid_old = lore.remember("old code fix", type="code") m_recent = store.get(mid_recent) m_old = store.get(mid_old) @@ -209,8 +209,8 @@ def test_older_memory_still_scores_lower(self) -> None: lore = Lore(store=store, embedding_fn=_fixed_embed) now = datetime.now(timezone.utc) - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) + mid1 = lore.remember("stripe 429 — backoff") + mid2 = lore.remember("stripe 429 — backoff") m1 = store.get(mid1) m2 = store.get(mid2) @@ -225,39 +225,11 @@ def test_older_memory_still_scores_lower(self) -> None: scores = {r.memory.id: r.score for r in results} assert scores[mid1] > scores[mid2] - def test_upvotes_still_boost_score(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) - - for _ in range(5): - lore.upvote(mid1) - - results = lore.recall("stripe rate limit", limit=10) - scores = {r.memory.id: r.score for r in results} - assert scores[mid1] > scores[mid2] - - def test_downvotes_still_reduce_score(self) -> None: - store = MemoryStore() - lore = Lore(store=store, embedding_fn=_fixed_embed) - - mid1 = lore.remember("stripe 429 — backoff", confidence=0.9) - mid2 = lore.remember("stripe 429 — backoff", confidence=0.9) - - for _ in range(3): - lore.downvote(mid2) - - results = lore.recall("stripe rate limit", limit=10) - scores = {r.memory.id: r.score for r in results} - assert scores[mid1] > scores[mid2] - def test_vote_factor_clamped_at_0_1(self) -> None: store = MemoryStore() lore = Lore(store=store, embedding_fn=_seeded_embed) - mid = lore.remember("test memory", confidence=0.9) + mid = lore.remember("test memory") for _ in range(100): lore.downvote(mid) diff --git a/tests/test_session_snapshots.py b/tests/test_session_snapshots.py index a942ee9..80014b2 100644 --- a/tests/test_session_snapshots.py +++ b/tests/test_session_snapshots.py @@ -57,7 +57,6 @@ def test_save_with_content_only(self): assert mem is not None assert mem.type == "session_snapshot" assert mem.tier == "long" - assert mem.importance_score == 0.95 assert "session_snapshot" in mem.tags assert mem.metadata is not None assert mem.metadata["extraction_method"] == "raw" @@ -89,11 +88,6 @@ def test_save_whitespace_content_raises(self): with pytest.raises(ValueError, match="non-empty"): lore.save_snapshot(" ") - def test_importance_score_is_0_95(self): - lore = _make_lore() - mem = lore.save_snapshot("important context") - assert mem.importance_score == 0.95 - def test_tags_include_session_id_and_type(self): lore = _make_lore() mem = lore.save_snapshot("test", session_id="mysession") @@ -208,16 +202,6 @@ def test_snapshot_prefix_in_cli_format(self): output = format_cli(result) assert "[Session Snapshot]" in output - def test_snapshot_has_high_importance(self): - lore = _make_lore() - lore.save_snapshot("important snapshot") - result = lore.recent_activity(hours=24) - for group in result.groups: - snapshots = [m for m in group.memories if m.type == "session_snapshot"] - for s in snapshots: - assert s.importance_score == 0.95 - - # ── E3-S5: save_snapshot MCP tool ───────────────────────────────── diff --git a/tests/test_stores.py b/tests/test_stores.py index d567c89..a22ea47 100644 --- a/tests/test_stores.py +++ b/tests/test_stores.py @@ -204,13 +204,6 @@ def test_list_limit(self) -> None: lore.remember("test") assert len(lore.list_memories(limit=3)) == 3 - def test_confidence_validation(self) -> None: - lore = Lore(store=MemoryStore(), embedding_fn=_stub_embed) - with pytest.raises(ValueError, match="confidence"): - lore.remember("test", confidence=1.5) - with pytest.raises(ValueError, match="confidence"): - lore.remember("test", confidence=-0.1) - def test_context_manager(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: os.path.join(tmpdir, "test.db") diff --git a/tests/test_temporal.py b/tests/test_temporal.py index bb4c4e7..a005a7c 100644 --- a/tests/test_temporal.py +++ b/tests/test_temporal.py @@ -28,7 +28,6 @@ def _make_memory( mid: str, content: str, created_at: str, - importance_score: float = 1.0, tier: str = "long", project: str | None = None, archived: bool = False, @@ -41,7 +40,6 @@ def _make_memory( content=content, created_at=created_at, updated_at=created_at, - importance_score=importance_score, tier=tier, project=project, archived=archived, @@ -98,18 +96,18 @@ def test_returns_dict_grouped_by_year(self): assert isinstance(results, dict) assert set(results.keys()) == {2024, 2023, 2022} - def test_ordered_by_year_desc_then_importance_desc(self): + def test_ordered_by_year_desc_then_created_at_desc(self): store = MemoryStore() - store.save(_make_memory("m1", "low", "2024-03-06T10:00:00+00:00", importance_score=0.5)) - store.save(_make_memory("m2", "high", "2024-03-06T12:00:00+00:00", importance_score=0.9)) - store.save(_make_memory("m3", "old", "2022-03-06T10:00:00+00:00", importance_score=0.8)) + store.save(_make_memory("m1", "earlier", "2024-03-06T10:00:00+00:00")) + store.save(_make_memory("m2", "later", "2024-03-06T12:00:00+00:00")) + store.save(_make_memory("m3", "old", "2022-03-06T10:00:00+00:00")) engine = OnThisDayEngine(store) results = engine.on_this_day(month=3, day=6, date_window_days=0) years = list(results.keys()) assert years == [2024, 2022] # DESC - # Within 2024, high importance first + # Within 2024, most recent first assert results[2024][0].id == "m2" assert results[2024][1].id == "m1" @@ -185,7 +183,7 @@ def test_all_memory_fields_preserved(self): store = MemoryStore() mem = _make_memory( "m1", "test content", "2024-03-06T10:00:00+00:00", - importance_score=0.75, tier="short", project="proj1", + tier="short", project="proj1", source="test", tags=["tag1"], ) store.save(mem) @@ -198,7 +196,6 @@ def test_all_memory_fields_preserved(self): assert m.content == "test content" assert m.tier == "short" assert m.project == "proj1" - assert m.importance_score == 0.75 assert m.source == "test" assert m.tags == ["tag1"] @@ -459,7 +456,6 @@ def test_limit_and_offset(self): for i in range(5): store.save(_make_memory( f"m{i}", f"mem {i}", "2024-03-06T10:00:00+00:00", - importance_score=float(i), )) engine = OnThisDayEngine(store) @@ -472,7 +468,6 @@ def test_offset(self): for i in range(5): store.save(_make_memory( f"m{i}", f"mem {i}", "2024-03-06T10:00:00+00:00", - importance_score=float(i), )) engine = OnThisDayEngine(store) diff --git a/tests/test_temporal_filters.py b/tests/test_temporal_filters.py index 41c59eb..1fea890 100644 --- a/tests/test_temporal_filters.py +++ b/tests/test_temporal_filters.py @@ -24,7 +24,7 @@ def _fake_embed(text: str) -> List[float]: def _make_lore() -> Lore: - return Lore(store=MemoryStore(), embedding_fn=_fake_embed, importance_threshold=0.0) + return Lore(store=MemoryStore(), embedding_fn=_fake_embed, decay_threshold=0.0) def _utc(year, month, day, hour=0, minute=0, second=0):