triage-mcp 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- triage_mcp-1.0.0/LICENSE +21 -0
- triage_mcp-1.0.0/PKG-INFO +79 -0
- triage_mcp-1.0.0/README.md +64 -0
- triage_mcp-1.0.0/pyproject.toml +24 -0
- triage_mcp-1.0.0/setup.cfg +4 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/PKG-INFO +79 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/SOURCES.txt +10 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/dependency_links.txt +1 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/entry_points.txt +2 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/requires.txt +1 -0
- triage_mcp-1.0.0/triage_mcp.egg-info/top_level.txt +1 -0
- triage_mcp-1.0.0/triage_mcp.py +174 -0
triage_mcp-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Al Amin Bashir Afara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triage-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server over a support ticket triage queue: four read-only tools for pending reviews and the decision log
|
|
5
|
+
Author: Al Amin Bashir Afara
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aminafara123/triage-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/aminafara123/triage-mcp
|
|
9
|
+
Keywords: mcp,ticket-triage,support-tickets,helpdesk,human-in-the-loop,audit-log
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: mcp
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# triage-mcp
|
|
17
|
+
|
|
18
|
+
An MCP server over my [ai-triage-agent](https://github.com/aminafara123/ai-triage-agent) queue. A local LLM classifies support tickets and drafts replies, a human approves or rejects every one, and this server lets any MCP-capable AI client read that queue and the decision log.
|
|
19
|
+
|
|
20
|
+
It answers questions like "whats waiting for review?" or "what did I reject this week and why?" straight from the files the agent writes. Four tools, all read-only. The model can look at the queue but it cant approve, reject or send anything.
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
| Tool | What it does |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `queue_status()` | Counts for inbox, pending review by priority, outbox and logged decisions |
|
|
27
|
+
| `pending_items(priority)` | Tickets waiting for review, most urgent first, optional priority filter |
|
|
28
|
+
| `recent_decisions(limit)` | Latest human decisions from the audit log, newest first |
|
|
29
|
+
| `search_decisions(query)` | Search the decision log by id, outcome, category, priority or reject reason |
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
.venv/bin/pip install mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It reads `~/aiProjects/aminWork/automation-portfolio/ai-triage-agent/`. Change `DATA_DIR` at the top of `triage_mcp.py` if your copy lives somewhere else.
|
|
39
|
+
|
|
40
|
+
Wire it into Claude Code:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
claude mcp add triage -- /path/to/triage-mcp/.venv/bin/python /path/to/triage-mcp/triage_mcp.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Demo
|
|
47
|
+
|
|
48
|
+
What it looks like from Claude Code:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
> anything urgent sitting in the triage queue?
|
|
52
|
+
|
|
53
|
+
⏺ triage · pending_items()
|
|
54
|
+
|
|
55
|
+
2 pending item(s):
|
|
56
|
+
demo-014 P1 security_incident -> security-team (high): User entered their password on a fake helpdesk link and asks what to do next.
|
|
57
|
+
demo-017 P3 software_issue -> service-desk (medium): Spreadsheet app crashes when opening one monthly report file.
|
|
58
|
+
|
|
59
|
+
⏺ One P1. demo-014 looks like a phishing hit and is routed to security.
|
|
60
|
+
Id review that one first. The Excel crash can wait.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The tickets above are demo data. The format is exactly what the server returns.
|
|
64
|
+
|
|
65
|
+
## Design notes
|
|
66
|
+
|
|
67
|
+
- **Read-only by construction.** No tool writes to the queue, the outbox or the log. Approving stays in `review.py` with a human at the keyboard.
|
|
68
|
+
- **Only metadata crosses the protocol.** Ticket bodies, extracted entities and reply text never leave the server. Summaries and reject reasons are cut to 100 characters with emails and home paths masked.
|
|
69
|
+
- **Small on purpose.** One file, standard library plus the `mcp` SDK.
|
|
70
|
+
|
|
71
|
+
## Honest notes
|
|
72
|
+
|
|
73
|
+
- It only reads what ai-triage-agent has already written. With an empty `pending/` folder most answers are zeros, which is normal between triage runs.
|
|
74
|
+
- The masking is regex based. It catches emails and home paths, not names written into a summary.
|
|
75
|
+
- All ticket content in the demo and the tests is made up. The logic is tested against a synthetic queue in `test_server.py`. Run `python test_server.py`.
|
|
76
|
+
|
|
77
|
+
## About
|
|
78
|
+
|
|
79
|
+
Al Amin Bashir Afara, Dubai · [github.com/aminafara123](https://github.com/aminafara123) · [linkedin.com/in/aminafara](https://www.linkedin.com/in/aminafara)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# triage-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server over my [ai-triage-agent](https://github.com/aminafara123/ai-triage-agent) queue. A local LLM classifies support tickets and drafts replies, a human approves or rejects every one, and this server lets any MCP-capable AI client read that queue and the decision log.
|
|
4
|
+
|
|
5
|
+
It answers questions like "whats waiting for review?" or "what did I reject this week and why?" straight from the files the agent writes. Four tools, all read-only. The model can look at the queue but it cant approve, reject or send anything.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | What it does |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `queue_status()` | Counts for inbox, pending review by priority, outbox and logged decisions |
|
|
12
|
+
| `pending_items(priority)` | Tickets waiting for review, most urgent first, optional priority filter |
|
|
13
|
+
| `recent_decisions(limit)` | Latest human decisions from the audit log, newest first |
|
|
14
|
+
| `search_decisions(query)` | Search the decision log by id, outcome, category, priority or reject reason |
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 -m venv .venv
|
|
20
|
+
.venv/bin/pip install mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It reads `~/aiProjects/aminWork/automation-portfolio/ai-triage-agent/`. Change `DATA_DIR` at the top of `triage_mcp.py` if your copy lives somewhere else.
|
|
24
|
+
|
|
25
|
+
Wire it into Claude Code:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude mcp add triage -- /path/to/triage-mcp/.venv/bin/python /path/to/triage-mcp/triage_mcp.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Demo
|
|
32
|
+
|
|
33
|
+
What it looks like from Claude Code:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
> anything urgent sitting in the triage queue?
|
|
37
|
+
|
|
38
|
+
⏺ triage · pending_items()
|
|
39
|
+
|
|
40
|
+
2 pending item(s):
|
|
41
|
+
demo-014 P1 security_incident -> security-team (high): User entered their password on a fake helpdesk link and asks what to do next.
|
|
42
|
+
demo-017 P3 software_issue -> service-desk (medium): Spreadsheet app crashes when opening one monthly report file.
|
|
43
|
+
|
|
44
|
+
⏺ One P1. demo-014 looks like a phishing hit and is routed to security.
|
|
45
|
+
Id review that one first. The Excel crash can wait.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The tickets above are demo data. The format is exactly what the server returns.
|
|
49
|
+
|
|
50
|
+
## Design notes
|
|
51
|
+
|
|
52
|
+
- **Read-only by construction.** No tool writes to the queue, the outbox or the log. Approving stays in `review.py` with a human at the keyboard.
|
|
53
|
+
- **Only metadata crosses the protocol.** Ticket bodies, extracted entities and reply text never leave the server. Summaries and reject reasons are cut to 100 characters with emails and home paths masked.
|
|
54
|
+
- **Small on purpose.** One file, standard library plus the `mcp` SDK.
|
|
55
|
+
|
|
56
|
+
## Honest notes
|
|
57
|
+
|
|
58
|
+
- It only reads what ai-triage-agent has already written. With an empty `pending/` folder most answers are zeros, which is normal between triage runs.
|
|
59
|
+
- The masking is regex based. It catches emails and home paths, not names written into a summary.
|
|
60
|
+
- All ticket content in the demo and the tests is made up. The logic is tested against a synthetic queue in `test_server.py`. Run `python test_server.py`.
|
|
61
|
+
|
|
62
|
+
## About
|
|
63
|
+
|
|
64
|
+
Al Amin Bashir Afara, Dubai · [github.com/aminafara123](https://github.com/aminafara123) · [linkedin.com/in/aminafara](https://www.linkedin.com/in/aminafara)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "triage-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP server over a support ticket triage queue: four read-only tools for pending reviews and the decision log"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "Al Amin Bashir Afara"}]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["mcp"]
|
|
14
|
+
keywords = ["mcp", "ticket-triage", "support-tickets", "helpdesk", "human-in-the-loop", "audit-log"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/aminafara123/triage-mcp"
|
|
18
|
+
Repository = "https://github.com/aminafara123/triage-mcp"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
triage-mcp = "triage_mcp:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
py-modules = ["triage_mcp"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triage-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server over a support ticket triage queue: four read-only tools for pending reviews and the decision log
|
|
5
|
+
Author: Al Amin Bashir Afara
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aminafara123/triage-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/aminafara123/triage-mcp
|
|
9
|
+
Keywords: mcp,ticket-triage,support-tickets,helpdesk,human-in-the-loop,audit-log
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: mcp
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# triage-mcp
|
|
17
|
+
|
|
18
|
+
An MCP server over my [ai-triage-agent](https://github.com/aminafara123/ai-triage-agent) queue. A local LLM classifies support tickets and drafts replies, a human approves or rejects every one, and this server lets any MCP-capable AI client read that queue and the decision log.
|
|
19
|
+
|
|
20
|
+
It answers questions like "whats waiting for review?" or "what did I reject this week and why?" straight from the files the agent writes. Four tools, all read-only. The model can look at the queue but it cant approve, reject or send anything.
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
| Tool | What it does |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `queue_status()` | Counts for inbox, pending review by priority, outbox and logged decisions |
|
|
27
|
+
| `pending_items(priority)` | Tickets waiting for review, most urgent first, optional priority filter |
|
|
28
|
+
| `recent_decisions(limit)` | Latest human decisions from the audit log, newest first |
|
|
29
|
+
| `search_decisions(query)` | Search the decision log by id, outcome, category, priority or reject reason |
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
.venv/bin/pip install mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It reads `~/aiProjects/aminWork/automation-portfolio/ai-triage-agent/`. Change `DATA_DIR` at the top of `triage_mcp.py` if your copy lives somewhere else.
|
|
39
|
+
|
|
40
|
+
Wire it into Claude Code:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
claude mcp add triage -- /path/to/triage-mcp/.venv/bin/python /path/to/triage-mcp/triage_mcp.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Demo
|
|
47
|
+
|
|
48
|
+
What it looks like from Claude Code:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
> anything urgent sitting in the triage queue?
|
|
52
|
+
|
|
53
|
+
⏺ triage · pending_items()
|
|
54
|
+
|
|
55
|
+
2 pending item(s):
|
|
56
|
+
demo-014 P1 security_incident -> security-team (high): User entered their password on a fake helpdesk link and asks what to do next.
|
|
57
|
+
demo-017 P3 software_issue -> service-desk (medium): Spreadsheet app crashes when opening one monthly report file.
|
|
58
|
+
|
|
59
|
+
⏺ One P1. demo-014 looks like a phishing hit and is routed to security.
|
|
60
|
+
Id review that one first. The Excel crash can wait.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The tickets above are demo data. The format is exactly what the server returns.
|
|
64
|
+
|
|
65
|
+
## Design notes
|
|
66
|
+
|
|
67
|
+
- **Read-only by construction.** No tool writes to the queue, the outbox or the log. Approving stays in `review.py` with a human at the keyboard.
|
|
68
|
+
- **Only metadata crosses the protocol.** Ticket bodies, extracted entities and reply text never leave the server. Summaries and reject reasons are cut to 100 characters with emails and home paths masked.
|
|
69
|
+
- **Small on purpose.** One file, standard library plus the `mcp` SDK.
|
|
70
|
+
|
|
71
|
+
## Honest notes
|
|
72
|
+
|
|
73
|
+
- It only reads what ai-triage-agent has already written. With an empty `pending/` folder most answers are zeros, which is normal between triage runs.
|
|
74
|
+
- The masking is regex based. It catches emails and home paths, not names written into a summary.
|
|
75
|
+
- All ticket content in the demo and the tests is made up. The logic is tested against a synthetic queue in `test_server.py`. Run `python test_server.py`.
|
|
76
|
+
|
|
77
|
+
## About
|
|
78
|
+
|
|
79
|
+
Al Amin Bashir Afara, Dubai · [github.com/aminafara123](https://github.com/aminafara123) · [linkedin.com/in/aminafara](https://www.linkedin.com/in/aminafara)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
triage_mcp.py
|
|
5
|
+
triage_mcp.egg-info/PKG-INFO
|
|
6
|
+
triage_mcp.egg-info/SOURCES.txt
|
|
7
|
+
triage_mcp.egg-info/dependency_links.txt
|
|
8
|
+
triage_mcp.egg-info/entry_points.txt
|
|
9
|
+
triage_mcp.egg-info/requires.txt
|
|
10
|
+
triage_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
triage_mcp
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""triage-mcp: an MCP server over my ai-triage-agent queue.
|
|
3
|
+
|
|
4
|
+
Exposes the triage queue and the human decision log as read-only tools
|
|
5
|
+
any MCP-capable model can call. What is waiting for review, what got
|
|
6
|
+
approved or rejected, and why.
|
|
7
|
+
|
|
8
|
+
Run (stdio): python server.py
|
|
9
|
+
Wire into Claude Code:
|
|
10
|
+
claude mcp add triage -- /path/to/.venv/bin/python /path/to/server.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from mcp.server.mcpserver import MCPServer
|
|
19
|
+
from mcp.types import ToolAnnotations
|
|
20
|
+
|
|
21
|
+
DATA_DIR = Path.home() / "aiProjects/aminWork/automation-portfolio/ai-triage-agent"
|
|
22
|
+
CAP = 25
|
|
23
|
+
|
|
24
|
+
server = MCPServer(
|
|
25
|
+
"triage",
|
|
26
|
+
instructions=(
|
|
27
|
+
"Read-only tools over a local support ticket triage queue. A local "
|
|
28
|
+
"LLM classifies tickets into pending/, a human approves or rejects "
|
|
29
|
+
"each draft reply, and every decision lands in decisions.jsonl."
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Every tool only reads local queue files, so one shared annotation set.
|
|
34
|
+
READ_ONLY = ToolAnnotations(
|
|
35
|
+
read_only_hint=True,
|
|
36
|
+
destructive_hint=False,
|
|
37
|
+
idempotent_hint=True,
|
|
38
|
+
open_world_hint=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
|
|
42
|
+
HOME_PATH = re.compile(r"(/home/|/Users/|[A-Za-z]:\\Users\\)\S*")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _clip(text, n: int = 100) -> str:
|
|
46
|
+
# Summaries and reasons are free text, so emails and home paths go
|
|
47
|
+
# and everything is cut short. Message bodies and replies never leave.
|
|
48
|
+
text = HOME_PATH.sub("[path]", EMAIL.sub("[email]", str(text or "")))
|
|
49
|
+
text = " ".join(text.split())
|
|
50
|
+
return text if len(text) <= n else text[: n - 3] + "..."
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _capped(lines: list) -> str:
|
|
54
|
+
out = "\n".join(lines[:CAP])
|
|
55
|
+
if len(lines) > CAP:
|
|
56
|
+
out += f"\n...and {len(lines) - CAP} more"
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _pending() -> list:
|
|
61
|
+
items = []
|
|
62
|
+
for p in sorted((DATA_DIR / "pending").glob("*.json")):
|
|
63
|
+
try:
|
|
64
|
+
items.append(json.loads(p.read_text(encoding="utf-8")))
|
|
65
|
+
except (json.JSONDecodeError, OSError):
|
|
66
|
+
continue
|
|
67
|
+
return items
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _decisions() -> list:
|
|
71
|
+
log = DATA_DIR / "decisions.jsonl"
|
|
72
|
+
if not log.exists():
|
|
73
|
+
return []
|
|
74
|
+
out = []
|
|
75
|
+
for line in log.read_text(encoding="utf-8").splitlines():
|
|
76
|
+
try:
|
|
77
|
+
out.append(json.loads(line))
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
continue
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _count(folder: str) -> int:
|
|
84
|
+
d = DATA_DIR / folder
|
|
85
|
+
return sum(1 for p in d.iterdir() if p.is_file()) if d.is_dir() else 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fmt_decision(d: dict) -> str:
|
|
89
|
+
line = (f"[{str(d.get('decided', '?'))[:10]}] {_clip(d.get('id'), 40)} "
|
|
90
|
+
f"{d.get('decision', '?')} (AI said {d.get('ai_category', '?')} "
|
|
91
|
+
f"{d.get('ai_priority', '?')})")
|
|
92
|
+
if d.get("reason"):
|
|
93
|
+
line += f": {_clip(d['reason'])}"
|
|
94
|
+
return line
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@server.tool(annotations=READ_ONLY)
|
|
98
|
+
def queue_status() -> str:
|
|
99
|
+
"""Counts across the pipeline: inbox, pending review by priority,
|
|
100
|
+
outbox, and logged decisions by outcome."""
|
|
101
|
+
pending = _pending()
|
|
102
|
+
decisions = _decisions()
|
|
103
|
+
prios = Counter(r.get("triage", {}).get("priority", "?") for r in pending)
|
|
104
|
+
outcomes = Counter(d.get("decision", "?") for d in decisions)
|
|
105
|
+
lines = [
|
|
106
|
+
f"Inbox: {_count('inbox')} message(s)",
|
|
107
|
+
f"Pending review: {len(pending)}"
|
|
108
|
+
+ (" (" + ", ".join(f"{k} {v}" for k, v in sorted(prios.items())) + ")"
|
|
109
|
+
if pending else ""),
|
|
110
|
+
f"Outbox: {_count('outbox')} reply file(s)",
|
|
111
|
+
f"Decisions logged: {len(decisions)}"
|
|
112
|
+
+ (" (" + ", ".join(f"{k} {v}" for k, v in sorted(outcomes.items())) + ")"
|
|
113
|
+
if decisions else ""),
|
|
114
|
+
]
|
|
115
|
+
return "\n".join(lines)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@server.tool(annotations=READ_ONLY)
|
|
119
|
+
def pending_items(priority: str = "") -> str:
|
|
120
|
+
"""Tickets waiting for human review, most urgent first, with category,
|
|
121
|
+
route, model confidence and a short summary. Optional priority filter
|
|
122
|
+
like "P1"."""
|
|
123
|
+
items = _pending()
|
|
124
|
+
if priority:
|
|
125
|
+
items = [r for r in items
|
|
126
|
+
if r.get("triage", {}).get("priority", "").upper() == priority.upper()]
|
|
127
|
+
if not items:
|
|
128
|
+
return "No pending items" + (f" at {priority}." if priority else ".")
|
|
129
|
+
items.sort(key=lambda r: (r.get("triage", {}).get("priority", "P9"),
|
|
130
|
+
str(r.get("id"))))
|
|
131
|
+
lines = []
|
|
132
|
+
for r in items:
|
|
133
|
+
t = r.get("triage", {})
|
|
134
|
+
lines.append(f"{_clip(r.get('id'), 40)} {t.get('priority', '?')} "
|
|
135
|
+
f"{t.get('category', '?')} -> {t.get('route', '?')} "
|
|
136
|
+
f"({t.get('confidence', '?')}): {_clip(t.get('summary'))}")
|
|
137
|
+
return f"{len(items)} pending item(s):\n" + _capped(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@server.tool(annotations=READ_ONLY)
|
|
141
|
+
def recent_decisions(limit: int = 10) -> str:
|
|
142
|
+
"""The latest human decisions from the audit log, newest first: what
|
|
143
|
+
the AI proposed and whether the reviewer approved, edited or rejected."""
|
|
144
|
+
decisions = _decisions()
|
|
145
|
+
if not decisions:
|
|
146
|
+
return "No decisions logged."
|
|
147
|
+
limit = max(1, min(limit, CAP))
|
|
148
|
+
recent = decisions[::-1][:limit]
|
|
149
|
+
return (f"{len(decisions)} decision(s) logged, latest {len(recent)}:\n"
|
|
150
|
+
+ "\n".join(_fmt_decision(d) for d in recent))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@server.tool(annotations=READ_ONLY)
|
|
154
|
+
def search_decisions(query: str) -> str:
|
|
155
|
+
"""Case-insensitive search over the decision log by ticket id, outcome,
|
|
156
|
+
AI category, priority or reject reason."""
|
|
157
|
+
q = query.lower().strip()
|
|
158
|
+
if not q:
|
|
159
|
+
return "Empty query."
|
|
160
|
+
fields = ("id", "decision", "ai_category", "ai_priority", "reason")
|
|
161
|
+
hits = [d for d in reversed(_decisions())
|
|
162
|
+
if any(q in str(d.get(f, "")).lower() for f in fields)]
|
|
163
|
+
if not hits:
|
|
164
|
+
return f"No decisions matching {query!r}."
|
|
165
|
+
return (f"{len(hits)} decision(s) matching {query!r}:\n"
|
|
166
|
+
+ _capped([_fmt_decision(d) for d in hits]))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main():
|
|
170
|
+
server.run()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|