136 lines
5.5 KiB
Python
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()]
|