Files
commander-ai-lab/coach/prompt_template.py
T
KoalaTrapLord a5fe8cb3fa v23: React + TypeScript SPA migration
Complete frontend rewrite from vanilla HTML/CSS/JS to React 19 + TypeScript + Tailwind CSS v4.

Frontend (frontend/commander-ai-lab-ui/):
- Vite + React 19 + TypeScript + Tailwind CSS v4 + React Router
- 7 pages: BatchSim, Collection, Decks, AutoGen, Simulator, Coach, Training
- Reusable components: Spinner, StatusBadge, CardImage, ManaSymbols, EmptyState
- Full typed API client layer (8 modules matching all backend endpoints)
- Recharts for mana curves, win rate charts, progress bars
- Lucide icons throughout
- Custom dark theme with MTG mana color tokens
- Code splitting (recharts + react in separate chunks)

Backend (lab_api.py):
- SPA-aware static file serving with catch-all for client-side routing
- Fallback to legacy ui/ folder if React dist not built
- Updated startup log with SPA route list

Build system:
- .gitignore added (was missing - removes pyc, db, node_modules, etc.)
- build-full-installer-v2.py: v23 with Node.js check + React build step
- Vite build config with manual chunks for optimal loading

Also includes: V3 deck generator fixes, coach improvements, DeepSeek sim modules
2026-03-12 01:12:02 +00:00

248 lines
11 KiB
Python

