From cc302e3f629cc184dbd72145b60a8729a4708f85 Mon Sep 17 00:00:00 2001 From: Ojus Chugh Date: Mon, 22 Jun 2026 00:57:11 +0530 Subject: [PATCH] feat: support AgentFacts in registration Add an optional agent_facts arg to register_with_registry and the NANDA constructor. Falls back to AGENT_FACTS_PATH (JSON file) when running agent_bridge directly. No-op if unset, so existing setups keep working. Closes #21 --- README.md | 22 +++++++++++ nanda_adapter/core/agent_bridge.py | 18 ++++++++- nanda_adapter/core/nanda.py | 12 +++++- tests/__init__.py | 0 tests/test_register_payload.py | 62 ++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_register_payload.py diff --git a/README.md b/README.md index 7ca262a..94cba33 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,28 @@ if __name__ == "__main__": main() ``` +### Registering with AgentFacts + +Pass an `agent_facts` dict to publish skills and capabilities at registration. +See [agentfacts](https://github.com/projnanda/agentfacts) for the schema. + +```python +agent_facts = { + "agent_name": "pirate-bot", + "label": "Pirate Translator", + "description": "Translates messages into pirate English", + "version": "1.0.0", + "skills": [ + {"id": "translate.pirate", "description": "Translate to pirate English"} + ], +} + +nanda = NANDA(my_improvement, agent_facts=agent_facts) +nanda.start_server_api(anthropic_key, domain) +``` + +If running `agent_bridge.py` directly, set `AGENT_FACTS_PATH` to a JSON file. + ### Deploy a LangChain Agent ```python diff --git a/nanda_adapter/core/agent_bridge.py b/nanda_adapter/core/agent_bridge.py index 26361d6..e6d97f2 100644 --- a/nanda_adapter/core/agent_bridge.py +++ b/nanda_adapter/core/agent_bridge.py @@ -76,7 +76,7 @@ def get_registry_url(): print(f"Using default registry URL: {default_url}") return default_url -def register_with_registry(agent_id, agent_url, api_url): +def register_with_registry(agent_id, agent_url, api_url, agent_facts=None): """Register this agent with the registry""" registry_url = get_registry_url() try: @@ -89,6 +89,8 @@ def register_with_registry(agent_id, agent_url, api_url): "agent_url": agent_url, "api_url": api_url } + if agent_facts: + data["agent_facts"] = agent_facts print(f"Registering agent {agent_id} with URL {agent_url} at registry {registry_url}...") response = requests.post(f"{registry_url}/register", json=data) if response.status_code == 200: @@ -878,13 +880,25 @@ def handle_message(self, msg: Message) -> Message: conversation_id = conversation_id ) +def _load_agent_facts_from_env(): + """Load agent_facts JSON from AGENT_FACTS_PATH if set""" + facts_path = os.getenv("AGENT_FACTS_PATH") + if not facts_path: + return None + try: + with open(facts_path, "r") as f: + return json.load(f) + except Exception as e: + print(f"Could not read agent facts from {facts_path}: {e}") + return None + if __name__ == "__main__": # Register with the registry if PUBLIC_URL is set public_url = os.getenv("PUBLIC_URL") api_url = os.getenv("API_URL") if public_url: agent_id = get_agent_id() - register_with_registry(agent_id, public_url, api_url) + register_with_registry(agent_id, public_url, api_url, agent_facts=_load_agent_facts_from_env()) else: print("WARNING: PUBLIC_URL environment variable not set. Agent will not be registered.") diff --git a/nanda_adapter/core/nanda.py b/nanda_adapter/core/nanda.py index 9bc8dc7..dacb82e 100644 --- a/nanda_adapter/core/nanda.py +++ b/nanda_adapter/core/nanda.py @@ -28,14 +28,18 @@ class NANDA: """NANDA class to create agent_bridge with custom improvement logic""" - def __init__(self, improvement_logic): + def __init__(self, improvement_logic, agent_facts=None): """ Initialize NANDA with custom improvement logic Args: improvement_logic: Function that takes (message_text: str) -> str + agent_facts: Optional dict sent to the registry alongside the + agent_id/agent_url/api_url. See projnanda/agentfacts for + the schema. """ self.improvement_logic = improvement_logic + self.agent_facts = agent_facts self.bridge = None print(f"🤖 NANDA initialized with custom improvement logic: {improvement_logic.__name__}") @@ -44,6 +48,10 @@ def __init__(self, improvement_logic): # Create agent bridge with custom logic self.create_agent_bridge() + + def set_agent_facts(self, agent_facts): + """Set agent_facts to send at registration""" + self.agent_facts = agent_facts def register_custom_improver(self): """Register the custom improvement logic with agent_bridge""" @@ -88,7 +96,7 @@ def start_server(self): # os.environ["UI_CLIENT_URL"] = f"{api_url}/api/receive_message" if public_url: - register_with_registry(agent_id, public_url, api_url) + register_with_registry(agent_id, public_url, api_url, agent_facts=self.agent_facts) else: print("WARNING: PUBLIC_URL environment variable not set. Agent will not be registered.") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_register_payload.py b/tests/test_register_payload.py new file mode 100644 index 0000000..026b94c --- /dev/null +++ b/tests/test_register_payload.py @@ -0,0 +1,62 @@ +"""Verify the registration payload includes agent_facts when supplied.""" +import os +import sys +import unittest +from unittest import mock + +# Ensure the package is importable when running this file directly +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from nanda_adapter.core import agent_bridge + + +class _FakeResponse: + def __init__(self, status_code=200, text="ok"): + self.status_code = status_code + self.text = text + + +class RegisterPayloadTest(unittest.TestCase): + + def test_payload_omits_agent_facts_by_default(self): + captured = {} + + def fake_post(url, json=None, **kwargs): + captured["url"] = url + captured["json"] = json + return _FakeResponse() + + with mock.patch.object(agent_bridge.requests, "post", side_effect=fake_post): + ok = agent_bridge.register_with_registry( + "agent-x", "https://x.example.com", "https://api.example.com" + ) + + self.assertTrue(ok) + self.assertNotIn("agent_facts", captured["json"]) + self.assertEqual(captured["json"]["agent_id"], "agent-x") + + def test_payload_includes_agent_facts_when_provided(self): + captured = {} + + def fake_post(url, json=None, **kwargs): + captured["json"] = json + return _FakeResponse() + + facts = { + "agent_name": "pirate-bot", + "skills": [{"id": "translate.pirate"}], + } + + with mock.patch.object(agent_bridge.requests, "post", side_effect=fake_post): + agent_bridge.register_with_registry( + "agent-x", + "https://x.example.com", + "https://api.example.com", + agent_facts=facts, + ) + + self.assertEqual(captured["json"]["agent_facts"], facts) + + +if __name__ == "__main__": + unittest.main()