- 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)
112 lines
4.5 KiB
Python
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,
|
|
}
|