feat(pipeline): implement parliamentary embedding pipeline MVP
- Add 4 migration files: mp_votes, mp_metadata, svd_vectors, fused_embeddings - Extend database.py with 5 new helper methods and table init - Add pipeline/ package: extract_mp_votes, fetch_mp_metadata, text_pipeline, svd_pipeline (with Procrustes alignment), fusion - Add full test suite (17 tests) covering all pipeline modules and migrations - Fix Procrustes alignment bug: scipy scale is a norm value, not a multiplier - Fix DuckDB date type handling in test assertions (datetime.date vs string) - Remove duckdb.py shim; tests now run against real duckdb + scipy via uv Ref: thoughts/shared/plans/2026-03-21-parliamentary-embedding-pipeline-plan.md
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
---
|
||||
date: 2026-03-19
|
||||
topic: "Stemwijzer AI & DB design"
|
||||
status: draft
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
We need a clear, low-risk design to improve AI usage and query ergonomics in this repository. The codebase currently ingests motions, stores them in DuckDB, and generates AI-driven layman summaries via an OpenRouter/OpenAI client. There are a few maintenance issues (e.g., missing config keys, a broken reset script) and no embedding/search infrastructure.
|
||||
|
||||
**Goal:**
|
||||
- Centralize AI/LLM usage behind a provider abstraction so we can swap or prefer providers later.
|
||||
- Introduce minimal embeddings storage and search so we can add semantic features without heavy infra.
|
||||
- Prefer ibis for read/query paths where that improves clarity and maintainability (the repo already imports ibis in read.py).
|
||||
|
||||
|
||||
## Constraints
|
||||
|
||||
- Work must be incremental and non-disruptive: keep existing DuckDB schema and write paths where possible.
|
||||
- Do not add external services (vector DB) in the first iteration — store embeddings in DuckDB as JSON for now.
|
||||
- Secrets must remain environment-driven (no checked-in secrets). Add env var defaults only.
|
||||
- Keep changes small and well-tested; make it easy to roll back.
|
||||
|
||||
|
||||
## Approach (chosen)
|
||||
|
||||
I'll introduce two small layers:
|
||||
- **ai_provider**: a thin adapter that exposes get_embedding(text) and chat_completion(messages). It will use the existing OpenRouter/OpenAI path by default and can be extended to prefer other providers if/when desired.
|
||||
- **query_dal**: read-focused utilities implemented with ibis to replace direct SQL reads in the app and other read-heavy paths. Writes (insert_motion, update_user_vote) stay in database.py initially.
|
||||
|
||||
This gives the benefits of abstraction and pythonic query composition while keeping risk low.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
High level components (repo root):
|
||||
- api_client.py — fetches motion data from Tweede Kamer OData (unchanged)
|
||||
- scraper.py — optional HTML scraping fallback (unchanged)
|
||||
- database.py — current writes, schema initialization (add small embeddings table)
|
||||
- summarizer.py — generate layman summaries (refactor to use ai_provider)
|
||||
- app.py — Streamlit UI (switch read paths to query_dal)
|
||||
- scheduler.py — orchestrates ingestion and triggers summarization (unchanged)
|
||||
|
||||
Additions:
|
||||
- ai_provider.py — single place for LLM/embedding calls and retries
|
||||
- query_dal.py — ibis-based read helpers (get_filtered_motions, calculate_party_matches)
|
||||
- minimal embeddings table in DuckDB (motion_id, model, vector JSON, created_at)
|
||||
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- **ai_provider**: choose provider, handle retries/backoff, return plain Python objects (list[float] embeddings, str completions). Keep error classes small and testable.
|
||||
- **database (existing)**: add store_embedding and search_similar helpers (naive in-Python cosine scan). Keep insert_motion/update_user_vote unchanged to minimize risk.
|
||||
- **query_dal**: use ibis for read queries used by Streamlit paths (get_filtered_motions, session lookups). Return parsed JSON fields.
|
||||
- **summarizer**: call ai_provider.chat_completion to get summary; update motions.layman_explanation; optionally compute embedding via ai_provider.get_embedding and store via database.store_embedding.
|
||||
- **app.py**: replace direct duckdb selects with query_dal functions.
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Ingest: scheduler / scraper / api_client fetch motions and call database.insert_motion(motion).
|
||||
2. Summarize: summarizer calls ai_provider.chat_completion(summary prompt) → writes layman_explanation to motions table. Optionally computes embedding and writes to embeddings table.
|
||||
3. Query: Streamlit app calls query_dal.get_filtered_motions (ibis) to load motions for sessions and query_dal.calculate_party_matches for results.
|
||||
4. Semantic search (future): query_dal or app can call database.search_similar by providing an embedding computed with ai_provider.get_embedding.
|
||||
|
||||
|
||||
## Error Handling
|
||||
|
||||
- ai_provider: retries with exponential backoff for transient errors; raises a ProviderError for terminal failures so callers can decide retry semantics.
|
||||
- Summarizer: non-fatal on AI failures — store an empty/fallback summary and log the failure; surface a user-facing message in Streamlit if generating summaries fails interactively.
|
||||
- DB functions: existing try/except patterns retained; ensure connections are closed on error.
|
||||
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit tests for ai_provider using mocks for HTTP/openai responses.
|
||||
- DB tests using temporary DuckDB files to verify store_embedding and search_similar behavior.
|
||||
- query_dal tests using ibis against a temporary DB file; ensure JSON fields parse correctly.
|
||||
- Summarizer tests mock ai_provider to assert DB writes happen.
|
||||
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Store embeddings inside motions table vs separate embeddings table? Recommendation: separate embeddings table for clarity and easier upserts.
|
||||
- Do we want to prefer other providers (Copilot) automatically? This repo currently references OPENROUTER. If user wants Copilot preference, we can add env vars and selection logic later.
|
||||
|
||||
|
||||
## Next steps (short)
|
||||
|
||||
1. Add ai_provider.py (adapter) and tests.
|
||||
2. Add embeddings table and store/search helpers in database.py and tests.
|
||||
3. Add query_dal.py with ibis reads and tests.
|
||||
4. Refactor summarizer.py to use ai_provider and optionally store embeddings.
|
||||
5. Update Streamlit app read paths to use query_dal.
|
||||
6. Fix housekeeping bugs: reset.py references reset_database(), scraper uses undefined SCRAPING_DELAY — address these small fixes in a separate patch.
|
||||
|
||||
|
||||
I'm proceeding to save this design to thoughts/shared/designs/2026-03-19-stemwijzer-design.md and will spawn the planner to create a detailed implementation plan. Interrupt if you want changes to the design text above.
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
date: 2026-03-21
|
||||
topic: "Reuse motions as a guided policy explorer"
|
||||
status: draft
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
We want to repurpose existing "motions" data so it becomes a lightweight, discovery-driven way for users to explore policy positions and discover related content. This is not a full proposal system; it's a guided exploration and bookmarking flow that leverages our existing ingestion, summarization, embeddings, and session voting work.
|
||||
|
||||
**Why now:** We already ingest motions, generate layman explanations, compute embeddings, and store per-session votes. Reusing those building blocks gives high user value with modest effort.
|
||||
|
||||
## Constraints
|
||||
|
||||
**Non-negotiables and technical limits:**
|
||||
- Use the existing database schema where possible (motions table, embeddings table, user_sessions). Do not require a new external vector DB for MVP.
|
||||
- Keep the Streamlit UI model (app.py) and session-based votes intact for the initial rollout.
|
||||
- Avoid breaking migrations: rely on existing migrations and add new ones when necessary (no forced drops).
|
||||
- Respect current error-handling posture: network calls can fail; system must degrade gracefully.
|
||||
|
||||
## Chosen Approach
|
||||
|
||||
I'm choosing a "Guided Policy Explorer" approach because it reuses thehighest-value existing pieces (summaries, embeddings, session voting) and delivers a clear UX that fits the current codebase. This gives immediate product value with low risk.
|
||||
|
||||
**Core idea:** present curated short sessions and motion detail pages that combine the existing layman explanation, party-match results, and semantic "related motions" powered by stored embeddings.
|
||||
|
||||
Alternatives considered:
|
||||
- "Motion-as-Proposal platform": full lifecycle (draft → comment → vote). Rejected for MVP due to high complexity and data model changes.
|
||||
- "Motion Digest / Research Assistant": read-only pages and newsletters. Lower effort, but less interactive and reuses fewer of our current session features.
|
||||
|
||||
## Architecture
|
||||
|
||||
High-level view (existing pieces in bold):
|
||||
- Ingest: **api_client.py** + **scraper.py** gather motions and create motion records in the DB.
|
||||
- Persist: **database.py** stores motions, embeddings, and user_sessions.
|
||||
- Enrichment: **summarizer.py** + **ai_provider.py** generate layman explanations and embeddings.
|
||||
- Background jobs: **scheduler.py** runs ingest, summarization, and periodic clustering.
|
||||
- UI: **app.py** current Streamlit session flow — extend with "Explore" and "Motion detail" pages.
|
||||
- New: small **clusterer / similarity API** to compute and cache related-motion lists per motion.
|
||||
|
||||
## Key Components & Responsibilities
|
||||
|
||||
- Motion Ingest (existing): keep ingest as-is; add metadata flags (e.g., curated, candidate).
|
||||
- Motion Store (existing): motions table + embeddings table; add an **events/audit** table for user actions and important state transitions.
|
||||
- Summarizer / Embedding Worker (existing): scheduled job that ensures motions have layman_explanation and embeddings; add retry/backoff and logging.
|
||||
- Similarity service (new): computes nearest neighbors using stored vectors in-process for MVP and caches results in a small table. Swap to a vector index later if needed.
|
||||
- Session & Voting (existing): continue using user_sessions JSON blob for individual sessions; add optional event log entries for each vote.
|
||||
- UI (update): add "Explore" landing, motion detail view with layman text, party-match snapshot, related motions, and bookmark/flag actions. Reuse Streamlit components.
|
||||
- Admin tooling (new): migration scripts, a CLI to recompute embeddings/similarity, and an audit query helper.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Ingest job (api_client/scraper) produces motion records and calls db.insert_motion.
|
||||
2. Summarizer worker picks up motions without layman_explanation or embeddings, calls ai_provider, and writes layman_explanation + embeddings.
|
||||
3. Clusterer/similarity job computes related-motion lists using stored embeddings and writes them to a cache table.
|
||||
4. UI "Explore" shows curated motion lists; "Motion detail" reads motion, layman_explanation, party-match snapshot, and cached related motions.
|
||||
5. User vote actions update user_sessions and also append an event to the audit table for traceability.
|
||||
6. Background analytics (optional) reuses user_events and embeddings for offline insights.
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
- External calls: add retries with exponential backoff for AI provider and external APIs. Failures set a marker (e.g., summary_missing) and the system continues.
|
||||
- Missing embeddings: UI gracefully disables "related motions" and offers "compute on demand".
|
||||
- Idempotency: make insert_motion idempotent by URL/external id check at DB layer; use optimistic handling for duplicates.
|
||||
- Concurrency: avoid read-modify-write races by writing user events (append-only) and deriving session state from events when race-prone updates are detected.
|
||||
- Observability: replace prints with structured logging (module-level logger) and add basic metrics for worker errors, API failures, and queue lags.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit tests: DB helpers (insert_motion, store_embedding, similarity cache), summarizer functions (mock ai_provider), and session vote logic.
|
||||
- Migration tests: follow the existing pattern of applying migration SQL in a temp DB and asserting schema.
|
||||
- Integration tests: end-to-end ingest → summarize → embedding → similarity → UI-read path in CI (use monkeypatch for AI calls).
|
||||
- Load tests: simulate a few thousand embeddings search calls against the in-process search to validate performance assumptions for MVP.
|
||||
- Acceptance: confirm UX flows: Explore session, Motion detail, Vote -> party match, Related motions populated.
|
||||
|
||||
## High-level Plan & Estimates
|
||||
|
||||
Assumptions: one full-stack engineer (Python + Streamlit) and one part-time reviewer. All estimates are rough.
|
||||
|
||||
Milestone 0 — Validate & quick discovery (1 day)
|
||||
- Locate user's added markdown plan and extract exact requirements. (I'm assuming the file exists in thoughts/shared; if not, we validated by searching.)
|
||||
|
||||
Milestone 1 — MVP (8–12 engineer days)
|
||||
- Add similarity cache table and migration.
|
||||
- Summarizer: make embedding generation robust with retries and store vectors.
|
||||
- Clusterer job: compute and cache related motions.
|
||||
- UI: Explore landing, Motion detail page, related motion UI, bookmark/flag button.
|
||||
- Add event/audit table and write events on user votes and bookmarks.
|
||||
|
||||
Milestone 2 — Hardening & instrumentation (3–5 engineer days)
|
||||
- Replace prints with structured logging across touched modules.
|
||||
- Add migration tests and CI integration tests (mock AI).
|
||||
- Add health metrics & basic alerting for worker failures.
|
||||
|
||||
Milestone 3 — Polish & UX feedback (3–5 engineer days)
|
||||
- UX tweaks, performance tuning, compute on-demand fallback for embeddings, documentation, admin CLI.
|
||||
|
||||
Total MVP + polish: ~2–3 weeks of focused work.
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
- Risk: Naive in-process embedding search will not scale. Mitigation: cache nearest neighbors per motion and plan a migration path to a vector index.
|
||||
- Risk: AI provider flakiness. Mitigation: retries, timeouts, and clear UI fallback. Tests must mock provider in CI.
|
||||
- Risk: Race conditions on session votes. Mitigation: append-only event log and derive authoritative session view from events when needed.
|
||||
- Risk: Schema drift and missing migrations. Mitigation: add migration tests and document required migrations in repo.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Which exact user journeys do we want first (single-session discover vs. persistent account/bookmarking)?
|
||||
- Do we want bookmarks persisted globally or per-session only? (Privacy implications.)
|
||||
- What's acceptable latency for "related motions" — precomputed nightly vs. near-real-time?
|
||||
- Any policy/legal ban on storing full body_text or on long-term retention of user votes?
|
||||
|
||||
---
|
||||
|
||||
I'm proceeding to create the design doc file at thoughts/shared/designs/2026-03-21-motions-guided-explorer-design.md and will spawn the implementation planner next. Interrupt if you want changes to the approach or scope now.
|
||||
@@ -0,0 +1,335 @@
|
||||
# Guided Policy Explorer — Implementation Plan
|
||||
|
||||
**Goal:** Implement the Guided Policy Explorer MVP that reuses existing motions, layman summaries, embeddings and session votes to provide an Explore landing, Motion detail view, cached related motions (similarity cache), and accompanying background jobs and admin tooling.
|
||||
|
||||
Design: thoughts/shared/designs/2026-03-21-motions-guided-explorer-design.md
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Batch 1 (parallel): 1.1, 1.2, 1.3, 1.4, 1.5 [foundation - migrations, types, migration-tests]
|
||||
Batch 2 (parallel): 2.1, 2.2, 2.3, 2.4 [core - similarity service, cache repo, audit repo, embeddings worker]
|
||||
Batch 3 (parallel): 3.1, 3.2, 3.3, 3.4 [components - clusterer worker, CLI, API, Streamlit page]
|
||||
Batch 4 (parallel): 4.1 [integration tests & docs - depends on 2.x & 3.x]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes on planning choices
|
||||
- Design requires a similarity cache and a small in-process nearest-neighbor search for MVP. I'm implementing this as: store precomputed top-N neighbor lists (IDs + scores) in a small SQL table and compute neighbors by scanning embeddings in-memory per batch job. Reason: avoids external vector DB and keeps implementation simple and testable.
|
||||
- Design requires robust embedding generation. I'll implement exponential-backoff retry logic with a configurable retry count and timeouts in embeddings_worker; tests will monkeypatch the ai_provider to simulate failures.
|
||||
- Migration tests: design asks to have migration tests, but migration SQL content is omitted per instructions. Tests will assert that migration files are present and follow naming conventions and will be marked to skip applying SQL unless a TEST_DB_URL env var is provided. This keeps CI safe while satisfying test coverage and developer verification.
|
||||
|
||||
---
|
||||
|
||||
## Batch 1: Foundation (parallel - 5 implementers)
|
||||
All tasks in this batch have NO dependencies and run simultaneously.
|
||||
|
||||
### Task 1.1: Add similarity cache migration (placeholder)
|
||||
**Title:** Migration: add similarity_cache table
|
||||
**Description:** Add a migration file to create a similarity cache table that stores precomputed related-motion lists per motion (motion_id, neighbors_json, computed_at). SQL content intentionally left out per instructions; file is a placeholder that CI/tests will detect.
|
||||
**Files:**
|
||||
- migrations/2026-03-22-add-similarity-cache.sql
|
||||
**Tests:**
|
||||
- tests/migrations/test_2026_03_22_add_similarity_cache.py
|
||||
**Estimated:** 1.0h
|
||||
**Priority:** high
|
||||
**Depends:** none
|
||||
**Acceptance criteria:**
|
||||
- Migration file exists at migrations/2026-03-22-add-similarity-cache.sql
|
||||
- test_migration file runs and passes in default mode (it will only check filename & header). If TEST_DB_URL is set in env, test will attempt to run the SQL and must not error (SQL may be empty; test expects a no-op or valid SQL). Test is marked to skip DB application when TEST_DB_URL is unset.
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: Add audit/events migration (placeholder)
|
||||
**Title:** Migration: add audit_events table
|
||||
**Description:** Add a migration placeholder to create an audit/events table for append-only user events (vote, bookmark, flag). Actual SQL omitted.
|
||||
**Files:**
|
||||
- migrations/2026-03-22-add-audit-events.sql
|
||||
**Tests:**
|
||||
- tests/migrations/test_2026_03_22_add_audit_events.py
|
||||
**Estimated:** 1.0h
|
||||
**Priority:** high
|
||||
**Depends:** none
|
||||
**Acceptance criteria:**
|
||||
- migrations/2026-03-22-add-audit-events.sql exists
|
||||
- migration test verifies filename and is safe to run in CI (skips DB apply unless TEST_DB_URL provided).
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3: Shared types for motions & similarity entries
|
||||
**Title:** Types: motion and similarity types
|
||||
**Description:** Add a small types module that centralizes typed dataclasses/interfaces used by similarity and cache modules (MotionId, Embedding vector typed alias, SimilarityNeighbor). This reduces coupling and makes tests easier to write.
|
||||
**Files:**
|
||||
- src/types/motion_types.py
|
||||
**Tests:**
|
||||
- tests/types/test_motion_types.py
|
||||
**Estimated:** 1.5h
|
||||
**Priority:** medium
|
||||
**Depends:** none
|
||||
**Acceptance criteria:**
|
||||
- src/types/motion_types.py defines MotionId, Embedding, SimilarityNeighbor types and basic helpers (e.g., serialize/deserialize neighbors). Tests validate JSON round-trip of neighbors.
|
||||
|
||||
---
|
||||
|
||||
### Task 1.4: CI migration test helper
|
||||
**Title:** Test helper: migration test utils
|
||||
**Description:** Add a small test helper that other migration tests can use. It provides a pytest fixture that reads TEST_DB_URL and yields a DB connection or None and marks tests appropriately.
|
||||
**Files:**
|
||||
- tests/utils/migration_fixtures.py
|
||||
**Tests:**
|
||||
- tests/migrations/test_migration_fixtures_smoke.py
|
||||
**Estimated:** 1.0h
|
||||
**Priority:** medium
|
||||
**Depends:** none
|
||||
**Acceptance criteria:**
|
||||
- migration_fixtures.py provides `test_db` fixture. The smoke test asserts fixture yields None when TEST_DB_URL unset and yields a connection-like object when set.
|
||||
|
||||
---
|
||||
|
||||
### Task 1.5: Add README admin docs for recomputing
|
||||
**Title:** Docs: admin CLI usage and migration notes
|
||||
**Description:** Add a short markdown doc describing the admin CLI, migration filenames, and how to run recompute/clusterer jobs locally for dev.
|
||||
**Files:**
|
||||
- docs/admin/recompute_similarity.md
|
||||
**Tests:** none (doc only)
|
||||
**Estimated:** 0.5h
|
||||
**Priority:** low
|
||||
**Depends:** none
|
||||
**Acceptance criteria:**
|
||||
- docs/admin/recompute_similarity.md exists and documents commands and env vars: TEST_DB_URL, AI_PROVIDER_MOCK, SIMILARITY_TOP_N.
|
||||
|
||||
---
|
||||
|
||||
## Batch 2: Core Modules (parallel - 4 implementers)
|
||||
Depends: Batch 1
|
||||
|
||||
### Task 2.1: Similarity service (in-process search + utility)
|
||||
**Title:** Similarity service implementation
|
||||
**Description:** New service that, given motion embeddings, computes cosine similarity and returns top-N neighbors. Also exposes a convenience function to compute neighbors for one motion and return a list of (motion_id, score). This is pure Python and testable in-memory.
|
||||
**Files:**
|
||||
- src/services/similarity_service.py
|
||||
**Tests:**
|
||||
- tests/services/test_similarity_service.py
|
||||
**Estimated:** 5.0h
|
||||
**Priority:** high
|
||||
**Depends:** 1.3
|
||||
**Acceptance criteria:**
|
||||
- similarity_service.py exposes compute_neighbors(embedding: list[float], all_embeddings: Dict[motion_id, embedding], top_n: int) -> List[SimilarityNeighbor]
|
||||
- Unit tests cover exact small matrices and edge cases (empty, identical embeddings). All tests pass with `pytest tests/services/test_similarity_service.py`.
|
||||
|
||||
---
|
||||
|
||||
### Task 2.2: DB repo for similarity cache
|
||||
**Title:** Repo: similarity_cache read/write
|
||||
**Description:** Provide a small repository abstraction that reads and writes cached neighbor lists to the DB (serialize neighbors as JSON). Keep DB interactions minimal and testable using sqlite in-memory.
|
||||
**Files:**
|
||||
- src/db/similarity_cache_repo.py
|
||||
**Tests:**
|
||||
- tests/db/test_similarity_cache_repo.py
|
||||
**Estimated:** 4.0h
|
||||
**Priority:** high
|
||||
**Depends:** 1.1, 1.3
|
||||
**Acceptance criteria:**
|
||||
- similarity_cache_repo provides functions: get_cached_neighbors(motion_id) -> Optional[List[SimilarityNeighbor]] and upsert_cached_neighbors(motion_id, neighbors, computed_at)
|
||||
- Unit tests run against sqlite in-memory and assert correct serialization/deserialization.
|
||||
|
||||
---
|
||||
|
||||
### Task 2.3: Audit/events repository
|
||||
**Title:** Repo: audit_events append-only writer
|
||||
**Description:** Small repo to append audit events (user_id, session_id, motion_id, event_type, payload JSON, created_at). Provides an append_event function used by UI and session logic.
|
||||
**Files:**
|
||||
- src/db/audit_repo.py
|
||||
**Tests:**
|
||||
- tests/db/test_audit_repo.py
|
||||
**Estimated:** 3.0h
|
||||
**Priority:** medium
|
||||
**Depends:** 1.2
|
||||
**Acceptance criteria:**
|
||||
- append_event writes a row to sqlite in-memory in test and read-back verifies fields and created_at presence. Functions are well typed and handle JSON payloads.
|
||||
|
||||
---
|
||||
|
||||
### Task 2.4: Embeddings worker helper (retries/backoff)
|
||||
**Title:** Worker: robust embedding generator
|
||||
**Description:** Add a worker helper that ensures embeddings exist for a motion. It calls ai_provider.get_embedding with retry/backoff and writes embedding via an abstracted DB function (the put function will be dependency-injected in tests). This module contains no long-running loop — it's a single-run helper function used by the scheduler.
|
||||
**Files:**
|
||||
- src/ai/embeddings_worker.py
|
||||
**Tests:**
|
||||
- tests/ai/test_embeddings_worker.py
|
||||
**Estimated:** 4.0h
|
||||
**Priority:** high
|
||||
**Depends:** 1.3
|
||||
**Acceptance criteria:**
|
||||
- embeddings_worker.explain_and_embed(motion_id, text, put_embedding_fn) calls ai_provider and retries on simulated transient errors. Tests monkeypatch ai_provider to simulate 2 failing attempts then success and verify put_embedding_fn called exactly once with a vector-like object.
|
||||
|
||||
---
|
||||
|
||||
## Batch 3: Components (parallel - 4 implementers)
|
||||
Depends: Batch 2
|
||||
|
||||
### Task 3.1: Clusterer scheduled job
|
||||
**Title:** Worker: clusterer job that computes & writes caches
|
||||
**Description:** Background job module that loads all embeddings, computes top-N neighbors for each motion using similarity_service, and writes cache rows via similarity_cache_repo. Designed to be runnable from CLI. It should respect a MAX runtime parameter (process batch size) for safe operation in dev.
|
||||
**Files:**
|
||||
- src/workers/clusterer.py
|
||||
**Tests:**
|
||||
- tests/workers/test_clusterer.py
|
||||
**Estimated:** 6.0h
|
||||
**Priority:** high
|
||||
**Depends:** 2.1, 2.2, 2.4
|
||||
**Acceptance criteria:**
|
||||
- clusterer.run_batch(batch_size, top_n, load_embeddings_fn, upsert_cache_fn) exists and can be unit-tested by injecting small in-memory embeddings and verifying upsert_cache_fn called with expected neighbor lists.
|
||||
|
||||
---
|
||||
|
||||
### Task 3.2: Admin CLI: recompute-similarity
|
||||
**Title:** CLI: recompute similarity & options
|
||||
**Description:** Small CLI script (click or argparse) to trigger the clusterer job (full-run or limited). CLI accepts --top-n, --batch-size, --dry-run flags. Tests will monkeypatch clusterer.run_batch.
|
||||
**Files:**
|
||||
- src/cli/recompute_similarity.py
|
||||
**Tests:**
|
||||
- tests/cli/test_recompute_similarity.py
|
||||
**Estimated:** 2.5h
|
||||
**Priority:** medium
|
||||
**Depends:** 3.1
|
||||
**Acceptance criteria:**
|
||||
- CLI parses flags and calls clusterer.run_batch with parsed args. tests assert proper arguments passed and dry-run does not call run_batch.
|
||||
|
||||
---
|
||||
|
||||
### Task 3.3: HTTP API endpoint for compute-on-demand / cached
|
||||
**Title:** API: similarity endpoint
|
||||
**Description:** Small Flask/FastAPI/WSGI handler module that returns cached related motions for a motion_id; if cache missing and a query param compute=true, it calls the similarity service to compute neighbors on demand (without persisting) and returns them. Keep the handler framework-agnostic so it can be wired into existing web framework; tests will call the handler function directly.
|
||||
**Files:**
|
||||
- src/api/similarity_api.py
|
||||
**Tests:**
|
||||
- tests/api/test_similarity_api.py
|
||||
**Estimated:** 3.5h
|
||||
**Priority:** medium
|
||||
**Depends:** 2.1, 2.2
|
||||
**Acceptance criteria:**
|
||||
- Handler get_related(motion_id, compute=False, load_embedding_fn, load_all_embeddings_fn, cache_repo) returns cached neighbors when present and computes on demand when compute=True. Tests cover both code paths.
|
||||
|
||||
---
|
||||
|
||||
### Task 3.4: Streamlit UI: Explore landing & Motion detail module
|
||||
**Title:** UI: explore page and motion detail component
|
||||
**Description:** Add a Streamlit helper module providing functions to render the Explore landing and Motion detail sections. Avoid modifying existing app.py in this MVP; instead provide a module that app.py can import. The module will expose pure functions where possible to ease testing; tests will verify behavior by calling functions and mocking DB/AI calls.
|
||||
**Files:**
|
||||
- src/ui/explore_page.py
|
||||
**Tests:**
|
||||
- tests/ui/test_explore_page.py
|
||||
**Estimated:** 5.0h
|
||||
**Priority:** medium
|
||||
**Depends:** 2.2, 2.3, 2.4
|
||||
**Acceptance criteria:**
|
||||
- explore_page.render_explore(session, load_curated_fn, load_cached_neighbors_fn) returns a data structure (not direct Streamlit calls) that app.py can choose to render. Tests assert correct payload for a sample session and that missing embeddings gracefully remove related motions.
|
||||
|
||||
---
|
||||
|
||||
## Batch 4: Integration & Docs (parallel - 2 implementers)
|
||||
Depends: Batch 2 & 3
|
||||
|
||||
### Task 4.1: Integration test: ingest → summarize → embed → cluster → UI read
|
||||
**Title:** Integration test for the end-to-end path (mvp)
|
||||
**Description:** Add an integration pytest that simulates: create 3 synthetic motions, call embeddings_worker (monkeypatched AI provider), run clusterer on the in-memory dataset, and assert similarity cache rows exist and explore_page returns related motions. Use sqlite in-memory and monkeypatch ai_provider to return deterministic vectors.
|
||||
**Files:**
|
||||
- tests/integration/test_end_to_end_explore_flow.py
|
||||
**Tests:**
|
||||
- (this is the test file)
|
||||
**Estimated:** 8.0h
|
||||
**Priority:** high
|
||||
**Depends:** 1.3, 2.1, 2.2, 2.4, 3.1, 3.4
|
||||
**Acceptance criteria:**
|
||||
- Running `pytest tests/integration/test_end_to_end_explore_flow.py` passes locally with no external network calls when AI provider is monkeypatched via monkeypatch fixture. The test asserts that at least one neighbor exists for a motion and the explore_page data includes it.
|
||||
|
||||
---
|
||||
|
||||
## CI / Test instructions
|
||||
|
||||
- Run unit tests: pytest tests/unit (or full suite: pytest)
|
||||
- Run a single module test: pytest tests/services/test_similarity_service.py::test_compute_neighbors_basic
|
||||
- Integration tests: pytest tests/integration/test_end_to_end_explore_flow.py
|
||||
|
||||
Monkeypatching AI provider in CI/local tests:
|
||||
- Use the `monkeypatch` pytest fixture to patch `src.ai.ai_provider.get_embedding` and `src.ai.ai_provider.summarize` (if used). Example in tests: monkeypatch.setattr('src.ai.ai_provider.get_embedding', fake_get_embedding)
|
||||
- CI should set env var AI_PROVIDER_MOCK=1 for additional safety; tests will check this var and use mocks if present.
|
||||
|
||||
Temp DB setup for tests:
|
||||
- Unit tests should use sqlite in-memory ("sqlite:///:memory:") via a `test_db` fixture in tests/utils/migration_fixtures.py.
|
||||
- Migration tests: If TEST_DB_URL env var is set, the migration tests will attempt to apply SQL to that DB; otherwise they will run in dry-run / skip-apply mode and only validate filename and header.
|
||||
|
||||
Example pytest commands:
|
||||
- pytest -q
|
||||
- pytest -q tests/services/test_similarity_service.py -k compute_neighbors
|
||||
|
||||
Notes for CI pipeline:
|
||||
- Ensure Python dependencies include pytest, pytest-mock and any DB driver required (sqlite built-in is fine). No external AI keys required — tests must mock AI provider.
|
||||
|
||||
---
|
||||
|
||||
## 3-Sprint Schedule (2-week sprints)
|
||||
|
||||
Sprint 1 (Weeks 1–2) — Milestone 1: MVP foundation + core similarity
|
||||
- Goals: Add migrations, types, similarity service, similarity cache repo, audit repo, embeddings worker helper
|
||||
- Tasks: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4
|
||||
|
||||
Sprint 2 (Weeks 3–4) — Milestone 1 continued: background job, CLI, API, UI
|
||||
- Goals: Implement clusterer job, CLI, similarity API, explore_page UI module; initial integration smoke tests
|
||||
- Tasks: 3.1, 3.2, 3.3, 3.4, initial lightweight integration test scaffolding
|
||||
|
||||
Sprint 3 (Weeks 5–6) — Milestone 2 & 3: hardening, integration tests, docs
|
||||
- Goals: Full integration tests, migration tests, docs, logging hardening, small UX polish
|
||||
- Tasks: 4.1, docs improvements from 1.5, logging conversion across modules (follow-up small PRs as needed)
|
||||
|
||||
Notes:
|
||||
- Estimates assume 1 full-stack engineer + 1 reviewer. Sprint 1 is AMA-heavy; reviewer will focus on migrations and core algorithms. Sprint 2 focuses on wiring and UI; reviewer focuses on integration and UX. Sprint 3 finishes tests and polish.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The repository uses Python 3.10+ and pytest for tests. If different, adjust test fixtures accordingly.
|
||||
- Existing DB access helpers exist (a simple execute/connection helper). If not, tests use sqlite3 directly and repository code will accept a DB connection/cursor via dependency injection.
|
||||
- The project already has an ai_provider abstraction at src/ai/ai_provider.py with functions `get_embedding(text) -> list[float]` and `summarize(text) -> str` — tests will monkeypatch these. If the names differ, adapt imports when implementing.
|
||||
- Streamlit app remains `app.py` and can import src/ui/explore_page.py — I deliberately do not modify app.py in this plan to keep the change set minimal.
|
||||
- We will store embeddings as arrays in an embeddings table; similarity modules will load them via an injected loader function to keep unit tests pure.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions / Implementation Clarifications
|
||||
|
||||
1. Bookmarks persistence: design left bookmarks as open (session vs. persistent). For MVP we will record bookmark events in the audit_events table (append-only) and treat them as per-session by default. If persistent bookmarks required later, a new table/migration will be added.
|
||||
2. Which web framework to wire the similarity_api into? The plan keeps handler framework-agnostic; we need guidance whether app uses Flask/FastAPI/Starlette to add the route. Implementer should wire into existing HTTP routing pattern.
|
||||
3. Embedding storage format: assume float arrays stored as JSON or array type in DB. If project uses a binary blob, adjust serialization in similarity_cache_repo and tests accordingly.
|
||||
4. Acceptable top-N neighbor size for caches. Default SIMILARITY_TOP_N = 10; CLI and worker accept override. If product wants 50, increase later.
|
||||
|
||||
---
|
||||
|
||||
## How a single implementer should proceed (step-by-step)
|
||||
|
||||
1. Start with Batch 1 tasks 1.1–1.4. Create migrations placeholders and types module. Run migration filename tests.
|
||||
2. Implement similarity_service (2.1) and its unit tests. This is the critical algorithm that must be rock-solid.
|
||||
3. Implement similarity_cache_repo (2.2) and audit_repo (2.3) using sqlite in-memory for tests. Run unit tests.
|
||||
4. Implement embeddings_worker helper (2.4) and add tests that mock ai_provider. Ensure CI will not call real AI.
|
||||
5. Implement clusterer (3.1) and test with in-memory data by injecting loader/upsert functions.
|
||||
6. Add admin CLI (3.2) to run clusterer; add small doc (1.5) describing how to run it locally.
|
||||
7. Implement API handler (3.3) and UI helper (3.4). Tests should mock DB and AI as needed.
|
||||
8. Finish with integration test (4.1) to stitch the pieces together. Iterate on bug fixes and reviewer feedback.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria for the feature (MVP)
|
||||
|
||||
- Explore landing exists and can present curated motions (using existing curated flag). Data payload returned by explore_page includes motion metadata and layman_explanation.
|
||||
- Motion detail returns layman_explanation, party-match snapshot (existing), and related motions computed from cached neighbor lists when available.
|
||||
- Background clusterer job can recompute cached neighbor lists and the CLI can trigger it.
|
||||
- Tests cover core algorithm (similarity computation), cache repo serialization, embedders (mocked), and at least one end-to-end smoke integration test.
|
||||
|
||||
---
|
||||
|
||||
If anything in this plan should be narrowed further (for a smaller initial PR) I recommend focusing Sprint 1 + clusterer CLI (Tasks 1.x + 2.x + 3.1 + 3.2) and deferring UI wiring until clusterer and cache are validated.
|
||||
Reference in New Issue
Block a user