From 578a641e7e2706a40f48e0da13fe9ea88ebc7b07 Mon Sep 17 00:00:00 2001
From: ComBba
Date: Tue, 10 Feb 2026 00:33:38 +0900
Subject: [PATCH 1/3] fix(full_techniques): add proper result handling for
75-technique evaluation mode
- Add full_techniques branch in save_evaluation_results() to convert BMAD category scores to sommelier_outputs format
- Add BMAD category themes (Problem Definition, Technical Design, Implementation, Documentation) to frontend sommeliers.ts
- Add SOMMELIER_ROLES mapping for full_techniques categories in api.ts
Fixes issue where all 75-technique results displayed as Jean-Pierre due to missing mode-specific handling
---
backend/app/services/evaluation_service.py | 53 ++++++++++++++++++++++
frontend/src/lib/api.ts | 4 ++
frontend/src/lib/sommeliers.ts | 52 +++++++++++++++++++++
3 files changed, 109 insertions(+)
diff --git a/backend/app/services/evaluation_service.py b/backend/app/services/evaluation_service.py
index a5ef479..0b7e7ee 100644
--- a/backend/app/services/evaluation_service.py
+++ b/backend/app/services/evaluation_service.py
@@ -309,6 +309,7 @@ async def save_evaluation_results(
jeanpierre_result = evaluation_data.get("jeanpierre_result")
is_grand_tasting = evaluation_mode == "grand_tasting"
+ is_full_techniques = evaluation_mode == "full_techniques"
if is_grand_tasting:
cellar_result = cellar_result or {}
@@ -341,6 +342,58 @@ async def save_evaluation_results(
recommendations=technique_names,
)
)
+ elif is_full_techniques:
+ normalized_score = evaluation_data.get("normalized_score", 0)
+ overall_score = int(normalized_score)
+ rating_tier = get_rating_tier(overall_score)
+ quality_gate = evaluation_data.get("quality_gate", "")
+ coverage = evaluation_data.get("coverage_rate", 0)
+
+ summary = (
+ f"Comprehensive evaluation using 75 techniques. "
+ f"Quality Gate: {quality_gate}. "
+ f"Coverage: {coverage * 100:.1f}%."
+ )
+
+ BMAD_CATEGORY_CONFIG = {
+ "A": ("Problem Definition", 25, ["aroma", "palate"]),
+ "B": ("Technical Design", 25, ["body", "balance"]),
+ "C": ("Implementation", 30, ["vintage", "terroir"]),
+ "D": ("Documentation", 20, ["cellar", "finish"]),
+ }
+
+ category_scores = evaluation_data.get("category_scores", {})
+ trace_metadata = evaluation_data.get("trace_metadata", {})
+
+ sommelier_outputs = []
+ for cat_id, (name, max_score, tasting_cats) in BMAD_CATEGORY_CONFIG.items():
+ cat_data = category_scores.get(cat_id, {})
+ raw_score = cat_data.get("score", 0)
+ scaled_score = int((raw_score / max_score) * 100) if max_score > 0 else 0
+
+ technique_info = []
+ for tasting_cat in tasting_cats:
+ cat_trace = trace_metadata.get(tasting_cat, {})
+ succeeded = cat_trace.get("techniques_succeeded", 0)
+ total = cat_trace.get("techniques_count", 0)
+ if total > 0:
+ technique_info.append(
+ f"{tasting_cat.capitalize()}: {succeeded}/{total}"
+ )
+
+ cat_summary = (
+ f"Category {cat_id} ({name}): Scored {raw_score:.1f}/{max_score} points. "
+ f"Techniques evaluated: {', '.join(technique_info) if technique_info else 'N/A'}."
+ )
+
+ sommelier_outputs.append(
+ SommelierOutput(
+ sommelier_name=name,
+ score=scaled_score,
+ summary=cat_summary,
+ recommendations=technique_info,
+ )
+ )
else:
jeanpierre_result = jeanpierre_result or {}
overall_score = jeanpierre_result.get("total_score", 0)
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 61b5dad..e7457c7 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -193,6 +193,10 @@ export const api = {
'Balance Notes': 'Feasibility',
'Vintage Notes': 'Opportunity',
'Terroir Notes': 'Presentation',
+ 'Problem Definition': 'BMAD Category A',
+ 'Technical Design': 'BMAD Category B',
+ 'Implementation': 'BMAD Category C',
+ 'Documentation': 'BMAD Category D',
};
const sommelierOutputs = response.final_evaluation?.sommelier_outputs || [];
diff --git a/frontend/src/lib/sommeliers.ts b/frontend/src/lib/sommeliers.ts
index e69e14f..700d15c 100644
--- a/frontend/src/lib/sommeliers.ts
+++ b/frontend/src/lib/sommeliers.ts
@@ -96,6 +96,58 @@ export const SOMMELIER_THEMES: Record = {
description: 'Final Synthesis',
image: '/sommeliers/jeanpierre.png',
},
+ problemdefinition: {
+ id: 'problemdefinition',
+ name: 'Problem Definition',
+ role: 'BMAD Category A',
+ emoji: '🔍',
+ color: '#8B7355',
+ bgColor: 'bg-amber-800',
+ borderColor: 'border-amber-700',
+ textColor: 'text-amber-800',
+ lightBg: 'bg-amber-50',
+ description: 'Problem Analysis',
+ image: '/sommeliers/marcel.png',
+ },
+ technicaldesign: {
+ id: 'technicaldesign',
+ name: 'Technical Design',
+ role: 'BMAD Category B',
+ emoji: '🏗️',
+ color: '#2F4F4F',
+ bgColor: 'bg-slate-700',
+ borderColor: 'border-slate-600',
+ textColor: 'text-slate-700',
+ lightBg: 'bg-slate-50',
+ description: 'Architecture & Design',
+ image: '/sommeliers/heinrich.png',
+ },
+ implementation: {
+ id: 'implementation',
+ name: 'Implementation',
+ role: 'BMAD Category C',
+ emoji: '🛠️',
+ color: '#228B22',
+ bgColor: 'bg-emerald-700',
+ borderColor: 'border-emerald-600',
+ textColor: 'text-emerald-700',
+ lightBg: 'bg-emerald-50',
+ description: 'Code Quality',
+ image: '/sommeliers/laurent.png',
+ },
+ documentation: {
+ id: 'documentation',
+ name: 'Documentation',
+ role: 'BMAD Category D',
+ emoji: '📚',
+ color: '#DAA520',
+ bgColor: 'bg-yellow-600',
+ borderColor: 'border-yellow-500',
+ textColor: 'text-yellow-700',
+ lightBg: 'bg-yellow-50',
+ description: 'Docs & Guides',
+ image: '/sommeliers/sofia.png',
+ },
};
export function getSommelierTheme(id: string): SommelierTheme {
From f515cb0e179b9edf2d6a3baa62410421517b4b47 Mon Sep 17 00:00:00 2001
From: ComBba
Date: Tue, 10 Feb 2026 00:39:50 +0900
Subject: [PATCH 2/3] feat(full_techniques): show 8 tasting note categories
with expandable technique details
- Change full_techniques result to use 8 tasting note categories instead of 4 BMAD categories
- Add TechniqueDetail model to backend and type to frontend
- Add expandable technique details section in SommelierCard showing individual technique results
- Add 8 tasting note themes (Aroma, Palate, Body, Finish, Balance, Vintage, Terroir, Cellar Notes)
- Each category shows success/failed/skipped techniques with expand/collapse UI
---
backend/app/models/results.py | 17 ++--
backend/app/services/evaluation_service.py | 95 +++++++++++++++-----
frontend/src/components/SommelierCard.tsx | 59 ++++++++++++-
frontend/src/components/TastingNotesTab.tsx | 1 +
frontend/src/lib/api.ts | 23 ++++-
frontend/src/lib/sommeliers.ts | 96 ++++++++++++++++-----
frontend/src/types/index.ts | 10 +++
7 files changed, 247 insertions(+), 54 deletions(-)
diff --git a/backend/app/models/results.py b/backend/app/models/results.py
index ac33245..9c6389a 100644
--- a/backend/app/models/results.py
+++ b/backend/app/models/results.py
@@ -11,7 +11,7 @@
from datetime import datetime
from enum import Enum
-from typing import List
+from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
@@ -63,18 +63,25 @@ def get_rating_tier(score: int) -> RatingTier:
return RatingTier.Corked
-class SommelierOutput(BaseModel):
- """Model for individual sommelier AI agent evaluation output.
+class TechniqueDetail(BaseModel):
+ id: str = Field(..., description="Technique ID")
+ name: str = Field(..., description="Technique name")
+ status: str = Field(..., description="success, failed, or skipped")
+ score: Optional[float] = Field(None, description="Score if evaluated")
+ max_score: Optional[float] = Field(None, description="Maximum possible score")
+ error: Optional[str] = Field(None, description="Error message if failed")
- Each of the six AI sommeliers produces this output.
- """
+class SommelierOutput(BaseModel):
sommelier_name: str = Field(..., description="Name of the sommelier agent")
score: int = Field(..., ge=0, le=100, description="Score from 0 to 100")
summary: str = Field(..., description="Brief summary of the evaluation")
recommendations: List[str] = Field(
default_factory=list, description="List of recommendations"
)
+ technique_details: List[TechniqueDetail] = Field(
+ default_factory=list, description="Detailed technique results"
+ )
class FinalEvaluation(BaseModel):
diff --git a/backend/app/services/evaluation_service.py b/backend/app/services/evaluation_service.py
index 0b7e7ee..b40dc22 100644
--- a/backend/app/services/evaluation_service.py
+++ b/backend/app/services/evaluation_service.py
@@ -303,7 +303,12 @@ async def save_evaluation_results(
"""
repo = ResultRepository()
- from app.models.results import get_rating_tier, SommelierOutput, FinalEvaluation
+ from app.models.results import (
+ get_rating_tier,
+ SommelierOutput,
+ FinalEvaluation,
+ TechniqueDetail,
+ )
cellar_result = evaluation_data.get("cellar_result")
jeanpierre_result = evaluation_data.get("jeanpierre_result")
@@ -343,6 +348,8 @@ async def save_evaluation_results(
)
)
elif is_full_techniques:
+ from app.techniques.router import TechniqueRouter
+
normalized_score = evaluation_data.get("normalized_score", 0)
overall_score = int(normalized_score)
rating_tier = get_rating_tier(overall_score)
@@ -355,36 +362,79 @@ async def save_evaluation_results(
f"Coverage: {coverage * 100:.1f}%."
)
- BMAD_CATEGORY_CONFIG = {
- "A": ("Problem Definition", 25, ["aroma", "palate"]),
- "B": ("Technical Design", 25, ["body", "balance"]),
- "C": ("Implementation", 30, ["vintage", "terroir"]),
- "D": ("Documentation", 20, ["cellar", "finish"]),
+ TASTING_NOTE_CONFIG = {
+ "aroma": ("Aroma Notes", "Problem Analysis", 11),
+ "palate": ("Palate Notes", "Innovation", 13),
+ "body": ("Body Notes", "Risk Analysis", 7),
+ "finish": ("Finish Notes", "User-Centricity", 7),
+ "balance": ("Balance Notes", "Feasibility", 8),
+ "vintage": ("Vintage Notes", "Opportunity", 8),
+ "terroir": ("Terroir Notes", "Presentation", 6),
+ "cellar": ("Cellar Notes", "Synthesis", 15),
}
- category_scores = evaluation_data.get("category_scores", {})
trace_metadata = evaluation_data.get("trace_metadata", {})
+ techniques_used = set(evaluation_data.get("techniques_used", []))
+
+ router = TechniqueRouter()
sommelier_outputs = []
- for cat_id, (name, max_score, tasting_cats) in BMAD_CATEGORY_CONFIG.items():
- cat_data = category_scores.get(cat_id, {})
- raw_score = cat_data.get("score", 0)
- scaled_score = int((raw_score / max_score) * 100) if max_score > 0 else 0
+ for cat_key, (name, role, expected_count) in TASTING_NOTE_CONFIG.items():
+ cat_trace = trace_metadata.get(cat_key, {})
+ succeeded = cat_trace.get("techniques_succeeded", 0)
+ total = cat_trace.get("techniques_count", expected_count)
+ failed_list = cat_trace.get("failed_techniques", [])
- technique_info = []
- for tasting_cat in tasting_cats:
- cat_trace = trace_metadata.get(tasting_cat, {})
- succeeded = cat_trace.get("techniques_succeeded", 0)
- total = cat_trace.get("techniques_count", 0)
- if total > 0:
- technique_info.append(
- f"{tasting_cat.capitalize()}: {succeeded}/{total}"
- )
+ success_rate = (succeeded / total * 100) if total > 0 else 0
+ scaled_score = int(success_rate)
cat_summary = (
- f"Category {cat_id} ({name}): Scored {raw_score:.1f}/{max_score} points. "
- f"Techniques evaluated: {', '.join(technique_info) if technique_info else 'N/A'}."
+ f"{name} ({role}): {succeeded}/{total} techniques succeeded. "
+ f"Success rate: {success_rate:.1f}%."
+ )
+
+ technique_info = []
+ if succeeded > 0:
+ technique_info.append(f"{succeeded} passed")
+ if failed_list:
+ technique_info.append(f"{len(failed_list)} failed")
+
+ cat_technique_ids = router.select_techniques(
+ mode="full_techniques", category=cat_key
)
+ technique_details = []
+ for tech_id in cat_technique_ids:
+ tech_def = router.get_technique(tech_id)
+ tech_name = tech_def.name if tech_def else tech_id
+
+ failed_entry = next(
+ (f for f in failed_list if f.get("technique_id") == tech_id), None
+ )
+ if failed_entry:
+ technique_details.append(
+ TechniqueDetail(
+ id=tech_id,
+ name=tech_name,
+ status="failed",
+ error=failed_entry.get("error"),
+ )
+ )
+ elif tech_id in techniques_used:
+ technique_details.append(
+ TechniqueDetail(
+ id=tech_id,
+ name=tech_name,
+ status="success",
+ )
+ )
+ else:
+ technique_details.append(
+ TechniqueDetail(
+ id=tech_id,
+ name=tech_name,
+ status="skipped",
+ )
+ )
sommelier_outputs.append(
SommelierOutput(
@@ -392,6 +442,7 @@ async def save_evaluation_results(
score=scaled_score,
summary=cat_summary,
recommendations=technique_info,
+ technique_details=technique_details,
)
)
else:
diff --git a/frontend/src/components/SommelierCard.tsx b/frontend/src/components/SommelierCard.tsx
index 02f40b0..7b60612 100644
--- a/frontend/src/components/SommelierCard.tsx
+++ b/frontend/src/components/SommelierCard.tsx
@@ -2,8 +2,9 @@
import React, { useState } from 'react';
import Image from 'next/image';
-import { ChevronDown, ChevronUp, Lightbulb } from 'lucide-react';
+import { ChevronDown, ChevronUp, Lightbulb, CheckCircle2, XCircle, MinusCircle } from 'lucide-react';
import { getSommelierTheme } from '../lib/sommeliers';
+import { TechniqueDetail } from '../types';
interface SommelierCardProps {
id: string;
@@ -13,6 +14,7 @@ interface SommelierCardProps {
feedback: string;
recommendations?: string[];
pairingSuggestion?: string;
+ techniqueDetails?: TechniqueDetail[];
delay?: number;
}
@@ -48,9 +50,11 @@ export function SommelierCard({
feedback,
recommendations,
pairingSuggestion,
+ techniqueDetails,
delay = 0,
}: SommelierCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
+ const [showTechniques, setShowTechniques] = useState(false);
const theme = getSommelierTheme(id);
return (
@@ -165,6 +169,59 @@ export function SommelierCard({
)}
+
+ {/* Technique Details - Expandable */}
+ {techniqueDetails && techniqueDetails.length > 0 && (
+
+
+
+ {showTechniques && (
+
+ {techniqueDetails.map((tech) => (
+
+ {tech.status === 'success' && (
+
+ )}
+ {tech.status === 'failed' && (
+
+ )}
+ {tech.status === 'skipped' && (
+
+ )}
+
+
+ {tech.name}
+
+ {tech.error && (
+
{tech.error}
+ )}
+
+ {tech.score !== undefined && tech.maxScore !== undefined && (
+
+ {tech.score}/{tech.maxScore}
+
+ )}
+
+ ))}
+
+ )}
+
+ )}
);
}
diff --git a/frontend/src/components/TastingNotesTab.tsx b/frontend/src/components/TastingNotesTab.tsx
index 4c2efce..6a06180 100644
--- a/frontend/src/components/TastingNotesTab.tsx
+++ b/frontend/src/components/TastingNotesTab.tsx
@@ -199,6 +199,7 @@ export function TastingNotesTab({ result }: TastingNotesTabProps) {
feedback={somm.feedback}
recommendations={somm.recommendations}
pairingSuggestion={somm.pairingSuggestion}
+ techniqueDetails={somm.techniqueDetails}
delay={index * 100}
/>
))}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index e7457c7..4e6eb1d 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -44,11 +44,21 @@ export interface QuotaStatus {
const BASE_API_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.somm.dev';
const TOKEN_STORAGE_KEY = 'somm_auth_token';
+interface BackendTechniqueDetail {
+ id: string;
+ name: string;
+ status: string;
+ score?: number;
+ max_score?: number;
+ error?: string;
+}
+
interface BackendSommelierOutput {
sommelier_name?: string;
score?: number;
summary?: string;
recommendations?: string[];
+ technique_details?: BackendTechniqueDetail[];
}
interface BackendHistoryItem {
@@ -193,10 +203,7 @@ export const api = {
'Balance Notes': 'Feasibility',
'Vintage Notes': 'Opportunity',
'Terroir Notes': 'Presentation',
- 'Problem Definition': 'BMAD Category A',
- 'Technical Design': 'BMAD Category B',
- 'Implementation': 'BMAD Category C',
- 'Documentation': 'BMAD Category D',
+ 'Cellar Notes': 'Synthesis',
};
const sommelierOutputs = response.final_evaluation?.sommelier_outputs || [];
@@ -208,6 +215,14 @@ export const api = {
feedback: output.summary || '',
recommendations: output.recommendations || [],
pairingSuggestion: output.recommendations?.[0] || undefined,
+ techniqueDetails: output.technique_details?.map((t: BackendTechniqueDetail) => ({
+ id: t.id,
+ name: t.name,
+ status: t.status as 'success' | 'failed' | 'skipped',
+ score: t.score,
+ maxScore: t.max_score,
+ error: t.error,
+ })),
}));
return {
diff --git a/frontend/src/lib/sommeliers.ts b/frontend/src/lib/sommeliers.ts
index 700d15c..6ae4258 100644
--- a/frontend/src/lib/sommeliers.ts
+++ b/frontend/src/lib/sommeliers.ts
@@ -96,10 +96,10 @@ export const SOMMELIER_THEMES: Record = {
description: 'Final Synthesis',
image: '/sommeliers/jeanpierre.png',
},
- problemdefinition: {
- id: 'problemdefinition',
- name: 'Problem Definition',
- role: 'BMAD Category A',
+ aromanotes: {
+ id: 'aromanotes',
+ name: 'Aroma Notes',
+ role: 'Problem Analysis',
emoji: '🔍',
color: '#8B7355',
bgColor: 'bg-amber-800',
@@ -109,45 +109,97 @@ export const SOMMELIER_THEMES: Record = {
description: 'Problem Analysis',
image: '/sommeliers/marcel.png',
},
- technicaldesign: {
- id: 'technicaldesign',
- name: 'Technical Design',
- role: 'BMAD Category B',
- emoji: '🏗️',
+ palatenotes: {
+ id: 'palatenotes',
+ name: 'Palate Notes',
+ role: 'Innovation',
+ emoji: '🎭',
+ color: '#C41E3A',
+ bgColor: 'bg-rose-700',
+ borderColor: 'border-rose-600',
+ textColor: 'text-rose-700',
+ lightBg: 'bg-rose-50',
+ description: 'Innovation',
+ image: '/sommeliers/isabella.png',
+ },
+ bodynotes: {
+ id: 'bodynotes',
+ name: 'Body Notes',
+ role: 'Risk Analysis',
+ emoji: '🔍',
color: '#2F4F4F',
bgColor: 'bg-slate-700',
borderColor: 'border-slate-600',
textColor: 'text-slate-700',
lightBg: 'bg-slate-50',
- description: 'Architecture & Design',
+ description: 'Risk Analysis',
image: '/sommeliers/heinrich.png',
},
- implementation: {
- id: 'implementation',
- name: 'Implementation',
- role: 'BMAD Category C',
- emoji: '🛠️',
+ finishnotes: {
+ id: 'finishnotes',
+ name: 'Finish Notes',
+ role: 'User-Centricity',
+ emoji: '🎯',
+ color: '#722F37',
+ bgColor: 'bg-[#722F37]',
+ borderColor: 'border-[#722F37]',
+ textColor: 'text-[#722F37]',
+ lightBg: 'bg-[#F7E7CE]',
+ description: 'User-Centricity',
+ image: '/sommeliers/jeanpierre.png',
+ },
+ balancenotes: {
+ id: 'balancenotes',
+ name: 'Balance Notes',
+ role: 'Feasibility',
+ emoji: '⚖️',
color: '#228B22',
bgColor: 'bg-emerald-700',
borderColor: 'border-emerald-600',
textColor: 'text-emerald-700',
lightBg: 'bg-emerald-50',
- description: 'Code Quality',
+ description: 'Feasibility',
image: '/sommeliers/laurent.png',
},
- documentation: {
- id: 'documentation',
- name: 'Documentation',
- role: 'BMAD Category D',
- emoji: '📚',
+ vintagenotes: {
+ id: 'vintagenotes',
+ name: 'Vintage Notes',
+ role: 'Opportunity',
+ emoji: '🌱',
color: '#DAA520',
bgColor: 'bg-yellow-600',
borderColor: 'border-yellow-500',
textColor: 'text-yellow-700',
lightBg: 'bg-yellow-50',
- description: 'Docs & Guides',
+ description: 'Opportunity',
image: '/sommeliers/sofia.png',
},
+ terroirnotes: {
+ id: 'terroirnotes',
+ name: 'Terroir Notes',
+ role: 'Presentation',
+ emoji: '🏛️',
+ color: '#6B4423',
+ bgColor: 'bg-amber-900',
+ borderColor: 'border-amber-800',
+ textColor: 'text-amber-900',
+ lightBg: 'bg-amber-50',
+ description: 'Presentation',
+ image: '/sommeliers/marcel.png',
+ },
+ cellarnotes: {
+ id: 'cellarnotes',
+ name: 'Cellar Notes',
+ role: 'Synthesis',
+ emoji: '🍷',
+ color: '#4A1C24',
+ bgColor: 'bg-[#4A1C24]',
+ borderColor: 'border-[#4A1C24]',
+ textColor: 'text-[#4A1C24]',
+ lightBg: 'bg-rose-50',
+ description: 'Synthesis',
+ image: '/sommeliers/jeanpierre.png',
+ },
};
export function getSommelierTheme(id: string): SommelierTheme {
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index e0a10ab..1023753 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -10,6 +10,15 @@ export interface EvaluationRequest {
evaluationMode: EvaluationMode;
}
+export interface TechniqueDetail {
+ id: string;
+ name: string;
+ status: 'success' | 'failed' | 'skipped';
+ score?: number;
+ maxScore?: number;
+ error?: string;
+}
+
export interface SommelierResult {
id: string;
name: string;
@@ -18,6 +27,7 @@ export interface SommelierResult {
feedback: string;
recommendations?: string[];
pairingSuggestion?: string;
+ techniqueDetails?: TechniqueDetail[];
}
export interface EvaluationResult {
From fad13aa603d58a3a964673bba58e4b8f64bdedd6 Mon Sep 17 00:00:00 2001
From: ComBba
Date: Tue, 10 Feb 2026 00:53:11 +0900
Subject: [PATCH 3/3] refactor(full_techniques): apply CodeRabbit/Gemini review
feedback
- Move TASTING_NOTE_CONFIG to module-level constant for maintainability
- Use round() instead of int() for normalized_score (more accurate)
- Clamp scaled_score to 0-100 range to prevent UI issues
---
backend/app/services/evaluation_service.py | 28 ++++++++++++----------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/backend/app/services/evaluation_service.py b/backend/app/services/evaluation_service.py
index b40dc22..ba6f7ab 100644
--- a/backend/app/services/evaluation_service.py
+++ b/backend/app/services/evaluation_service.py
@@ -24,6 +24,19 @@
logger = logging.getLogger(__name__)
+# Module-level constant for full_techniques mode tasting note categories
+# Maps category key -> (display_name, role, expected_technique_count)
+TASTING_NOTE_CONFIG = {
+ "aroma": ("Aroma Notes", "Problem Analysis", 11),
+ "palate": ("Palate Notes", "Innovation", 13),
+ "body": ("Body Notes", "Risk Analysis", 7),
+ "finish": ("Finish Notes", "User-Centricity", 7),
+ "balance": ("Balance Notes", "Feasibility", 8),
+ "vintage": ("Vintage Notes", "Opportunity", 8),
+ "terroir": ("Terroir Notes", "Presentation", 6),
+ "cellar": ("Cellar Notes", "Synthesis", 15),
+}
+
async def _get_stored_key(
user_id: str, provider: str = "google"
@@ -351,7 +364,7 @@ async def save_evaluation_results(
from app.techniques.router import TechniqueRouter
normalized_score = evaluation_data.get("normalized_score", 0)
- overall_score = int(normalized_score)
+ overall_score = round(normalized_score)
rating_tier = get_rating_tier(overall_score)
quality_gate = evaluation_data.get("quality_gate", "")
coverage = evaluation_data.get("coverage_rate", 0)
@@ -362,17 +375,6 @@ async def save_evaluation_results(
f"Coverage: {coverage * 100:.1f}%."
)
- TASTING_NOTE_CONFIG = {
- "aroma": ("Aroma Notes", "Problem Analysis", 11),
- "palate": ("Palate Notes", "Innovation", 13),
- "body": ("Body Notes", "Risk Analysis", 7),
- "finish": ("Finish Notes", "User-Centricity", 7),
- "balance": ("Balance Notes", "Feasibility", 8),
- "vintage": ("Vintage Notes", "Opportunity", 8),
- "terroir": ("Terroir Notes", "Presentation", 6),
- "cellar": ("Cellar Notes", "Synthesis", 15),
- }
-
trace_metadata = evaluation_data.get("trace_metadata", {})
techniques_used = set(evaluation_data.get("techniques_used", []))
@@ -386,7 +388,7 @@ async def save_evaluation_results(
failed_list = cat_trace.get("failed_techniques", [])
success_rate = (succeeded / total * 100) if total > 0 else 0
- scaled_score = int(success_rate)
+ scaled_score = min(max(int(success_rate), 0), 100)
cat_summary = (
f"{name} ({role}): {succeeded}/{total} techniques succeeded. "