Skip to content

Commit 7c48b68

Browse files
committed
feat: Enhance SLA Breach Risk functionality and UI integration
- Increased max_length for agent prompt to 5000 - Added fields parameter to list and search tickets for selective data retrieval - Updated timeout for usecase demo agent to 300 seconds - Introduced SLA Breach Risk demo with detailed prompt and ticket analysis - Added E2E tests for SLA Breach Risk demo page
1 parent 405f359 commit 7c48b68

5 files changed

Lines changed: 163 additions & 32 deletions

File tree

backend/agents.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class AgentRequest(BaseModel):
7676
prompt: str = Field(
7777
...,
7878
min_length=1,
79-
max_length=2000,
79+
max_length=5000,
8080
description="User prompt for the agent to process"
8181
)
8282
agent_type: Literal["task_assistant"] = Field(
@@ -313,6 +313,7 @@ def _csv_list_tickets(
313313
status: str | None = None,
314314
assigned_group: str | None = None,
315315
has_assignee: bool | None = None,
316+
fields: str | None = None,
316317
limit: int = 50,
317318
) -> str:
318319
try:
@@ -321,7 +322,14 @@ def _csv_list_tickets(
321322
status_enum = None
322323
tickets = service.list_tickets(status=status_enum, assigned_group=assigned_group, has_assignee=has_assignee)
323324
bounded_limit = max(1, min(limit, 100))
324-
return json.dumps([t.model_dump() for t in tickets[:bounded_limit]], default=str)
325+
items = tickets[:bounded_limit]
326+
if fields:
327+
field_list = [f.strip() for f in fields.split(",")]
328+
return json.dumps([
329+
{k: v for k, v in t.model_dump().items() if k in field_list}
330+
for t in items
331+
], default=str)
332+
return json.dumps([t.model_dump() for t in items], default=str)
325333

326334
def _csv_get_ticket(ticket_id: str) -> str:
327335
try:
@@ -333,7 +341,7 @@ def _csv_get_ticket(ticket_id: str) -> str:
333341
return json.dumps({"error": "not found"})
334342
return json.dumps(ticket.model_dump(), default=str)
335343

336-
def _csv_search_tickets(query: str, limit: int = 25) -> str:
344+
def _csv_search_tickets(query: str, fields: str | None = None, limit: int = 25) -> str:
337345
q = query.lower()
338346
tickets = service.list_tickets()
339347
matched = []
@@ -348,7 +356,11 @@ def _csv_search_tickets(query: str, limit: int = 25) -> str:
348356
t.city or "",
349357
]).lower()
350358
if q in text:
351-
matched.append(t.model_dump())
359+
dump = t.model_dump()
360+
if fields:
361+
field_list = [f.strip() for f in fields.split(",")]
362+
dump = {k: v for k, v in dump.items() if k in field_list}
363+
matched.append(dump)
352364
if len(matched) >= limit:
353365
break
354366
return json.dumps(matched, default=str)
@@ -365,7 +377,10 @@ def _csv_ticket_fields() -> str:
365377
description=(
366378
"List tickets from CSV with optional filters: status "
367379
"(new, assigned, in_progress, pending, resolved, closed, cancelled), "
368-
"assigned_group, has_assignee (true/false), and limit (default 50, max 100). "
380+
"assigned_group, has_assignee (true/false), limit (default 50, max 100), "
381+
"and fields (comma-separated field names to return, e.g. "
382+
"'id,summary,priority,assignee,assigned_group,created_at' — "
383+
"omit to return all fields). Use fields to reduce response size and speed up queries. "
369384
"Returns JSON array."
370385
),
371386
),
@@ -377,7 +392,7 @@ def _csv_ticket_fields() -> str:
377392
StructuredTool.from_function(
378393
func=_csv_search_tickets,
379394
name="csv_search_tickets",
380-
description="Search tickets by text across summary, description, notes, resolution, requester, group, city. Returns JSON array.",
395+
description="Search tickets by text across summary, description, notes, resolution, requester, group, city. Optional fields param (comma-separated) to limit returned fields. Returns JSON array.",
381396
),
382397
StructuredTool.from_function(
383398
func=_csv_ticket_fields,

backend/usecase_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from agents import AgentRequest, agent_service
2424

2525
USECASE_DEMO_AGENT_TIMEOUT_SECONDS = float(
26-
os.getenv("USECASE_DEMO_AGENT_TIMEOUT_SECONDS", "120")
26+
os.getenv("USECASE_DEMO_AGENT_TIMEOUT_SECONDS", "300")
2727
)
2828

2929

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@ const OPS_DEFAULT_PROMPT = `Analysiere Tickets zu "Outlook" oder "E-Mail" und er
44
Liefere nur eine kurze, handlungsorientierte Zusammenfassung mit Prioritäten und nächstem Schritt.
55
Nutze ausschließlich CSV-Daten und nenne die verwendeten Ticket-IDs in Fließtext.`
66

7+
const SLA_BREACH_DEFAULT_PROMPT = `Find all tickets where the "assignee" field is empty (no individual person assigned — note: Status "Assigned" means assigned to a group, NOT to a person. A ticket can have Status="Assigned" and still have no individual assignee).
8+
9+
IMPORTANT: When calling csv_list_tickets, use the "fields" parameter to request ONLY the fields you need: "id,summary,priority,urgency,assignee,assigned_group,created_at". Also use has_assignee=false to filter server-side. This avoids fetching unnecessary data.
10+
11+
Check these tickets against priority-based SLA thresholds measuring time since created_at (the reported date).
12+
13+
IMPORTANT: For the reference timestamp ("now"), do NOT use the current system time. Instead, find the most recent date across all date fields in the CSV data and use that as "now". This ensures meaningful age calculations for demo/historical data.
14+
15+
SLA thresholds by priority:
16+
- Critical (1): 4 hours
17+
- High (2): 24 hours (1 day)
18+
- Medium (3): 72 hours (3 days)
19+
- Low (4): 120 hours (5 days)
20+
21+
For each matching ticket, compute:
22+
- age_hours: hours elapsed from reported_date to the reference timestamp
23+
- sla_threshold_hours: the SLA threshold for its priority
24+
- breach_status: "breached" if age_hours > sla_threshold_hours, "at risk" if age_hours > 0.75 * sla_threshold_hours, otherwise "ok"
25+
26+
Return ALL tickets with breach_status "breached" or "at risk". Group them by breach_status (all "breached" first, then all "at risk"), and within each group sort by age_hours descending.
27+
28+
Output a JSON array of objects with these fields: ticket_id, summary, priority, urgency, assigned_group (the team responsible), assignee (should be empty for all results), reported_date, age_hours (rounded to 1 decimal), sla_threshold_hours, breach_status.
29+
After the JSON block, provide a short markdown summary that:
30+
1. States the reference timestamp used
31+
2. Groups ticket counts by breach_status and assigned_group
32+
3. Recommends actions for the most critical breaches`
33+
734
/**
835
* Add new demos here to create additional pages without duplicating UI logic.
936
* Each definition configures route, prompt, and which result views are rendered.
@@ -78,6 +105,48 @@ export const USECASE_DEMO_DEFINITIONS = [
78105
enabled: false,
79106
},
80107
},
108+
{
109+
id: 'usecase-demo-sla-breach',
110+
route: '/usecase_demo_sla_breach',
111+
tabValue: 'usecase-demo-sla-breach',
112+
tabLabel: 'SLA Breach Risk',
113+
tabTestId: 'tab-usecase-demo-sla-breach',
114+
testIdPrefix: 'sla-breach',
115+
title: 'SLA Breach Risk',
116+
menuPointBadge: '1 menu point',
117+
pageDescription:
118+
'Identifies tickets assigned to a support group but with no individual assignee, that are approaching or have exceeded priority-based SLA thresholds. Helps teams prioritize pickup before service level agreements are breached.',
119+
promptLabel: 'SLA Breach Prompt',
120+
promptDescription:
121+
'Edit thresholds or filters in the prompt, then run the agent to scan for at-risk tickets.',
122+
defaultPrompt: SLA_BREACH_DEFAULT_PROMPT,
123+
runHistoryLimit: 25,
124+
pollIntervalMs: 2000,
125+
resultViews: ['table', 'markdown'],
126+
resultSectionTitle: 'SLA Breach Results',
127+
resultSectionDescription:
128+
'Tickets at risk or already past their SLA threshold, sorted by severity.',
129+
ticketIdFields: ['ticket_ids', 'ticket_id', 'ticketIds'],
130+
matchingTickets: {
131+
enabled: true,
132+
title: 'Affected Tickets (Group-Assigned, No Individual Assignee)',
133+
description:
134+
'These tickets are routed to a support group but no individual has picked them up. The "Assigned Group" column shows the responsible team; "Assignee" is empty for all.',
135+
fields: [
136+
'id',
137+
'summary',
138+
'assigned_group',
139+
'assignee',
140+
'status',
141+
'priority',
142+
'urgency',
143+
'reported_date',
144+
'last_modified_date',
145+
'requester_name',
146+
'service',
147+
],
148+
},
149+
},
81150
]
82151

83152
export const DEFAULT_USECASE_DEMO_DEFINITION = USECASE_DEMO_DEFINITIONS[0]

net.drawio

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,103 @@
11
<mxfile host="65bd71144e">
22
<diagram id="tzn5bnltuvCgxXnNHkHx" name="Page-1">
3-
<mxGraphModel dx="999" dy="524" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
3+
<mxGraphModel dx="0" dy="371" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
44
<root>
55
<mxCell id="0"/>
66
<mxCell id="1" parent="0"/>
7-
<mxCell id="9" value="VNET 10.x" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;arcSize=5;" vertex="1" parent="1">
7+
<mxCell id="9" value="VNET 10.x" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;arcSize=5;" parent="1" vertex="1">
88
<mxGeometry x="839" y="50" width="581" height="380" as="geometry"/>
99
</mxCell>
10-
<mxCell id="3" value="BIT On-Premise" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" vertex="1" parent="1">
10+
<mxCell id="3" value="BIT On-Premise" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" parent="1" vertex="1">
1111
<mxGeometry x="150" y="70" width="280" height="200" as="geometry"/>
1212
</mxCell>
13-
<mxCell id="5" style="edgeStyle=none;html=1;" edge="1" parent="1" source="4" target="3">
13+
<mxCell id="5" style="edgeStyle=none;html=1;" parent="1" source="4" target="3" edge="1">
1414
<mxGeometry relative="1" as="geometry"/>
1515
</mxCell>
16-
<mxCell id="4" value="User" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
16+
<mxCell id="4" value="User" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
1717
<mxGeometry x="20" y="260" width="120" height="60" as="geometry"/>
1818
</mxCell>
19-
<mxCell id="7" style="edgeStyle=none;html=1;" edge="1" parent="1" source="6">
19+
<mxCell id="7" style="edgeStyle=none;html=1;" parent="1" source="6" edge="1">
2020
<mxGeometry relative="1" as="geometry">
2121
<mxPoint x="410" y="220" as="targetPoint"/>
2222
</mxGeometry>
2323
</mxCell>
24-
<mxCell id="12" style="edgeStyle=none;html=1;" edge="1" parent="1" source="6">
24+
<mxCell id="12" style="edgeStyle=none;html=1;" parent="1" source="6" edge="1">
2525
<mxGeometry relative="1" as="geometry">
2626
<mxPoint x="360.55555555555566" y="223.33333333333337" as="targetPoint"/>
2727
</mxGeometry>
2828
</mxCell>
29-
<mxCell id="20" style="edgeStyle=none;html=1;" edge="1" parent="1" source="6" target="21">
29+
<mxCell id="20" style="edgeStyle=none;html=1;" parent="1" source="6" target="21" edge="1">
3030
<mxGeometry relative="1" as="geometry"/>
3131
</mxCell>
32-
<mxCell id="6" value="FW/Proxy" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
32+
<mxCell id="6" value="FW/Proxy" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
3333
<mxGeometry x="370" y="130" width="70" height="40" as="geometry"/>
3434
</mxCell>
35-
<mxCell id="16" value="BMC" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
35+
<mxCell id="16" value="BMC" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1">
3636
<mxGeometry x="250" y="100" width="45" height="45" as="geometry"/>
3737
</mxCell>
38-
<mxCell id="21" value="Subnet" style="rounded=1;whiteSpace=wrap;html=1;arcSize=16;verticalAlign=top;" vertex="1" parent="1">
38+
<mxCell id="21" value="Subnet" style="rounded=1;whiteSpace=wrap;html=1;arcSize=16;verticalAlign=top;" parent="1" vertex="1">
3939
<mxGeometry x="870" y="75" width="320" height="190" as="geometry"/>
4040
</mxCell>
41-
<mxCell id="2" value="App Service" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
41+
<mxCell id="2" value="App Service" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
4242
<mxGeometry x="939" y="160" width="125" height="60" as="geometry"/>
4343
</mxCell>
44-
<mxCell id="8" value="Auth" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
44+
<mxCell id="8" value="Auth" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1">
4545
<mxGeometry x="914" y="200" width="50" height="50" as="geometry"/>
4646
</mxCell>
47-
<mxCell id="10" value="Public IP" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
47+
<mxCell id="10" value="Public IP" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
4848
<mxGeometry x="894" y="150" width="50" height="50" as="geometry"/>
4949
</mxCell>
50-
<mxCell id="22" value="Network Security Groups&lt;div&gt;source: internet/any&amp;nbsp;&lt;/div&gt;&lt;div&gt;target: public deny&lt;/div&gt;" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;verticalAlign=top;" vertex="1" parent="1">
50+
<mxCell id="22" value="Network Security Groups&lt;div&gt;source: internet/any&amp;nbsp;&lt;/div&gt;&lt;div&gt;target: public deny&lt;/div&gt;" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;verticalAlign=top;" parent="1" vertex="1">
5151
<mxGeometry x="930" y="280" width="220" height="80" as="geometry"/>
5252
</mxCell>
53-
<mxCell id="24" value="Subnet" style="rounded=1;whiteSpace=wrap;html=1;arcSize=16;verticalAlign=top;" vertex="1" parent="1">
53+
<mxCell id="24" value="Subnet" style="rounded=1;whiteSpace=wrap;html=1;arcSize=16;verticalAlign=top;" parent="1" vertex="1">
5454
<mxGeometry x="1190" y="95" width="230" height="305" as="geometry"/>
5555
</mxCell>
56-
<mxCell id="25" value="Azure PG" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
56+
<mxCell id="25" value="Azure PG" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
5757
<mxGeometry x="1540" y="100" width="240" height="200" as="geometry"/>
5858
</mxCell>
59-
<mxCell id="26" value="Endpoint" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
59+
<mxCell id="26" value="Endpoint" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1">
6060
<mxGeometry x="1510" y="180" width="80" height="80" as="geometry"/>
6161
</mxCell>
62-
<mxCell id="28" style="edgeStyle=none;html=1;" edge="1" parent="1" source="27" target="26">
62+
<mxCell id="28" style="edgeStyle=none;html=1;" parent="1" source="27" target="26" edge="1">
6363
<mxGeometry relative="1" as="geometry"/>
6464
</mxCell>
65-
<mxCell id="27" value="IP" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
65+
<mxCell id="27" value="IP" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
6666
<mxGeometry x="1500" y="400" width="120" height="60" as="geometry"/>
6767
</mxCell>
68-
<mxCell id="29" value="v6: VNET/Subnet" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
68+
<mxCell id="29" value="v6: VNET/Subnet" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
6969
<mxGeometry x="1520" y="400" width="100" height="20" as="geometry"/>
7070
</mxCell>
71-
<mxCell id="30" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
71+
<mxCell id="30" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
7272
<mxGeometry x="1620" y="220" width="120" height="60" as="geometry"/>
7373
</mxCell>
74-
<mxCell id="32" style="edgeStyle=none;html=1;" edge="1" parent="1" source="31" target="26">
74+
<mxCell id="32" style="edgeStyle=none;html=1;" parent="1" source="31" target="26" edge="1">
7575
<mxGeometry relative="1" as="geometry"/>
7676
</mxCell>
77-
<mxCell id="31" value="Private Link&lt;div&gt;NIC&lt;/div&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
77+
<mxCell id="31" value="Private Link&lt;div&gt;NIC&lt;/div&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1">
7878
<mxGeometry x="1080" y="180" width="80" height="80" as="geometry"/>
7979
</mxCell>
80-
<mxCell id="23" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1">
80+
<mxCell id="23" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
8181
<mxGeometry x="1694" y="130" width="60" height="80" as="geometry"/>
8282
</mxCell>
8383
</root>
8484
</mxGraphModel>
8585
</diagram>
86+
<diagram id="XqkKmIq0oGWWFroz0_pl" name="Page-2">
87+
<mxGraphModel dx="586" dy="496" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
88+
<root>
89+
<mxCell id="0"/>
90+
<mxCell id="1" parent="0"/>
91+
<mxCell id="8wV5sjgPgGiqjB_m4-tT-3" style="edgeStyle=none;html=1;" edge="1" parent="1" source="8wV5sjgPgGiqjB_m4-tT-1" target="8wV5sjgPgGiqjB_m4-tT-2">
92+
<mxGeometry relative="1" as="geometry"/>
93+
</mxCell>
94+
<mxCell id="8wV5sjgPgGiqjB_m4-tT-1" value="&quot;I want new usecase menu item&quot;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
95+
<mxGeometry x="190" y="50" width="120" height="60" as="geometry"/>
96+
</mxCell>
97+
<mxCell id="8wV5sjgPgGiqjB_m4-tT-2" value="&quot;What do I need to describe a new usecase for this demo project&quot;?" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
98+
<mxGeometry x="190" y="140" width="120" height="60" as="geometry"/>
99+
</mxCell>
100+
</root>
101+
</mxGraphModel>
102+
</diagram>
86103
</mxfile>

0 commit comments

Comments
 (0)