Skip to content

Commit 7780ec9

Browse files
authored
Merge pull request #25 from GilesStrong/feat/live_deck_feedback
Feat/live deck feedback
2 parents 35a4606 + d43a445 commit 7780ec9

24 files changed

Lines changed: 1138 additions & 103 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 6.0.3 on 2026-03-09 12:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
('appai', '0004_alter_deckbuildtask_deck'),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name='deckbuildtask',
14+
name='deck_size',
15+
field=models.IntegerField(blank=True, null=True),
16+
),
17+
migrations.AddField(
18+
model_name='deckbuildtask',
19+
name='n_replacements',
20+
field=models.IntegerField(blank=True, default=0, null=True),
21+
),
22+
migrations.AddField(
23+
model_name='deckbuildtask',
24+
name='n_searches',
25+
field=models.IntegerField(blank=True, default=0, null=True),
26+
),
27+
migrations.AddField(
28+
model_name='deckbuildtask',
29+
name='n_total_replacements',
30+
field=models.IntegerField(blank=True, null=True),
31+
),
32+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 6.0.3 on 2026-03-09 12:59
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
('appai', '0005_deckbuildtask_deck_size_deckbuildtask_n_replacements_and_more'),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name='deckbuildtask',
14+
name='prompt',
15+
field=models.TextField(blank=True, null=True),
16+
),
17+
]

app/appai/models/deck_build.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class DeckBuildTask(models.Model):
3636
result = models.JSONField(null=True, blank=True)
3737
created_at = models.DateTimeField(auto_now_add=True)
3838
updated_at = models.DateTimeField(auto_now=True)
39+
prompt = models.TextField(null=True, blank=True)
40+
deck_size = models.IntegerField(
41+
null=True,
42+
blank=True,
43+
)
44+
n_searches = models.IntegerField(default=0, blank=True, null=True)
45+
n_replacements = models.IntegerField(default=0, blank=True, null=True)
46+
n_total_replacements = models.IntegerField(null=True, blank=True)
3947

4048
if TYPE_CHECKING:
4149
deck_id: UUID

app/appai/modules/construct_deck.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
from uuid import UUID
1717

1818
import logfire
19-
from appcards.models.deck import Deck
19+
from appcards.models.deck import Deck, DeckCard
2020
from appcore.modules.beartype import beartype
21+
from django.db.models import Sum
2122
from pydantic import BaseModel, Field
2223

24+
from appai.models.deck_build import DeckBuildTask
2325
from appai.services.graphs.deck_construction import construct_deck as construct_deck_graph
2426

2527

