Skip to content

Commit 5bd3c51

Browse files
committed
status card update
1 parent 3729305 commit 5bd3c51

File tree

5 files changed

+85
-32
lines changed

5 files changed

+85
-32
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ Protolink takes a **centralized agent** approach compared to Google's A2A protoc
8888
- Simple interface-based design
8989
- No complex configuration needed for common use cases
9090

91+
Once the Agent has been initiated, it automatically exposes a web interface at `/status` where it exposes the agent's information.
9192

93+
<div align="center">
94+
<img src="https://raw.githubusercontent.com/nMaroulis/protolink/main/docs/assets/agent_status_card.png" alt="Agent Status Card" width="50%">
95+
</div>
9296

9397
## Why Protolink? 🚀
9498
- **Real Multi-Agent Systems**: Build **autonomous agents** with embedded LLMs, tools, and memory that communicate directly.

docs/agents.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ agent = Agent(card_dict, transport, llm)
6161

6262
You can then attach tools and start the agent.
6363

64+
Once the Agent has been initiated, it automatically exposes a web interface at `/status` where it exposes the agent's information.
65+
66+
<div align="center">
67+
<img src="https://raw.githubusercontent.com/nMaroulis/protolink/main/docs/assets/agent_status_card.png" alt="Agent Status Card" width="50%">
68+
</div>
69+
6470
## Agent-to-Agent Communication
6571

6672
Agents communicate over a chosen transport.

docs/assets/agent_status_card.png

1.36 MB
Loading

protolink/core/agent_card.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ class AgentCapabilities:
4040
rag: bool = False
4141
code_execution: bool = False
4242

43+
def as_dict(self) -> dict[str, Any]:
44+
"""Return all capabilities as a dict."""
45+
return asdict(self)
46+
47+
def enabled(self) -> list[str]:
48+
"""Return a list of enabled capabilities (truthy ones)."""
49+
result = []
50+
for k, v in asdict(self).items():
51+
if isinstance(v, bool) and v:
52+
result.append(k)
53+
elif isinstance(v, int) and v > 0:
54+
result.append(f"{k}: {v}")
55+
return result
56+
4357

4458
@dataclass
4559
class AgentSkill:

protolink/utils/renderers.py

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,29 @@ def _now_utc() -> str:
3434

3535

