-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreview_queue.py
More file actions
117 lines (93 loc) · 4.38 KB
/
Copy pathreview_queue.py
File metadata and controls
117 lines (93 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
"""Human-review queue CLI — the human-in-the-loop half of governed routing.
Commands:
python scripts/review_queue.py list [--status pending|resolved|all]
python scripts/review_queue.py show TCK-10105
python scripts/review_queue.py decide TCK-10105 --action approve
[--note "reached out by phone"]
Actions: approve (agent handles and closes), send-answer (the withheld AI
draft was actually fine — send it), reassign (route to the standard queue).
Every decision updates outputs/human_queue.json and appends an entry to
outputs/review_log.jsonl, so the human trail is as auditable as the AI one.
"""
import argparse
import getpass
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
QUEUE_PATH = Path("outputs/human_queue.json")
REVIEW_LOG = Path("outputs/review_log.jsonl")
ACTIONS = ["approve", "send-answer", "reassign"]
def load_queue() -> list[dict]:
if not QUEUE_PATH.exists():
sys.exit(f"no human queue at {QUEUE_PATH} — run the pipeline first")
return json.loads(QUEUE_PATH.read_text(encoding="utf-8"))
def save_queue(queue: list[dict]) -> None:
QUEUE_PATH.write_text(json.dumps(queue, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8")
def cmd_list(args) -> None:
queue = load_queue()
rows = [e for e in queue if args.status == "all" or e["status"] == args.status]
pending = sum(1 for e in queue if e["status"] == "pending")
print(f"{len(queue)} tickets in human queue, {pending} pending "
f"(showing {len(rows)}: --status {args.status})\n")
for e in rows:
reason = e["reasons"][0] if e["reasons"] else ""
print(f" {e['ticket_id']} [{e['status']:^8}] {e['severity']} "
f"{e['customer_tier']:<10} conf={e['confidence']:.2f} "
f"{e['subject'][:48]:<48} {reason}")
def cmd_show(args) -> None:
queue = load_queue()
entry = next((e for e in queue if e["ticket_id"] == args.ticket_id), None)
if entry is None:
sys.exit(f"{args.ticket_id} is not in the human queue")
print(json.dumps(entry, indent=2, ensure_ascii=False))
def cmd_decide(args) -> None:
queue = load_queue()
entry = next((e for e in queue if e["ticket_id"] == args.ticket_id), None)
if entry is None:
sys.exit(f"{args.ticket_id} is not in the human queue")
if entry["status"] == "resolved" and not args.force:
sys.exit(f"{args.ticket_id} is already resolved "
f"(by {entry['review']['reviewer']}); use --force to override")
if args.action == "send-answer" and "draft_answer" not in entry:
sys.exit(f"{args.ticket_id} has no withheld AI draft to send")
review = {
"action": args.action,
"note": args.note,
"reviewer": getpass.getuser(),
"timestamp": datetime.now(timezone.utc).isoformat(),
}
entry["status"] = "resolved"
entry["review"] = review
save_queue(queue)
REVIEW_LOG.parent.mkdir(parents=True, exist_ok=True)
with REVIEW_LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps({"ticket_id": entry["ticket_id"], **review},
ensure_ascii=False) + "\n")
remaining = sum(1 for e in queue if e["status"] == "pending")
print(f"{entry['ticket_id']} resolved: {args.action}"
+ (f' — "{args.note}"' if args.note else "")
+ f" ({remaining} still pending)")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = parser.add_subparsers(dest="command", required=True)
p_list = sub.add_parser("list", help="list queue entries")
p_list.add_argument("--status", choices=["pending", "resolved", "all"],
default="pending")
p_list.set_defaults(func=cmd_list)
p_show = sub.add_parser("show", help="show one entry in full")
p_show.add_argument("ticket_id")
p_show.set_defaults(func=cmd_show)
p_decide = sub.add_parser("decide", help="record a review decision")
p_decide.add_argument("ticket_id")
p_decide.add_argument("--action", choices=ACTIONS, required=True)
p_decide.add_argument("--note", default="")
p_decide.add_argument("--force", action="store_true",
help="re-decide an already-resolved ticket")
p_decide.set_defaults(func=cmd_decide)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()