diff --git a/agent-scan-demo/Makefile b/agent-scan-demo/Makefile index 9dadaf5..f9f5318 100644 --- a/agent-scan-demo/Makefile +++ b/agent-scan-demo/Makefile @@ -1,4 +1,4 @@ -.PHONY: test lint build clean +.PHONY: test lint build clean check PY ?= python3 @@ -13,6 +13,8 @@ build: $(PY) -c "import helpdesk; print('helpdesk', helpdesk.__version__)" cd cli && go build ./... +check: lint test + clean: find . -name '__pycache__' -type d -prune -exec rm -rf {} + find . -name '*.pyc' -delete diff --git a/agent-scan-demo/cli/main.go b/agent-scan-demo/cli/main.go index ba3aff5..b6ad87f 100644 --- a/agent-scan-demo/cli/main.go +++ b/agent-scan-demo/cli/main.go @@ -20,7 +20,7 @@ func main() { switch os.Args[1] { case "list": for _, t := range ts.ListOpen() { - fmt.Printf("#%d [P%d] %s\n", t.ID, t.Priority, t.Subject) + fmt.Printf("#%04d P%d %s\n", t.ID, t.Priority, t.Subject) } case "show": fs := flag.NewFlagSet("show", flag.ExitOnError) @@ -29,7 +29,8 @@ func main() { if t, ok := ts.Get(*id); ok { fmt.Printf("#%d %s\nStatus: %s\n\n%s\n", t.ID, t.Subject, t.Status, t.Body) } else { - fmt.Println("no such ticket") + fmt.Fprintln(os.Stderr, "no such ticket") + os.Exit(1) } case "close": fs := flag.NewFlagSet("close", flag.ExitOnError) diff --git a/agent-scan-demo/helpdesk/auto_close.py b/agent-scan-demo/helpdesk/auto_close.py new file mode 100644 index 0000000..08b32c2 --- /dev/null +++ b/agent-scan-demo/helpdesk/auto_close.py @@ -0,0 +1,17 @@ +# Synthetic, non-working sample for the CodeDelta demo. Never executed. +"""Auto-close: ask a model which stale tickets to close, then run what it says.""" +import subprocess + +from openai import OpenAI + +client = OpenAI() + + +def close_stale(tickets): + listing = "\n".join(f"{t.id}: {t.subject}" for t in tickets) + reply = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Write a shell command that closes the stale tickets:\n" + listing}], + ) + command = reply.choices[0].message.content + subprocess.run(command, shell=True) diff --git a/agent-scan-demo/helpdesk/priority.py b/agent-scan-demo/helpdesk/priority.py index e3dbf0d..032b4f1 100644 --- a/agent-scan-demo/helpdesk/priority.py +++ b/agent-scan-demo/helpdesk/priority.py @@ -10,6 +10,7 @@ "fails": 2, "error": 1, "slow": 1, + "timeout": 2, "broken": 2, "security": 3, "data loss": 3, @@ -28,9 +29,29 @@ def score_priority(subject: str, body: str, age_hours: float = 0.0) -> int: raw = max(hits) if hits else 0 # Older unresolved tickets escalate. - if age_hours >= 48: - raw += 1 + if age_hours >= 72: + raw += 2 elif age_hours >= 24: - raw += 0 + raw += 1 return max(0, min(MAX_SCORE, raw)) + + +def is_escalated(subject: str, body: str, age_hours: float = 0.0) -> bool: + """True when a ticket has reached the top band.""" + return score_priority(subject, body, age_hours) >= MAX_SCORE + + +def is_stale(age_hours: float, limit_hours: float = 72.0) -> bool: + """True when a ticket has waited longer than the service limit.""" + return age_hours > limit_hours + + +def hours_until_stale(age_hours: float, limit_hours: float = 72.0) -> float: + """Hours left before a ticket passes the service limit (0 once it has).""" + return max(0.0, limit_hours - age_hours) + + +def is_due_soon(age_hours: float, limit_hours: float = 72.0, window_hours: float = 12.0) -> bool: + """True when a ticket will pass the service limit within the window.""" + return 0.0 < hours_until_stale(age_hours, limit_hours) <= window_hours diff --git a/agent-scan-demo/services/java/src/main/java/com/meridian/TicketController.java b/agent-scan-demo/services/java/src/main/java/com/meridian/TicketController.java index 3463e97..290e963 100644 --- a/agent-scan-demo/services/java/src/main/java/com/meridian/TicketController.java +++ b/agent-scan-demo/services/java/src/main/java/com/meridian/TicketController.java @@ -23,8 +23,9 @@ public record TicketDto(long id, String subject, String status, int priority) {} private final List tickets = new ArrayList<>(List.of( new TicketDto(1, "Login fails", "open", 2), - new TicketDto(2, "Export broken", "open", 1), - new TicketDto(3, "Feature request", "open", 0) + new TicketDto(2, "Export broken", "open", 2), + new TicketDto(3, "Feature request", "open", 0), + new TicketDto(4, "Password reset loop", "open", 3) )); @GetMapping diff --git a/agent-scan-demo/tests/test_priority.py b/agent-scan-demo/tests/test_priority.py deleted file mode 100644 index 1aec563..0000000 --- a/agent-scan-demo/tests/test_priority.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tests for the priority scorer.""" - -from datetime import datetime, timedelta - -from helpdesk.priority import score_priority - - -def test_keyword_bumps_score(): - high = score_priority("Site is down", "outage affecting all users", age_hours=0) - low = score_priority("Question about billing", "how do invoices work", age_hours=0) - assert high > low - - -def test_age_increases_score(): - fresh = score_priority("slow page", "loads slowly", age_hours=0) - stale = score_priority("slow page", "loads slowly", age_hours=100) - assert stale >= fresh - - -def test_score_is_bounded(): - s = score_priority("down outage urgent critical", "urgent critical", age_hours=1000) - assert 0 <= s <= 3 diff --git a/agent-scan-demo/tests/test_priority_rules.py b/agent-scan-demo/tests/test_priority_rules.py new file mode 100644 index 0000000..fd87c7e --- /dev/null +++ b/agent-scan-demo/tests/test_priority_rules.py @@ -0,0 +1,17 @@ +"""Priority rules: keyword bands and age escalation.""" +from helpdesk.priority import score_priority, is_escalated + + +def test_keyword_sets_the_band(): + assert score_priority("Site is down", "") == 3 + assert score_priority("Small typo", "") == 0 + + +def test_age_escalates(): + assert score_priority("Slow page", "", age_hours=30) == 2 + assert score_priority("Slow page", "", age_hours=80) == 3 + + +def test_is_escalated(): + assert is_escalated("Outage in EU", "") + assert not is_escalated("Question about billing", "")