Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 90fb0eb

Browse files
mosiddiCopilot
andcommitted
feat: add trust score visualization dashboard
Self-contained HTML dashboard with Chart.js showing trust scores, history, and tier distribution for all registered agents. Closes #157 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 12104c4 commit 90fb0eb

3 files changed

Lines changed: 496 additions & 0 deletions

File tree

examples/trust-dashboard/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Trust Score Visualization Dashboard
2+
3+
A self-contained HTML dashboard for visualizing AgentMesh trust scores,
4+
history, and tier distribution. Uses only the Python standard library and
5+
Chart.js (loaded from CDN) — no external Python packages required.
6+
7+
## Quick Start
8+
9+
```bash
10+
cd examples/trust-dashboard
11+
python demo.py
12+
```
13+
14+
Then open <http://localhost:8050> in your browser.
15+
16+
The demo creates sample agents with varying trust scores, starts the
17+
dashboard server, and simulates trust score changes every few seconds so
18+
you can watch the charts update in real time.
19+
20+
## Running the Dashboard Standalone
21+
22+
```bash
23+
python dashboard.py # default port 8050
24+
python dashboard.py --port 9090 # custom port
25+
```
26+
27+
## Features
28+
29+
| Feature | Description |
30+
|---|---|
31+
| **Agent Scores** | Horizontal bar chart of current trust scores for every registered agent |
32+
| **Score History** | Line chart showing how each agent's score evolved over time |
33+
| **Tier Distribution** | Doughnut chart of trust-tier counts (Verified Partner → Untrusted) |
34+
| **Auto-Refresh** | Page polls `/api/data` every 5 seconds and re-renders charts |
35+
36+
## Trust Tiers
37+
38+
| Tier | Score Range |
39+
|---|---|
40+
| Verified Partner | 900 – 1000 |
41+
| Trusted | 700 – 899 |
42+
| Standard | 500 – 699 |
43+
| Probationary | 300 – 499 |
44+
| Untrusted | 0 – 299 |
45+
46+
## Architecture
47+
48+
```
49+
demo.py — creates agents, starts dashboard, simulates changes
50+
dashboard.py — http.server serving HTML + JSON API (/api/data)
51+
```
52+
53+
Both files are pure Python 3.9+ stdlib. The HTML page loads
54+
[Chart.js](https://www.chartjs.org/) from a CDN for rendering.
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
"""
2+
AgentMesh Trust Score Dashboard (stdlib-only)
3+
=============================================
4+
Serves a self-contained HTML dashboard that visualises trust scores,
5+
score history, and trust-tier distribution for registered agents.
6+
7+
Usage:
8+
python dashboard.py [--port PORT]
9+
10+
The page auto-refreshes every 5 seconds by polling ``/api/data``.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import argparse
16+
import json
17+
import threading
18+
from http.server import HTTPServer, BaseHTTPRequestHandler
19+
from typing import Any
20+
21+
# ---------------------------------------------------------------------------
22+
# Shared data store — mutate via ``update_data()``
23+
# ---------------------------------------------------------------------------
24+
25+
_lock = threading.Lock()
26+
_data: dict[str, Any] = {
27+
"agents": {}, # name -> {score, protocol, did}
28+
"history": {}, # name -> [(timestamp_iso, score), ...]
29+
"tiers": { # tier_name -> count
30+
"Verified Partner": 0,
31+
"Trusted": 0,
32+
"Standard": 0,
33+
"Probationary": 0,
34+
"Untrusted": 0,
35+
},
36+
}
37+
38+
TIER_RANGES = [
39+
("Verified Partner", 900, 1000),
40+
("Trusted", 700, 899),
41+
("Standard", 500, 699),
42+
("Probationary", 300, 499),
43+
("Untrusted", 0, 299),
44+
]
45+
46+
47+
def _tier_for_score(score: int) -> str:
48+
for name, lo, hi in TIER_RANGES:
49+
if lo <= score <= hi:
50+
return name
51+
return "Untrusted"
52+
53+
54+
def _recompute_tiers() -> None:
55+
counts = {name: 0 for name, _, _ in TIER_RANGES}
56+
for info in _data["agents"].values():
57+
counts[_tier_for_score(info["score"])] += 1
58+
_data["tiers"] = counts
59+
60+
61+
def update_data(
62+
agents: dict[str, dict] | None = None,
63+
history: dict[str, list] | None = None,
64+
) -> None:
65+
"""Thread-safe update of the shared data store."""
66+
with _lock:
67+
if agents is not None:
68+
_data["agents"] = agents
69+
_recompute_tiers()
70+
if history is not None:
71+
_data["history"] = history
72+
73+
74+
def get_data() -> dict[str, Any]:
75+
"""Return a snapshot of the current data."""
76+
with _lock:
77+
return json.loads(json.dumps(_data))
78+
79+
80+
# ---------------------------------------------------------------------------
81+
# HTML page (embedded)
82+
# ---------------------------------------------------------------------------
83+
84+
_HTML_PAGE = r"""<!DOCTYPE html>
85+
<html lang="en">
86+
<head>
87+
<meta charset="utf-8" />
88+
<title>AgentMesh Trust Dashboard</title>
89+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
90+
<style>
91+
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;
92+
--accent:#58a6ff}
93+
*{box-sizing:border-box;margin:0;padding:0}
94+
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);
95+
padding:24px}
96+
h1{text-align:center;margin-bottom:24px;color:var(--accent)}
97+
.grid{display:grid;gap:20px}
98+
.two{grid-template-columns:1fr 1fr}
99+
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;
100+
padding:20px}
101+
.card h2{font-size:1rem;margin-bottom:12px;color:var(--accent)}
102+
canvas{width:100%!important}
103+
.kpi-row{display:flex;gap:16px;justify-content:center;margin-bottom:20px;
104+
flex-wrap:wrap}
105+
.kpi{background:var(--card);border:1px solid var(--border);border-radius:8px;
106+
padding:14px 28px;text-align:center;min-width:140px}
107+
.kpi .value{font-size:1.6rem;font-weight:700;color:var(--accent)}
108+
.kpi .label{font-size:.75rem;color:#8b949e;margin-top:4px}
109+
.tier-badge{display:inline-block;padding:2px 8px;border-radius:4px;
110+
font-size:.75rem;font-weight:600;margin-left:6px}
111+
.tier-verified{background:#1a7f37;color:#fff}
112+
.tier-trusted{background:#2ea043;color:#fff}
113+
.tier-standard{background:#d29922;color:#fff}
114+
.tier-probationary{background:#db6d28;color:#fff}
115+
.tier-untrusted{background:#da3633;color:#fff}
116+
#agent-table{width:100%;border-collapse:collapse;font-size:.85rem}
117+
#agent-table th,#agent-table td{padding:8px 12px;text-align:left;
118+
border-bottom:1px solid var(--border)}
119+
#agent-table th{color:#8b949e;font-weight:600}
120+
.bar-cell{position:relative;height:20px;background:#21262d;border-radius:4px;
121+
overflow:hidden}
122+
.bar-fill{height:100%;border-radius:4px;transition:width .4s}
123+
@media(max-width:860px){.two{grid-template-columns:1fr}}
124+
</style>
125+
</head>
126+
<body>
127+
<h1>&#x1f6e1; AgentMesh Trust Dashboard</h1>
128+
<div class="kpi-row" id="kpis"></div>
129+
<div class="card" style="margin-bottom:20px;overflow-x:auto">
130+
<h2>Registered Agents</h2>
131+
<table id="agent-table"><thead><tr>
132+
<th>Agent</th><th>Protocol</th><th>Score</th><th>Tier</th><th></th>
133+
</tr></thead><tbody id="agent-tbody"></tbody></table>
134+
</div>
135+
<div class="grid two">
136+
<div class="card"><h2>Trust Score History</h2>
137+
<canvas id="historyChart"></canvas></div>
138+
<div class="card"><h2>Trust Tier Distribution</h2>
139+
<canvas id="tierChart"></canvas></div>
140+
</div>
141+
<script>
142+
const TIER_COLORS={
143+
"Verified Partner":"#1a7f37","Trusted":"#2ea043",
144+
"Standard":"#d29922","Probationary":"#db6d28","Untrusted":"#da3633"};
145+
const TIER_CSS={
146+
"Verified Partner":"verified","Trusted":"trusted","Standard":"standard",
147+
"Probationary":"probationary","Untrusted":"untrusted"};
148+
const LINE_COLORS=[
149+
"#58a6ff","#f78166","#3fb950","#d2a8ff","#79c0ff",
150+
"#ffa657","#7ee787","#ff7b72","#a5d6ff","#d29922"];
151+
152+
function tierFor(s){
153+
if(s>=900) return "Verified Partner";
154+
if(s>=700) return "Trusted";
155+
if(s>=500) return "Standard";
156+
if(s>=300) return "Probationary";
157+
return "Untrusted";
158+
}
159+
function barColor(s){return TIER_COLORS[tierFor(s)];}
160+
161+
let histChart=null, tierChart=null;
162+
163+
function renderKPIs(agents){
164+
const scores=Object.values(agents).map(a=>a.score);
165+
const n=scores.length, avg=n? (scores.reduce((a,b)=>a+b,0)/n).toFixed(0) :0;
166+
const mn=n? Math.min(...scores):0, mx=n? Math.max(...scores):0;
167+
document.getElementById("kpis").innerHTML=
168+
kpi("Agents",n)+kpi("Avg Score",avg)+kpi("Min",mn)+kpi("Max",mx);
169+
}
170+
function kpi(label,value){
171+
return `<div class="kpi"><div class="value">${value}</div><div class="label">${label}</div></div>`;
172+
}
173+
174+
function renderTable(agents){
175+
const tbody=document.getElementById("agent-tbody");
176+
const sorted=Object.entries(agents).sort((a,b)=>b[1].score-a[1].score);
177+
tbody.innerHTML=sorted.map(([name,info])=>{
178+
const tier=tierFor(info.score);
179+
const pct=(info.score/1000*100).toFixed(1);
180+
return `<tr><td><b>${name}</b></td><td>${info.protocol||""}</td>
181+
<td>${info.score}</td>
182+
<td><span class="tier-badge tier-${TIER_CSS[tier]}">${tier}</span></td>
183+
<td style="width:30%"><div class="bar-cell"><div class="bar-fill"
184+
style="width:${pct}%;background:${barColor(info.score)}"></div></div></td></tr>`;
185+
}).join("");
186+
}
187+
188+
function renderHistory(history){
189+
const datasets=[];
190+
let idx=0;
191+
for(const[name,pts] of Object.entries(history)){
192+
datasets.push({
193+
label:name,
194+
data:pts.map(p=>({x:p[0],y:p[1]})),
195+
borderColor:LINE_COLORS[idx%LINE_COLORS.length],
196+
borderWidth:2,fill:false,tension:.3,pointRadius:0
197+
});
198+
idx++;
199+
}
200+
if(histChart){histChart.data.datasets=datasets;histChart.update();}
201+
else{
202+
histChart=new Chart(document.getElementById("historyChart"),{
203+
type:"line",data:{datasets},
204+
options:{responsive:true,
205+
scales:{x:{type:"category",ticks:{maxTicksToShow:10,color:"#8b949e"}},
206+
y:{min:0,max:1000,ticks:{color:"#8b949e"},grid:{color:"#21262d"}}},
207+
plugins:{legend:{labels:{color:"#c9d1d9",boxWidth:12}}}}
208+
});
209+
}
210+
}
211+
212+
function renderTiers(tiers){
213+
const labels=Object.keys(tiers);
214+
const data=Object.values(tiers);
215+
const bg=labels.map(l=>TIER_COLORS[l]);
216+
if(tierChart){tierChart.data.datasets[0].data=data;tierChart.update();}
217+
else{
218+
tierChart=new Chart(document.getElementById("tierChart"),{
219+
type:"doughnut",
220+
data:{labels,datasets:[{data,backgroundColor:bg,borderWidth:0}]},
221+
options:{responsive:true,
222+
plugins:{legend:{labels:{color:"#c9d1d9"}}}}
223+
});
224+
}
225+
}
226+
227+
async function refresh(){
228+
try{
229+
const r=await fetch("/api/data");
230+
const d=await r.json();
231+
renderKPIs(d.agents);
232+
renderTable(d.agents);
233+
renderHistory(d.history);
234+
renderTiers(d.tiers);
235+
}catch(e){console.error("refresh failed",e);}
236+
}
237+
238+
refresh();
239+
setInterval(refresh,5000);
240+
</script>
241+
</body>
242+
</html>"""
243+
244+
245+
# ---------------------------------------------------------------------------
246+
# HTTP request handler
247+
# ---------------------------------------------------------------------------
248+
249+
class _Handler(BaseHTTPRequestHandler):
250+
"""Serves the HTML page at ``/`` and JSON data at ``/api/data``."""
251+
252+
def do_GET(self) -> None: # noqa: N802
253+
if self.path == "/api/data":
254+
payload = json.dumps(get_data()).encode()
255+
self._respond(200, "application/json", payload)
256+
elif self.path in ("/", "/index.html"):
257+
self._respond(200, "text/html; charset=utf-8", _HTML_PAGE.encode())
258+
else:
259+
self._respond(404, "text/plain", b"Not Found")
260+
261+
def _respond(self, code: int, content_type: str, body: bytes) -> None:
262+
self.send_response(code)
263+
self.send_header("Content-Type", content_type)
264+
self.send_header("Content-Length", str(len(body)))
265+
self.send_header("Access-Control-Allow-Origin", "*")
266+
self.end_headers()
267+
self.wfile.write(body)
268+
269+
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
270+
"""Silence default request logging."""
271+
pass
272+
273+
274+
# ---------------------------------------------------------------------------
275+
# Server lifecycle
276+
# ---------------------------------------------------------------------------
277+
278+
def start_server(port: int = 8050) -> HTTPServer:
279+
"""Start the dashboard server in a daemon thread and return the server."""
280+
server = HTTPServer(("", port), _Handler)
281+
t = threading.Thread(target=server.serve_forever, daemon=True)
282+
t.start()
283+
print(f"Dashboard running at http://localhost:{port}")
284+
return server
285+
286+
287+
def main() -> None:
288+
parser = argparse.ArgumentParser(description="AgentMesh Trust Dashboard")
289+
parser.add_argument("--port", type=int, default=8050)
290+
args = parser.parse_args()
291+
292+
# Seed with demo data so the page isn't blank
293+
_seed_demo_data()
294+
server = start_server(args.port)
295+
try:
296+
server.serve_forever()
297+
except KeyboardInterrupt:
298+
print("\nShutting down.")
299+
server.shutdown()
300+
301+
302+
def _seed_demo_data() -> None:
303+
"""Populate the store with sample agents for standalone use."""
304+
import datetime as dt
305+
import random
306+
307+
random.seed(42)
308+
agents = {
309+
"payment-agent": {"score": 920, "protocol": "A2A", "did": "did:web:payments.mesh.io"},
310+
"customer-service": {"score": 870, "protocol": "A2A", "did": "did:web:cs.mesh.io"},
311+
"data-analyst": {"score": 810, "protocol": "MCP", "did": "did:web:analytics.mesh.io"},
312+
"fraud-detector": {"score": 940, "protocol": "IATP", "did": "did:web:fraud.mesh.io"},
313+
"inventory-manager": {"score": 720, "protocol": "MCP", "did": "did:web:inventory.mesh.io"},
314+
"email-dispatcher": {"score": 650, "protocol": "A2A", "did": "did:web:email.mesh.io"},
315+
"auth-gateway": {"score": 950, "protocol": "IATP", "did": "did:web:auth.mesh.io"},
316+
"report-generator": {"score": 580, "protocol": "MCP", "did": "did:web:reports.mesh.io"},
317+
"scheduler": {"score": 780, "protocol": "A2A", "did": "did:web:scheduler.mesh.io"},
318+
"compliance-bot": {"score": 890, "protocol": "IATP", "did": "did:web:compliance.mesh.io"},
319+
}
320+
321+
now = dt.datetime.now(dt.timezone.utc)
322+
history: dict[str, list] = {}
323+
for name, info in agents.items():
324+
pts = []
325+
score = info["score"]
326+
for i in range(48):
327+
t = now - dt.timedelta(minutes=15 * (47 - i))
328+
score = max(0, min(1000, score + random.randint(-15, 15)))
329+
pts.append((t.strftime("%H:%M"), score))
330+
# Reset final score to the canonical value
331+
pts[-1] = (pts[-1][0], info["score"])
332+
history[name] = pts
333+
334+
update_data(agents=agents, history=history)
335+
336+
337+
if __name__ == "__main__":
338+
main()

0 commit comments

Comments
 (0)