docs: fix PSA ticket management spec — API contract, actions format, file routing

- Explicitly call out search_tickets breaking change and all existing callers
- Fix [ACTIONS] marker to use JSON array format matching existing parser
- Route system prompt change to assistant_chat_service.py, not flowpilot_engine
- Pivot detail panel hydration to existing getTicketContext + listResources

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 01:44:34 +00:00
parent 52b369680b
commit 2b3d52ad77

View File

@@ -41,7 +41,16 @@ All added to `backend/app/api/endpoints/integrations.py`, backed by `backend/app
| `DELETE` | `/integrations/psa/tickets/{id}/resources/{member_id}` | Remove a resource |
| `POST` | `/integrations/psa/tickets/ai-parse` | Natural language → structured pre-fill payload |
Existing endpoints (`search_tickets`, `get_ticket`, `get_ticket_statuses`, `list_members`, `list_boards`) are unchanged.
**Breaking change — `search_tickets` response shape updated to `TicketListResponse`.**
The existing `/integrations/psa/tickets/search` endpoint currently returns `list[PSATicketSearchResult]`. This spec changes it to return `TicketListResponse` (adds `total`, `page`, `page_size` wrapper).
Current callers that must be migrated:
- `integrationsApi.searchTickets()` in `frontend/src/api/integrations.ts` (line 18) — update return type
- `integrationsApi.searchTicketsQueue()` in `frontend/src/api/integrations.ts` (line 20) — update return type
- `frontend/src/components/dashboard/TicketQueue.tsx` — update to read `.items` from response
- `frontend/src/components/session/TicketPickerModal.tsx` — update to read `.items` from response
All other existing endpoints (`get_ticket`, `get_ticket_statuses`, `list_members`, `list_boards`) are unchanged.
### ticket_service.py
@@ -251,10 +260,16 @@ Adding or removing a filter is a one-line config change, not a component edit.
### TicketDetailPanel — Optimistic Hydration
The panel uses the **existing** `/integrations/psa/tickets/{id}/context` endpoint (client: `psaContextApi.getTicketContext()` in `frontend/src/api/psaContext.ts`) which already returns company, contact, configurations, notes, and related tickets in one call. This avoids creating redundant endpoints.
1. Panel opens immediately with list row data (id, summary, company, board, status, priority) — no loading state for these fields
2. Two parallel fetches fire: full ticket detail + `list_resources`
3. Detail sections (contact, notes, configs, related) render skeletons until hydrated
4. Resources section renders skeleton until hydrated
2. Two parallel fetches fire on open:
- `psaContextApi.getTicketContext(ticketId)` — hydrates contact, notes, configs, related tickets
- `ticketsApi.listResources(ticketId)` — hydrates assignees (new endpoint)
3. All detail sections (contact, notes, configs, related) render skeletons until `getTicketContext` resolves
4. Resources section renders skeleton until `listResources` resolves
`get_ticket` (the simpler single-ticket endpoint) is **not** used by the panel — `getTicketContext` is a strict superset of the data needed.
### NewTicketModal — State Ownership
@@ -273,23 +288,23 @@ Adding or removing a filter is a one-line config change, not a component edit.
**1. AI-suggested (via `[ACTIONS]` marker)**
When the AI identifies a second distinct issue during a session, it emits:
When the AI identifies a second distinct issue during a session, it emits a JSON array inside the `[ACTIONS]` marker — matching the exact format `_parse_actions_marker()` in `unified_chat_service.py` expects (a list of objects with `label`, `command`, `description`):
```
[ACTIONS]
create_spin_off_ticket: "Printer offline on 2nd floor"
[
{
"label": "Create ticket: Printer offline on 2nd floor",
"command": "create_spin_off_ticket",
"description": "Printer offline on 2nd floor"
}
]
[/ACTIONS]
```
Action payload shape (parsed by `unified_chat_service.py`):
```typescript
{
type: "create_spin_off_ticket",
label: string, // displayed as button text in TaskLane
summary_hint?: string // pre-populates the Quick Create prompt input only
}
```
The existing `_parse_actions_marker()` parser in `unified_chat_service.py` already handles this format — no parser changes needed. The frontend reads `action.command === "create_spin_off_ticket"` to render the "Create Ticket" button in TaskLane, and uses `action.description` as the `summary_hint` pre-populated into the Quick Create prompt input.
`summary_hint` populates the AI prompt input, not the summary field directly. The engineer still runs the AI parse step and reviews all output. This prevents bypassing review with potentially hallucinated values.
`summary_hint` (from `action.description`) populates the AI prompt input only, not the summary field directly. The engineer still runs the AI parse step and reviews all output. This prevents bypassing review with potentially hallucinated values.
**2. Engineer-initiated**
@@ -315,13 +330,17 @@ When no linked ticket exists: `initialValues` is omitted. `company_id` and `boar
### System Prompt Addition
New rule added to the ResolutionAssist system prompt:
New rule added to `ASSISTANT_SYSTEM_PROMPT` in `backend/app/services/assistant_chat_service.py`:
> When you identify a second distinct issue that is separate from the primary topic of this session, suggest creating a spin-off ticket using the `[ACTIONS]` marker with type `create_spin_off_ticket`. Only suggest this when the issue is clearly separate — do not suggest for every tangential mention.
> When you identify a second distinct issue that is clearly separate from the primary topic of this session, suggest creating a spin-off ticket using the `[ACTIONS]` marker. Use `"command": "create_spin_off_ticket"` and put the issue description in `"description"`. Only suggest this when the issue is genuinely separate — do not suggest for every tangential mention.
### Backend
`create_spin_off_ticket` added to the recognized action types list in `flowpilot_engine.py`. No new backend endpoints — the modal reuses `POST /integrations/psa/tickets` and `POST /integrations/psa/tickets/ai-parse`.
- **`assistant_chat_service.py`** — system prompt updated with spin-off ticket instruction (above)
- **`unified_chat_service.py`** — no parser changes needed; the existing `_parse_actions_marker()` already handles the JSON array format. The frontend reads `command === "create_spin_off_ticket"` to route the action
- **`flowpilot_engine.py`** — no changes needed for this feature; guided FlowPilot sessions do not use this action type in the current scope
No new backend endpoints — the modal reuses `POST /integrations/psa/tickets` and `POST /integrations/psa/tickets/ai-parse`.
---
@@ -374,8 +393,9 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the
- `backend/app/api/endpoints/integrations.py` — 6 new endpoints, update search to return `TicketListResponse`
- `backend/app/services/psa/base.py` — 4 new abstract methods
- `backend/app/services/psa/connectwise/provider.py` — implement 4 new methods
- `backend/app/services/flowpilot_engine.py` — add `create_spin_off_ticket` action type
- `backend/app/services/unified_chat_service.py`parse `create_spin_off_ticket` from `[ACTIONS]`
- `backend/app/services/assistant_chat_service.py` — add spin-off ticket rule to `ASSISTANT_SYSTEM_PROMPT`
- ~~`backend/app/services/flowpilot_engine.py`~~no changes (FlowPilot out of scope for this feature)
- ~~`backend/app/services/unified_chat_service.py`~~ — no changes (existing `[ACTIONS]` parser handles the format)
### New Frontend Files
- `frontend/src/pages/TicketsPage.tsx`
@@ -396,8 +416,11 @@ Backend `search_tickets()` adds `orderBy=priority desc,dateEntered desc` to the
### Modified Frontend Files
- `frontend/src/router.tsx``/tickets` route
- `frontend/src/components/layout/AppLayout.tsx` — Tickets nav item
- `frontend/src/pages/AssistantChatPage.tsx` — spin-off action + New Ticket button
- `frontend/src/pages/AssistantChatPage.tsx`handle `create_spin_off_ticket` command in action renderer + add "New Ticket" button to session header
- `frontend/src/pages/QuickStartPage.tsx` — MyQueueWidget
- `frontend/src/api/integrations.ts` — update `searchTickets()` and `searchTicketsQueue()` return types to `TicketListResponse`
- `frontend/src/components/dashboard/TicketQueue.tsx` — read `.items` from paginated response
- `frontend/src/components/session/TicketPickerModal.tsx` — read `.items` from paginated response
---