@@ -77,6 +79,12 @@ async def construct_deck(
7779
deck = await Deck.objects.acreate(name="New Deck", user_id=user_id)
7880
logfire.info(f"Constructing new deck, with ID: {deck.id}")
7981

82+
total_cards = await DeckCard.objects.filter(deck=deck).aaggregate(Sum('quantity'))
83+
total_cards = total_cards['quantity__sum'] or 0
84+
await DeckBuildTask.objects.filter(id=build_task_id).aupdate(
85+
deck_size=total_cards, n_searches=0, n_replacements=None, n_total_replacements=None
86+
)
87+
8088
# Run deck build
8189
generation_history = deck.generation_history if deck.generation_history else []
8290
if len(generation_history) > 5:

app/appai/routes/build_deck.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def build_deck(request: HttpRequest, payload: BuildDeckPostIn) -> BuildDeckPostO
146146
deck_id = payload.deck_id
147147

148148
# Enqueue the task to build the deck
149-
build = DeckBuildTask.objects.create(deck_id=deck_id, status=DeckBuildStatus.PENDING)
149+
build = DeckBuildTask.objects.create(deck_id=deck_id, status=DeckBuildStatus.PENDING, prompt=payload.prompt)
150150

151151
task: AsyncResult = cast(Any, construct_deck.apply_async)(
152152
kwargs={
@@ -181,6 +181,13 @@ def check_deck_build_status(request: HttpRequest, path_params: Path[BuildDeckSta
181181
Check the status of a deck building task using the task ID.
182182
183183
You can use the task ID received when you initiated the deck building process to check if the task is still processing, has completed successfully, or has failed. The response will include the current status of the task and the associated deck ID.
184+
185+
Args:
186+
request (HttpRequest): The incoming HTTP request object, used to identify the user making the request.
187+
path_params (Path[BuildDeckStatusIn]): The path parameters containing the task ID for which to check the status.
188+
189+
Returns:
190+
BuildDeckStatusOut: An object containing the current status of the deck building task
184191
"""
185192

186193
try:
@@ -192,4 +199,12 @@ def check_deck_build_status(request: HttpRequest, path_params: Path[BuildDeckSta
192199
except DeckBuildTask.DoesNotExist:
193200
raise HttpError(404, 'Deck build task not found')
194201

195-
return BuildDeckStatusOut(status=build.status, deck_id=deck_id)
202+
return BuildDeckStatusOut(
203+
status=build.status,
204+
deck_id=deck_id,
205+
prompt=build.prompt,
206+
n_cards_so_far=build.deck_size,
207+
n_searches_so_far=build.n_searches,
208+
n_replacements_so_far=build.n_replacements,
209+
n_replacements_total=build.n_total_replacements,
210+
)

app/appai/serializers/build_deck.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ class BuildDeckStatusIn(Schema):
9393
class BuildDeckStatusOut(Schema):
9494
status: str
9595
deck_id: UUID
96+
prompt: str | None
97+
n_cards_so_far: int | None
98+
n_searches_so_far: int | None
99+
n_replacements_so_far: int | None
100+
n_replacements_total: int | None
96101

97102
@field_validator('deck_id', mode='after')
98103
@classmethod

app/appai/services/agents/deck_constructor.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,19 @@ def validate_tags(cls, value: list[str]) -> list[str]:
166166
@beartype
167167
@retry(stop=stop_after_attempt(APP_SETTINGS.DECK_BUILD_RETRY_LIMIT), wait=wait_exponential(multiplier=1, min=2, max=10))
168168
async def run_deck_constructor_agent(
169-
deck_id: UUID, deck_description: str, generation_history: list[str], available_set_codes: Optional[set[str]] = None
169+
deck_id: UUID,
170+
build_task_id: UUID,
171+
deck_description: str,
172+
generation_history: list[str],
173+
available_set_codes: Optional[set[str]] = None,
170174
) -> DeckConstructionOutput:
171175
"""
172176
Constructs a deck based on a natural language description.
173177
This function uses an agent to interpret the description and perform the necessary operations to build the deck.
174178
175179
Args:
176180
deck_id (UUID): The ID of the deck to construct.
181+
build_task_id (UUID): The ID of the deck build task associated with this deck construction, used for tracking and updating the status of the build task.
177182
deck_description (str): A natural language description of the desired deck, including its strategy, key cards, and any specific requirements or constraints.
178183
generation_history (list[str]): A list of previous generation requests for the deck, used to inform the construction process.
179184
available_set_codes (Optional[set[str]]): An optional set of available set codes to restrict the card selection to specific sets. If not provided, it will default to the current standard set codes.
@@ -201,7 +206,7 @@ async def run_deck_constructor_agent(
201206
deck_id=deck_id,
202207
deck_description=deck_description,
203208
available_set_codes=available_set_codes if available_set_codes is not None else CURRENT_STANDARD_SET_CODES,
204-
build_task_id=None,
209+
build_task_id=build_task_id,
205210
)
206211

207212
deck = await Deck.objects.aget(id=deck_id)

app/appai/services/agents/deps.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Optional
1615
from uuid import UUID
1716

1817
from appcards.constants.cards import CURRENT_STANDARD_SET_CODES
@@ -26,6 +25,4 @@ class DeckBuildingDeps(BaseModel):
2625
description="The set codes that the deck is allowed to include cards from",
2726
)
2827
deck_description: str = Field(..., description="A natural language description of the desired deck")
29-
build_task_id: Optional[UUID] = Field(
30-
..., description="The ID of the deck build task associated with this deck construction"
31-
)
28+
build_task_id: UUID = Field(..., description="The ID of the deck build task associated with this deck construction")

app/appai/services/agents/tools/deck_tools.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from django.db.models import Sum
2222
from pydantic_ai import RunContext
2323

24+
from appai.models.deck_build import DeckBuildTask
2425
from appai.services.agents.deps import DeckBuildingDeps
2526

2627

@@ -62,6 +63,7 @@ async def add_card_to_deck(ctx: RunContext[DeckBuildingDeps], card_id: UUID, num
6263
total_cards = await DeckCard.objects.filter(deck=deck).aaggregate(Sum('quantity'))
6364
total_cards = total_cards['quantity__sum'] or 0
6465
message = f"Added {number_to_add}x '{card.name}' to deck '{deck.name}'. Deck now has {total_cards} total cards ({deck_card.quantity}x {card.name})."
66+
await DeckBuildTask.objects.filter(id=ctx.deps.build_task_id).aupdate(deck_size=total_cards)
6567
return message
6668

6769

@@ -108,13 +110,15 @@ async def remove_card_from_deck(ctx: RunContext[DeckBuildingDeps], card_id: UUID
108110
message = (
109111
f"Removed all copies of '{card.name}' from deck '{deck.name}'. Deck now has {total_cards} total cards."
110112
)
111-
return message
112113
else:
113114
await deck_card.asave()
114115
total_cards = await DeckCard.objects.filter(deck=deck).aaggregate(Sum('quantity'))
115116
total_cards = total_cards['quantity__sum'] or 0
116117
message = f"Removed {number_to_remove}x '{card.name}' from deck '{deck.name}'. Deck now has {total_cards} total cards ({deck_card.quantity}x {card.name} remaining)."
117-
return message
118+
119+
await DeckBuildTask.objects.filter(id=ctx.deps.build_task_id).aupdate(deck_size=total_cards)
120+
121+
return message
118122

119123

120124
@beartype
@@ -133,6 +137,8 @@ async def clear_deck(ctx: RunContext[DeckBuildingDeps]) -> str:
133137

134138
await DeckCard.objects.filter(deck=deck).adelete()
135139
message = f"All cards removed from deck '{deck.name}'."
140+
141+
await DeckBuildTask.objects.filter(id=ctx.deps.build_task_id).aupdate(deck_size=0)
136142
return message
137143

138144

app/appai/services/agents/tools/query_tools.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
from appsearch.services.qdrant.search import run_query_from_dsl
2424
from appsearch.services.qdrant.search_dsl import Filter, MatchAnyCondition, Query
2525
from asgiref.sync import sync_to_async
26+
from django.db.models import F
2627
from pydantic import BaseModel, Field
2728
from pydantic_ai import RunContext
2829

30+
from appai.models.deck_build import DeckBuildTask
2931
from appai.services.agents.deps import DeckBuildingDeps
3032
from appai.services.agents.filter_constructor import filter_constructor
3133

@@ -132,6 +134,8 @@ async def search_for_cards(
132134
card_infos.append(await sync_to_async(card_to_info)(card))
133135
except Card.DoesNotExist:
134136
continue
137+
138+
await DeckBuildTask.objects.filter(id=ctx.deps.build_task_id).aupdate(n_searches=F("n_searches") + 1)
135139
return CardSearchResults(cards=card_infos, max_results=max_results)
136140

137141

0 commit comments

Comments
 (0)