Files

274 lines
10 KiB
Python

"""
Commander AI Lab — Coach Data Models
═════════════════════════════════════
Pydantic v2 models for deck reports, coach requests/responses,
and coaching session persistence.
"""
from typing import List, Optional, Dict
from pydantic import BaseModel, Field
# ══════════════════════════════════════════════════════════════
# Deck Report Models (mirrors Java DeckReport)
# ══════════════════════════════════════════════════════════════
class CardPerformance(BaseModel):
"""Per-card aggregated stats from simulation data."""
name: str
drawnRate: float = 0.0
castRate: float = 0.0
keptInOpeningHandRate: float = 0.0
deadCardRate: float = 0.0
impactScore: float = 0.0
synergyScore: float = 0.0
clunkinessScore: float = 0.0
avgTurnCast: Optional[float] = None
avgDamageDealt: float = 0.0
tags: List[str] = Field(default_factory=list)
class MatchupRecord(BaseModel):
"""Win rate against a specific opponent deck."""
opponentDeck: str
opponentCommander: str = ""
gamesPlayed: int = 0
winRate: float = 0.0
class DeckMeta(BaseModel):
"""High-level deck statistics."""
gamesSimulated: int = 0
overallWinRate: float = 0.0
avgGameLength: float = 0.0
perArchetypeWinRates: Dict[str, float] = Field(default_factory=dict)
class DeckStructure(BaseModel):
"""Deck composition breakdown."""
landCount: int = 0
curveBuckets: List[int] = Field(default_factory=lambda: [0]*8)
cardTypeCounts: Dict[str, int] = Field(default_factory=dict)
functionalCounts: Dict[str, int] = Field(default_factory=dict)
class ComboRecord(BaseModel):
"""Known combo with performance data."""
cardNames: List[str]
winRateWhenAssembled: float = 0.0
assemblyRate: float = 0.0
class DeckReport(BaseModel):
"""
Complete deck performance report — produced by the Java
ReportAggregator and consumed by the Python coach service.
"""
deckId: str
commander: str
colorIdentity: List[str] = Field(default_factory=list)
meta: DeckMeta = Field(default_factory=DeckMeta)
matchups: List[MatchupRecord] = Field(default_factory=list)
structure: DeckStructure = Field(default_factory=DeckStructure)
cards: List[CardPerformance] = Field(default_factory=list)
underperformers: List[str] = Field(default_factory=list)
overperformers: List[str] = Field(default_factory=list)
knownCombos: List[ComboRecord] = Field(default_factory=list)
lastUpdated: str = ""
# ══════════════════════════════════════════════════════════════
# Coach Request / Response Models
# ══════════════════════════════════════════════════════════════
class CoachGoals(BaseModel):
"""User-specified coaching goals."""
targetPowerLevel: Optional[int] = None
metaFocus: Optional[str] = None
budget: Optional[str] = None
focusAreas: List[str] = Field(default_factory=list)
class CoachRequest(BaseModel):
"""Request body for POST /api/coach/decks/{deckId}."""
deck_id: str = ""
goals: Optional[CoachGoals] = None
class SuggestedCut(BaseModel):
"""A card the coach suggests removing."""
cardName: str
reason: str
replacementOptions: List[str] = Field(default_factory=list)
currentImpactScore: float = 0.0
class SuggestedAdd(BaseModel):
"""A card the coach suggests adding."""
cardName: str
role: str = ""
reason: str = ""
synergyWith: List[str] = Field(default_factory=list)
estimatedManaValue: Optional[float] = None
# ══════════════════════════════════════════════════════════════
# Phase 2: Structured Analysis Models
# ══════════════════════════════════════════════════════════════
class UpgradePriorityItem(BaseModel):
"""A single prioritized cut/add swap ranked by expected impact."""
rank: int
cut: str
add: str
reasoning: str = ""
expectedImpact: str = ""
class CommanderDependency(BaseModel):
"""How reliant the deck is on having the commander in play."""
score: int = 5
dependentCards: List[str] = Field(default_factory=list)
recoveryPlan: str = ""
class MulliganAnalysis(BaseModel):
"""Opening hand keepability assessment."""
estimatedKeepRate: str = ""
worstOffenders: List[str] = Field(default_factory=list)
recommendation: str = ""
# ══════════════════════════════════════════════════════════════
# Phase 3: Qualitative Analysis Models
# ══════════════════════════════════════════════════════════════
class RemovalCoverage(BaseModel):
"""Categorized removal package by permanent type."""
creatures: str = ""
artifacts: str = ""
enchantments: str = ""
planeswalkers: str = ""
lands: str = ""
massRemoval: str = ""
gaps: List[str] = Field(default_factory=list)
class RampQuality(BaseModel):
"""Ramp package breakdown by subtype and fragility."""
manaRocks: str = ""
landFetch: str = ""
manaDorks: str = ""
costReducers: str = ""
fragility: str = ""
canReachFourByTurnThree: str = ""
class DrawEngineProfile(BaseModel):
"""Card draw sustainability analysis."""
burstDraw: List[str] = Field(default_factory=list)
repeatableEngines: List[str] = Field(default_factory=list)
assessment: str = ""
sustainability: str = ""
class WinCondition(BaseModel):
"""A named win condition with independence rating."""
name: str
cards: List[str] = Field(default_factory=list)
independence: str = ""
description: str = ""
class AntiSynergyFlag(BaseModel):
"""An internal conflict between cards in the deck."""
cards: List[str] = Field(default_factory=list)
conflict: str = ""
severity: str = ""
# ══════════════════════════════════════════════════════════════
# Phase 5: Pod Politics & Meta Awareness Models
# ══════════════════════════════════════════════════════════════
class PodPresence(BaseModel):
"""Commander pod politics and threat assessment."""
threatLevel: str = "" # early/medium/late/low
politicalTools: List[str] = Field(default_factory=list)
adversarialRating: str = "" # low/medium/high
recommendation: str = ""
class TempoAssessment(BaseModel):
"""When the deck 'comes online' and whether that's fast enough."""
peakTurn: int = 0
profile: str = "" # early/mid/late
survivalWindow: str = "" # Assessment of whether it survives to peak
assessment: str = ""
class MetaMatchup(BaseModel):
"""Strategic reasoning about deck vs a meta archetype."""
archetype: str # fast-combo, stax, control, aggro, etc.
assessment: str = ""
keyThreats: List[str] = Field(default_factory=list)
keyAnswers: List[str] = Field(default_factory=list)
overallRating: str = "" # favored/even/unfavored
# ══════════════════════════════════════════════════════════════
# Coach Session (persisted result)
# ══════════════════════════════════════════════════════════════
class CoachSession(BaseModel):
"""
Complete coaching session result.
Persisted to coach-sessions/{sessionId}.json.
"""
sessionId: str
deckId: str
timestamp: str = ""
summary: str = ""
suggestedCuts: List[SuggestedCut] = Field(default_factory=list)
suggestedAdds: List[SuggestedAdd] = Field(default_factory=list)
heuristicHints: List[str] = Field(default_factory=list)
manaBaseAdvice: Optional[str] = None
rawTextExplanation: str = ""
modelUsed: str = ""
promptTokens: int = 0
completionTokens: int = 0
goals: Optional[CoachGoals] = None
isQuickDigest: bool = False # True for quick digest, False for full analysis
# Phase 2: Structured analysis fields
upgradePriority: List[UpgradePriorityItem] = Field(default_factory=list)
commanderDependency: Optional[CommanderDependency] = None
mulliganAnalysis: Optional[MulliganAnalysis] = None
# Phase 3: Qualitative analysis fields
removalCoverage: Optional[RemovalCoverage] = None
rampQuality: Optional[RampQuality] = None
drawEngineProfile: Optional[DrawEngineProfile] = None
winConditions: List[WinCondition] = Field(default_factory=list)
antiSynergyFlags: List[AntiSynergyFlag] = Field(default_factory=list)
# Phase 5: Pod politics & meta
podPresence: Optional[PodPresence] = None
tempoAssessment: Optional[TempoAssessment] = None
metaMatchups: List[MetaMatchup] = Field(default_factory=list)
class ApplyRequest(BaseModel):
"""Request body for applying coach suggestions to a deck."""
acceptedCuts: List[str] = Field(default_factory=list)
acceptedAdds: List[str] = Field(default_factory=list)
class CoachStatus(BaseModel):
"""Status of the coach subsystem."""
llmConnected: bool = False
llmModel: Optional[str] = None
llmModels: List[str] = Field(default_factory=list)
embeddingsLoaded: bool = False
embeddingCards: int = 0
deckReportsAvailable: int = 0
error: Optional[str] = None