Skip to content

Commit 547fded

Browse files
committed
feat: Enhance ticket handling by adding incident ID support and improve UI components for better user experience
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 37f74cb commit 547fded

10 files changed

Lines changed: 184 additions & 126 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ logs/
5252
*.db
5353
csv/*.csv
5454
screenshots/
55+
*.png

backend/app.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,19 @@
3131
# Import unified operation system
3232

3333
# Agent Fabric
34-
from agent_workbench import (
35-
AgentDefinitionCreate,
36-
AgentDefinitionUpdate,
37-
AgentRunCreate,
38-
CriteriaType,
39-
RunStatus,
40-
)
41-
34+
from agent_workbench import (AgentDefinitionCreate, AgentDefinitionUpdate,
35+
AgentRunCreate, CriteriaType, RunStatus)
4236
# Agent service for OpenAI LangGraph agents
4337
from agents import AgentRequest, AgentResponse, agent_service
4438
from api_decorators import get_operation, operation
45-
4639
# CSV ticket service
4740
from csv_data import Ticket, get_csv_ticket_service
48-
4941
# FastMCP client for direct ticket MCP calls (no AI)
5042
from fastmcp import Client as MCPClient
5143
from mcp_handler import handle_mcp_request
52-
from operations import (
53-
CSV_TICKET_FIELDS,
54-
op_create_task,
55-
op_delete_task,
56-
op_get_task,
57-
op_get_task_stats,
58-
op_list_tasks,
59-
op_update_task,
60-
task_service,
61-
)
44+
from operations import (CSV_TICKET_FIELDS, op_create_task, op_delete_task,
45+
op_get_task, op_get_task_stats, op_list_tasks,
46+
op_update_task, task_service)
6247
from usecase_demo import UsecaseDemoRunCreate, usecase_demo_run_service
6348
from workbench_integration import _tool_registry, workbench_service
6449

@@ -68,9 +53,9 @@
6853
from pydantic import ValidationError
6954
from quart import Quart, jsonify, request, send_from_directory
7055
from quart_cors import cors
71-
7256
# Import Pydantic models and service
73-
from tasks import Task, TaskCreate, TaskFilter, TaskService, TaskStats, TaskUpdate
57+
from tasks import (Task, TaskCreate, TaskFilter, TaskService, TaskStats,
58+
TaskUpdate)
7459

7560
# ============================================================================
7661
# APPLICATION SETUP
@@ -748,17 +733,21 @@ def get_sort_key(ticket: Ticket):
748733
@app.route("/api/csv-tickets/<ticket_id>", methods=["GET"])
749734
async def get_csv_ticket(ticket_id: str):
750735
"""
751-
Get one CSV ticket by ID.
736+
Get one CSV ticket by INC number (e.g. INC000016349327) or UUID.
752737
753738
Query params:
754739
- fields: optional comma-separated list of fields to include
755740
"""
756-
try:
757-
parsed_id = UUID(ticket_id)
758-
except ValueError:
759-
return jsonify({"error": "Invalid ticket ID"}), 400
741+
# Try INC number first (primary identifier)
742+
if ticket_id.upper().startswith("INC"):
743+
ticket = _csv_ticket_service.get_ticket_by_incident_id(ticket_id)
744+
else:
745+
try:
746+
parsed_id = UUID(ticket_id)
747+
except ValueError:
748+
return jsonify({"error": "Invalid ticket ID. Use an INC number (e.g. INC000016349327) or UUID."}), 400
749+
ticket = _csv_ticket_service.get_ticket(parsed_id)
760750

761-
ticket = _csv_ticket_service.get_ticket(parsed_id)
762751
if ticket is None:
763752
return jsonify({"error": "Ticket not found"}), 404
764753

backend/csv_data.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ class CSVTicketService:
440440

441441
def __init__(self):
442442
self._tickets: dict[UUID, Ticket] = {}
443+
self._tickets_by_incident_id: dict[str, Ticket] = {}
443444
self._loaded_files: set[str] = set()
444445

445446
def load_csv(self, file_path: str | Path) -> int:
@@ -455,13 +456,19 @@ def load_csv(self, file_path: str | Path) -> int:
455456

456457
for ticket in tickets:
457458
self._tickets[ticket.id] = ticket
459+
if ticket.incident_id:
460+
self._tickets_by_incident_id[ticket.incident_id] = ticket
458461

459462
self._loaded_files.add(file_key)
460463
return len(tickets)
461464

462465
def get_ticket(self, ticket_id: UUID) -> Optional[Ticket]:
463-
"""Get ticket by ID."""
466+
"""Get ticket by UUID."""
464467
return self._tickets.get(ticket_id)
468+
469+
def get_ticket_by_incident_id(self, incident_id: str) -> Optional[Ticket]:
470+
"""Get ticket by INC number (e.g. INC000016349327)."""
471+
return self._tickets_by_incident_id.get(incident_id)
465472

466473
def list_tickets(
467474
self,

backend/operations.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,14 @@
99
from typing import Any
1010
from uuid import UUID
1111

12-
from agent_workbench import AgentDefinitionCreate, AgentDefinitionUpdate, AgentRunCreate
12+
from agent_workbench import (AgentDefinitionCreate, AgentDefinitionUpdate,
13+
AgentRunCreate)
1314
from api_decorators import operation
1415
from csv_data import get_csv_ticket_service
15-
from tasks import Task, TaskCreate, TaskFilter, TaskService, TaskStats, TaskUpdate
16-
from tickets import (
17-
SlaBreachReport,
18-
Ticket,
19-
TicketSlaInfo,
20-
TicketStatus,
21-
get_sla_breach_report,
22-
)
16+
from tasks import (Task, TaskCreate, TaskFilter, TaskService, TaskStats,
17+
TaskUpdate)
18+
from tickets import (SlaBreachReport, Ticket, TicketSlaInfo, TicketStatus,
19+
get_sla_breach_report)
2320

2421
# Service instances shared across interfaces
2522
_task_service = TaskService()
@@ -198,12 +195,16 @@ async def op_csv_list_tickets(
198195

199196
@operation(
200197
name="csv_get_ticket",
201-
description="Get a single CSV ticket by UUID",
198+
description="Get a single CSV ticket by INC number (e.g. INC000016349327) or UUID",
202199
http_method="GET",
203200
)
204201
async def op_csv_get_ticket(ticket_id: str) -> Ticket | None:
205-
"""Get one CSV ticket."""
202+
"""Get one CSV ticket by INC number or UUID."""
206203
_ensure_csv_loaded()
204+
# Try INC number first (primary identifier)
205+
if ticket_id.upper().startswith("INC"):
206+
return _csv_service.get_ticket_by_incident_id(ticket_id)
207+
# Fall back to UUID for internal use
207208
try:
208209
parsed_id = UUID(ticket_id)
209210
except ValueError:
@@ -213,7 +214,7 @@ async def op_csv_get_ticket(ticket_id: str) -> Ticket | None:
213214

214215
@operation(
215216
name="csv_search_tickets",
216-
description="Search CSV tickets by text across summary, description, notes, requester and location fields",
217+
description="Search CSV tickets by text across incident ID, summary, description, notes, requester and location fields",
217218
http_method="GET",
218219
)
219220
async def op_csv_search_tickets(query: str, limit: int = 50) -> list[Ticket]:
@@ -228,6 +229,7 @@ async def op_csv_search_tickets(query: str, limit: int = 50) -> list[Ticket]:
228229
for ticket in _csv_service.list_tickets():
229230
haystack = " ".join(
230231
[
232+
ticket.incident_id or "",
231233
ticket.summary or "",
232234
ticket.description or "",
233235
ticket.notes or "",

frontend/src/features/usecase-demo/UsecaseDemoPage.jsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export default function UsecaseDemoPage({ definition }) {
204204
)
205205

206206
const selectedTicket = useMemo(
207-
() => matchingTickets.find((ticket) => ticket.id === selectedTicketId) || null,
207+
() => matchingTickets.find((ticket) => ticket.incident_id === selectedTicketId || ticket.id === selectedTicketId) || null,
208208
[matchingTickets, selectedTicketId]
209209
)
210210

@@ -254,10 +254,10 @@ export default function UsecaseDemoPage({ definition }) {
254254
const loadedTickets = tickets.filter((ticket) => !ticket._error)
255255
setMatchingTickets(loadedTickets)
256256
setSelectedTicketId((previousId) => {
257-
if (previousId && loadedTickets.some((ticket) => ticket.id === previousId)) {
257+
if (previousId && loadedTickets.some((ticket) => ticket.incident_id === previousId || ticket.id === previousId)) {
258258
return previousId
259259
}
260-
return loadedTickets[0]?.id || null
260+
return loadedTickets[0]?.incident_id || loadedTickets[0]?.id || null
261261
})
262262

263263
if (!loadedTickets.length) {
@@ -462,31 +462,33 @@ export default function UsecaseDemoPage({ definition }) {
462462
</tr>
463463
</thead>
464464
<tbody>
465-
{matchingTickets.map((ticket, index) => (
466-
<tr key={ticket.id} className={index % 2 ? styles.rowAlt : ''}>
467-
<td className={styles.td}>{ticket.id}</td>
465+
{matchingTickets.map((ticket, index) => {
466+
const ticketKey = ticket.incident_id || ticket.id
467+
return (
468+
<tr key={ticketKey} className={index % 2 ? styles.rowAlt : ''}>
469+
<td className={styles.td}>{ticketKey}</td>
468470
<td className={styles.td}>{ticket.summary || '—'}</td>
469471
<td className={styles.td}>{ticket.status || '—'}</td>
470472
<td className={styles.td}>
471473
<Button
472474
size="small"
473-
appearance={selectedTicketId === ticket.id ? 'primary' : 'secondary'}
474-
onClick={() => setSelectedTicketId(ticket.id)}
475-
data-testid={`${testIdPrefix}-ticket-open-${ticket.id}`}
475+
appearance={selectedTicketId === ticketKey ? 'primary' : 'secondary'}
476+
onClick={() => setSelectedTicketId(ticketKey)}
477+
data-testid={`${testIdPrefix}-ticket-open-${ticketKey}`}
476478
>
477479
Open
478480
</Button>
479481
</td>
480482
</tr>
481-
))}
483+
)})}
482484
</tbody>
483485
</table>
484486
</div>
485487

486488
{selectedTicket && (
487489
<div className={styles.ticketDetails} data-testid={`${testIdPrefix}-ticket-details`}>
488490
<Text weight="semibold">{selectedTicket.summary || 'Ticket Details'}</Text>
489-
<Text>ID: {selectedTicket.id}</Text>
491+
<Text>INC: {selectedTicket.incident_id || '—'}</Text>
490492
<Text>Status: {selectedTicket.status || '—'}</Text>
491493
<Text>Priority: {selectedTicket.priority || '—'}</Text>
492494
<Text>Assignee: {selectedTicket.assignee || '—'}</Text>

frontend/src/features/usecase-demo/demoDefinitions.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
const VPN_DEFAULT_PROMPT = `Find VPN issues where you think it's more a skill issue than a technical issue.
22
33
For speed:
4-
- First call csv_search_tickets with fields="id,summary,status,priority,assignee,assigned_group,created_at" and limit=20.
5-
- Use csv_get_ticket only for the top 3-5 most relevant IDs when deeper context is required.
4+
- First call csv_search_tickets with fields="incident_id,summary,status,priority,assignee,assigned_group,created_at" and limit=20.
5+
- Use csv_get_ticket only for the top 3-5 most relevant INC numbers when deeper context is required.
66
- Stop after the first sufficient result set; avoid extra tool loops.
77
- Do not request notes or resolution by default; only request them when explicitly needed for evidence.`;
88

99
const OPS_DEFAULT_PROMPT = `Analysiere Tickets zu "Outlook" oder "E-Mail" und erstelle einen einzigen Operations-Usecase.
1010
Für schnelle Ausführung:
11-
- Nutze zuerst csv_search_tickets mit fields="id,summary,status,priority,assigned_group,created_at" und limit=20.
12-
- Nutze csv_get_ticket nur für wenige ausgewählte Ticket-IDs, wenn Details nötig sind.
11+
- Nutze zuerst csv_search_tickets mit fields="incident_id,summary,status,priority,assigned_group,created_at" und limit=20.
12+
- Nutze csv_get_ticket nur für wenige ausgewählte INC-Nummern, wenn Details nötig sind.
1313
- Beende nach dem ersten ausreichenden Datensatz und vermeide zusätzliche Tool-Schleifen.
1414
- Fordere notes/resolution nicht standardmäßig an, nur wenn sie für die Aussage zwingend notwendig sind.
1515
Liefere nur eine kurze, handlungsorientierte Zusammenfassung mit Prioritäten und nächstem Schritt.
16-
Nutze ausschließlich CSV-Daten und nenne die verwendeten Ticket-IDs in Fließtext.`;
16+
Nutze ausschließlich CSV-Daten und nenne die verwendeten INC-Nummern in Fließtext.`;
1717

1818
const SLA_BREACH_DEFAULT_PROMPT = `Call csv_sla_breach_tickets with default parameters (unassigned_only=true, include_ok=false).
1919
@@ -58,6 +58,7 @@ export const USECASE_DEMO_DEFINITIONS = [
5858
description:
5959
"Ticket IDs from the agent result are resolved against CSV data. Click a ticket to inspect details.",
6060
fields: [
61+
"incident_id",
6162
"id",
6263
"summary",
6364
"status",
Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,69 @@
11
const UUID_PATTERN =
2-
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi
2+
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
3+
4+
const INC_PATTERN = /INC\d{12,15}/gi;
35

46
export function formatDateTime(value) {
5-
if (!value) return '—'
6-
const parsed = new Date(value)
7-
if (Number.isNaN(parsed.getTime())) return value
8-
return parsed.toLocaleString()
7+
if (!value) return "—";
8+
const parsed = new Date(value);
9+
if (Number.isNaN(parsed.getTime())) return value;
10+
return parsed.toLocaleString();
911
}
1012

1113
export function upsertRun(runs, updatedRun, maxSize = 25) {
12-
const index = runs.findIndex((item) => item.id === updatedRun.id)
14+
const index = runs.findIndex((item) => item.id === updatedRun.id);
1315
if (index === -1) {
14-
return [updatedRun, ...runs].slice(0, maxSize)
16+
return [updatedRun, ...runs].slice(0, maxSize);
1517
}
1618

17-
const next = [...runs]
18-
next[index] = updatedRun
19-
next.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
20-
return next
19+
const next = [...runs];
20+
next[index] = updatedRun;
21+
next.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
22+
return next;
2123
}
2224

2325
export function parseTicketIds(rawValue) {
24-
if (rawValue == null) return []
26+
if (rawValue == null) return [];
2527

2628
const asText = Array.isArray(rawValue)
27-
? rawValue.join(',')
28-
: String(rawValue)
29+
? rawValue.join(",")
30+
: String(rawValue);
31+
32+
// Try INC numbers first (primary identifier)
33+
const incMatches = asText.match(INC_PATTERN);
34+
if (incMatches?.length) {
35+
return incMatches.map((value) => value.toUpperCase());
36+
}
2937

30-
const uuidMatches = asText.match(UUID_PATTERN)
38+
const uuidMatches = asText.match(UUID_PATTERN);
3139
if (uuidMatches?.length) {
32-
return uuidMatches.map((value) => value.toLowerCase())
40+
return uuidMatches.map((value) => value.toLowerCase());
3341
}
3442

3543
return asText
3644
.split(/[\n,;\s]+/)
3745
.map((value) => value.trim())
38-
.filter(Boolean)
46+
.filter(Boolean);
3947
}
4048

41-
export function extractTicketIdsFromRows(rows, ticketIdFields = ['ticket_ids', 'ticket_id', 'ticketIds']) {
42-
const ids = new Set()
49+
export function extractTicketIdsFromRows(
50+
rows,
51+
ticketIdFields = ["ticket_ids", "ticket_id", "ticketIds"],
52+
) {
53+
const ids = new Set();
4354
for (const row of rows || []) {
44-
if (!row || typeof row !== 'object') continue
55+
if (!row || typeof row !== "object") continue;
4556
for (const field of ticketIdFields) {
46-
const fieldValue = row[field]
57+
const fieldValue = row[field];
4758
for (const parsedId of parseTicketIds(fieldValue)) {
48-
ids.add(parsedId)
59+
ids.add(parsedId);
4960
}
5061
}
5162
}
52-
return Array.from(ids)
63+
return Array.from(ids);
5364
}
5465

5566
export function sanitizeMarkdownForDisplay(markdown) {
56-
if (!markdown) return ''
57-
return markdown.replace(/```json[\s\S]*?```/gi, '').trim()
67+
if (!markdown) return "";
68+
return markdown.replace(/```json[\s\S]*?```/gi, "").trim();
5869
}

frontend/src/services/api.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ export async function getCSVTicketStats() {
210210
}
211211

212212
/**
213-
* Get one CSV ticket by ID.
214-
* @param {string} ticketId - Ticket UUID
213+
* Get one CSV ticket by INC number (e.g. INC000016349327) or UUID.
214+
* @param {string} ticketId - INC number or UUID
215215
* @param {string[]} fields - Optional field selection
216216
* @returns {Promise<Object>} Ticket details
217217
*/
@@ -310,7 +310,7 @@ export async function deleteWorkbenchAgent(agentId) {
310310

311311
export async function runWorkbenchAgent(
312312
agentId,
313-
{ inputPrompt = "", requiredInputValue = "" } = {}
313+
{ inputPrompt = "", requiredInputValue = "" } = {},
314314
) {
315315
return fetchJSON(`${API_BASE_URL}/workbench/agents/${agentId}/runs`, {
316316
method: "POST",

0 commit comments

Comments
 (0)