Skip to content

WrzDJSet two-pass builder and agent#424

Merged
thewrz merged 2 commits into
mainfrom
feat/issue-390
Jun 14, 2026
Merged

WrzDJSet two-pass builder and agent#424
thewrz merged 2 commits into
mainfrom
feat/issue-390

Conversation

@thewrz

@thewrz thewrz commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Why

WrzDJSet needs the core v1 two-pass flow: deterministic pool + curve ordering first, then an LLM-assisted critique/chat editor for vibe, culture, and narrative adjustments.

What

  • Added deterministic pass 1 service with duration-based slot generation, weighted greedy scoring, saved-pairing boosts, locked-slot preservation, and capped swap refinement.
  • Added pass 2 gateway-only critique/chat agent with rationale-required mutation tools, read-only analysis tools, curve adjustment tools, and transition score recomputation after mutations.
  • Added build/critique/chat API endpoints, enriched slot responses, generated OpenAPI/TS types, and frontend API helpers.
  • Added setbuilder recompute confirmation and agent sidebar with collapsed rail, Peer/Pro toggle, critique flags, tool-call cards with args/rationale, suggestions, composer, and live score updates.

Testing

  • cd server && /home/adam/github/WrzDJ/server/.venv/bin/ruff check .
  • cd server && /home/adam/github/WrzDJ/server/.venv/bin/ruff format --check .
  • cd server && /home/adam/github/WrzDJ/server/.venv/bin/bandit -r app -c pyproject.toml -q
  • cd server && /home/adam/github/WrzDJ/server/.venv/bin/pytest --tb=short -q (2811 passed, coverage 87.89%)
  • cd dashboard && npx tsc --noEmit
  • cd dashboard && npm run lint
  • cd dashboard && npm test -- --run (83 files, 1143 tests)

Design decisions

  • Pass 1 uses stable DB ordering and no random tie-breakers so fixed inputs produce deterministic sets.
  • Camelot scoring is weighted by key_strictness and defaults to a low-impact contribution for open-format sets, matching the executive summary guidance.
  • Pass 2 sends all LLM work through services/llm/gateway.py; the current gateway exposes connector model hints rather than fast/strong tiers, so the code documents the conceptual fast/strong split without overriding connector-configured models.
  • Mutating agent tools must include rationale; API responses include both tool args and rationale so the chat log can provide an audit trail.
  • Recompute requires explicit confirmation because unlocked manual order may be overwritten; locked slots stay fixed and saved pairings are scored.

Closes #390

Co-Authored-By: Claude noreply@anthropic.com

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an interactive in-app agent chat sidebar for the set builder, including critique/grade display and tool-call visualization.
    • Added a deterministic Recompute flow with confirmation, progress/toast feedback, and automatic chat/slot refresh.
    • Set slots now surface richer track metadata and transition scoring/warnings.
  • Bug Fixes

    • Improved null-safe slot view defaults when extra track fields are missing.
  • Tests

    • Expanded unit and component tests for the chat, slot parsing, and set-builder API two-pass behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a two-pass set-building system to WrzDJSet. Pass 1 (pass1_deterministic.py) generates a deterministic ordered timeline via greedy scoring and swap refinement. Pass 2 (pass2_agent.py) provides LLM-driven critique and a chat agent with structured mutation tools. Three new POST endpoints are wired (/build, /critique, /agent/chat), dashboard ApiClient methods and types are extended, a ChatSidebar component and Recompute confirmation flow are integrated into the builder page, and a refreshToken pattern propagates slot reloads.

Changes

Two-pass Set Builder

