Files
Commander-Deck-App-backup/backend/app/services/ai/deck_service.py
T

136 lines
5.5 KiB
Python

"""
Deck service: orchestrates prompt building, Claude call, and DB persistence.
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.collection import CollectionCard
from app.models.deck import Deck, DeckCard, DeckMode
from app.schemas.deck import DeckConstraints, GenerateRequest, CompleteRequest, CullRequest
from app.services.ai import prompts
from app.services.ai.claude_client import call_claude, DeckPayload
from app.services.ai.constraints import build_constraint_context, build_owned_card_list
# A full 99-card deck with reasoning needs ~16000 tokens
GENERATE_MAX_TOKENS = 16000
COMPLETE_MAX_TOKENS = 16000
CULL_MAX_TOKENS = 16000
async def generate_deck(req: GenerateRequest, user_id: int, db: AsyncSession) -> Deck:
owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else []
system, user_msg = prompts.generate_prompt(
commander=req.commander,
playstyle=req.playstyle,
constraint_text=build_constraint_context(req.constraints, owned_names or None),
owned_list_text=build_owned_card_list(owned_names),
)
payload = await call_claude(system, user_msg, max_tokens=GENERATE_MAX_TOKENS)
return await _persist_deck(
payload=payload, user_id=user_id, mode=DeckMode.GENERATE,
commander=req.commander, name=req.name or payload.deck_name,
description=req.description or payload.strategy_summary,
playstyle=req.playstyle, constraints=req.constraints,
owned_name_set=set(n.lower() for n in owned_names), db=db,
)
async def complete_deck(req: CompleteRequest, user_id: int, db: AsyncSession) -> Deck:
owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else []
system, user_msg = prompts.complete_prompt(
commander=req.commander, playstyle=req.playstyle,
existing_cards=req.existing_cards,
constraint_text=build_constraint_context(req.constraints, owned_names or None),
owned_list_text=build_owned_card_list(owned_names),
)
payload = await call_claude(system, user_msg, max_tokens=COMPLETE_MAX_TOKENS)
return await _persist_deck(
payload=payload, user_id=user_id, mode=DeckMode.COMPLETE,
commander=req.commander, name=req.name or payload.deck_name,
description=payload.strategy_summary, playstyle=req.playstyle,
constraints=req.constraints,
owned_name_set=set(n.lower() for n in owned_names), db=db,
)
async def cull_deck(req: CullRequest, user_id: int, db: AsyncSession) -> Deck:
owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else []
owned_set = set(n.lower() for n in owned_names)
for card in req.existing_cards:
card["is_owned"] = card.get("card_name", "").lower() in owned_set
system, user_msg = prompts.cull_prompt(
commander=req.commander, existing_cards=req.existing_cards,
target_count=req.target_count,
constraint_text=build_constraint_context(req.constraints, owned_names or None),
owned_list_text=build_owned_card_list(owned_names),
prefer_owned=req.constraints.prefer_owned,
)
payload = await call_claude(system, user_msg, max_tokens=CULL_MAX_TOKENS)
return await _persist_deck(
payload=payload, user_id=user_id, mode=DeckMode.CULL,
commander=req.commander, name=req.name or payload.deck_name,
description=payload.strategy_summary, playstyle=None,
constraints=req.constraints, owned_name_set=owned_set, db=db,
)
async def _persist_deck(
payload: DeckPayload, user_id: int, mode: DeckMode,
commander: str, name: str, description: str | None,
playstyle: str | None, constraints: DeckConstraints,
owned_name_set: set[str], db: AsyncSession,
) -> Deck:
deck = Deck(
owner_id=user_id, name=name, commander=commander,
description=description, mode=mode, playstyle=playstyle,
prefer_owned=constraints.prefer_owned,
budget_enabled=constraints.budget_enabled,
budget_amount=constraints.budget_amount,
budget_scope=constraints.budget_scope,
ai_reasoning={
"strategy_summary": payload.strategy_summary,
"unresolved_cards": payload.unresolved,
"cuts": [{"name": c.card_name, "reasoning": c.reasoning} for c in payload.cuts],
},
)
db.add(deck)
await db.flush()
deck_cards = [
DeckCard(
deck_id=deck.id, scryfall_id="", card_name=commander,
slot="creature", quantity=1,
is_owned=commander.lower() in owned_name_set,
is_commander=True,
)
]
for entry in payload.cards:
if not entry.scryfall_id:
continue
is_owned = (
entry.card_name.lower() in owned_name_set
if constraints.prefer_owned
else entry.is_owned
)
deck_cards.append(DeckCard(
deck_id=deck.id, scryfall_id=entry.scryfall_id,
card_name=entry.card_name, slot=entry.slot,
quantity=entry.quantity, is_owned=is_owned,
is_commander=False, ai_reasoning=entry.reasoning,
scryfall_data=entry.scryfall_data,
))
db.add_all(deck_cards)
await db.commit()
await db.refresh(deck)
return deck
async def _owned_names(user_id: int, db: AsyncSession) -> list[str]:
result = await db.execute(
select(CollectionCard.card_name).where(CollectionCard.owner_id == user_id)
)
return [row[0] for row in result.all()]