|
33 | 33 | from agents import AgentRequest, AgentResponse, agent_service |
34 | 34 | from api_decorators import operation |
35 | 35 |
|
| 36 | +# CSV ticket service |
| 37 | +from csv_data import Ticket, get_csv_ticket_service |
| 38 | + |
36 | 39 | # FastMCP client for direct ticket MCP calls (no AI) |
37 | 40 | from fastmcp import Client as MCPClient |
38 | 41 | from mcp_handler import handle_mcp_request |
@@ -386,6 +389,180 @@ async def get_qa_tickets(): |
386 | 389 | # NON-TASK ENDPOINTS |
387 | 390 | # ============================================================================ |
388 | 391 |
|
| 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 | + |
389 | 566 | @app.route("/api/health", methods=["GET"]) |
390 | 567 | async def health_check(): |
391 | 568 | """Health check endpoint.""" |
|
0 commit comments