Commit graph

161 commits

Author SHA1 Message Date
c72b4415db feat(recipe_scan): use Qwen2-VL GGUF via cf-text OpenAI-compat API
Replace two-step docuvision OCR + LLM structuring pipeline with a
single multimodal VLM call. The bartowski Qwen2-VL-7B-Instruct Q5_K_M
GGUF is served by cf-text (llama.cpp) and accepts image_url content
blocks identical to the OpenAI vision API format.

Removes docuvision dependency for recipe scanning; the addict-missing /
DeepseekVLV2Processor-missing cf-docuvision error no longer blocks scans.
Receipt OCR (kiwi.ocr task) still routes to cf-docuvision separately.
2026-05-16 18:38:21 -07:00
2df17ec719 feat(recipe-scan): add SSE streaming endpoint for cold-start progress feedback
Some checks failed
CI / Frontend (Vue) (push) Waiting to run
CI / Backend (Python) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
POST /recipes/scan/stream emits live status events while cf-docuvision
allocates and processes, replacing the static spinner with phase-aware labels:
  allocating -> scanning -> structuring -> done|error

Uses asyncio.Queue bridge to route progress callbacks from the sync scanner
thread to the async SSE generator. Frontend updated to consume the stream via
fetch + ReadableStream (EventSource does not support POST multipart).