"""
Commander AI Lab — Coach Prompt Template
════════════════════════════════════════
Constructs system and user prompts for the LLM coach.
Injects deck report data, underperformer analysis, and
candidate replacement cards into a structured template.
"""
import json
from typing import List, Dict, Optional
from .models import DeckReport, CoachGoals, CardPerformance
from .config import MAX_CANDIDATES_PER_UNDERPERFORMER, MAX_UNDERPERFORMERS
# ══════════════════════════════════════════════════════════════
# System Prompt
# ══════════════════════════════════════════════════════════════
SYSTEM_PROMPT_TEMPLATE = """You are an expert Magic: The Gathering Commander deck coach.
You analyze deck performance reports from simulation data and provide specific,
actionable suggestions to improve deck construction.
RULES:
- NEVER suggest cutting the commander ({commander}).
- Only suggest cards within the deck's color identity: {color_identity}.
- Base suggestions on the simulation data provided, not general "goodstuff" advice.
- Consider mana curve, role balance (ramp, draw, removal, threats), and synergy.
- When suggesting replacements, prefer cards that fill the same functional role.
- IMPORTANT: Suggest 3-5 DIFFERENT cards to cut and 3-5 DIFFERENT cards to add.
- Each suggested add MUST be a different card — never suggest the same card twice.
- Vary your suggestions across different functional roles (ramp, draw, removal, threats, utility).
- If the deck is missing a functional role (e.g., no card draw, no removal), prioritize adding cards for that role.
- If candidate replacements are provided from embeddings search, prefer those over generic suggestions.
{goals_section}
You MUST respond ONLY with valid JSON matching this exact structure:
{{
"summary": "2-3 sentence overview of the deck's strengths and weaknesses",
"suggestedCuts": [
{{
"cardName": "Card To Remove",
"reason": "Why this card underperforms",
"replacementOptions": ["Better Card 1", "Better Card 2"]
}}
],
"suggestedAdds": [
{{
"cardName": "Card To Add",
"role": "ramp|draw|removal|threat|utility|combo|protection",
"reason": "Why this card improves the deck",
"synergyWith": ["Card In Deck 1", "Card In Deck 2"]
}}
],
"heuristicHints": [
"Concise strategic tip based on data"
],
"manaBaseAdvice": "Specific mana base improvement suggestion or null",
"rawTextExplanation": "Detailed paragraph explaining the analysis and reasoning"
}}
Do NOT include any text outside the JSON object. No markdown fences, no preamble."""
# ══════════════════════════════════════════════════════════════
# User Prompt Builder
# ══════════════════════════════════════════════════════════════
USER_PROMPT_TEMPLATE = """## Deck Report: {deck_name}
Commander: {commander}
Color Identity: {color_identity}
Games Simulated: {games_simulated}
Overall Win Rate: {win_rate:.1%}
Avg Game Length: {avg_game_length:.1f} turns
### Deck Structure
- Lands: {land_count}
- Mana Curve: {curve_summary}
- Card Types: {type_summary}
- Functional Roles: {role_summary}
### Matchup Data
{matchup_section}
### Top Performing Cards (by Impact Score)
{top_cards}
### Underperforming Cards (candidates for cuts)
{underperformer_section}
### Candidate Replacement Cards (from embeddings search)
{candidate_section}
{combo_section}
Based on this simulation data, provide your coaching suggestions."""
def _format_goals_section(goals: Optional[CoachGoals]) -> str:
"""Format goals into system prompt rules."""
if not goals:
return "- Aim for a balanced, well-rounded deck."
lines = []
if goals.targetPowerLevel is not None:
lines.append(f"- Target power level: {goals.targetPowerLevel}/10.")
if goals.targetPowerLevel <= 4:
lines.append("- Keep suggestions casual and fun-focused.")
elif goals.targetPowerLevel >= 8:
lines.append("- Prioritize efficiency and competitive staples.")
if goals.metaFocus:
lines.append(f"- Optimize for a {goals.metaFocus} strategy.")
if goals.budget:
budget_map = {"budget": "under $5 per card", "medium": "under $20 per card",
"no-limit": "no budget restrictions"}
lines.append(f"- Budget constraint: {budget_map.get(goals.budget, goals.budget)}.")
if goals.focusAreas:
lines.append(f"- Focus improvement on: {', '.join(goals.focusAreas)}.")
return "\n".join(lines) if lines else "- Aim for a balanced, well-rounded deck."
def _format_curve(buckets: List[int]) -> str:
"""Format mana curve buckets as a readable string."""
labels = ["0", "1", "2", "3", "4", "5", "6", "7+"]
parts = [f"{labels[i]}={buckets[i]}" for i in range(min(len(buckets), 8)) if buckets[i] > 0]
return ", ".join(parts) if parts else "not available (analyze deck structure manually)"
def _format_top_cards(cards: List[CardPerformance], top_n: int = 10) -> str:
"""Format the best performing cards."""
sorted_cards = sorted(cards, key=lambda c: c.impactScore, reverse=True)
lines = []
for c in sorted_cards[:top_n]:
tags = f" [{', '.join(c.tags)}]" if c.tags else ""
lines.append(f"- {c.name}: impact={c.impactScore:.3f}, "
f"castRate={c.castRate:.1%}, drawnRate={c.drawnRate:.1%}{tags}")
return "\n".join(lines) if lines else "No card performance data available."
def _format_underperformers(cards: List[CardPerformance],
underperformer_names: List[str]) -> str:
"""Format underperforming cards with their stats."""
card_map = {c.name: c for c in cards}
lines = []
for name in underperformer_names[:MAX_UNDERPERFORMERS]:
c = card_map.get(name)
if c:
lines.append(f"- {c.name}: impact={c.impactScore:.3f}, "
f"deadCardRate={c.deadCardRate:.1%}, "
f"castRate={c.castRate:.1%}, "
f"clunkiness={c.clunkinessScore:.3f}")
else:
lines.append(f"- {name}: (no detailed stats)")
return "\n".join(lines) if lines else "No clear underperformers identified."
def _format_candidates(candidates: Dict[str, List[dict]]) -> str:
"""Format replacement candidates grouped by the card they'd replace."""
if not candidates:
return ("No embedding-based candidates available. "
"Use your MTG knowledge to suggest cards that fit the deck's "
"color identity, strategy, and missing functional roles. "
"Suggest at least 3 different cards across different roles "
"(ramp, draw, removal, threats, utility).")
lines = []
for underperformer, replacements in list(candidates.items())[:MAX_UNDERPERFORMERS]:
lines.append(f"\nReplacements for '{underperformer}':")
for r in replacements[:MAX_CANDIDATES_PER_UNDERPERFORMER]:
name = r.get("name", "Unknown")
types = r.get("types", "")
mv = r.get("mana_value", "?")
sim = r.get("similarity", 0)
text_preview = r.get("text", "")[:80]
lines.append(f" - {name} (MV:{mv}, {types}) [similarity: {sim:.3f}]")
if text_preview:
lines.append(f" Oracle: {text_preview}")
lines.append("\nChoose from these candidates when possible, but also suggest ")
lines.append("cards NOT in this list if they better address the deck's weaknesses.")
return "\n".join(lines)
def _format_matchups(report: DeckReport) -> str:
"""Format matchup data."""
if not report.matchups:
return "No matchup data available."
lines = []
for m in report.matchups:
lines.append(f"- vs {m.opponentDeck}: {m.winRate:.1%} "
f"({m.gamesPlayed} games)")
return "\n".join(lines)
def _format_combos(report: DeckReport) -> str:
"""Format known combos section."""
if not report.knownCombos:
return ""
lines = ["### Known Combos"]
for combo in report.knownCombos:
cards = " + ".join(combo.cardNames)
lines.append(f"- {cards}: winRate={combo.winRateWhenAssembled:.1%}, "
f"assemblyRate={combo.assemblyRate:.1%}")
return "\n".join(lines)
# ══════════════════════════════════════════════════════════════
# Main Builder Functions
# ══════════════════════════════════════════════════════════════
def build_system_prompt(report: DeckReport,
goals: Optional[CoachGoals] = None) -> str:
"""Build the system prompt with deck-specific rules."""
color_str = "/".join(report.colorIdentity) if report.colorIdentity else "any"
return SYSTEM_PROMPT_TEMPLATE.format(
commander=report.commander,
color_identity=color_str,
goals_section=_format_goals_section(goals),
)
def build_user_prompt(report: DeckReport,
candidates: Dict[str, List[dict]] = None) -> str:
"""Build the user prompt with full deck report data."""
color_str = "/".join(report.colorIdentity) if report.colorIdentity else "unknown"
type_parts = [f"{k}={v}" for k, v in report.structure.cardTypeCounts.items()] \
if report.structure.cardTypeCounts else ["unknown"]
role_parts = [f"{k}={v}" for k, v in report.structure.functionalCounts.items()] \
if report.structure.functionalCounts else ["unknown"]
return USER_PROMPT_TEMPLATE.format(
deck_name=report.deckId,
commander=report.commander,
color_identity=color_str,
games_simulated=report.meta.gamesSimulated,
win_rate=report.meta.overallWinRate,
avg_game_length=report.meta.avgGameLength,
land_count=report.structure.landCount,
curve_summary=_format_curve(report.structure.curveBuckets),
type_summary=", ".join(type_parts),
role_summary=", ".join(role_parts),
matchup_section=_format_matchups(report),
top_cards=_format_top_cards(report.cards),
underperformer_section=_format_underperformers(
report.cards, report.underperformers),
candidate_section=_format_candidates(candidates or {}),
combo_section=_format_combos(report),
)