Initial commit - steps 1-6 complete
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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=10000)
|
||||
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()]
|
||||
Reference in New Issue
Block a user