Closes kiwi#136 (companion to the docuvision routing fix).
2026-05-16 16:24:32 -07:00
4ac24e7920 fix(recipe-scan): wire cf-docuvision OCR + LLMRouter for cloud recipe scanning (kiwi#136)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Two-step pipeline: task_allocate("kiwi", "recipe_scan", service_hint="cf-docuvision")
acquires a docuvision allocation, calls /extract per image to get OCR text, then
LLMRouter structures the combined OCR output into recipe JSON via the text
extraction prompt.

Also fixes DocuvisionClient bugs:
- POST field was "image" (ignored by Pydantic) — should be "image_b64"
- Response read "text" key — docuvision returns "raw_text"
- Add hint parameter (use "text" for recipe cards, dense prose)
- Configurable timeout (default 120s; docuvision lazy-loads model on first request)
2026-05-16 14:21:15 -07:00
cdbc24240a feat(orch): migrate OCR vision routing to task-based allocation with direct-allocate fallback 2026-05-13 10:46:07 -07:00
dd39418bc8 fix(orch): release Tier 2 allocation ctx when alloc is None; add fallback tests 2026-05-13 10:41:55 -07:00
02abc8e734 feat(orch): migrate meal plan LLM routing to task-based allocation with direct-allocate fallback
Replaces single-path cf-orch allocation with a three-tier strategy:
tier 1 task_allocate() (coordinator-driven), tier 2 direct CFOrchClient.allocate()
(TaskNotRegistered fallback), tier 3 local LLMRouter. Module-level imports for
CFOrchClient and LLMRouter make all three paths patchable in tests without
import caching issues.
2026-05-13 10:32:58 -07:00
61c428baf0 feat(orch): add task_inference helper for POST /api/inference/task routing 2026-05-13 10:27:47 -07:00
6e954c5c6e feat(ap): issue #113 — ActivityPub federation + Mastodon OAuth
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
Full ActivityPub implementation wired to cf-core.activitypub module:

Endpoints (root-level, not under /api/v1):
  GET  /.well-known/webfinger  — WebFinger JRD (AP_ENABLED only)
  GET  /ap/actor               — Instance actor document
  POST /ap/actor/inbox         — Incoming Follow/Undo (dedup + Accept dispatch)
  GET  /ap/outbox              — OrderedCollection of community posts
  GET  /ap/posts/{slug}        — Individual AP Note
  GET  /ap/followers           — Follower count collection
  GET  /ap/following           — Empty following collection

Mastodon OAuth (under /api/v1/social/mastodon/):
  POST   /connect      — Dynamic app registration + OAuth flow start
  GET    /callback     — Code exchange + token storage (Fernet-encrypted)
  DELETE /disconnect   — Token revocation
  GET    /status       — Connection status

Config: AP_ENABLED, AP_HOST, AP_KEY_PATH, AP_TOKEN_ENCRYPTION_KEY
Migration 042: ap_followers, ap_deliveries, ap_received, mastodon_tokens tables
Key manager: auto-generates RSA-2048 keypair on first boot if AP_ENABLED
Delivery service: deliver_to_followers() with 3-retry exponential backoff + DB log
Post publish: background fan-out to AP followers + Mastodon when opted-in

All AP endpoints gracefully degrade (404) when AP_ENABLED=false.
2026-05-11 17:55:51 -07:00
ef04064728 feat(community): issue #119 — recipe dedup + variation clustering on submit
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Three-layer dedup check before community post submission:
- L1: title ILIKE search against existing posts in community DB
- L2: Jaccard ingredient overlap using local corpus (≥0.70 very_similar, ≥0.35 somewhat_similar)
- L3: similar_to_ref FK — user can explicitly mark post as variation of existing

New endpoint: POST /api/v1/community/check-similar (gracefully no-ops if community DB absent)
New service: app/services/community/dedup.py — jaccard(), similarity_tier(), build_similar_post_result()
Both publish modals (plan + outcome) now check similarity before submit; user can proceed as-is,
mark as variation, or cancel. similar_to_ref passed in final publish payload.
2026-05-11 17:25:06 -07:00
b4624fba84 feat(ask): add POST /recipes/ask endpoint for natural-language recipe search
Free tier: keyword extraction + FTS ingredient search + title probe search.
Paid tier / BYOK: same search, then LLM synthesis of a conversational answer
(8s timeout so an unresponsive model degrades gracefully to recipe list only).

- AskRequest / AskRecipeHit / AskResponse schemas in recipe.py
- _extract_ask_keywords(): tokenize question, strip stopwords
- _ask_in_thread(): two-pronged search (ingredient FTS + title LIKE)
  merges by ID, computes pantry match_pct when pantry_items provided
- Endpoint registered before /{recipe_id} to avoid integer coercion on /ask
- LLM synthesis gated to paid/premium/ultra only (not "local" dev tier)

Closes #134 (backend)
2026-05-11 13:07:53 -07:00
8c765b7da2 fix(barcode): look up product info before checking auto_add_to_inventory
Previously, get_or_create_product was only called when auto_add was true,
so scan responses with auto_add=false returned no product details. Now the
DB lookup always runs when product_info is available; inventory insertion
is still conditional on auto_add_to_inventory. Fixes preview-only barcode
scans returning empty product fields.
2026-05-11 11:33:02 -07:00
e57f46f4b6 feat(streaming): add native SSE fallback for L3/L4 recipe generation (closes #126)
Two-phase streaming architecture:
  Phase 1 (sync thread): IngredientClassifier builds element profiles +
    gap list from SQLite — thread-safe, no async context needed
  Phase 2 (async): LLMRecipeGenerator.stream_generate() yields tokens via
    cf-orch warm vllm (existing /stream-token path) or AsyncOpenAI against
    Ollama if the coordinator is unavailable

Backend (app/services/recipe/llm_recipe.py):
  - stream_generate() async generator; _try_alloc_for_stream() sync helper
  - _stream_openai_compat() static method handles __auto__ model resolution
  - LLMRecipeGenerator(None) is safe for streaming (store not used)

Endpoint (app/api/endpoints/recipes.py):
  - ?stream=true on POST /recipes/suggest returns StreamingResponse
  - X-Accel-Buffering: no prevents nginx buffering without nginx.conf edits

Frontend (api.ts, recipes.ts, RecipesView.vue):
  - suggestRecipeStream() uses fetch + ReadableStream (POST; EventSource
    only supports GET)
  - streamSuggest() action in recipes store builds request internally
  - RecipesView.streamRecipe() silently falls back to native SSE when
    cf-orch token fetch fails rather than surfacing an error
2026-05-11 11:32:54 -07:00
04dbdddbad feat(mcp): add Kiwi MCP server for corpus DB access (closes #124)
Exposes four read-only tools to Claude Code:
  kiwi_query_corpus   — parameterised SELECT against kiwi.db (200-row cap)
  kiwi_count_fts      — FTS5 MATCH hit count for keyword coverage audits
  kiwi_sample_tags    — tag frequency distribution by prefix
  kiwi_browse_preview — first-page results from the live browse API

DB opened in SQLite URI read-only mode (mode=ro); any write statement is
rejected at the driver level. Configure via KIWI_DB_PATH and KIWI_API_URL
env vars (see module docstring for settings.json snippet).
2026-05-11 11:32:40 -07:00
7498995092 feat(filters): split time filter into hands-on and total time (kiwi#52)
Some checks failed
CI / Backend (Python) (push) Has been cancelled
CI / Frontend (Vue) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
Adds max_active_min request field and backend filter. Active time uses
parse_time_effort().active_min (passive waits excluded). Recipes with
no parsed active time signal are not excluded (avoid hiding unlabelled
results). Total and active limits are AND'd when both set.

UI: two pill rows — "Hands-on time" (15/30/45/1hr) and "Total time"
(30m/1hr/90m/2hr/3hr/4+hr). Replaces single row capped at 90 min.
2026-04-27 16:03:27 -07:00
640fcefa9e fix(ui): compact recipe cards, batch ingredient classifier queries
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Recipe cards were rendering full directions, all nutrition chips,
prep notes, swap candidates, and grocery links inline in the grid —
making each card tall enough to push the second row below the fold at
3-column widths. Cards now show title, match/complexity/time badges,
up to 4 pantry ingredient chips, missing count, and calorie hint.
Full detail remains in RecipeDetailPanel on "Make this".

ElementClassifier.classify_batch() was issuing N separate DB queries
(one per pantry item). Replaced with a single WHERE name IN (...)
query + heuristic fallback for misses — same result, one round-trip.
2026-04-27 14:56:00 -07:00
7fd92d5179 feat(tags): add meal type inference from recipe titles (#125)
Adds _MEAL_SIGNALS table to tag_inferrer with title-only matching for:
  meal:Breakfast — pancakes, waffles, frittata, oatmeal, granola, etc.
  meal:Dessert   — cake, cookie, brownie, pudding, ice cream, tart, etc.
  meal:Snack     — dip, chips, popcorn, nachos, energy balls, etc.
  meal:Beverage  — smoothie, cocktail, juice, lemonade, etc.
  meal:Lunch     — sandwich, wrap, panini, grilled cheese, etc.
  meal:Bread     — bread, sourdough, focaccia, dinner roll, etc.

Uses word-boundary + optional-plural regex (\bWORD(?:s|es)?\b) so:
- "pancakes" matches the "pancake" pattern but "pancake" != "cake"
- "tartare" does not match "tart" (no word boundary after tart in tartare)
- "dipping" does not match "dip" (extra chars prevent boundary)

Title-only matching (not ingredient text) avoids false positives from
ingredient names like "cake flour" or "sandwich bread".

Estimated browse impact after backfill (--force on 3.19M recipes):
  Breakfast: 43 → ~70k
  Dessert:   372 → ~350k  (real desserts, not flavor:Sweet)
  Snack:     57  → ~60k
  Beverage:  43  → ~36k
  Lunch:     69  → ~26k
2026-04-27 12:24:31 -07:00
6f097cd43d fix: wire browse domains to inferred_tag vocabulary, fix can_be leak in dietary
- Dinner: replace non-matching text keywords with main:X protein inferred tags (0 -> 815k results)
- All meal_type categories: add meal:X structured tag phrases
- Dietary: switch to dietary:X-only phrases; bare text keywords matched can_be:X
  tags (nearly all recipes), inflating counts to 1.3M+ falsely
- Cuisine: add cuisine:X structured tag phrases to Italian, Mexican, Asian,
  Indian, Mediterranean, American, BBQ, European, Latin American
- Side Dish: use main:Vegetables + main:Grains as proxy (no meal:Side Dish tag exists)
- Dessert: remove 'sweet' keyword (matched flavor:Sweet on all recipes)
- New dietary categories: Low-Sodium, Paleo

Closes #122. Partial progress on #123.
Follow-up: #125 (expand meal: tag inferrer coverage)
2026-04-27 11:38:37 -07:00
896b4e048c feat: recipe scanner — photo to structured recipe (kiwi#9)
New feature: photograph a recipe card, cookbook page, or handwritten
note and have it extracted into a structured, editable recipe.

Backend:
- POST /recipes/scan: accept 1-4 photos, run VLM extraction, return
  structured JSON for review (not auto-saved)
- POST /recipes/scan/save: persist a reviewed/edited recipe
- GET/DELETE /recipes/user: user-created recipe CRUD
- Vision backend priority: cf-orch -> local Qwen2.5-VL -> Anthropic BYOK
- 503 with clear config hint when no vision backend available
- Multi-photo support: facing pages (ingredients/directions) sent together
- Pantry cross-reference: marks which ingredients are already on hand
- migration 041: user_recipes table (title, servings, cook_time, steps,
  ingredients JSON, source, pantry_match_pct)
- Tier gate: recipe_scan -> paid, BYOK-unlockable

Frontend:
- "Scan" button in the Recipes tab bar (camera icon)
- RecipeScanModal: upload step (drag-drop + file picker, up to 4 photos,
  live previews), processing step (spinner), review/edit step (all
  fields inline-editable before save), pantry match badge, warning banner
  for low-confidence or incomplete scans

Tests: 35 new tests (23 unit + 12 API), 404 total passing
2026-04-27 08:23:01 -07:00
c9fcfde694 feat(browse): active time estimation, prep scaling, required-ingredient filter
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Time effort (time_effort.py):
- Passive defaults per cooking technique (bake 30 min, slow cook 300 min, etc.)
- Prep action detection with n^0.75 quantity scaling for prep-needing ingredients
- Cross-reference ingredients/ingredient_names arrays to distribute quantity across steps
- Effort label now time-based (quick ≤20 min, moderate ≤45 min, involved >45 min)
- prep_min field added to StepAnalysis schema and Pydantic model
- All parse_time_effort call sites updated to pass ingredients + ingredient_names

Browse required-ingredient filter:
- New required_ingredient query param on GET /recipes/browse/{domain}/{category}
- Enter-to-commit input in RecipeBrowserPanel with auto-clear-on-empty watch
- Substring match via FTS5 ingredient_names column prefix filter
- FTS5 replaces LIKE '%X%' throughout browse_recipes and _browse_by_match
- _all + required_ingredient: 8.4s → 74ms; category + required_ingredient: 2s → 35ms
- _ingredient_fts_term() helper builds 'ingredient_names : "X"*' prefix queries
- Combined keywords + ingredient into single FTS MATCH to avoid secondary scans

Tests: 369/369 passing
2026-04-27 07:13:12 -07:00
12ab63e2fb feat: corrections router (#73) + Magpie flywheel hook (#28)
Corrections router (kiwi#73):
- Wire make_corrections_router() from cf-core at /api/v1/corrections
- Add get_db() dependency in session.py yielding store.conn (raw
  sqlite3.Connection as cf-core expects); cloud-aware via get_session
- Migration 040: corrections table + indexes (copied from cf-core DDL)
- Feeds Avocet SFT training pipeline via GET /corrections/export JSONL

Magpie flywheel hook (kiwi#28):
- app/services/magpie_hook.py: async fire_recipe_signal() that reads
  magpie_opt_in setting, checks external_id, POSTs anonymized payload
  to MAGPIE_INGEST_URL; stubs gracefully when URL unset or Magpie
  unreachable (DEBUG log, never raises)
- Hooks into save_recipe and update_saved_recipe as background tasks
- MAGPIE_INGEST_URL config key added to Settings
- SettingsView: "Data Sharing" toggle for magpie_opt_in, cloud-only
  (v-if VITE_CLOUD_MODE), plain-language consent label
2026-04-25 23:31:20 -07:00
9350719516 feat(recipes): LLM style classifier (#27) + cooked leftovers shelf-life (#112)
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Style classifier (kiwi#27):
- app/services/recipe/style_classifier.py: LLM prompt with curated vocab,
  cf-orch/LLMRouter fallback, JSON + regex tag extraction
- POST /recipes/saved/{recipe_id}/classify-style: Paid/BYOK tier gate,
  fetches recipe from corpus, returns {suggested_tags:[...]}
- SaveRecipeModal.vue: "Suggest tags" button with loading state; merges
  LLM suggestions into existing tags without overwriting user's choices
- 403/empty list silently ignored — button is a no-op when tier not met

Cooked leftovers shelf-life (kiwi#112):
- app/services/leftovers_predictor.py: deterministic FDA/USDA lookup table
  with shortest-component-wins for proteins and dish-type override for
  assembled dishes; special entries for ceviche (2d, acid != heat),
  fermented/cured (kimchi 14d, confit/lardo 7d), soups, rice, pasta, etc.
- POST /recipes/{recipe_id}/leftovers: free tier, no gate
- RecipeDetailPanel.vue: shelf-life section appears after "I cooked this"
  with fridge/freeze days, freeze-by advice, per-instance dismiss; calm
  framing per no-panic UX policy
- LeftoversResponse Pydantic schema added to recipe.py
2026-04-25 23:18:16 -07:00
9c4d8b7883 feat(recipe-engine): time-effort profile, product-label tokenisation, L1 tuning
Some checks failed
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
- Add TimeEffortProfile + StepAnalysis Pydantic schemas; serialised into
  RecipeSuggestion so the frontend receives active/passive/total minutes,
  effort label, and detected equipment per suggestion.
- parse_time_effort() now drives max_total_min filter (falls back to step-count
  estimate when directions contain no explicit time mentions).
- _PRODUCT_TOKEN_STOPWORDS: strips marketing/packaging words from multi-word
  product labels before adding individual ingredient tokens to pantry_set.
  "Organic Extra Firm Tofu" → adds "tofu"; improves packaged-food pantry match.
- L1 candidate pool raised to 60 (was 20); min_match_ratio lowered to 0.35
  (was 0.60) to keep enough results for plant-based / packaged-food pantries.
- household.py: tighten import to pull HEIMDALL_URL/ADMIN_TOKEN from
  services.heimdall_orch (matches refactor in cloud_session.py).
2026-04-25 21:44:26 -07:00
ed04b655be fix(saved-recipes): resolve FK constraint, null title, and load reliability
- Migration 039: drop saved_recipes.recipe_id FK (SQLite table rebuild).
  The FK referenced main.recipes but corpus lives in an ATTACH'd DB — caused
  500 on every POST /recipes/saved in cloud mode.
- _to_summary: row.get("title") or "" to handle corpus JOIN returning NULL
  title (e.g. placeholder recipe_id 99999).
- list_collections: return [] for Free tier instead of 403 — prevents
  Promise.all in savedStore.load() from aborting the saved-recipes fetch.
- savedStore.load(): switched to Promise.allSettled so a collections failure
  never blocks the saved list from populating.
- RecipesView: star indicator now reflects savedStore.isSaved() (server-side
  saved state) rather than localStorage bookmarks; changed to <span> since
  the star is now read-only visual feedback.
- Removed { immediate: true } from saved-tab watcher — premature bounce to
  Build Your Own before onMounted load() completes.
2026-04-25 21:44:10 -07:00
f6b29693c8 refactor: replace hand-rolled JWT+Heimdall with cf-core CloudSessionFactory
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Delegates JWT validation, Heimdall provision/tier-resolve, bypass-IP
handling, and guest session management to circuitforge_core. Kiwi keeps
its own CloudUser (db path, household fields, BYOK flag) and DB helpers.
detect_byok() is now imported from cf-core instead of a local copy.
household_id/is_household_owner/license_key flow through core_user.meta
(cf-core already forwards all Heimdall response extras into meta).
Removes ~217 lines of duplicated auth code.

Note: guest cookie name changes from kiwi_guest_id to cf_guest_id (cf-core
managed). Existing guest sessions get a new UUID on first visit — acceptable
for alpha.
2026-04-25 16:35:56 -07:00
e2c358c90a fix: extend source CHECK constraints to include visual_capture (kiwi#79)
Some checks failed
CI / Frontend (Vue) (push) Waiting to run
CI / Backend (Python) (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled
Migrations 037 and 038 rebuild products and inventory_items tables
to add 'visual_capture' as a valid source value, which the label-confirm
endpoint sets when saving user-verified nutrition label data.
Adds 2 schema tests covering the new allowed value.
2026-04-25 08:46:44 -07:00
17e62c451f feat: visual label capture for unenriched barcodes (kiwi#79)
When a barcode scan finds no product in FDC/OFF, paid-tier users now see a
"Capture label" offer instead of a dead-end "add manually" prompt.

Backend:
- Migration 036: captured_products local cache table (keyed by barcode,
  UPSERT on conflict so re-capture refreshes rather than errors)
- store.get_captured_product / save_captured_product (with JSON decode for
  ingredient_names and allergens)
- app/services/label_capture.py: wraps cf-core VisionRouter (caption API);
  graceful fallback to zero-confidence mock when stub/error; JSON fence
  stripping; confidence clamped to [0,1]; KIWI_LABEL_CAPTURE_MOCK=1 for tests
- New schemas: LabelCaptureResponse, LabelConfirmRequest, LabelConfirmResponse
- POST /inventory/scan/label-capture — image to extraction (paid+ gate, 403)
- POST /inventory/scan/label-confirm — save confirmed product + optional
  inventory add
- Both scan endpoints now: check captured_products cache before FDC/OFF;
  set needs_visual_capture=True for gap products on paid tier; BarcodeScanResult
  gains needs_visual_capture field
- visual_label_capture feature gate added to tiers.py (paid)

Tests: 42 new tests (service, store/migration, API endpoints) — 367 total passing

Frontend:
- InventoryList.vue: capturePhase state machine (offer => uploading => reviewing)
- Offer card appears after scan gap (calm UX: no urgency, Discard always visible)
- Review form: pre-populated from extraction; amber label highlights for
  unread fields (confidence < 0.7); comma-separated ingredients/allergens
- api.ts: LabelCaptureResult + LabelConfirmRequest types; captureLabelPhoto()
  and confirmLabelCapture() API methods
2026-04-24 17:57:25 -07:00
3463aa1e17 feat: wire dietary constraints into secondary use filter on all inventory endpoints
_user_constraints() loads dietary_constraints from user_settings once per
request. All 7 _enrich_item call sites now pass constraints so wine (and
any future alcohol-containing entries) are suppressed for halal/alcohol-free
users at the API response layer.
2026-04-24 17:12:39 -07:00
e45b07c203 feat: expand secondary use windows + dietary constraint filter (kiwi#110)
Adds 10 new secondary use entries and corrects all 8 existing ones.
New: apples/soft, leafy_greens/wilting, tomatoes/soft, cooked_pasta/day-old,
cooked_potatoes/day-old, yogurt/tangy, cream/sour, wine/open,
cooked_beans/day-old, cooked_meat/leftover.

Corrections: milk uses (specific recipes, not 'baking'/'sauces'); dairy uses
expanded; cheese label well-aged→rind-ready with named dishes (minestrone,
ribollita); rice uses (onigiri, arancini, congee); tortillas warning added;
bakery uses and synonyms expanded to named pastries; bananas synonyms
(spotty/brown/black/mushy); rice synonyms (old rice).

New fields on every SECONDARY_WINDOW entry:
- discard_signs: qualitative cues for when the item has gone past its
  secondary window (shown in UI alongside uses)
- constraints_exclude: dietary labels that suppress the entry entirely
  (wine suppressed for halal/alcohol-free)

ExpirationPredictor.filter_secondary_by_constraints() applies constraint
suppression; _enrich_item() now accepts user_constraints and passes
secondary_discard_signs through to the API response.
2026-04-24 17:08:45 -07:00
b5eb8e4772 feat: cross-encoder reranker for recipe suggestions (kiwi#117)
Integrates cf-core reranker into the L1/L2 recipe engine. Paid+ tier
gets a BGE cross-encoder pass over the top-20 FTS candidates, scoring
each recipe against the user's full context: pantry state, dietary
constraints, allergies, expiry urgency, style preference, and effort
preference. Free tier keeps the existing overlap sort unchanged.

- New app/services/recipe/reranker.py: build_query, build_candidate_string,
  rerank_suggestions with tier gate (_RERANKER_TIERS) and graceful fallback
- rerank_score field added to RecipeSuggestion (None on free tier, float on paid+)
- recipe_engine.py: single call after candidate assembly, before final sort;
  hard_day_mode tier grouping preserved as primary sort when reranker active
- Fix pre-existing circular import in app/services/__init__.py (eager import
  of ReceiptService triggered store.py → services → receipt_service → store)
- 27 unit tests (mock backend, no model weights) + 2 engine-level tier tests;
  325 tests passing, no regressions
2026-04-24 16:39:51 -07:00
63517d135b feat(streaming): add POST /recipes/stream-token endpoint 2026-04-24 10:22:30 -07:00
2547f80893 feat(streaming): add StreamTokenRequest/Response schemas 2026-04-24 10:19:18 -07:00
0996ea8c7a feat(streaming): add coordinator_proxy service module 2026-04-24 10:18:40 -07:00
c3e7dc1ea4 feat: time-first recipe entry (kiwi#52)
- Add max_total_min to RecipeRequest schema and TypeScript interface
- Add _within_time() helper to recipe_engine using parse_time_effort()
  with graceful degradation (empty directions or no signals -> pass)
- Wire max_total_min filter into suggest() loop after max_time_min
- Add time_first_layout to allowed settings keys
- Add timeFirstLayout ref to settings store (preserves sensoryPreferences)
- Add maxTotalMin ref to recipes store, wired into _buildRequest()
- Add time bucket selector UI (15/30/45/60/90 min) in RecipesView
  Find tab, gated by timeFirstLayout != 'normal'
- Add time-first layout selector section in SettingsView
- Add 5 _within_time unit tests and 2 settings key tests
2026-04-24 10:15:58 -07:00
521cb419bc feat: sensory profile filter — texture/smell/noise filtering for Browse and Find (kiwi#51)
- Migration 035: add sensory_tags column to recipes (default '{}')
- scripts/tag_sensory_profiles.py: batch tagger using ingredient names,
  direction keywords, and ingredient_profiles texture data
- app/services/recipe/sensory.py: SensoryExclude frozen dataclass,
  build_sensory_exclude(), passes_sensory_filter() with graceful degradation
  (untagged recipes always pass; malformed JSON always passes)
- store.browse_recipes and _browse_by_match: accept SensoryExclude, apply
  filter in recipe-building loop (default path) and scoring loop (match sort)
- recipe_engine.suggest: load sensory_preferences from settings, apply
  passes_sensory_filter() after exclude_set check in the rows loop
- settings endpoint: add sensory_preferences to _ALLOWED_KEYS
- Frontend: SensoryPreferences types in api.ts; sensoryPreferences state and
  saveSensory() action in settings store; Sensory section in SettingsView with
  texture avoid pills, smell/noise tolerance scale pills with ok/limit/neutral
  color coding
- 66 new tests (29 classification + 13 sensory service + 2 settings); 281 total
2026-04-24 09:47:48 -07:00
b1e187c779 feat: time & effort signals — active/passive split, effort cards, annotated steps (kiwi#50)
- Add app/services/recipe/time_effort.py: parse_time_effort(), TimeEffortProfile,
  StepAnalysis dataclasses; two-branch regex for time ranges and single values;
  whole-word passive keyword detection; 480 min/step cap; 1825 day global cap
- Add directions to browse_recipes and _browse_by_match SELECT queries in store.py
- Enrich browse and detail endpoints with active_min/passive_min/time_effort fields
- Add StepAnalysis, TimeEffortProfile TS interfaces to api.ts
- RecipeBrowserPanel: split pill badge showing active/passive time
- RecipeDetailPanel: collapsible ingredients summary, effort cards (Active/Hands-off/Total),
  equipment chips, annotated step list with Active/Wait badges and passive hints
- 45 new tests (40 unit + 5 API); 215 total passing
2026-04-24 09:29:54 -07:00
9697c7b64f feat(recipe-tags): merge accepted community tags into browse counts + FTS fallback
browse_counts_cache.py: after FTS counts, _merge_community_tag_counts() queries
  accepted tags (upvotes>=2) grouped by (domain,category,subcategory) and adds
  distinct recipe_id counts to the cached keyword-set totals. Skips silently
  when community Postgres is unavailable.

store.py: fetch_recipes_by_ids() fetches corpus recipes by explicit ID list,
  used by the FTS fallback when a subcategory returns zero FTS results.

recipes.py (browse endpoint): when FTS total==0 for a subcategory, queries
  community store for accepted tag IDs and serves those recipes directly.
  Sets community_tagged=True in the response so the UI can surface context.
  Refs kiwi#118.
2026-04-22 12:37:44 -07:00
f962748073 feat(recipe-tags): community subcategory tagging API endpoints
GET  /recipes/community-tags/{recipe_id} — all tags for a recipe
POST /recipes/community-tags             — submit tag (requires pseudonym)
POST /recipes/community-tags/{id}/upvote — vote on a tag

Validates (domain, category, subcategory) against DOMAINS taxonomy before
accepting. Returns 409 on duplicate submission or double-vote. Fails soft
(503) when community Postgres is unavailable so the browse path is unaffected.
Refs kiwi#118.
2026-04-22 12:37:32 -07:00
a507deddbf feat(tagger): add BBQ cuisine inference signals for tag_inferrer
food.com rarely tags BBQ in its own taxonomy fields, so BBQ recipes were
previously untagged. Added content-derived signals (brisket, pulled pork,
dry rub, regional styles) so infer_recipe_tags.py correctly tags them
as cuisine:BBQ. Companion to the browser_domains.py BBQ keyword expansion.
2026-04-21 15:06:04 -07:00
7a7eae4666 chore(cf-orch): route recipe LLM calls through vllm with model candidates + CF_APP_NAME
Switches recipe generation service type from 'cf-text' to 'vllm' so the
coordinator can route to quantized small models (Qwen2.5-3B, Phi-4-mini)
rather than the full text backend. Passes CF_APP_NAME for per-product
VRAM/request analytics in the coordinator dashboard.

- llm_recipe.py: _SERVICE_TYPE = 'vllm'; _MODEL_CANDIDATES list; passes
  model_candidates and pipeline= to CFOrchClient.allocate()
- compose.cloud.yml: CF_APP_NAME=kiwi env var for coordinator attribution
2026-04-21 15:05:38 -07:00
b223325d77 feat(shopping): locale-aware grocery links with region settings UI
Shopping links previously hardcoded to US storefronts. Users in other regions
got broken Amazon Fresh and Instacart links. Now locale is stored as a user
setting and passed to GroceryLinkBuilder at request time.

- locale_config.py: per-locale Amazon domain/dept config (already existed)
- grocery_links.py: GroceryLinkBuilder accepts locale=; routes Instacart to .ca
  for Canada, uses amazon_domain per locale, Instacart/Walmart US/CA only
- settings.py: adds 'shopping_locale' to allowed settings keys
- shopping.py: reads locale from user's stored setting on all list/add/update paths
- SettingsView.vue: Shopping Region selector (NA, Europe, APAC, LATAM)
- stores/settings.ts: shoppingLocale reactive state, saves via settings API
2026-04-21 15:05:28 -07:00
f1d35dd1ac feat(recipes): 'Not today' per-session ingredient exclusions
Users often have ingredients they want to avoid today (out of stock, not feeling it)
that aren't true allergies. The new 'Not today' filter lets them exclude specific
ingredients per session without permanently modifying their allergy list.

- recipe.py schema: exclude_ingredients field (list[str], default [])
- recipe_engine.py: filters corpus results when any ingredient is in exclude_set
- llm_recipe.py: injects exclusions into both prompt templates so LLM-generated
  recipes respect the constraint at generation time
- RecipesView.vue: tag-chip UI with Enter/comma input, removes on × click
- stores/recipes.ts: excludeIngredients reactive list (not persisted to localStorage)
2026-04-21 15:05:16 -07:00
1ac7e3d76a feat(browse): sort recipes by pantry match percentage
Adds 'Best match' sort button to the recipe browser. When selected, recipes are
ordered by the fraction of their ingredients that are in the user's pantry.

- store.py: _browse_by_match() pushes match_pct computation into SQL via json_each()
  so ORDER BY can sort the full result set before LIMIT/OFFSET pagination
- recipes.py: extends sort pattern validation to accept 'match'; falls back to
  default when no pantry_items provided
- RecipeBrowserPanel.vue: adds 'Best match' button (disabled when pantry empty);
  watcher auto-engages match sort when pantry goes from empty to non-empty
2026-04-21 15:04:34 -07:00
1a7a94a344 feat(browse-counts): add pre-computed FTS counts cache with nightly refresh
Multiple concurrent users browsing the 3.2M recipe corpus would cause FTS5 page
cache contention and slow per-request queries. Solution: pre-compute counts for
all category/subcategory keyword sets into a small SQLite cache.

- browse_counts_cache.py: refresh(), load_into_memory(), is_stale() helpers
- config.py: BROWSE_COUNTS_PATH setting (default DATA_DIR/browse_counts.db)
- main.py: warms in-memory cache on startup; runs nightly refresh task every 24h
- infer_recipe_tags.py: auto-refreshes cache after a successful tag run so the
  app picks up updated FTS counts without a restart
2026-04-21 15:04:23 -07:00
5d0ee2493e feat(browser): expand taxonomy keyword coverage for BBQ and regional subcategories
Top-level category keywords were too narrow, missing common food.com corpus terms
like 'barbecue', 'smoky', 'charcoal'. Subcategory terms also expanded to cover
broader corpus vocabulary so FTS counts register hits across more recipes.
2026-04-21 15:04:13 -07:00
69e2ca7914 feat(browser): expand cuisine taxonomy to 13 categories + 105 subcategories
Some checks failed
CI / Frontend (Vue) (push) Has been cancelled
CI / Backend (Python) (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
- 5 new top-level categories: BBQ & Smoke, Central American, African,
  Pacific & Oceania, Central Asian & Caucasus
- British/Irish split into British + Irish + Scottish with regional keywords
- Scandinavian: dish-level keyword expansion to fix zero-count gap
- Mediterranean: Israeli → Jewish (Ashkenazi/Sephardic/NY deli/z'houg/hawaiij);
  Palestinian, Yemeni, Egyptian, Syrian added; Moroccan moved to African
- Mexican: +Baja/Cal-Mex, +Mexico City
- Asian: +Hong Kong, +Cambodian, +Laotian, +Mongolian (16 subcategories)
- Indian: +Bangladeshi, +Pakistani, +Sri Lankan, +Nepali (8 subcategories)
- Latin American: full Caribbean depth (Jamaican, Puerto Rican, Dominican,
  Haitian, Trinidad); +Argentinian, +Venezuelan, +Chilean
- American: +Pacific Northwest, +Hawaiian; BBQ promoted to own category
- BBQ & Smoke: 8 regional styles (Texas, Carolina, KC, Memphis, Alabama,
  Kentucky, St. Louis, Backyard)
- feat(shopping): locale_config.py — Amazon/Instacart/Walmart locale routing
  for multi-currency affiliate link support (#114)
- chore: gitleaks allowlist for Amazon grocery dept IDs in locale_config.py
2026-04-21 10:15:58 -07:00
be050f5492 feat(scheduler): auto-detect OrchestratedScheduler when cf-orch is installed
Paid+ local users with circuitforge_orch installed now get the coordinator-
aware scheduler automatically — no env var needed. The coordinator's
allocation queue already prefers the local GPU first, so latency stays low.

Priority: USE_ORCH_SCHEDULER env override > CLOUD_MODE > cf-orch importable.
Free-tier local users without cf-orch installed get LocalScheduler as before.
USE_ORCH_SCHEDULER=false can force LocalScheduler even when cf-orch is present.
2026-04-19 22:12:44 -07:00
e2658f743f feat(scheduler): OrchestratedScheduler for cloud/multi-GPU, configurable via env
Switches to OrchestratedScheduler in cloud mode so concurrent recipe_llm
jobs fan out across all registered cf-orch GPU nodes instead of serializing
on one. Under load this eliminates poll timeouts from queue backup.

USE_ORCH_SCHEDULER env var gives explicit control independent of CLOUD_MODE:
  unset        follow CLOUD_MODE (cloud=orch, local=local)
  true         OrchestratedScheduler always (e.g. multi-GPU local rig)
  false        LocalScheduler always (e.g. cloud single-GPU dev instance)

ImportError fallback: if circuitforge_orch is not installed and orch is
requested, logs a warning and falls back to LocalScheduler gracefully.
2026-04-19 22:11:34 -07:00
ed4595d960 feat(recipes): async L3/L4 recipe job queue with poll endpoint
Adds the recipe_jobs table and background task pipeline for level 3/4
recipe generation. POST ?async=true returns 202 with job_id; clients
poll GET /recipes/jobs/{job_id} until status=done.

Key fix: _enqueue_recipe_job now calls scheduler.enqueue() after
insert_task() to wake the in-memory work queue immediately. Without
this, tasks sat in 'queued' until the scheduler's 30s idle cycle or
an API restart triggered _load_queued_tasks().

- Migration 034: recipe_jobs table (job_id, user_id, status, request,
  result, error) with indexes on job_id and user_id/created_at
- Store: create/get/update_running/complete/fail recipe job methods
- runner.py: recipe_llm task type + _run_recipe_llm handler; MUST
  call fail_recipe_job() before re-raising so status stays consistent
- CLOUD_MODE guard: falls back to sync generation (scheduler only
  polls shared settings DB, not per-user DBs)
- L4 wildcard is covered by the same req.level in (3, 4) dispatch
2026-04-19 21:44:27 -07:00
eba536070c fix(recipe): fail fast on cf-orch 429 instead of slow LLMRouter fallback
When the coordinator returns 429 (all nodes at max_concurrent limit), the previous
code fell back to LLMRouter which is also overloaded at high concurrency. This
caused the request to hang for ~60s before nginx returned a 504.

Now: detect 429/max_concurrent in the RuntimeError message and return "" immediately
so the caller gets an empty RecipeResult (graceful degradation) rather than a timeout.
2026-04-19 20:24:21 -07:00
5385adc52a feat: title search and sort controls in recipe browser
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Adds minimal sort/search to the recipe browser for cognitive access diversity —
linear scanners, alphabet browsers, and keyword diggers each get a different
way in without duplicating the full search tab.

- browse_recipes: q (LIKE title filter) + sort (default/alpha/alpha_desc)
- API endpoint: q/sort query params with validation
- Frontend: debounced search input (350ms) + sort pills (Default/A→Z/Z→A)
- Search and sort reset on domain/category change
- _all path supports q+sort; keyword-FTS path adds AND filter on top
2026-04-18 22:14:36 -07:00