3636
def to_status_html(agent: AgentCard, start_time: float) -> str:
37-
"""Render a stylish agent status page with logo, ping, favicon, and uptime."""
37+
def _fmt(value: str | None, default: str = "—") -> str:
38+
from html import escape
39+
40+
return escape(value) if value else default
41+
42+
def _list(items: list[str], empty: str = "None") -> str:
43+
if not items:
44+
return f"<li><em>{empty}</em></li>"
45+
return "".join(f"<li>{escape(item)}</li>" for item in items)
46+
47+
import json
48+
3849
return f"""<!DOCTYPE html>
3950
<html lang="en">
4051
<head>
4152
<meta charset="utf-8" />
4253
<meta name="viewport" content="width=device-width, initial-scale=1" />
43-
<title>{escape(agent.name)} · Agent Status</title>
54+
<title>{_fmt(agent.name)} · Agent Status</title>
4455
<link rel="icon" href="https://raw.githubusercontent.com/nMaroulis/protolink/main/docs/assets/logo_sm.png" />
4556
<style>
4657
:root {{
4758
--bg: #070b1a;
48-
--card: rgba(17, 22, 42, 0.85);
59+
--card-base: rgba(17, 22, 42, 0.85);
4960
--border: rgba(56, 189, 248, 0.25);
5061
--text: #e5e7eb;
5162
--muted: #9ca3af;
@@ -72,13 +83,21 @@ def to_status_html(agent: AgentCard, start_time: float) -> str:
7283
.logo img:hover {{ opacity: 1; }}
7384
.card {{
7485
width: min(520px, 92vw);
75-
background: var(--card);
76-
backdrop-filter: blur(10px);
86+
background: var(--card-base);
87+
backdrop-filter: blur(12px);
7788
border: 1px solid var(--border);
7889
border-radius: 18px;
7990
padding: 26px 28px;
8091
box-shadow: 0 20px 60px rgba(0,0,0,.6), inset 0 0 0 1px rgba(255,255,255,.02);
81-
transition: transform .2s ease, box-shadow .2s ease;
92+
transition: transform .2s ease, box-shadow .2s ease, background-position 5s linear;
93+
background: linear-gradient(135deg, #11162a, #0b3a55, #11162a);
94+
background-size: 400% 400%;
95+
animation: gradientShift 30s ease infinite;
96+
}}
97+
@keyframes gradientShift {{
98+
0% {{ background-position: 0% 50%; }}
99+
50% {{ background-position: 100% 50%; }}
100+
100% {{ background-position: 0% 50%; }}
82101
}}
83102
.card:hover {{ transform: translateY(-2px); box-shadow: 0 30px 80px rgba(0,0,0,.7); }}
84103
header {{
@@ -100,16 +119,16 @@ def to_status_html(agent: AgentCard, start_time: float) -> str:
100119
}}
101120
.status::before {{
102121
content: "";
103-
width: 8px;
104-
height: 8px;
122+
width: 10px;
123+
height: 10px;
105124
border-radius: 50%;
106125
background: currentColor;
107-
box-shadow: 0 0 0 0 rgba(34,197,94,.7);
108-
animation: pulse 2s infinite;
126+
box-shadow: 0 0 6px currentColor;
127+
animation: pulse 1.8s infinite;
109128
}}
110129
@keyframes pulse {{
111130
0% {{ box-shadow: 0 0 0 0 rgba(34,197,94,.7); }}
112-
70% {{ box-shadow: 0 0 0 8px rgba(34,197,94,0); }}
131+
50% {{ box-shadow: 0 0 12px 6px rgba(34,197,94,0.2); }}
113132
100% {{ box-shadow: 0 0 0 0 rgba(34,197,94,0); }}
114133
}}
115134
.grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; margin-bottom: 20px; }}
@@ -119,7 +138,7 @@ def to_status_html(agent: AgentCard, start_time: float) -> str:
119138
section h2 {{ font-size: .75rem; margin: 0 0 8px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }}
120139
ul {{ margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }}
121140
ul li {{ background: var(--accent-soft); border: 1px solid var(--border); padding: 4px 10px; border-radius: 999px; font-size: .75rem; }}
122-
.actions {{ margin-top: 22px; display: flex; align-items: center; gap: 12px; }}
141+
.actions {{ margin-top: 22px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }}
123142
button {{
124143
background: linear-gradient(135deg, transparent, rgba(56,189,248,.08));
125144
border: 1px solid var(--border);
@@ -134,13 +153,17 @@ def to_status_html(agent: AgentCard, start_time: float) -> str:
134153
button:disabled {{ opacity: .6; cursor: not-allowed; }}
135154
.ping-result {{ font-size: .75rem; color: var(--muted); }}
136155
footer {{ margin-top: 22px; display: flex; justify-content: space-between; font-size: .7rem; color: var(--muted); border-top: 1px solid var(--border); padding-top: 10px; }}
137-
.uptime {{ font-size: .75rem; color: var(--accent); }}
138-
156+
.uptime {{
157+
font-size: .75rem;
158+
color: var(--accent);
159+
text-shadow: 0 0 6px var(--accent);
160+
transition: text-shadow .3s ease;
161+
}}
162+
.uptime:hover {{ text-shadow: 0 0 12px var(--accent), 0 0 24px var(--accent-soft); }}
139163
</style>
140164
</head>
141165
<body>
142166
<div class="card">
143-
144167
<header>
145168
<div class="logo">
146169
<img src="https://raw.githubusercontent.com/nMaroulis/protolink/main/docs/assets/logo_sm.png" alt="Protolink logo" />
@@ -149,33 +172,39 @@ def to_status_html(agent: AgentCard, start_time: float) -> str:
149172
<div id="status" class="status">RUNNING</div>
150173
</header>
151174
175+
<p style="color: var(--muted); font-size:.85rem; margin-bottom:16px;">{_fmt(agent.description)}</p>
176+
152177
<div class="grid">
178+
<div><div class="label">Version</div><div class="value">{_fmt(agent.version)}</div></div>
179+
<div><div class="label">Protocol</div><div class="value">{_fmt(agent.protocol_version)}</div></div>
180+
<div><div class="label">Transport</div><div class="value">{_fmt(agent.transport.upper())}</div></div>
181+
<div><div class="label">Endpoint</div><div class="value">{_fmt(agent.url)}</div></div>
182+
</div>
183+
184+
<section>
185+
<h2>Capabilities</h2>
186+
<ul>{_list([str(c) for c in agent.capabilities.enabled() or []], empty="None")}</ul>
187+
</section>
188+
189+
<section style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;">
153190
<div>
154-
<div class="label">Version</div>
155-
<div class="value">{_fmt(agent.version)}</div>
156-
</div>
157-
<div>
158-
<div class="label">Protocol</div>
159-
<div class="value">{_fmt(agent.protocol_version)}</div>
160-
</div>
161-
<div>
162-
<div class="label">Transport</div>
163-
<div class="value">{_fmt(agent.transport.upper())}</div>
191+
<h2>Input Formats</h2>
192+
<ul>{_list(agent.input_formats, empty="text/plain")}</ul>
164193
</div>
165194
<div>
166-
<div class="label">Endpoint</div>
167-
<div class="value">{_fmt(agent.url)}</div>
195+
<h2>Output Formats</h2>
196+
<ul>{_list(agent.output_formats, empty="text/plain")}</ul>
168197
</div>
169-
</div>
198+
</section>
170199
171200
<section>
172-
<h2>Skills</h2>
173-
<ul>{_list([s.name for s in agent.skills], empty="No skills declared")}</ul>
201+
<h2>Security Schemes</h2>
202+
<ul>{_list([f"{k}: {json.dumps(v)}" for k, v in (agent.security_schemes or {}).items()], empty="None")}</ul>
174203
</section>
175204
176205
<section>
177-
<h2>Formats</h2>
178-
<ul>{_list(agent.input_formats + agent.output_formats, empty="text/plain")}</ul>
206+
<h2>Skills</h2>
207+
<ul>{_list([s.id for s in agent.skills], empty="No skills declared")}</ul>
179208
</section>
180209
181210
<div class="actions">

0 commit comments

Comments
 (0)