Skip to content

Commit fb79065

Browse files
committed
NEW registry transport, server and status
1 parent 5bd3c51 commit fb79065

File tree

14 files changed

+1197
-80
lines changed

14 files changed

+1197
-80
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,6 @@ 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.
92-
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>
96-
9791
## Why Protolink? 🚀
9892
- **Real Multi-Agent Systems**: Build **autonomous agents** with embedded LLMs, tools, and memory that communicate directly.
9993
- **Simple API**: Built from the ground-up for **minimal boilerplate**, letting you focus on agent logic rather than infrastructure.
@@ -142,7 +136,7 @@ cd protolink
142136
uv pip install -e ".[dev]"
143137
```
144138

145-
## Quick Start
139+
## Hello World Example
146140

147141
```python
148142
from protolink.agents import Agent
@@ -183,6 +177,12 @@ agent.add_tool(mcp_tool)
183177
agent.start()
184178
```
185179

180+
Once the Agent has been initiated, it automatically exposes a web interface at `/status` where it exposes the agent's information.
181+
182+
<div align="center">
183+
<img src="https://raw.githubusercontent.com/nMaroulis/protolink/main/docs/assets/agent_status_card.png" alt="Agent Status Card" width="50%">
184+
</div>
185+
186186
## Documentation
187187

188188
Follow the API documentation here: [Documentation](https://nmaroulis.github.io/protolink/)

examples/notebooks/agent_registry_test/registry.ipynb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
"registry = Registry(transport=transport)\n",
3232
"\n",
3333
"# Start registry\n",
34-
"await registry.start()\n",
35-
"print(\"Registry is running...\")"
34+
"await registry.start()"
3635
]
3736
}
3837
],

examples/notebooks/agent_registry_test/weather_agent.ipynb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,15 @@
4040
" return task.complete(f\"Weather data: {result}\")\n",
4141
"\n",
4242
"\n",
43-
"# -------------------------\n",
44-
"# Transports\n",
45-
"# -------------------------\n",
46-
"\n",
4743
"# A2A Transport\n",
4844
"transport = HTTPAgentTransport(url=URL)\n",
4945
"\n",
5046
"# Define Agent Card using the Agent Card class\n",
5147
"card = AgentCard(url=URL, name=\"WeatherAgent\", description=\"Produces weather data\")\n",
5248
"\n",
49+
"# Initialize the agent\n",
5350
"agent = WeatherAgent(card=card, transport=transport, registry=REGISTRY_URL)\n",
5451
"\n",
55-
"# -------------------------\n",
56-
"# Tools\n",
57-
"# -------------------------\n",
58-
"\n",
5952
"\n",
6053
"# Add Native tool using the decorator\n",
6154
"@agent.tool(name=\"get_weather\", description=\"Return weather data for a city\")\n",
@@ -64,7 +57,7 @@
6457
" return {\"city\": city, \"temperature\": 28, \"condition\": \"sunny\"}\n",
6558
"\n",
6659
"\n",
67-
"await agent.start(register=False)"
60+
"await agent.start(register=True)"
6861
]
6962
},
7063
{

protolink/agents/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __init__(
102102
self.start_time: float | None = None
103103

104104
# ----------------------------------------------------------------------
105-
# Agent Lifecycle Init - A2A Server Operations
105+
# Agent Server Lifecycle - A2A Operations
106106
# ----------------------------------------------------------------------
107107

108108
async def start(self, *, register: bool = True) -> None:

protolink/discovery/registry.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# protolink/registry/registry.py
2+
import time
23
from typing import Any
34

45
from protolink.client import RegistryClient
56
from protolink.models import AgentCard
7+
from protolink.server import RegistryServer
68
from protolink.transport import HTTPRegistryTransport, RegistryTransport
79
from protolink.utils.logging import get_logger
10+
from protolink.utils.renderers import to_registry_status_html
811

912

1013
class Registry:
@@ -28,49 +31,66 @@ def __init__(self, transport: RegistryTransport | None = None, url: str | None =
2831

2932
# Create default HTTP transport if none provided
3033
if transport is None:
31-
transport = HTTPRegistryTransport(url=url)
32-
33-
self.transport = transport
34-
self.client = RegistryClient(self.transport)
34+
if url is None:
35+
raise ValueError("At least one of transport or url must be provided")
36+
else:
37+
self.logger.info(f"Creating default HTTPRegistryTransport using the provided URL: {url}")
38+
transport = HTTPRegistryTransport(url=url)
3539

3640
# Local store for agent cards
3741
self._agents: dict[str, AgentCard] = {}
3842

39-
# Wire server-side handlers
40-
self.transport._register_local = self._register_local
41-
self.transport._unregister_local = self._unregister_local
42-
self.transport._discover_local = self._discover_local
43+
self.start_time: float | None = None
44+
45+
# Setup registry client
46+
self._client = RegistryClient(transport)
47+
48+
# Setup registry server
49+
self._server = RegistryServer(
50+
transport,
51+
register_handler=self.handle_register,
52+
unregister_handler=self.handle_unregister,
53+
discover_handler=self.handle_discover,
54+
status_handler=self.handle_status_html,
55+
)
4356

4457
# ------------------------------------------------------------------
45-
# Lifecycle
58+
# Registry Server Lifecycle
4659
# ------------------------------------------------------------------
4760

4861
async def start(self) -> None:
4962
"""Start the registry server via the transport."""
50-
await self.transport.start()
63+
if self._server:
64+
try:
65+
await self._server.start()
66+
except Exception as e:
67+
self.logger.exception(f"Unexpected error during server start: {e}")
68+
raise
69+
self.start_time = time.time()
5170

5271
async def stop(self) -> None:
5372
"""Stop the registry server via the transport."""
54-
await self.transport.stop()
73+
if self._server:
74+
await self._server.stop()
5575

5676
# ------------------------------------------------------------------
5777
# Client API (agents call these)
5878
# ------------------------------------------------------------------
5979

6080
async def register(self, card: AgentCard) -> None:
61-
await self.client.register(card)
81+
await self._client.register(card)
6282

6383
async def unregister(self, agent_url: str) -> None:
64-
await self.client.unregister(agent_url)
84+
await self._client.unregister(agent_url)
6585

6686
async def discover(self, filter_by: dict[str, Any] | None = None) -> list[AgentCard]:
67-
return await self.client.discover(filter_by)
87+
return await self._client.discover(filter_by)
6888

6989
# ------------------------------------------------------------------
7090
# Server-side handlers
7191
# ------------------------------------------------------------------
7292

73-
async def _register_local(self, card: AgentCard) -> None:
93+
async def handle_register(self, card: AgentCard) -> None:
7494
self._agents[card.url] = card
7595

7696
self.logger.info(
@@ -81,10 +101,10 @@ async def _register_local(self, card: AgentCard) -> None:
81101
},
82102
)
83103

84-
async def _unregister_local(self, agent_url: str) -> None:
104+
async def handle_unregister(self, agent_url: str) -> None:
85105
self._agents.pop(agent_url, None)
86106

87-
async def _discover_local(self, filter_by: dict[str, Any] | None = None) -> list[AgentCard]:
107+
async def handle_discover(self, filter_by: dict[str, Any] | None = None) -> list[AgentCard]:
88108
if not filter_by:
89109
return list(self._agents.values())
90110

@@ -93,6 +113,14 @@ def match(card: AgentCard) -> bool:
93113

94114
return [c for c in self._agents.values() if match(c)]
95115

116+
def handle_status_html(self) -> str:
117+
"""Return the registry's status as HTML.
118+
119+
Returns:
120+
HTML string with registry status information
121+
"""
122+
return to_registry_status_html("Registry", "HTTP", self._agents, self.start_time)
123+
96124
# ------------------------------------------------------------------
97125
# Utilities
98126
# ------------------------------------------------------------------

protolink/llms/api/openai_client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ def _to_message(self, response: Any) -> Message:
6767
"""Convert OpenAI completion to internal Message format."""
6868

6969
output_text: str = ""
70-
print(type(response))
71-
print(response)
7270
for item in response.output or []:
7371
# item: ResponseOutputMessage
7472
if item.type != "message":

protolink/server/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from .agent_server import AgentServer
1+
from .agent import AgentServer
2+
from .registry import RegistryServer
23

34
__all__ = [
45
"AgentServer",
6+
"RegistryServer",
57
]

protolink/server/registry.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Registry server implementation for handling incoming requests."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Awaitable, Callable
6+
from typing import Any
7+
8+
from protolink.models import AgentCard
9+
from protolink.transport import RegistryTransport
10+
11+
12+
class RegistryServer:
13+
"""Thin wrapper that wires a task handler into a transport."""
14+
15+
def __init__(
16+
self,
17+
transport: RegistryTransport,
18+
register_handler: Callable[[AgentCard], Awaitable[None]] | None = None,
19+
unregister_handler: Callable[[str], Awaitable[None]] | None = None,
20+
discover_handler: Callable[[dict[str, Any]], Awaitable[list[AgentCard]]] | None = None,
21+
status_handler: Callable[[], Awaitable[str]] | None = None,
22+
) -> None:
23+
if transport is None:
24+
raise ValueError("RegistryServer requires a transport instance")
25+
26+
self._transport = transport
27+
self._register_handler = None
28+
self._unregister_handler = None
29+
self._discover_handler = None
30+
self._status_handler = None
31+
self._is_running = False
32+
33+
if register_handler is not None:
34+
self.set_register_handler(register_handler)
35+
36+
if unregister_handler is not None:
37+
self.set_unregister_handler(unregister_handler)
38+
39+
if discover_handler is not None:
40+
self.set_discover_handler(discover_handler)
41+
42+
if status_handler is not None:
43+
self.set_status_handler(status_handler)
44+
45+
def set_register_handler(self, handler: Callable[[AgentCard], Awaitable[None]]) -> None:
46+
"""Register the coroutine used to process incoming register requests from agents."""
47+
48+
self._register_handler = handler
49+
self._transport.on_register_received(handler)
50+
51+
def set_unregister_handler(self, handler: Callable[[str], Awaitable[None]]) -> None:
52+
"""Register the coroutine used to process incoming unregister requests from agents."""
53+
54+
self._unregister_handler = handler
55+
self._transport.on_unregister_received(handler)
56+
57+
def set_discover_handler(self, handler: Callable[[dict[str, Any]], Awaitable[list[AgentCard]]]) -> None:
58+
"""Register the coroutine used to process incoming discover requests from agents."""
59+
60+
self._discover_handler = handler
61+
self._transport.on_discover_received(handler)
62+
63+
def set_status_handler(self, handler: Callable[[], Awaitable[str]]) -> None:
64+
"""Register the coroutine used to process incoming status requests from agents."""
65+
self._status_handler = handler
66+
self._transport.on_status_received(handler)
67+
68+
async def start(self) -> None:
69+
"""Start the underlying transport."""
70+
71+
if self._is_running:
72+
return
73+
74+
if not self._register_handler:
75+
raise RuntimeError("No register handler registered. Call set_register_handler() first.")
76+
77+
if not self._unregister_handler:
78+
raise RuntimeError("No unregister handler registered. Call set_unregister_handler() first.")
79+
80+
await self._transport.start()
81+
self._is_running = True
82+
83+
async def stop(self) -> None:
84+
"""Stop the underlying transport and mark the server as idle."""
85+
86+
if not self._is_running:
87+
return
88+
89+
await self._transport.stop()
90+
self._is_running = False

protolink/transport/agent/backends/starlette.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ def setup_routes(self, transport: "HTTPAgentTransport") -> None: # noqa: F821
3636
"""Register all HTTP routes on the Starlette application.
3737
3838
This method wires the public HTTP API to the internal transport handlers.
39-
Each route is registered via a dedicated helper for clarity and separation
40-
of concerns.
39+
Each route is registered via a dedicated helper for clarity and separation of concerns.
4140
"""
4241
self._setup_task_routes(transport)
4342
self._setup_agent_card_routes(transport)

0 commit comments

Comments
 (0)