mycelium-agents 0.1.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.
- mycelium_agents-0.1.0/PKG-INFO +71 -0
- mycelium_agents-0.1.0/README.md +29 -0
- mycelium_agents-0.1.0/mycelium/__init__.py +13 -0
- mycelium_agents-0.1.0/mycelium/cli/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/core/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/core/agent.py +438 -0
- mycelium_agents-0.1.0/mycelium/core/capability.py +101 -0
- mycelium_agents-0.1.0/mycelium/core/card.py +103 -0
- mycelium_agents-0.1.0/mycelium/core/errors.py +70 -0
- mycelium_agents-0.1.0/mycelium/core/message.py +195 -0
- mycelium_agents-0.1.0/mycelium/discovery/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/network/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/network/client.py +261 -0
- mycelium_agents-0.1.0/mycelium/protocol/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/trust/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium/utils/__init__.py +1 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/PKG-INFO +71 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/SOURCES.txt +23 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/dependency_links.txt +1 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/entry_points.txt +2 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/requires.txt +20 -0
- mycelium_agents-0.1.0/mycelium_agents.egg-info/top_level.txt +1 -0
- mycelium_agents-0.1.0/pyproject.toml +70 -0
- mycelium_agents-0.1.0/setup.cfg +4 -0
- mycelium_agents-0.1.0/tests/test_agent.py +230 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mycelium-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The networking protocol for AI agents. Discover, communicate, and collaborate.
|
|
5
|
+
Author-email: Your Name <your@email.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/mycelium
|
|
8
|
+
Project-URL: Documentation, https://mycelium.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/yourusername/mycelium
|
|
10
|
+
Project-URL: Issues, https://github.com/yourusername/mycelium/issues
|
|
11
|
+
Keywords: ai,agents,protocol,networking,multi-agent,llm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: uuid7>=0.1.0
|
|
27
|
+
Requires-Dist: cryptography>=41.0.0
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: typer>=0.9.0
|
|
30
|
+
Requires-Dist: uvicorn>=0.24.0
|
|
31
|
+
Provides-Extra: server
|
|
32
|
+
Requires-Dist: fastapi>=0.104.0; extra == "server"
|
|
33
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "server"
|
|
34
|
+
Requires-Dist: chromadb>=0.4.0; extra == "server"
|
|
35
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "server"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
39
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
42
|
+
|
|
43
|
+
<div align="center">
|
|
44
|
+
|
|
45
|
+
# 🍄 Mycelium
|
|
46
|
+
|
|
47
|
+
### The Networking Protocol for AI Agents
|
|
48
|
+
|
|
49
|
+
*Discover. Communicate. Collaborate.*
|
|
50
|
+
|
|
51
|
+
[](https://opensource.org/licenses/MIT)
|
|
52
|
+
[](https://www.python.org/downloads/)
|
|
53
|
+
[]()
|
|
54
|
+
|
|
55
|
+
[Getting Started](#-quick-start) •
|
|
56
|
+
[Documentation](#-documentation) •
|
|
57
|
+
[Examples](#-examples) •
|
|
58
|
+
[Contributing](#-contributing) •
|
|
59
|
+
[Discord](#)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
*Like mycelium networks connect trees in a forest, this protocol connects AI agents across the internet.*
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## The Problem
|
|
70
|
+
|
|
71
|
+
AI agents are everywhere. But they're all **isolated**.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🍄 Mycelium
|
|
4
|
+
|
|
5
|
+
### The Networking Protocol for AI Agents
|
|
6
|
+
|
|
7
|
+
*Discover. Communicate. Collaborate.*
|
|
8
|
+
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://www.python.org/downloads/)
|
|
11
|
+
[]()
|
|
12
|
+
|
|
13
|
+
[Getting Started](#-quick-start) •
|
|
14
|
+
[Documentation](#-documentation) •
|
|
15
|
+
[Examples](#-examples) •
|
|
16
|
+
[Contributing](#-contributing) •
|
|
17
|
+
[Discord](#)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
*Like mycelium networks connect trees in a forest, this protocol connects AI agents across the internet.*
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## The Problem
|
|
28
|
+
|
|
29
|
+
AI agents are everywhere. But they're all **isolated**.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Mycelium."""
|
|
2
|
+
|
|
3
|
+
# mycelium/__init__.py
|
|
4
|
+
from .core.agent import Agent
|
|
5
|
+
|
|
6
|
+
__all__ = ["Agent"]
|
|
7
|
+
|
|
8
|
+
# mycelium/__init__.py
|
|
9
|
+
|
|
10
|
+
from .core.agent import Agent
|
|
11
|
+
from .network.client import Network # <-- Ye nayi line add karni hai
|
|
12
|
+
|
|
13
|
+
__all__ = ["Agent", "Network"] # <-- Yahan Network ko bhi list mein daal de
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI tool for Mycelium."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core components of Mycelium."""
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent — The core building block of Mycelium.
|
|
3
|
+
|
|
4
|
+
This is what developers interact with the most.
|
|
5
|
+
Create an agent, add capabilities, register it, serve requests.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from mycelium import Agent
|
|
9
|
+
|
|
10
|
+
agent = Agent(
|
|
11
|
+
name="Translator",
|
|
12
|
+
description="Translates text between languages"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
@agent.on("translate")
|
|
16
|
+
def handle_translate(text: str, to: str):
|
|
17
|
+
# your translation logic
|
|
18
|
+
return {"translated": do_translate(text, to)}
|
|
19
|
+
|
|
20
|
+
agent.register()
|
|
21
|
+
agent.serve()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import signal
|
|
28
|
+
import time
|
|
29
|
+
from typing import Any, Callable, Optional
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.panel import Panel
|
|
35
|
+
from rich.table import Table
|
|
36
|
+
|
|
37
|
+
from mycelium.core.card import AgentCard, PricingInfo
|
|
38
|
+
from mycelium.core.capability import Capability, CapabilitySchema
|
|
39
|
+
from mycelium.core.message import Message, MessageType, StatusCode
|
|
40
|
+
from mycelium.core.errors import (
|
|
41
|
+
CapabilityNotFoundError,
|
|
42
|
+
RegistrationError,
|
|
43
|
+
CommunicationError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console = Console()
|
|
47
|
+
|
|
48
|
+
DEFAULT_REGISTRY = "http://localhost:8000"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Agent:
|
|
52
|
+
"""
|
|
53
|
+
A Mycelium AI Agent.
|
|
54
|
+
|
|
55
|
+
Create agents that can:
|
|
56
|
+
- Register on the Mycelium network
|
|
57
|
+
- Be discovered by other agents
|
|
58
|
+
- Receive and handle requests
|
|
59
|
+
- Send requests to other agents
|
|
60
|
+
- Build trust through successful interactions
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
description: str,
|
|
67
|
+
version: str = "0.1.0",
|
|
68
|
+
author: Optional[str] = None,
|
|
69
|
+
languages: Optional[list[str]] = None,
|
|
70
|
+
tags: Optional[list[str]] = None,
|
|
71
|
+
pricing: Optional[PricingInfo] = None,
|
|
72
|
+
registry_url: str = DEFAULT_REGISTRY,
|
|
73
|
+
endpoint: Optional[str] = None,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Create a new Mycelium agent.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name: Human-readable name for your agent
|
|
80
|
+
description: What does your agent do?
|
|
81
|
+
version: Semantic version of your agent
|
|
82
|
+
author: Your name or email
|
|
83
|
+
languages: Languages your agent supports
|
|
84
|
+
tags: Search tags for discovery
|
|
85
|
+
pricing: How you charge for requests
|
|
86
|
+
registry_url: URL of the Mycelium registry
|
|
87
|
+
endpoint: URL where your agent receives requests
|
|
88
|
+
"""
|
|
89
|
+
self._capabilities: dict[str, Capability] = {}
|
|
90
|
+
self._handlers: dict[str, Callable] = {}
|
|
91
|
+
self._registry_url = registry_url
|
|
92
|
+
self._is_registered = False
|
|
93
|
+
self._is_serving = False
|
|
94
|
+
|
|
95
|
+
# Build the Agent Card
|
|
96
|
+
self.card = AgentCard(
|
|
97
|
+
name=name,
|
|
98
|
+
description=description,
|
|
99
|
+
version=version,
|
|
100
|
+
author=author,
|
|
101
|
+
languages=languages or ["english"],
|
|
102
|
+
tags=tags or [],
|
|
103
|
+
pricing=pricing or PricingInfo(),
|
|
104
|
+
endpoint=endpoint,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
console.print(
|
|
108
|
+
f"[green]🍄 Agent '{name}' created![/green] "
|
|
109
|
+
f"ID: [cyan]{self.card.agent_id}[/cyan]"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def agent_id(self) -> str:
|
|
114
|
+
return self.card.agent_id
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def name(self) -> str:
|
|
118
|
+
return self.card.name
|
|
119
|
+
|
|
120
|
+
# =========================================================
|
|
121
|
+
# CAPABILITY REGISTRATION
|
|
122
|
+
# =========================================================
|
|
123
|
+
|
|
124
|
+
def on(
|
|
125
|
+
self,
|
|
126
|
+
capability_name: str,
|
|
127
|
+
description: str = "",
|
|
128
|
+
input_schema: Optional[dict[str, str]] = None,
|
|
129
|
+
output_schema: Optional[dict[str, str]] = None,
|
|
130
|
+
tags: Optional[list[str]] = None,
|
|
131
|
+
) -> Callable:
|
|
132
|
+
"""
|
|
133
|
+
Decorator to register a capability handler.
|
|
134
|
+
|
|
135
|
+
Usage:
|
|
136
|
+
@agent.on("translate", description="Translates text")
|
|
137
|
+
def handle_translate(text: str, to: str):
|
|
138
|
+
return {"translated": my_translate(text, to)}
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def decorator(func: Callable) -> Callable:
|
|
142
|
+
cap = Capability(
|
|
143
|
+
name=capability_name,
|
|
144
|
+
description=description or f"Handler for {capability_name}",
|
|
145
|
+
input_schema=CapabilitySchema(
|
|
146
|
+
fields=input_schema or {},
|
|
147
|
+
required=list((input_schema or {}).keys()),
|
|
148
|
+
),
|
|
149
|
+
output_schema=CapabilitySchema(
|
|
150
|
+
fields=output_schema or {},
|
|
151
|
+
),
|
|
152
|
+
tags=tags or [],
|
|
153
|
+
)
|
|
154
|
+
cap.set_handler(func)
|
|
155
|
+
|
|
156
|
+
self._capabilities[capability_name] = cap
|
|
157
|
+
self._handlers[capability_name] = func
|
|
158
|
+
|
|
159
|
+
# Update card
|
|
160
|
+
self.card.capabilities.append(cap.to_card_dict())
|
|
161
|
+
|
|
162
|
+
console.print(
|
|
163
|
+
f" [blue]⚡ Capability registered:[/blue] "
|
|
164
|
+
f"[bold]{capability_name}[/bold] — {cap.description}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return func
|
|
168
|
+
|
|
169
|
+
return decorator
|
|
170
|
+
|
|
171
|
+
def add_capability(
|
|
172
|
+
self,
|
|
173
|
+
name: str,
|
|
174
|
+
handler: Callable,
|
|
175
|
+
description: str = "",
|
|
176
|
+
input_schema: Optional[dict[str, str]] = None,
|
|
177
|
+
output_schema: Optional[dict[str, str]] = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Add a capability without using the decorator."""
|
|
180
|
+
cap = Capability(
|
|
181
|
+
name=name,
|
|
182
|
+
description=description or f"Handler for {name}",
|
|
183
|
+
input_schema=CapabilitySchema(
|
|
184
|
+
fields=input_schema or {},
|
|
185
|
+
required=list((input_schema or {}).keys()),
|
|
186
|
+
),
|
|
187
|
+
output_schema=CapabilitySchema(fields=output_schema or {}),
|
|
188
|
+
)
|
|
189
|
+
cap.set_handler(handler)
|
|
190
|
+
self._capabilities[name] = cap
|
|
191
|
+
self._handlers[name] = handler
|
|
192
|
+
self.card.capabilities.append(cap.to_card_dict())
|
|
193
|
+
|
|
194
|
+
# =========================================================
|
|
195
|
+
# NETWORK OPERATIONS
|
|
196
|
+
# =========================================================
|
|
197
|
+
|
|
198
|
+
async def register_async(self) -> bool:
|
|
199
|
+
"""Register this agent on the Mycelium network (async)."""
|
|
200
|
+
try:
|
|
201
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
202
|
+
response = await client.post(
|
|
203
|
+
f"{self._registry_url}/api/v1/agents/register",
|
|
204
|
+
json=self.card.model_dump(mode="json"),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if response.status_code == 200:
|
|
208
|
+
data = response.json()
|
|
209
|
+
self._is_registered = True
|
|
210
|
+
self.card.status = "online"
|
|
211
|
+
|
|
212
|
+
console.print(
|
|
213
|
+
Panel(
|
|
214
|
+
f"[green]✅ Agent '{self.name}' registered on Mycelium![/green]\n"
|
|
215
|
+
f" Agent ID: [cyan]{self.agent_id}[/cyan]\n"
|
|
216
|
+
f" Capabilities: {len(self._capabilities)}\n"
|
|
217
|
+
f" Registry: {self._registry_url}",
|
|
218
|
+
title="🍄 Registration Successful",
|
|
219
|
+
border_style="green",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return True
|
|
223
|
+
else:
|
|
224
|
+
raise RegistrationError(
|
|
225
|
+
f"Registry returned {response.status_code}: {response.text}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
except httpx.ConnectError:
|
|
229
|
+
raise RegistrationError(
|
|
230
|
+
f"Cannot connect to registry at {self._registry_url}. "
|
|
231
|
+
f"Is the registry server running?"
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise RegistrationError(str(e))
|
|
235
|
+
|
|
236
|
+
def register(self) -> bool:
|
|
237
|
+
"""Register this agent on the Mycelium network (sync wrapper)."""
|
|
238
|
+
return asyncio.run(self.register_async())
|
|
239
|
+
|
|
240
|
+
# =========================================================
|
|
241
|
+
# HANDLE INCOMING REQUESTS
|
|
242
|
+
# =========================================================
|
|
243
|
+
|
|
244
|
+
async def handle_message(self, message: Message) -> Message:
|
|
245
|
+
"""Process an incoming message and return a response."""
|
|
246
|
+
start_time = time.time()
|
|
247
|
+
|
|
248
|
+
# Handle ping
|
|
249
|
+
if message.envelope.message_type == MessageType.PING:
|
|
250
|
+
return Message.create_response(
|
|
251
|
+
original=message,
|
|
252
|
+
outputs={"pong": True, "agent": self.name},
|
|
253
|
+
status=StatusCode.SUCCESS,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Handle capability request
|
|
257
|
+
if message.envelope.message_type == MessageType.REQUEST:
|
|
258
|
+
capability_name = message.payload.get("capability")
|
|
259
|
+
inputs = message.payload.get("inputs", {})
|
|
260
|
+
|
|
261
|
+
if capability_name not in self._capabilities:
|
|
262
|
+
return Message.create_error(
|
|
263
|
+
original=message,
|
|
264
|
+
error_message=f"Capability '{capability_name}' not found",
|
|
265
|
+
status=StatusCode.CAPABILITY_NOT_FOUND,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
capability = self._capabilities[capability_name]
|
|
270
|
+
result = await capability.execute(inputs)
|
|
271
|
+
processing_time = (time.time() - start_time) * 1000
|
|
272
|
+
|
|
273
|
+
# Update stats
|
|
274
|
+
self.card.total_requests_served += 1
|
|
275
|
+
|
|
276
|
+
return Message.create_response(
|
|
277
|
+
original=message,
|
|
278
|
+
outputs=result if isinstance(result, dict) else {"result": result},
|
|
279
|
+
status=StatusCode.SUCCESS,
|
|
280
|
+
processing_time_ms=processing_time,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
return Message.create_error(
|
|
285
|
+
original=message,
|
|
286
|
+
error_message=str(e),
|
|
287
|
+
status=StatusCode.ERROR,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return Message.create_error(
|
|
291
|
+
original=message,
|
|
292
|
+
error_message=f"Unknown message type: {message.envelope.message_type}",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# =========================================================
|
|
296
|
+
# SEND REQUESTS TO OTHER AGENTS
|
|
297
|
+
# =========================================================
|
|
298
|
+
|
|
299
|
+
async def request_async(
|
|
300
|
+
self,
|
|
301
|
+
agent_id: str,
|
|
302
|
+
capability: str,
|
|
303
|
+
inputs: dict[str, Any],
|
|
304
|
+
timeout: int = 30,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
"""Send a request to another agent on the network."""
|
|
307
|
+
message = Message.create_request(
|
|
308
|
+
from_agent=self.agent_id,
|
|
309
|
+
to_agent=agent_id,
|
|
310
|
+
capability=capability,
|
|
311
|
+
inputs=inputs,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
316
|
+
response = await client.post(
|
|
317
|
+
f"{self._registry_url}/api/v1/messages/send",
|
|
318
|
+
json=message.model_dump(mode="json"),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if response.status_code == 200:
|
|
322
|
+
data = response.json()
|
|
323
|
+
return data.get("payload", {}).get("outputs", {})
|
|
324
|
+
else:
|
|
325
|
+
raise CommunicationError(
|
|
326
|
+
self.agent_id,
|
|
327
|
+
agent_id,
|
|
328
|
+
f"Registry returned {response.status_code}",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
except httpx.ConnectError:
|
|
332
|
+
raise CommunicationError(
|
|
333
|
+
self.agent_id,
|
|
334
|
+
agent_id,
|
|
335
|
+
"Cannot connect to registry",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def request(
|
|
339
|
+
self,
|
|
340
|
+
agent_id: str,
|
|
341
|
+
capability: str,
|
|
342
|
+
inputs: dict[str, Any],
|
|
343
|
+
timeout: int = 30,
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
"""Send a request to another agent (sync wrapper)."""
|
|
346
|
+
return asyncio.run(
|
|
347
|
+
self.request_async(agent_id, capability, inputs, timeout)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# =========================================================
|
|
351
|
+
# SERVE (Keep agent alive and listening)
|
|
352
|
+
# =========================================================
|
|
353
|
+
|
|
354
|
+
async def _serve_async(self, host: str = "0.0.0.0", port: int = 8001):
|
|
355
|
+
"""Start serving requests (async)."""
|
|
356
|
+
try:
|
|
357
|
+
from fastapi import FastAPI
|
|
358
|
+
import uvicorn
|
|
359
|
+
except ImportError:
|
|
360
|
+
raise ImportError(
|
|
361
|
+
"FastAPI and uvicorn required for serving. "
|
|
362
|
+
"Install with: pip install mycelium-ai[server]"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
app = FastAPI(title=f"🍄 {self.name} — Mycelium Agent")
|
|
366
|
+
|
|
367
|
+
@app.post("/mycelium/handle")
|
|
368
|
+
async def handle_request(raw_message: dict):
|
|
369
|
+
message = Message(**raw_message)
|
|
370
|
+
response = await self.handle_message(message)
|
|
371
|
+
return response.model_dump(mode="json")
|
|
372
|
+
|
|
373
|
+
@app.get("/mycelium/card")
|
|
374
|
+
async def get_card():
|
|
375
|
+
return self.card.model_dump(mode="json")
|
|
376
|
+
|
|
377
|
+
@app.get("/mycelium/health")
|
|
378
|
+
async def health():
|
|
379
|
+
return {
|
|
380
|
+
"status": "healthy",
|
|
381
|
+
"agent": self.name,
|
|
382
|
+
"agent_id": self.agent_id,
|
|
383
|
+
"capabilities": list(self._capabilities.keys()),
|
|
384
|
+
"requests_served": self.card.total_requests_served,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
self._is_serving = True
|
|
388
|
+
self.card.status = "online"
|
|
389
|
+
self.card.endpoint = f"http://{host}:{port}"
|
|
390
|
+
|
|
391
|
+
console.print(
|
|
392
|
+
Panel(
|
|
393
|
+
f"[green]🍄 Agent '{self.name}' is now LIVE![/green]\n\n"
|
|
394
|
+
f" Endpoint: [cyan]http://{host}:{port}[/cyan]\n"
|
|
395
|
+
f" Capabilities: {', '.join(self._capabilities.keys())}\n"
|
|
396
|
+
f" Agent ID: {self.agent_id}\n\n"
|
|
397
|
+
f" [dim]Press Ctrl+C to stop[/dim]",
|
|
398
|
+
title="🌐 Agent Serving",
|
|
399
|
+
border_style="green",
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
|
404
|
+
server = uvicorn.Server(config)
|
|
405
|
+
await server.serve()
|
|
406
|
+
|
|
407
|
+
def serve(self, host: str = "0.0.0.0", port: int = 8001):
|
|
408
|
+
"""Start serving requests (sync wrapper)."""
|
|
409
|
+
asyncio.run(self._serve_async(host, port))
|
|
410
|
+
|
|
411
|
+
# =========================================================
|
|
412
|
+
# DISPLAY
|
|
413
|
+
# =========================================================
|
|
414
|
+
|
|
415
|
+
def info(self) -> None:
|
|
416
|
+
"""Print agent information in a nice format."""
|
|
417
|
+
table = Table(title=f"🍄 Agent: {self.name}")
|
|
418
|
+
|
|
419
|
+
table.add_column("Property", style="cyan")
|
|
420
|
+
table.add_column("Value", style="white")
|
|
421
|
+
|
|
422
|
+
table.add_row("Agent ID", self.card.agent_id)
|
|
423
|
+
table.add_row("Version", self.card.version)
|
|
424
|
+
table.add_row("Description", self.card.description)
|
|
425
|
+
table.add_row("Status", self.card.status)
|
|
426
|
+
table.add_row("Capabilities", ", ".join(self._capabilities.keys()) or "None")
|
|
427
|
+
table.add_row("Languages", ", ".join(self.card.languages))
|
|
428
|
+
table.add_row("Tags", ", ".join(self.card.tags) or "None")
|
|
429
|
+
table.add_row("Trust Score", f"{'⭐' * int(self.card.trust_score)} ({self.card.trust_score}/5.0)")
|
|
430
|
+
table.add_row("Requests Served", str(self.card.total_requests_served))
|
|
431
|
+
table.add_row("Registered", "✅ Yes" if self._is_registered else "❌ No")
|
|
432
|
+
table.add_row("Serving", "✅ Yes" if self._is_serving else "❌ No")
|
|
433
|
+
|
|
434
|
+
console.print(table)
|
|
435
|
+
|
|
436
|
+
def __repr__(self) -> str:
|
|
437
|
+
caps = ", ".join(self._capabilities.keys())
|
|
438
|
+
return f"Agent(name='{self.name}', capabilities=[{caps}])"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capability — What an agent CAN DO.
|
|
3
|
+
|
|
4
|
+
Each agent has one or more capabilities.
|
|
5
|
+
Think of these as "skills" that other agents can request.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CapabilitySchema(BaseModel):
|
|
15
|
+
"""Schema definition for capability inputs/outputs."""
|
|
16
|
+
|
|
17
|
+
fields: dict[str, str] = Field(
|
|
18
|
+
default_factory=dict,
|
|
19
|
+
description="Field name → type description mapping",
|
|
20
|
+
)
|
|
21
|
+
required: list[str] = Field(
|
|
22
|
+
default_factory=list,
|
|
23
|
+
description="Required field names",
|
|
24
|
+
)
|
|
25
|
+
example: Optional[dict[str, Any]] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Capability(BaseModel):
|
|
29
|
+
"""
|
|
30
|
+
A single capability of an agent.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
cap = Capability(
|
|
34
|
+
name="translate",
|
|
35
|
+
description="Translates text between languages",
|
|
36
|
+
input_schema=CapabilitySchema(
|
|
37
|
+
fields={"text": "string", "to": "language code"},
|
|
38
|
+
required=["text", "to"],
|
|
39
|
+
example={"text": "Hello", "to": "hi"}
|
|
40
|
+
),
|
|
41
|
+
output_schema=CapabilitySchema(
|
|
42
|
+
fields={"translated": "string", "confidence": "float"},
|
|
43
|
+
example={"translated": "नमस्ते", "confidence": 0.98}
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name: str = Field(..., description="Unique capability name")
|
|
49
|
+
description: str = Field(..., description="What this capability does")
|
|
50
|
+
input_schema: CapabilitySchema = Field(
|
|
51
|
+
default_factory=CapabilitySchema,
|
|
52
|
+
description="Expected input format",
|
|
53
|
+
)
|
|
54
|
+
output_schema: CapabilitySchema = Field(
|
|
55
|
+
default_factory=CapabilitySchema,
|
|
56
|
+
description="Expected output format",
|
|
57
|
+
)
|
|
58
|
+
tags: list[str] = Field(default_factory=list)
|
|
59
|
+
version: str = Field(default="1.0.0")
|
|
60
|
+
is_async: bool = Field(default=False, description="Whether this capability runs async")
|
|
61
|
+
avg_response_time_ms: Optional[float] = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Average response time in milliseconds",
|
|
64
|
+
)
|
|
65
|
+
cost_per_request: Optional[float] = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="Cost per request in USD",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Internal — not serialized to network
|
|
71
|
+
_handler: Optional[Callable] = None
|
|
72
|
+
|
|
73
|
+
class Config:
|
|
74
|
+
arbitrary_types_allowed = True
|
|
75
|
+
|
|
76
|
+
def set_handler(self, handler: Callable) -> None:
|
|
77
|
+
"""Attach a handler function to this capability."""
|
|
78
|
+
self._handler = handler
|
|
79
|
+
|
|
80
|
+
async def execute(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
"""Execute this capability with given inputs."""
|
|
82
|
+
if self._handler is None:
|
|
83
|
+
raise ValueError(f"No handler registered for capability '{self.name}'")
|
|
84
|
+
|
|
85
|
+
# Validate required fields
|
|
86
|
+
for field in self.input_schema.required:
|
|
87
|
+
if field not in inputs:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Missing required field '{field}' for capability '{self.name}'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
import asyncio
|
|
93
|
+
|
|
94
|
+
if asyncio.iscoroutinefunction(self._handler):
|
|
95
|
+
return await self._handler(**inputs)
|
|
96
|
+
else:
|
|
97
|
+
return self._handler(**inputs)
|
|
98
|
+
|
|
99
|
+
def to_card_dict(self) -> dict:
|
|
100
|
+
"""Export capability info for the Agent Card (no handler)."""
|
|
101
|
+
return self.model_dump(exclude={"_handler"})
|