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. "