Files
commander-ai-lab/coach/schemas/substitution_schema.py
T
KoalaTrapLord 825946ae75 fix(deckgen): Phase 1 - non-destructive smart substitution + 99-card guarantee (#111)
- Add SubstitutionMethod enum to track how each card was resolved
- smart_substitute() now always returns a valid card string (never None/drops)
  fallback chain: exact -> fuzzy -> functional analog -> original kept -> basic land
- apply_substitutions() loop no longer silently drops cards on failure
- _pad_to_99() circuit breaker added before BuildResult validation
- Pre-validation count logging per category for fast regression diagnosis
- Startup assertion: sum(SLOT_BUDGET) == 99 raises on app boot if misconfigured
- Prompt injection: slot budget + exact card count requirement sent to LLM
- run_substitution() anthropic fallback failure now pads with basic land instead of leaving slot missing
- SubstitutionBatchResult gains padded_slots field (> 0 = circuit breaker fired)
2026-03-27 03:27:52 -04:00

112 lines
4.5 KiB
Python

"""
Commander AI Lab — Substitution Schemas
════════════════════════════════════════
Pydantic models and JSON Schema dicts for the
Smart Substitution Engine output.
Phase 1 changes:
- SubstitutionBatchResult gains padded_slots field
- DeckCardWithStatus gains original_card field (was missing)
"""
from typing import List, Optional
from pydantic import BaseModel, Field
# ══════════════════════════════════════════════════════════════
# Pydantic Models
# ══════════════════════════════════════════════════════════════
class CardAlternative(BaseModel):
"""A single substitute card suggestion."""
name: str = Field(description="Exact card name")
similarity_score: float = Field(default=0.0, description="Cosine similarity score (0-1)")
reason: str = Field(default="", description="Why this card is a good substitute")
source: str = Field(default="embedding", description="How it was found: 'embedding' or 'anthropic'")
role_overlap: List[str] = Field(default_factory=list, description="Shared functional roles")
cmc_delta: float = Field(default=0.0, description="CMC difference from original")
class DeckCardWithStatus(BaseModel):
"""A deck card enriched with ownership and substitution data."""
class Config:
extra = "ignore"
name: str
count: int = 1
category: str = ""
role_tags: List[str] = Field(default_factory=list)
reason: str = ""
estimated_price_usd: float = 0.0
synergy_with: List[str] = Field(default_factory=list)
# Ownership & substitution
owned: bool = True
owned_qty: int = 0
scryfall_id: str = ""
status: str = Field(default="owned", description="owned | substituted | missing")
original_card: Optional[str] = Field(
default=None,
description="Original card name before substitution (None if not substituted)"
)
alternatives: List[CardAlternative] = Field(default_factory=list)
selected_substitute: Optional[str] = Field(
default=None,
description="The substitute card name chosen for this slot"
)
class SubstitutionRequest(BaseModel):
"""Request for substitution on a single card."""
card_name: str
role_tags: List[str] = Field(default_factory=list)
category: str = ""
cmc: float = 0.0
color_identity: List[str] = Field(default_factory=list)
class SubstitutionBatchResult(BaseModel):
"""Result of a batch substitution pass."""
total_cards: int = 0
owned_count: int = 0
substituted_count: int = 0
missing_count: int = 0
padded_slots: int = Field(
default=0,
description="Number of slots filled by the pad-to-99 circuit breaker. "
"Any value > 0 indicates a bug in the substitution pipeline."
)
cards: List[DeckCardWithStatus] = Field(default_factory=list)
# ══════════════════════════════════════════════════════════════
# JSON Schema dicts (for LLM fallback substitution)
# ══════════════════════════════════════════════════════════════
SUBSTITUTION_RESPONSE_SCHEMA = {
"type": "object",
"properties": {
"substitutions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"original": {"type": "string", "description": "Card that needs replacing"},
"substitute": {"type": "string", "description": "Recommended replacement from the allowed list"},
"reason": {"type": "string", "description": "Why this substitute works"},
"role_overlap": {
"type": "array",
"items": {"type": "string"},
"description": "Shared functional roles",
},
},
"required": ["original", "substitute", "reason"],
"additionalProperties": False,
},
},
},
"required": ["substitutions"],
"additionalProperties": False,
}