This document explains the dedicated deck-building flow across frontend, backend, and async workers. It is intended for developers who want to understand how a deck gets generated, tracked, and displayed.
Deck building takes a user prompt (optionally constrained by set codes), runs async AI orchestration, and persists a generated deck plus metadata.
At a high level:
- User submits prompt from the frontend.
- Backend validates request (auth, quota, ownership, guardrails).
- Backend creates a
DeckBuildTaskand enqueues Celery work. - Main deck agent can read relevant shared memories and write new memories.
- Frontend polls build status until completion/failure.
- On completion, frontend loads the full deck view.
frontend/app/decks/generate/page.tsx- prompt input
- set-code selection
- quota display
- task polling and redirect to deck page
frontend/app/dashboard/page.tsx- list deck summaries
- poll active generations and refresh status
frontend/app/decks/[deckId]/page.tsx- full deck details after generation
- regenerate flow entrypoint (reuses generate page)
frontend/lib/backend-auth.tsbackendFetchwrapper for authenticated BE requests- refresh/retry behavior for 401s
app/appai/routes/build_deck.py- API endpoints for start build, check status, status metadata, and remaining quota
app/appai/tasks/construct_deck.py- Celery task wrapper and status transitions (
IN_PROGRESS→COMPLETED/FAILED)
- Celery task wrapper and status transitions (
app/appai/models/deck_build.pyDeckBuildTask+DeckBuildStatus
app/appai/services/agents/tools/memory_tools.pysubagent_memory_searchfor retrievalwrite_memoryfor persistence
app/appai/models/memory.pyMemoryandMemoryMaintenanceReport
app/appcards/routes/deck.py- deck list/detail/full-detail/update/delete endpoints consumed before/after build
app/appcards/models/deck.py- deck and deck-card persistence model
The deck build runs as a pydantic_graph workflow in appai/services/graphs/deck_construction.py.
Top-level node order:
BuildDeckValidateDeckClassifyCardsSetSwaps
- Updates build task status to
BUILDING_DECK. - Runs the main deck-constructor agent (
run_deck_constructor_agent). - Increments
build_countin graph state. - Transitions to
ValidateDeck.
- Checks
Deck.validin DB. - If invalid, loops back to
BuildDeck(up toMAX_BUILD_ATTEMPTS, currently 3). - If valid, transitions to
ClassifyCards.
- Updates build task status to
CLASSIFYING_DECK_CARDS. - Runs card-classifier agent to assign each deck card:
roleimportance
- Transitions to
SetSwaps.
- Updates build task status to
FINDING_REPLACEMENT_CARDS. - Selects non-critical/non-high-synergy cards for replacement pass.
- Builds search filter from:
- allowed set codes
- deck colors
- Runs replacement concurrently (semaphore-limited).
Each candidate card replacement uses appai/services/graphs/replace_card.py:
SearchForReplacements- vector search in Qdrant using current card summary
FilterReplacements- LLM-based replacement selection (
run_card_replacement_agent)
- LLM-based replacement selection (
AddReplacements- stores selected replacement options on
DeckCard.replacement_cards
- stores selected replacement options on
The main builder is run_deck_constructor_agent in appai/services/agents/deck_constructor.py.
Runtime characteristics:
- Uses model
TOOL_MODEL_THINKING. - Runs with
DeckBuildingDepscontext:deck_iddeck_descriptionavailable_set_codesbuild_task_id
- Receives a composed prompt containing:
- generation request
- current deck state (name/summary/tags if present)
- generation history
- Has usage limits from app settings:
- max tool/request calls per task
- max input/output tokens
- Writes final structured output back to deck fields:
namellm_summaryshort_llm_summarytags- appends prompt to
generation_history
Tools available to the main deck-constructor agent:
subagent_memory_searchwrite_memorylist_deck_cardsadd_card_to_deckremove_card_from_decksearch_for_cardsinspect_cardvalidate_deckclear_deck
Tool behavior notes:
subagent_memory_searchretrieves a memory-grounded summary and related card UUIDs for the current task context.write_memorypersists structured memory to Postgres and Qdrant; up to 10 related card UUIDs are allowed.search_for_cardsapplies set-code constraints and can auto-build advanced filters.inspect_cardreturns detailed card info (with short-lived cache).validate_deckenforces basic deck legality checks (card count and copy-count constraints).- add/remove/clear tools mutate persisted
DeckCardrows directly.
The system prompt in run_deck_constructor_agent explicitly nudges memory usage:
- Early in the run: check prior memories via
subagent_memory_search. - During/near completion: write durable insights with
write_memorywhen useful.
Runtime controls:
DeckBuildingDeps.memory_searcheslimits memory search calls per build process.- Search tool limit is currently
MAX_MEMORY_SEARCHES = 10. - If the agent attempts to finish without checking/writing memories, output validation prompts one extra reflection pass before accepting final output.
All endpoints are under /api/app.
POST /ai/deck/- starts a build
- returns
task_id,deck_id, and status URL
GET /ai/deck/build_status/{task_id}/- returns current task status for polling
GET /ai/deck/statuses/- returns all statuses + pollable statuses
GET /ai/deck/remaining_quota/- returns current daily quota remaining
GET /cards/deck/- dashboard deck summaries (with generation status)
GET /cards/deck/{deck_id}/full/- final generated deck content (cards + summaries)
Frontend sends:
- prompt text
- selected set codes
- optional
deck_idwhen regenerating an existing deck
Backend route (build_deck) checks:
- user auth + deck ownership
- no currently active build for same deck
- daily quota available
- request relevance guardrail
If no deck_id is provided, backend creates a new deck shell first.
Backend creates a DeckBuildTask row (initial PENDING) and enqueues Celery task construct_deck with that task ID.
Task wrapper sets:
IN_PROGRESSat startCOMPLETEDon successFAILEDon exceptions
Frontend polls every ~2.5 seconds via GET /ai/deck/build_status/{task_id}/.
- On
COMPLETED: navigate to/decks/{deck_id}and fetch full deck. - On
FAILED: surface error and stop polling. - On timeout or polling failure: stop polling and show recovery message.
Dashboard and deck pages consume deck APIs to show:
- latest generation status
- generated card list
- AI summaries/tags and deck metadata
- replacement options for non-critical cards (when available)
Build statuses (from DeckBuildStatus) include:
PENDINGIN_PROGRESSBUILDING_DECKCLASSIFYING_DECK_CARDSFINDING_REPLACEMENT_CARDSCOMPLETEDFAILED
Clients should poll only statuses returned by GET /ai/deck/statuses/ as pollable.
- Daily build quota is enforced before enqueue; quota is then withdrawn.
- Relevance guardrail blocks non-MTG prompts.
- Ownership checks prevent reading/updating/deleting other users’ decks.
- In-progress lock prevents regenerating/deleting/editing while a build is active.
- Memory tool constraints include per-task memory-search limits and strict related-card UUID validation.
If deck generation appears stuck:
- Check
/ai/deck/build_status/{task_id}/response status progression. - Confirm Celery worker for
llmqueue is running. - Verify quota endpoint still returns expected remaining count.
- Inspect backend logs around
construct_decktask ID. - Confirm frontend polling loop was not cancelled by navigation or timeout.
If generation starts but results look poor:
- Validate card data ingestion sequence has been run recently:
1_add_cards2_generate_card_summaries3_embed_cards
- Confirm Qdrant collection exists and contains vectors.
If memory-backed behavior seems weak or inconsistent:
- Confirm
memoriescollection exists in Qdrant and has points. - Confirm memory rows exist in Postgres (
appai_memory). - Check logs for memory tool retries/validation failures (invalid related card UUIDs, search-limit warnings).
- High-level architecture guide:
docs/developer-architecture-guide.md - Memory system guide:
docs/memory-system.md - Project setup and runtime commands:
README.md