Skip to content

Integrate Espresso Afficionados Compass Guide for taste-based shot analysis #261

@hessius

Description

@hessius

Background

The Espresso Aficionados Coffee Compass is a widely-used diagnostic tool that maps taste attributes to extraction adjustments. It helps baristas systematically dial in espresso by identifying what's wrong with a shot (e.g., sour, bitter, astringent) and recommending specific changes (grind finer, lower temperature, adjust dose).

Currently, MeticAI's shot analysis (POST /api/shots/analyze-llm) relies solely on extraction curve data — it has no taste input. Adding taste feedback gives the LLM critical context that curve data alone cannot provide (e.g., a perfectly-extracted shot can still taste sour if the coffee is underdeveloped).

Coffee Compass Dimensions

Axis Low End High End Extraction Meaning
Sour ↔ Bitter Sour/acidic Bitter/burnt Under-extracted ↔ Over-extracted
Thin ↔ Harsh Watery/weak body Dry/astringent Low strength ↔ High strength + channeling
Sweet Spot Center of compass Balanced extraction

Additional taste descriptors: salty (severe under-extraction), sweet (good), clean, muddy, fruity, nutty, chocolatey, floral

Compass → Adjustments mapping:

  • Sour + thin → grind much finer, increase dose
  • Sour only → grind slightly finer, increase temperature
  • Bitter + harsh → grind coarser, decrease temperature, check channeling
  • Bitter only → grind slightly coarser, reduce contact time
  • Balanced but weak → increase dose or ratio

Current State

Analysis Pipeline

  • POST /api/shots/analyze-llm in shots.py accepts profile_name, shot_date, shot_filename via Form(...) parameters
  • Sends to Gemini: PROFILING_KNOWLEDGE (~10K chars) + profile structure + local analysis JSON
  • Returns 5-section structured markdown parsed by parseStructuredAnalysis() on frontend
  • No taste data is sent — the LLM has zero information about how the shot tasted
  • Cache key: (profile_name, shot_date, shot_filename) — would need extending with taste hash

Frontend Analysis

  • ExpertAnalysisView.tsx renders analysis sections as styled cards
  • ShotHistoryView.tsx hosts the analysis tabs with an "Analyze" button
  • FormView.tsx has a tag-based input pattern (preset clickable tags for profile generation) that can be reused for taste input UX consistency

Existing Knowledge

  • PROFILING_KNOWLEDGE in gemini_service.py already has a troubleshooting section with sour→finer, bitter→lower temp mappings
  • This knowledge exists but is never activated meaningfully — there's no taste signal to trigger it

Implementation Plan

Taste Input UI

  1. New TasteCompassInput.tsx component:

    • Interactive 2D compass: sour↔bitter (x-axis), thin↔harsh (y-axis)
    • User taps/drags a point on the compass to indicate where the shot falls
    • Center = balanced/good, edges = increasingly problematic
    • Visual zones with labels and color gradients (green center → yellow → red edges)
    • Returns coordinates as { sour_bitter: -1..1, thin_harsh: -1..1 }
  2. Strength slider: separate 1–5 slider for perceived strength (weak → intense)

  3. Quick taste tags: predefined clickable tags matching FormView.tsx pattern:

    • Positive: sweet, clean, fruity, chocolatey, nutty, floral, balanced
    • Negative: sour, bitter, salty, astringent, muddy, ashy, flat
    • Color-coded: green (positive) / red (negative) / neutral
    • Multiple selection allowed
  4. Optional free-text note — short textarea for additional observations

  5. Placement: Below the shot chart in ShotHistoryView, above the "Analyze" button. Taste input is optional — analysis works without it but is enhanced when provided.

Backend Changes

  1. Extend POST /api/shots/analyze-llm with optional Form(...) parameters:

    taste_sour_bitter: Optional[float] = Form(None)   # -1 (sour) to 1 (bitter)
    taste_thin_harsh: Optional[float] = Form(None)     # -1 (thin) to 1 (harsh)
    taste_strength: Optional[int] = Form(None)          # 1-5
    taste_tags: Optional[str] = Form(None)              # comma-separated
    taste_notes: Optional[str] = Form(None)             # free text
  2. Extend cache key to include taste data hash: (profile_name, shot_date, shot_filename, taste_hash) — same shot analyzed with different taste input returns different results

  3. Build taste_context string for the prompt:

    TASTE FEEDBACK:
    Compass position: slightly sour, slightly thin (coordinates: -0.3, -0.2)
    Perceived strength: 3/5 (moderate)
    Taste notes: fruity, slightly sour, clean
    Additional: "Pleasant acidity but could be sweeter"
    

AI Prompt Integration

  1. Add Espresso Compass knowledge section to the analysis prompt:

    ESPRESSO COMPASS GUIDE:
    The user has provided taste feedback using the Espresso Aficionados Coffee Compass.
    Compass coordinates: sour_bitter={x} (-1=very sour, 0=balanced, 1=very bitter),
                         thin_harsh={y} (-1=very thin/watery, 0=balanced, 1=very harsh/astringent)
    
    Interpretation guide:
    - Sour + thin (bottom-left): severely under-extracted → grind much finer, increase dose
    - Sour only (left): under-extracted → grind finer, increase temperature 1-2°C
    - Bitter + harsh (top-right): over-extracted + channeling → grind coarser, lower temp, check puck prep
    - Bitter only (right): over-extracted → grind coarser, decrease contact time
    - Thin only (bottom): low strength → increase dose or decrease yield
    - Harsh only (top): channeling or astringency → improve puck prep, check pressure profile
    
    Correlate the taste feedback with the extraction curve data to provide more accurate recommendations.
    When taste and curve data disagree, prioritize taste (the user's experience is ground truth).
    
  2. When taste data is provided, add a 6th analysis section: "Taste-Based Recommendations" that directly maps compass coordinates to specific adjustments, formatted for the profile's actual parameters.

UX Design Notes

  • Compass input should be visually engaging — it's a 2D touchable area, not a form
  • Show a small crosshair or dot where the user taps, with a label ("Slightly sour, good body")
  • Tags use the same pill/badge design as FormView.tsx for consistency
  • On mobile: compass should be at least 200×200px for accurate tapping
  • Taste input section collapsed by default with "Add Taste Feedback" expand button
  • After analysis, taste data persists so re-analysis includes it

Dependencies

Acceptance Criteria

  • TasteCompassInput component renders interactive 2D compass with draggable position indicator
  • Strength slider (1–5) works independently of compass
  • Quick taste tags are selectable (multiple) with visual feedback
  • Taste data sent as optional Form parameters to POST /api/shots/analyze-llm
  • Analysis prompt includes Compass knowledge when taste data is provided
  • Analysis returns a "Taste-Based Recommendations" section when taste data is present
  • Analysis without taste data works exactly as before (backward compatible)
  • Cache key includes taste data hash (different taste input → different analysis)
  • Taste input section collapsible with "Add Taste Feedback" toggle
  • All labels use i18n keys (6 locales)
  • Unit tests for taste context building and prompt extension
  • Frontend tests for compass interaction, tag selection, and form submission
  • Mobile-friendly compass input (minimum 200×200px touch target)

Key Files

File Change
apps/web/src/components/TasteCompassInput.tsx New interactive compass component
apps/web/src/components/ShotHistoryView.tsx Integrate taste input above Analyze button
apps/server/api/routes/shots.py Extended analyze-llm with taste parameters
apps/server/prompt_builder.py Compass knowledge section + taste context builder
apps/server/services/gemini_service.py Extended cache key with taste hash
apps/web/public/locales/*/translation.json New i18n keys

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions