feat(categorization): semantic embedding tier + richer LLM prompts - #79
feat(categorization): semantic embedding tier + richer LLM prompts#79gianniskotsas wants to merge 8 commits into
Conversation
Problem: LLM received only category NAMES, not descriptions or keywords —
vendor hints the user curated (Jumbo, Cloudflare, Efood, IBKR…) were
invisible. Batch size of 50 further diluted attention per line.
Changes:
- CategoryMatcher now renders each category as
"- <name> — <description> | hints: <categorization_instructions>"
in both single and batch LLM prompts, and instructs the model to
tolerate spelling/spacing/accent variants. (category_matcher.py)
- Default LLM batch size 50 → 15 for tighter attention budget.
- New CategoryEmbeddingService: OpenAI text-embedding-3-small anchors on
categories, pgvector cosine ANN lookup, accept when similarity ≥ 0.78
AND margin ≥ 0.06 over runner-up; otherwise defer to LLM.
- Post-import pipeline now runs embedding tier before LLM and only hands
uncertain transactions to the LLM, persisting method + confidence.
- Schema: pgvector extension + categories.embedding,
transactions.{embedding,categorization_confidence,categorization_method}
with HNSW cosine index on category anchors.
- seed_category_keywords.py pre-populates the four example categories
(Side Projects, Groceries, Internal Transfer, Food & Dining) and
rebuilds anchor embeddings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… literal - CI Postgres: swap postgres:16-alpine for pgvector/pgvector:pg16 so migration 0018 can CREATE EXTENSION vector. - category-manager.tsx: optimistic Category literal was missing the new embedding column; set it to null. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/tasks/post_import_pipeline.py">
<violation number="1" location="backend/tasks/post_import_pipeline.py:226">
P2: `categorization_method` is incorrectly hardcoded to `llm` for results that can come from user overrides.</violation>
</file>
<file name="backend/app/services/category_embedding.py">
<violation number="1" location="backend/app/services/category_embedding.py:115">
P1: Embedding output dimensions are not pinned to the database vector size, so changing the embedding model can generate incompatible vectors and break categorization writes/queries.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
…tor image Review feedback from cubic-dev-ai: - P1: Pass dimensions=EMBEDDING_DIMENSIONS to embeddings.create so swapping to a model with a different native size (e.g. text-embedding-3-large = 3072 dims) can't silently corrupt the vector(1536) columns. Also guard with a post-call length check. - P2: Restructure _batch_categorize_transactions into three explicit tiers (override → embedding → llm) so the persisted categorization_method is always accurate. Previously matches returned by match_categories_batch_llm could originate from user overrides but were tagged 'llm'. Also: fix Compose Smoke Test by pointing POSTGRES_IMAGE at pgvector/pgvector:pg16 so the compose stack can CREATE EXTENSION vector during migrate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds scripts/backfill_transaction_embeddings.py — populates the new
transactions.embedding column for existing rows. Does not touch
category_system_id, category_id, categorization_confidence, or
categorization_method. Existing categorizations stay exactly as-is.
Usage: python -m scripts.backfill_transaction_embeddings <user_id>
[--dry-run] [--batch-size N] [--limit N]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/scripts/backfill_transaction_embeddings.py">
<violation number="1" location="backend/scripts/backfill_transaction_embeddings.py:49">
P2: `q.all()` loads every matching transaction into memory, which can cause high memory usage for large backfills despite the batching loop.</violation>
<violation number="2" location="backend/scripts/backfill_transaction_embeddings.py:95">
P2: Validate `--batch-size` as a positive integer; otherwise invalid values can crash the script at runtime.</violation>
</file>
<file name="backend/app/services/category_embedding.py">
<violation number="1" location="backend/app/services/category_embedding.py:121">
P2: `dimensions` is sent unconditionally, but OpenAI only supports that parameter for `text-embedding-3*` models. If `CATEGORIZATION_EMBEDDING_MODEL` is set to `text-embedding-ada-002`, embedding requests fail and the service silently falls back by returning empty vectors.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
- Backfill: validate --batch-size and --limit as positive ints via a custom argparse type; fetch only transaction IDs up front and load full rows in per-chunk queries, so very large backfills don't load every row into memory. expunge_all() between chunks to release SQLAlchemy identity map. - Embedding service: only pass dimensions= when the model is text-embedding-3*. Older models (text-embedding-ada-002) would 400 if we sent that param, and the service would silently degrade to empty vectors. Post-call length validation retained to catch any mismatch either way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main added a richer prompt context system (_compose_prompt_context with budget degradation, account context for internal-transfer hints) that overlapped with our keyword-hint work in _render_category_list. Resolution: - Keep main's budget system, account block, dynamic instructions block, and transfer_rule in both single and batch LLM call sites. - Extend _render_category_list to also include categorization_instructions as "| hints: <...>" alongside description (truncated to 400 chars). - Switch the batch LLM to use _render_category_list instead of name-only join, so the batch path gets keyword hints too. - Keep our batch size reduction (50 → 15). Our embedding tier, persisted categorization method/confidence, and pgvector schema migration are unaffected by the merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai these five issues have already been addressed in earlier commits on this branch:
Looks like the review re-scanned the original commits in the diff. No further action needed. |
@gianniskotsas You're right — looking at the current code state in the latest commits, all five of those issues have been addressed exactly as described. It appears my previous feedback was based on an earlier iteration of the changes before those fixes were pushed.
The implementation looks solid and follows the requested best practices. No further action is needed. |
…cac256 # Conflicts: # frontend/lib/db/schema.ts
…cac256 # Conflicts: # backend/requirements.txt
Summary
categorization_confidence,categorization_method('override' | 'deterministic' | 'embedding' | 'llm'), plus a cachedembeddingvector for retry idempotency.Architecture
Pipeline now has four tiers:
user override → deterministic keywords → embedding semantic match → LLM fallbackFiles
backend/app/services/category_embedding.py— newCategoryEmbeddingServicebackend/app/services/category_matcher.py—_format_category_list_with_hintshelper; prompt updates; batch size defaultbackend/tasks/post_import_pipeline.py— embedding tier integrated into_batch_categorize_transactionsbackend/app/models.py+frontend/lib/db/schema.ts— new columnsfrontend/lib/db/migrations/0018_categorization_embeddings.manual.sql— pgvector extension, columns, HNSW indexbackend/scripts/seed_category_keywords.py— pre-populate curated keyword listsDeploy steps
pip install -r backend/requirements.txt(addspgvector>=0.3.0)psql $DATABASE_URL -f frontend/lib/db/migrations/0018_categorization_embeddings.manual.sql(orpnpm db:push)python -m scripts.seed_category_keywords <user_id>to backfill keywords and build anchor embeddingsTest plan
vectorextension + new columns existcategories.embedding IS NOT NULLfor the 4 seeded categoriescategorization_method = 'embedding'), (b) ambiguous ones fall through to LLMcategorization_confidenceroughly matches observed match qualitymatch_categories_batch_llmstill pass🤖 Generated with Claude Code
Summary by cubic
Adds a semantic embedding tier and richer LLM prompts to improve categorization accuracy and reduce LLM calls. We also persist method, confidence, and embeddings for auditability.
New Features
pgvectorcosine over category anchor embeddings; accept when similarity ≥ 0.78 and margin ≥ 0.06 over runner-up, else defer to LLM.categorization_method; storecategorization_confidence(0–100) and transactionembedding.dimensionsonly fortext-embedding-3*and always length-check. Scripts:seed_category_keywordsto add hints + rebuild anchors;backfill_transaction_embeddingsto populatetransactions.embeddingonly (ID-first, low-memory; positive-int flags).Migration
pgvectorand run migration 0018:categories.embedding;transactions.embedding,categorization_confidence,categorization_method; HNSW cosine index.pgvector>=0.3.0; use Postgres imagepgvector/pgvector:pg16in CI/Compose; Drizzlevector(1536)custom type; frontend setsembedding: nullon new categories.python -m scripts.seed_category_keywords <user_id>then rebuild anchors; optional backfill:python -m scripts.backfill_transaction_embeddings <user_id>.Written for commit a395ca5. Summary will update on new commits.