Skip to content

Commit e691d02

Browse files
authored
Work on csv (#12)
* feat: Implement CSV Ticket Viewer - Refactor App component to replace existing features with CSV Ticket Table. - Add CSVTicketTable component for displaying tickets from CSV data source. - Introduce API functions for fetching CSV ticket fields, tickets, and statistics. - Create CSV data source in backend to handle loading and processing of CSV files. - Enhance AgentChat component to display error details from API responses. - Update styles and layout for improved user experience in ticket viewing. Signed-off-by: Andre Bossard <anbossar@microsoft.com> * refactor: Update import formatting and enhance status badge display in CSVTicketTable component Signed-off-by: Andre Bossard <anbossar@microsoft.com> * feat: Add Nivo chart visualizations for CSV tickets and enhance documentation Signed-off-by: Andre Bossard <anbossar@microsoft.com> --------- Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 4049650 commit e691d02

17 files changed

Lines changed: 2034 additions & 64 deletions

File tree

.github/copilot-instructions.md

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
# Copilot Instructions
22

3-
## Architecture
3+
## Purpose
44

5-
- `backend/app.py` hosts REST (`/api/*`), MCP JSON-RPC (`/mcp`), and LangGraph agent (`/api/agents/run`) on port 5001; new capabilities should use the `@operation` decorator in `backend/operations.py` so all interfaces stay in sync.
6-
- Business logic lives in service modules: `tasks.py` (TaskService), `tickets.py` (ticket models + SLA calculations); keep services as the single source of truth.
7-
- The React side is feature-first: `frontend/src/App.jsx` switches tabs via React Router; each folder under `frontend/src/features` owns its state, calculations, and FluentUI layout.
8-
- All network calls go through `frontend/src/services/api.js`; localStorage helpers live in separate modules like `reminderStorage.js`.
5+
This repository is a **learning environment** for processing and visualizing CSV ticket data using Copilot.
96

10-
## Backend Patterns
7+
## Scope Boundaries
118

12-
- Define operations with `@operation` (see `op_create_task`, `op_get_task`): annotate parameters with Pydantic models or enums so MCP schemas and REST validation are generated automatically, then build thin Quart wrappers that only handle HTTP concerns.
13-
- Keep methods like `TaskService.create_task` and `TaskService.update_task` “deep”: they already validate, mutate `_tasks_db`, and return `Task` models, so avoid splitting them into tiny helpers or duplicating validation elsewhere.
14-
- Use the provided Pydantic models (`TaskCreate`, `TaskUpdate`, `TaskStats`, `TaskFilter`) for all inputs/outputs; MCP tooling and REST serializers expect `.model_dump()` objects with these exact field names (`created_at`, `completed`, etc.).
15-
- The SSE endpoint `time_stream` streams `{"time","date","timestamp"}` JSON strings; if you change its shape, also update `connectToTimeStream` consumers and the Dashboard expectations.
9+
**Do:**
1610

17-
## Frontend Patterns
11+
- Work with `csv/data.csv` and related parsing logic (`backend/csv_data.py`)
12+
- Write/adjust small, focused Python scripts or notebooks for data exploration (pandas, matplotlib, seaborn, plotly)
13+
- Compute summaries, statistics, filters, aggregations on ticket data
14+
- Propose visualizations and queries to understand ticket patterns
15+
- Help interpret CSV schema and column mappings
1816

19-
- Feature components isolate **calculations** (pure helpers like `getTaskStats`), **actions** (API calls, event handlers), and **data** (React state) in that order; preserve that structure when extending `TaskList` or `Dashboard`.
20-
- Stick to FluentUI v9 primitives with `makeStyles`/`tokens`; prefer adjusting the `useStyles` definitions over inline styles to maintain theming consistency.
21-
- Always go through `frontend/src/services/api.js` (`fetchJSON`, `createTask`, `updateTask`, etc.); components expect rejected promises to carry friendly `Error.message`, so don’t bypass these helpers.
22-
- Task UI and tests rely on deterministic `data-testid` attributes (`task-menu-${id}`, `filter-completed`, etc.); keep them stable or update the Playwright suite alongside UI changes.
23-
- Real-time widgets follow the `connectToTimeStream` contract (subscribe in `useEffect`, capture cleanup function, surface errors via component state); reuse that approach for any new SSE sources.
17+
**Do NOT:**
2418

25-
## Workflows
19+
- Change backend architecture (`backend/app.py`, `operations.py`, decorators)
20+
- Modify Pydantic models (`tickets.py`, `tasks.py`) or core services
21+
- Alter REST/MCP endpoints, React frontend structure, or tests
22+
- Touch `UNIFIED_ARCHITECTURE.md`, `README.md`, or Playwright tests
2623

27-
- One-time setup: run `./setup.sh` from the repo root to create the repo-level `.venv`, install frontend deps, and provision Playwright browsers.
28-
- Dev loop: `source .venv/bin/activate && cd backend && python app.py` (serves on 5001) plus `cd frontend && npm run dev`; `./start-dev.sh` or the VS Code “Full Stack: Backend + Frontend” launch config will start both for you.
29-
- MCP testing uses JSON-RPC POSTs to `http://localhost:5001/mcp` (`tools/list`, `tools/call`); no extra process is required because the Quart server already exposes it.
24+
## CSV Data Structure
3025

31-
## Testing
26+
Primary source: `csv/data.csv` (BMC Remedy/ITSM export).
27+
Key columns:
3228

33-
- E2E coverage lives in `tests/e2e/app.spec.js`; from the repo root run `npm run test:e2e` (`:ui` for interactive, `:report` to inspect results). Start both servers first to avoid cold-start delays.
34-
- The suite assumes sample tasks exist (created by `TaskService.initialize_sample_data()`), tabs are labeled via `data-testid`, and SSE data matches the backend format; keep those invariants or update the tests deliberately.
35-
- For deterministic assertions, prefer editing helpers like `waitForAppToLoad` or the shared selector strings instead of scattering hard-coded waits.
29+
- Identifiers: `Entry ID`, `Incident ID*+`, `Corporate ID`
30+
- Summary: `Summary*`, `Notes`, `Resolution`
31+
- Status/Priority: `Status*`, `Priority*`, `Urgency*`, `Impact*`
32+
- Assignment: `Assignee+`, `Assigned Group*+`, `Owner Group+`
33+
- Requester: `Full Name`, `First Name+`, `Last Name+`, `Internet E-mail`
34+
- Location: `City`, `Country`, `Site ID`, `Company`
35+
- Timestamps: `Reported Date+`, `Last Modified Date`, `Closed Date`
36+
- Categories: `Operational Categorization Tier 1+/2/3`, `Product Categorization Tier 1/2/3`
37+
38+
Reference implementation: `backend/csv_data.py` (column mapping, status/priority mapping, date parsing).
39+
40+
## Working with the Data
41+
42+
- Load: `pd.read_csv("csv/data.csv", encoding="latin-1")`
43+
- Dates: parse `DD.MM.YYYY HH:MM:SS` (e.g., `22.10.2025 11:53:33`)
44+
- Status mapping: New, Assigned, In Progress, Pending, Resolved, Closed, Cancelled
45+
- Priority mapping: 1-Critical, 2-High, 3-Medium, 4-Low
46+
- Prefer notebooks for exploration; keep scripts small and focused
3647

3748
## Conventions
3849

39-
- Follow “Grokking Simplicity”: keep actions (I/O) in services or event handlers, calculations pure (formatting, stats), and data as plain objects/React state; this is enforced heavily in docs and existing code.
40-
- Follow “A Philosophy of Software Design”: favor deep modules (e.g., extend `TaskService` instead of sprinkling logic across routes/components) and hide complexity behind simple interfaces.
41-
- When you touch the architecture (operation decorators, unified REST/MCP flow, feature structure), update `README.md` and `UNIFIED_ARCHITECTURE.md` so future agents inherit the correct mental model.
50+
- Follow “Grokking Simplicity”: separate data (CSV), calculations (pure), actions (I/O)
51+
- Keep changes minimal, targeted, and reversible
52+
- No broad refactors; prioritize clarity for learners
53+
- Document findings inline (markdown cells/comments)

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ Thumbs.db
5050
*.log
5151
logs/
5252
*.db
53+
csv/*.csv

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Quart + Vite + React Demo Application
22

3+
**Task:** Visualize the CSV tickets with richer panels (status, priority, timeline, geography, SLA). **How would you like to view the tickets?**
4+
35
> A teaching-oriented full-stack sample that pairs a Python Quart backend with a React + FluentUI frontend, real-time Server-Sent Events (SSE), and Playwright tests.
46
57
## Why this repo?

backend/app.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from agents import AgentRequest, AgentResponse, agent_service
3434
from api_decorators import operation
3535

36+
# CSV ticket service
37+
from csv_data import Ticket, get_csv_ticket_service
38+
3639
# FastMCP client for direct ticket MCP calls (no AI)
3740
from fastmcp import Client as MCPClient
3841
from mcp_handler import handle_mcp_request
@@ -386,6 +389,180 @@ async def get_qa_tickets():
386389
# NON-TASK ENDPOINTS
387390
# ============================================================================
388391

392+
# ============================================================================
393+
# CSV TICKET ENDPOINTS
394+
# ============================================================================
395+
396+
# Initialize CSV ticket service
397+
_csv_ticket_service = get_csv_ticket_service()
398+
399+
# Load CSV data on startup (using relative path from backend folder)
400+
_csv_data_path = Path(__file__).parent.parent / "csv" / "data.csv"
401+
if _csv_data_path.exists():
402+
_csv_loaded = _csv_ticket_service.load_csv(_csv_data_path)
403+
print(f"📊 Loaded {_csv_loaded} tickets from CSV")
404+
405+
406+
# Define which fields are available for display
407+
CSV_TICKET_FIELDS = [
408+
{"name": "id", "label": "ID", "type": "uuid"},
409+
{"name": "summary", "label": "Summary", "type": "string"},
410+
{"name": "status", "label": "Status", "type": "enum"},
411+
{"name": "priority", "label": "Priority", "type": "enum"},
412+
{"name": "assignee", "label": "Assignee", "type": "string"},
413+
{"name": "assigned_group", "label": "Assigned Group", "type": "string"},
414+
{"name": "requester_name", "label": "Requester", "type": "string"},
415+
{"name": "requester_email", "label": "Email", "type": "string"},
416+
{"name": "city", "label": "City", "type": "string"},
417+
{"name": "country", "label": "Country", "type": "string"},
418+
{"name": "service", "label": "Service", "type": "string"},
419+
{"name": "incident_type", "label": "Incident Type", "type": "string"},
420+
{"name": "product_name", "label": "Product", "type": "string"},
421+
{"name": "manufacturer", "label": "Manufacturer", "type": "string"},
422+
{"name": "created_at", "label": "Created", "type": "datetime"},
423+
{"name": "updated_at", "label": "Updated", "type": "datetime"},
424+
{"name": "urgency", "label": "Urgency", "type": "string"},
425+
{"name": "impact", "label": "Impact", "type": "string"},
426+
{"name": "resolution", "label": "Resolution", "type": "string"},
427+
{"name": "notes", "label": "Notes", "type": "string"},
428+
]
429+
430+
431+
@app.route("/api/csv-tickets/fields", methods=["GET"])
432+
async def get_csv_ticket_fields():
433+
"""Get metadata about available CSV ticket fields."""
434+
return jsonify({
435+
"fields": CSV_TICKET_FIELDS,
436+
"total_tickets": _csv_ticket_service.total_count,
437+
})
438+
439+
440+
@app.route("/api/csv-tickets", methods=["GET"])
441+
async def get_csv_tickets():
442+
"""
443+
Get CSV tickets with optional filtering, sorting, and field selection.
444+
445+
Query params:
446+
- fields: comma-separated list of field names to include
447+
- status: filter by status (new, assigned, in_progress, pending, resolved, closed)
448+
- has_assignee: filter by assignee presence (true/false)
449+
- assigned_group: filter by group name
450+
- sort: field name to sort by
451+
- sort_dir: asc or desc (default: asc)
452+
- limit: max number of results
453+
- offset: number of results to skip
454+
"""
455+
from tickets import TicketStatus
456+
457+
# Parse query params
458+
fields_param = request.args.get("fields", "")
459+
status_param = request.args.get("status")
460+
has_assignee_param = request.args.get("has_assignee")
461+
assigned_group_param = request.args.get("assigned_group")
462+
sort_param = request.args.get("sort", "created_at")
463+
sort_dir = request.args.get("sort_dir", "desc")
464+
limit = request.args.get("limit", type=int)
465+
offset = request.args.get("offset", 0, type=int)
466+
467+
# Determine which fields to include
468+
if fields_param:
469+
selected_fields = [f.strip() for f in fields_param.split(",")]
470+
else:
471+
# Default fields for table display
472+
selected_fields = ["summary", "status", "priority", "assignee", "assigned_group", "requester_name", "city", "created_at"]
473+
474+
# Parse filters
475+
status_filter = None
476+
if status_param:
477+
try:
478+
status_filter = TicketStatus(status_param)
479+
except ValueError:
480+
pass
481+
482+
has_assignee_filter = None
483+
if has_assignee_param is not None:
484+
has_assignee_filter = has_assignee_param.lower() == "true"
485+
486+
# Get tickets with filters
487+
tickets = _csv_ticket_service.list_tickets(
488+
status=status_filter,
489+
assigned_group=assigned_group_param,
490+
has_assignee=has_assignee_filter,
491+
)
492+
493+
# Sort tickets
494+
def get_sort_key(ticket: Ticket):
495+
val = getattr(ticket, sort_param, None)
496+
if val is None:
497+
return ""
498+
if hasattr(val, "value"): # Enum
499+
return val.value
500+
return val
501+
502+
try:
503+
tickets = sorted(tickets, key=get_sort_key, reverse=(sort_dir == "desc"))
504+
except TypeError:
505+
pass # Skip sorting if types are incompatible
506+
507+
total_count = len(tickets)
508+
509+
# Apply pagination
510+
if limit:
511+
tickets = tickets[offset:offset + limit]
512+
elif offset:
513+
tickets = tickets[offset:]
514+
515+
# Build response with selected fields only
516+
result = []
517+
for ticket in tickets:
518+
row = {}
519+
for field in selected_fields:
520+
val = getattr(ticket, field, None)
521+
if val is None:
522+
row[field] = None
523+
elif hasattr(val, "value"): # Enum
524+
row[field] = val.value
525+
elif hasattr(val, "isoformat"): # datetime
526+
row[field] = val.isoformat()
527+
elif hasattr(val, "hex"): # UUID
528+
row[field] = str(val)
529+
else:
530+
row[field] = val
531+
result.append(row)
532+
533+
return jsonify({
534+
"tickets": result,
535+
"total": total_count,
536+
"offset": offset,
537+
"limit": limit,
538+
"fields": selected_fields,
539+
})
540+
541+
542+
@app.route("/api/csv-tickets/stats", methods=["GET"])
543+
async def get_csv_ticket_stats():
544+
"""Get statistics about CSV tickets."""
545+
from collections import Counter
546+
547+
tickets = _csv_ticket_service.list_tickets()
548+
549+
statuses = Counter(t.status.value for t in tickets)
550+
priorities = Counter(t.priority.value for t in tickets)
551+
groups = Counter(t.assigned_group for t in tickets if t.assigned_group)
552+
cities = Counter(t.city for t in tickets if t.city)
553+
554+
unassigned_count = sum(1 for t in tickets if t.assignee is None and t.assigned_group is not None)
555+
556+
return jsonify({
557+
"total": len(tickets),
558+
"unassigned": unassigned_count,
559+
"by_status": dict(statuses),
560+
"by_priority": dict(priorities),
561+
"by_group": dict(groups.most_common(10)),
562+
"by_city": dict(cities.most_common(10)),
563+
})
564+
565+
389566
@app.route("/api/health", methods=["GET"])
390567
async def health_check():
391568
"""Health check endpoint."""

0 commit comments

Comments
 (0)