Layer / File(s) Summary
Data contracts: Pydantic schemas, OpenAPI spec, generated TS types
server/app/schemas/setbuilder.py, server/openapi.json, dashboard/lib/api-types.generated.ts, dashboard/lib/api-types.ts
SlotOut gains transition, pool-track, and track-metadata fields. New schemas added: BuildSetRequest/Response, TransitionScoreOut, SetCritiqueOut, CritiqueFlagOut, AgentChatIn/Out, AppliedToolCallOut. OpenAPI spec and generated TS types mirror all additions; hand-crafted TS re-exports expose the shapes to the dashboard.
Pass 1: deterministic timeline builder
server/app/services/setbuilder/pass1_deterministic.py, server/tests/test_setbuilder_pass1.py
New module with TrackMeta, TransitionScore, BuildResult dataclasses. build_set greedy-fills slots using weighted scoring (energy, BPM, Camelot, role, mood, artist diversity, pairing boost), runs bounded swap refinement, persists results, and recomputes transition scores. Tests verify determinism, locked-slot preservation, and adjacent-pair boost.
Pass 2: LLM critique + agent chat toolkit
server/app/services/setbuilder/pass2_agent.py, server/tests/test_setbuilder_pass2.py
New module implementing critique_set (structured critique tool dispatch), chat_with_agent (LLM turn with full tool surface), apply_tool_call (mutation router with rationale enforcement), all mutation/analysis handlers, and set-context serialization. Tests cover critique parsing, rationale validation, swap application, and locked-slot protection.
Server API endpoints: build, critique, agent/chat
server/app/api/setbuilder.py, server/tests/test_setbuilder_pass_api.py
_slots_out enriches SlotOut with joined SetPoolTrack metadata; list_set_slots updated to use it. Three new POST endpoints added with ownership checks, confirmed gating, and 400 error mapping for NoLlmConfigured/AgentToolError. API tests verify confirmation requirement, timeline creation, owner isolation, critique gateway usage, and swap tool propagation.
Dashboard API client methods and type re-exports
dashboard/lib/api.ts, dashboard/lib/__tests__/api.test.ts
ApiClient gains buildSet, critiqueSet, and chatWithSetAgent methods. Type imports and export type re-exports are expanded to include all new agent/build/critique types. Client tests validate request shapes, endpoint URLs, and response parsing.
ChatSidebar component, CSS, types, and tests
dashboard/app/(dj)/setbuilder/components/ChatSidebar.tsx, dashboard/app/(dj)/setbuilder/setbuilder.module.css, dashboard/app/(dj)/setbuilder/components/types.ts, dashboard/app/(dj)/setbuilder/components/__tests__/ChatSidebar.test.tsx, dashboard/app/(dj)/setbuilder/__tests__/*
New ChatSidebar client component with ToolCard, CritiqueCard, persona toggle, auto-scroll, critiqueSet on load, chatWithSetAgent on send, and onMutationApplied callback. slotViewFromApi updated to pull track metadata from extended SlotOut fields. 455+ line CSS module addition covers collapsed/open chat layout, critique cards, flag chips, message bubbles, tool-call cards, composer, confirmation modal, and toast. Component tests verify critique rendering and mutating tool-call UI. Test fixtures expanded with full slot metadata.
Builder page Recompute flow and BuilderWorkspace refreshToken
dashboard/app/(dj)/setbuilder/[setId]/page.tsx, dashboard/app/(dj)/setbuilder/components/BuilderWorkspace.tsx
Builder page adds chatOpen, refreshToken, confirmBuild, building, and toast state. runBuild calls api.buildSet behind a confirmation modal and sets a 3-second toast. ChatSidebar is wired with onMutationApplied that increments refreshToken. BuilderWorkspace accepts an optional refreshToken prop and re-fetches slots when it changes.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Page as SetBuilder Page
  participant API_Client as ApiClient
  participant Server as FastAPI /setbuilder
  participant P1 as pass1_deterministic
  participant P2 as pass2_agent
  participant LLM as Gateway

  rect rgba(59, 130, 246, 0.5)
    Note over User,P1: Recompute (Pass 1)
    User->>Page: click Recompute, confirm
    Page->>API_Client: buildSet(setId, confirmed=true)
    API_Client->>Server: POST /sets/{set_id}/build
    Server->>P1: build_set(db, set_obj)
    P1->>P1: greedy fill + swap refinement
    P1-->>Server: BuildResult
    Server-->>API_Client: BuildSetResponse
    API_Client-->>Page: slot_count, iterations
    Page->>Page: setRefreshToken(n+1)
  end

  rect rgba(16, 185, 129, 0.5)
    Note over User,LLM: Agent Chat (Pass 2)
    User->>Page: open ChatSidebar
    Page->>API_Client: critiqueSet(setId)
    API_Client->>Server: POST /sets/{set_id}/critique
    Server->>P2: critique_set(db, actor, set_obj)
    P2->>LLM: dispatch(critique_tool)
    LLM-->>P2: structured payload
    P2-->>Server: SetCritique
    Server-->>API_Client: SetCritiqueOut
    API_Client-->>Page: renders CritiqueCard

    User->>Page: sends chat message
    Page->>API_Client: chatWithSetAgent(setId, payload)
    API_Client->>Server: POST /sets/{set_id}/agent/chat
    Server->>P2: chat_with_agent(db, actor, set_obj, message)
    P2->>LLM: dispatch(message, agent_tools)
    LLM-->>P2: tool_calls
    P2->>P2: apply_tool_call mutations
    P2->>P1: recompute_transition_scores
    P1-->>P2: updated scores
    P2-->>Server: AgentChatResult
    Server-->>API_Client: AgentChatOut
    API_Client-->>Page: renders tools + scores
    Page->>Page: setRefreshToken(n+1)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hop hop, the tracks now align,
A greedy fill with scores so fine!
The agent chats, the LLM thinks,
Swaps slots and fixes all the kinks.
With rationale cards and critique grade,
The perfect DJ set is made! 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'WrzDJSet two-pass builder and agent' directly addresses the PR's primary objective of implementing a two-pass algorithm (deterministic builder + agent) for WrzDJSet, matching the linked issue scope.
Linked Issues check ✅ Passed The PR implements all required Pass 1 features (slot generation, greedy scoring, 2-opt refinement, locked slots, pairings boost) and Pass 2 features (critique, chat agent, mutation tools with rationale, live score updates) as specified in issue #390.
Out of Scope Changes check ✅ Passed All changes directly support the two-pass builder implementation: deterministic algorithm, agent toolkit, API endpoints, TypeScript types, UI components, tests, and OpenAPI specs. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-390

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
server/app/services/setbuilder/pass2_agent.py (1)

171-181: 💤 Low value

Minor: redundant _ordered_slots query.

slots is fetched at line 171, then fetched again at line 179. Since no mutations occur between these lines, the second query is redundant.

Suggested fix
     slots = _ordered_slots(db, set_obj.id)
     transition_scores: list[TransitionScore] = []
     if affected:
         affected_with_neighbors = _with_neighbors(affected)
         transition_scores = recompute_transition_scores(db, set_obj, slots, affected_with_neighbors)
     return AgentChatResult(
         message=response.text,
         tool_calls=applied,
-        slots=_ordered_slots(db, set_obj.id),
+        slots=slots,
         affected_transition_scores=transition_scores,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/services/setbuilder/pass2_agent.py` around lines 171 - 181, The
function calls _ordered_slots(db, set_obj.id) twice unnecessarily: once at the
beginning of the function to assign to the slots variable, and again when
constructing the AgentChatResult object. Since no mutations occur between these
two calls, replace the second _ordered_slots(db, set_obj.id) call in the
AgentChatResult constructor with the slots variable that was already fetched,
eliminating the redundant database query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dashboard/app/`(dj)/setbuilder/[setId]/page.tsx:
- Line 172: The toast notification div at line 172 lacks ARIA attributes
required for screen reader accessibility. Add `role="status"` and
`aria-live="polite"` attributes to the div element that renders the toast (the
one with className styles.builderToast) to ensure screen reader users are
properly notified when the toast notification appears or updates.
- Around line 140-170: The confirmBackdrop element has an onClick handler that
always closes the modal by calling setConfirmBuild(false), but during a build
operation (when building is true) the Cancel button is disabled to prevent
dismissal. To fix the UX inconsistency, make the backdrop onClick handler
conditional so it only calls setConfirmBuild(false) when building is false,
preventing users from dismissing the dialog while the build operation is in
progress. This aligns the backdrop behavior with the disabled state of the
Cancel button.

In `@dashboard/app/`(dj)/setbuilder/components/ChatSidebar.tsx:
- Around line 133-138: The `history` variable in the useMemo hook does not cap
the number of entries before sending to the server, but the
`AgentChatIn.history` schema is limited to 30 items maximum. This causes a 422
error when posting to `/agent/chat` if the chat history exceeds 30 items. Fix
this by limiting the filtered entries to the first 30 items within the useMemo
hook, ensuring the mapped history array never exceeds the server's constraint
before it is sent in the request.

In `@server/app/services/setbuilder/pass1_deterministic.py`:
- Around line 114-116: The dictionary comprehension for tracks_by_id calls
_track_meta(t) twice per iteration—once to extract the slot_track_id key and
once for the value—resulting in unnecessary object allocation. Restructure the
dictionary comprehension to call _track_meta(t) only once per track by storing
its result in a variable, then use that single result to both extract the key
and populate the value in the dictionary.

In `@server/openapi.json`:
- Around line 1545-1555: The BuildSetRequest schema makes the confirmed field
optional with a default of false, which allows clients to submit requests
without explicit confirmation. Change the confirmed field to be required (remove
the default) and constrain it to only accept true by adding const: true to its
definition in BuildSetRequest. Additionally, add documentation of the 400
response on the /build endpoint to model the rejection path when confirmation is
not properly provided, ensuring the generated API contract fully enforces the
explicit confirmation requirement.

---

Nitpick comments:
In `@server/app/services/setbuilder/pass2_agent.py`:
- Around line 171-181: The function calls _ordered_slots(db, set_obj.id) twice
unnecessarily: once at the beginning of the function to assign to the slots
variable, and again when constructing the AgentChatResult object. Since no
mutations occur between these two calls, replace the second _ordered_slots(db,
set_obj.id) call in the AgentChatResult constructor with the slots variable that
was already fetched, eliminating the redundant database query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 84cdfd53-bd17-4883-94ff-5e140462f07b

📥 Commits

Reviewing files that changed from the base of the PR and between ae6dad6 and ff6ee1b.

📒 Files selected for processing (20)
  • dashboard/app/(dj)/setbuilder/[setId]/page.tsx
  • dashboard/app/(dj)/setbuilder/__tests__/BuilderWorkspace.test.tsx
  • dashboard/app/(dj)/setbuilder/__tests__/curveMath.test.ts
  • dashboard/app/(dj)/setbuilder/components/BuilderWorkspace.tsx
  • dashboard/app/(dj)/setbuilder/components/ChatSidebar.tsx
  • dashboard/app/(dj)/setbuilder/components/__tests__/ChatSidebar.test.tsx
  • dashboard/app/(dj)/setbuilder/components/types.ts
  • dashboard/app/(dj)/setbuilder/setbuilder.module.css
  • dashboard/lib/__tests__/api.test.ts
  • dashboard/lib/api-types.generated.ts
  • dashboard/lib/api-types.ts
  • dashboard/lib/api.ts
  • server/app/api/setbuilder.py
  • server/app/schemas/setbuilder.py
  • server/app/services/setbuilder/pass1_deterministic.py
  • server/app/services/setbuilder/pass2_agent.py
  • server/openapi.json
  • server/tests/test_setbuilder_pass1.py
  • server/tests/test_setbuilder_pass2.py
  • server/tests/test_setbuilder_pass_api.py

Comment thread dashboard/app/(dj)/setbuilder/[setId]/page.tsx
Comment thread dashboard/app/(dj)/setbuilder/[setId]/page.tsx Outdated
Comment thread dashboard/app/(dj)/setbuilder/components/ChatSidebar.tsx
Comment thread server/app/services/setbuilder/pass1_deterministic.py Outdated
Comment thread server/openapi.json
Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/app/services/setbuilder/pass2_agent.py (1)

112-116: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

JSON fallback lacks error handling.

If the LLM doesn't return tool calls (despite force_tool) and response.text is not valid JSON, json.loads raises an unhandled JSONDecodeError. Since _critique_from_payload already defaults missing fields gracefully, wrap the parsing in try/except to provide a sensible fallback critique instead of a 500 error.

Proposed fix
     payload: dict[str, Any] = {}
     if response.tool_calls:
         payload = response.tool_calls[0].input
     elif response.text:
-        payload = json.loads(response.text)
+        try:
+            payload = json.loads(response.text)
+        except json.JSONDecodeError:
+            payload = {}  # _critique_from_payload handles missing fields
     return _critique_from_payload(payload)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/services/setbuilder/pass2_agent.py` around lines 112 - 116, The
json.loads call in the elif branch (when response.tool_calls is empty and
response.text exists) lacks error handling for invalid JSON. Wrap the
json.loads(response.text) statement in a try/except block to catch
JSONDecodeError, and provide a sensible fallback payload (such as an empty dict)
that can be safely passed to _critique_from_payload, allowing the function to
return a gracefully degraded critique instead of raising an unhandled exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/app/services/setbuilder/pass2_agent.py`:
- Around line 112-116: The json.loads call in the elif branch (when
response.tool_calls is empty and response.text exists) lacks error handling for
invalid JSON. Wrap the json.loads(response.text) statement in a try/except block
to catch JSONDecodeError, and provide a sensible fallback payload (such as an
empty dict) that can be safely passed to _critique_from_payload, allowing the
function to return a gracefully degraded critique instead of raising an
unhandled exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c3a1345-9944-4b0a-b86d-ab78b46cdce3

📥 Commits

Reviewing files that changed from the base of the PR and between ff6ee1b and 19267af.

📒 Files selected for processing (8)
  • dashboard/app/(dj)/setbuilder/[setId]/page.tsx
  • dashboard/app/(dj)/setbuilder/components/ChatSidebar.tsx
  • dashboard/lib/api-types.generated.ts
  • server/app/api/setbuilder.py
  • server/app/schemas/setbuilder.py
  • server/app/services/setbuilder/pass1_deterministic.py
  • server/app/services/setbuilder/pass2_agent.py
  • server/openapi.json
✅ Files skipped from review due to trivial changes (1)
  • dashboard/lib/api-types.generated.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • dashboard/app/(dj)/setbuilder/[setId]/page.tsx
  • server/app/schemas/setbuilder.py
  • dashboard/app/(dj)/setbuilder/components/ChatSidebar.tsx
  • server/app/api/setbuilder.py
  • server/app/services/setbuilder/pass1_deterministic.py
  • server/openapi.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WrzDJSet: Two-pass algorithm — deterministic pass + agent toolkit

1 participant