agentfield 0.1.22rc2__py3-none-any.whl
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.
- agentfield/__init__.py +66 -0
- agentfield/agent.py +3569 -0
- agentfield/agent_ai.py +1125 -0
- agentfield/agent_cli.py +386 -0
- agentfield/agent_field_handler.py +494 -0
- agentfield/agent_mcp.py +534 -0
- agentfield/agent_registry.py +29 -0
- agentfield/agent_server.py +1185 -0
- agentfield/agent_utils.py +269 -0
- agentfield/agent_workflow.py +323 -0
- agentfield/async_config.py +278 -0
- agentfield/async_execution_manager.py +1227 -0
- agentfield/client.py +1447 -0
- agentfield/connection_manager.py +280 -0
- agentfield/decorators.py +527 -0
- agentfield/did_manager.py +337 -0
- agentfield/dynamic_skills.py +304 -0
- agentfield/execution_context.py +255 -0
- agentfield/execution_state.py +453 -0
- agentfield/http_connection_manager.py +429 -0
- agentfield/litellm_adapters.py +140 -0
- agentfield/logger.py +249 -0
- agentfield/mcp_client.py +204 -0
- agentfield/mcp_manager.py +340 -0
- agentfield/mcp_stdio_bridge.py +550 -0
- agentfield/memory.py +723 -0
- agentfield/memory_events.py +489 -0
- agentfield/multimodal.py +173 -0
- agentfield/multimodal_response.py +403 -0
- agentfield/pydantic_utils.py +227 -0
- agentfield/rate_limiter.py +280 -0
- agentfield/result_cache.py +441 -0
- agentfield/router.py +190 -0
- agentfield/status.py +70 -0
- agentfield/types.py +710 -0
- agentfield/utils.py +26 -0
- agentfield/vc_generator.py +464 -0
- agentfield/vision.py +198 -0
- agentfield-0.1.22rc2.dist-info/METADATA +102 -0
- agentfield-0.1.22rc2.dist-info/RECORD +42 -0
- agentfield-0.1.22rc2.dist-info/WHEEL +5 -0
- agentfield-0.1.22rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DID Manager for AgentField SDK
|
|
3
|
+
|
|
4
|
+
Handles Decentralized Identity (DID) and Verifiable Credentials (VC) functionality
|
|
5
|
+
for agent nodes, reasoners, and skills.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import requests
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from .logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DIDIdentity:
|
|
20
|
+
"""Represents a DID identity with cryptographic keys."""
|
|
21
|
+
|
|
22
|
+
did: str
|
|
23
|
+
private_key_jwk: str
|
|
24
|
+
public_key_jwk: str
|
|
25
|
+
derivation_path: str
|
|
26
|
+
component_type: str
|
|
27
|
+
function_name: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DIDIdentityPackage:
|
|
32
|
+
"""Complete DID identity package for an agent."""
|
|
33
|
+
|
|
34
|
+
agent_did: DIDIdentity
|
|
35
|
+
reasoner_dids: Dict[str, DIDIdentity]
|
|
36
|
+
skill_dids: Dict[str, DIDIdentity]
|
|
37
|
+
agentfield_server_id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DIDExecutionContext:
|
|
42
|
+
"""Context for DID-enabled execution."""
|
|
43
|
+
|
|
44
|
+
execution_id: str
|
|
45
|
+
workflow_id: str
|
|
46
|
+
session_id: str
|
|
47
|
+
caller_did: str
|
|
48
|
+
target_did: str
|
|
49
|
+
agent_node_did: str
|
|
50
|
+
timestamp: datetime
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DIDManager:
|
|
54
|
+
"""
|
|
55
|
+
Manages DID operations for AgentField SDK agents.
|
|
56
|
+
|
|
57
|
+
Handles:
|
|
58
|
+
- Agent registration with AgentField Server
|
|
59
|
+
- DID resolution and verification
|
|
60
|
+
- Execution context creation
|
|
61
|
+
- Integration with agent lifecycle
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self, agentfield_server_url: str, agent_node_id: str, api_key: Optional[str] = None
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Initialize DID Manager.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
agentfield_server_url: URL of the AgentField Server
|
|
72
|
+
agent_node_id: Unique identifier for this agent node
|
|
73
|
+
api_key: Optional API key for authentication
|
|
74
|
+
"""
|
|
75
|
+
self.agentfield_server_url = agentfield_server_url.rstrip("/")
|
|
76
|
+
self.agent_node_id = agent_node_id
|
|
77
|
+
self.api_key = api_key
|
|
78
|
+
self.identity_package: Optional[DIDIdentityPackage] = None
|
|
79
|
+
self.enabled = False
|
|
80
|
+
|
|
81
|
+
def _get_auth_headers(self) -> Dict[str, str]:
|
|
82
|
+
"""Return auth headers if API key is configured."""
|
|
83
|
+
if not self.api_key:
|
|
84
|
+
return {}
|
|
85
|
+
return {"X-API-Key": self.api_key}
|
|
86
|
+
|
|
87
|
+
def register_agent(
|
|
88
|
+
self, reasoners: List[Dict[str, Any]], skills: List[Dict[str, Any]]
|
|
89
|
+
) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Register agent with AgentField Server and obtain DID identity package.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
reasoners: List of reasoner definitions
|
|
95
|
+
skills: List of skill definitions
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if registration successful, False otherwise
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"DID registration for agent: {self.agent_node_id} "
|
|
103
|
+
f"({len(reasoners)} reasoners, {len(skills)} skills)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Prepare registration request
|
|
107
|
+
registration_data = {
|
|
108
|
+
"agent_node_id": self.agent_node_id,
|
|
109
|
+
"reasoners": reasoners,
|
|
110
|
+
"skills": skills,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Send registration request to AgentField Server
|
|
114
|
+
headers = {"Content-Type": "application/json"}
|
|
115
|
+
headers.update(self._get_auth_headers())
|
|
116
|
+
response = requests.post(
|
|
117
|
+
f"{self.agentfield_server_url}/api/v1/did/register",
|
|
118
|
+
json=registration_data,
|
|
119
|
+
headers=headers,
|
|
120
|
+
timeout=30,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if response.status_code == 200:
|
|
124
|
+
result = response.json()
|
|
125
|
+
if result.get("success"):
|
|
126
|
+
# Parse identity package
|
|
127
|
+
package_data = result["identity_package"]
|
|
128
|
+
self.identity_package = self._parse_identity_package(package_data)
|
|
129
|
+
self.enabled = True
|
|
130
|
+
logger.debug(
|
|
131
|
+
f"Agent {self.agent_node_id} successfully registered with DID system"
|
|
132
|
+
)
|
|
133
|
+
return True
|
|
134
|
+
else:
|
|
135
|
+
error_msg = result.get("error", "Unknown error")
|
|
136
|
+
logger.error(f"DID registration failed: {error_msg}")
|
|
137
|
+
return False
|
|
138
|
+
else:
|
|
139
|
+
error_msg = f"{response.status_code} - {response.text}"
|
|
140
|
+
logger.error(f"DID registration request failed: {error_msg}")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Error during DID registration: {e}")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def create_execution_context(
|
|
148
|
+
self,
|
|
149
|
+
execution_id: str,
|
|
150
|
+
workflow_id: str,
|
|
151
|
+
session_id: str,
|
|
152
|
+
caller_function: str,
|
|
153
|
+
target_function: str,
|
|
154
|
+
) -> Optional[DIDExecutionContext]:
|
|
155
|
+
"""
|
|
156
|
+
Create execution context for DID-enabled execution.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
execution_id: Unique execution identifier
|
|
160
|
+
workflow_id: Workflow identifier
|
|
161
|
+
session_id: Session identifier
|
|
162
|
+
caller_function: Name of calling function
|
|
163
|
+
target_function: Name of target function
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ExecutionContext if successful, None otherwise
|
|
167
|
+
"""
|
|
168
|
+
if not self.enabled or not self.identity_package:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Resolve caller DID
|
|
173
|
+
caller_did = self._get_function_did(caller_function)
|
|
174
|
+
if not caller_did:
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"Could not resolve DID for caller function: {caller_function}"
|
|
177
|
+
)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
# Resolve target DID
|
|
181
|
+
target_did = self._get_function_did(target_function)
|
|
182
|
+
if not target_did:
|
|
183
|
+
logger.warning(
|
|
184
|
+
f"Could not resolve DID for target function: {target_function}"
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return DIDExecutionContext(
|
|
189
|
+
execution_id=execution_id,
|
|
190
|
+
workflow_id=workflow_id,
|
|
191
|
+
session_id=session_id,
|
|
192
|
+
caller_did=caller_did,
|
|
193
|
+
target_did=target_did,
|
|
194
|
+
agent_node_did=self.identity_package.agent_did.did,
|
|
195
|
+
timestamp=datetime.utcnow(),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Error creating execution context: {e}")
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def get_agent_did(self) -> Optional[str]:
|
|
203
|
+
"""Get the agent node DID."""
|
|
204
|
+
if self.identity_package:
|
|
205
|
+
return self.identity_package.agent_did.did
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def get_function_did(self, function_name: str) -> Optional[str]:
|
|
209
|
+
"""
|
|
210
|
+
Get DID for a specific function (reasoner or skill).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
function_name: Name of the function
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
DID string if found, None otherwise
|
|
217
|
+
"""
|
|
218
|
+
return self._get_function_did(function_name)
|
|
219
|
+
|
|
220
|
+
def resolve_did(self, did: str) -> Optional[Dict[str, Any]]:
|
|
221
|
+
"""
|
|
222
|
+
Resolve a DID to get its public information.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
did: DID to resolve
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
DID document if successful, None otherwise
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
response = requests.get(
|
|
232
|
+
f"{self.agentfield_server_url}/api/v1/did/resolve/{did}",
|
|
233
|
+
headers=self._get_auth_headers(),
|
|
234
|
+
timeout=10,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if response.status_code == 200:
|
|
238
|
+
return response.json()
|
|
239
|
+
else:
|
|
240
|
+
logger.warning(f"Failed to resolve DID {did}: {response.status_code}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Error resolving DID {did}: {e}")
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def is_enabled(self) -> bool:
|
|
248
|
+
"""Check if DID system is enabled and configured."""
|
|
249
|
+
return self.enabled and self.identity_package is not None
|
|
250
|
+
|
|
251
|
+
def get_identity_summary(self) -> Dict[str, Any]:
|
|
252
|
+
"""
|
|
253
|
+
Get summary of identity package for debugging/monitoring.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dictionary with identity information (no private keys)
|
|
257
|
+
"""
|
|
258
|
+
if not self.identity_package:
|
|
259
|
+
return {"enabled": False, "message": "No identity package available"}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"enabled": True,
|
|
263
|
+
"agent_did": self.identity_package.agent_did.did,
|
|
264
|
+
"agentfield_server_id": self.identity_package.agentfield_server_id,
|
|
265
|
+
"reasoner_count": len(self.identity_package.reasoner_dids),
|
|
266
|
+
"skill_count": len(self.identity_package.skill_dids),
|
|
267
|
+
"reasoner_dids": {
|
|
268
|
+
name: identity.did
|
|
269
|
+
for name, identity in self.identity_package.reasoner_dids.items()
|
|
270
|
+
},
|
|
271
|
+
"skill_dids": {
|
|
272
|
+
name: identity.did
|
|
273
|
+
for name, identity in self.identity_package.skill_dids.items()
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def _parse_identity_package(
|
|
278
|
+
self, package_data: Dict[str, Any]
|
|
279
|
+
) -> DIDIdentityPackage:
|
|
280
|
+
"""Parse identity package from registration response."""
|
|
281
|
+
# Parse agent DID
|
|
282
|
+
agent_data = package_data["agent_did"]
|
|
283
|
+
agent_did = DIDIdentity(
|
|
284
|
+
did=agent_data["did"],
|
|
285
|
+
private_key_jwk=agent_data["private_key_jwk"],
|
|
286
|
+
public_key_jwk=agent_data["public_key_jwk"],
|
|
287
|
+
derivation_path=agent_data["derivation_path"],
|
|
288
|
+
component_type=agent_data["component_type"],
|
|
289
|
+
function_name=agent_data.get("function_name"),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Parse reasoner DIDs
|
|
293
|
+
reasoner_dids = {}
|
|
294
|
+
for name, reasoner_data in package_data["reasoner_dids"].items():
|
|
295
|
+
reasoner_dids[name] = DIDIdentity(
|
|
296
|
+
did=reasoner_data["did"],
|
|
297
|
+
private_key_jwk=reasoner_data["private_key_jwk"],
|
|
298
|
+
public_key_jwk=reasoner_data["public_key_jwk"],
|
|
299
|
+
derivation_path=reasoner_data["derivation_path"],
|
|
300
|
+
component_type=reasoner_data["component_type"],
|
|
301
|
+
function_name=reasoner_data.get("function_name"),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Parse skill DIDs
|
|
305
|
+
skill_dids = {}
|
|
306
|
+
for name, skill_data in package_data["skill_dids"].items():
|
|
307
|
+
skill_dids[name] = DIDIdentity(
|
|
308
|
+
did=skill_data["did"],
|
|
309
|
+
private_key_jwk=skill_data["private_key_jwk"],
|
|
310
|
+
public_key_jwk=skill_data["public_key_jwk"],
|
|
311
|
+
derivation_path=skill_data["derivation_path"],
|
|
312
|
+
component_type=skill_data["component_type"],
|
|
313
|
+
function_name=skill_data.get("function_name"),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return DIDIdentityPackage(
|
|
317
|
+
agent_did=agent_did,
|
|
318
|
+
reasoner_dids=reasoner_dids,
|
|
319
|
+
skill_dids=skill_dids,
|
|
320
|
+
agentfield_server_id=package_data["agentfield_server_id"],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _get_function_did(self, function_name: str) -> Optional[str]:
|
|
324
|
+
"""Get DID for a function by name."""
|
|
325
|
+
if not self.identity_package:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
# Check reasoners
|
|
329
|
+
if function_name in self.identity_package.reasoner_dids:
|
|
330
|
+
return self.identity_package.reasoner_dids[function_name].did
|
|
331
|
+
|
|
332
|
+
# Check skills
|
|
333
|
+
if function_name in self.identity_package.skill_dids:
|
|
334
|
+
return self.identity_package.skill_dids[function_name].did
|
|
335
|
+
|
|
336
|
+
# Return agent DID as fallback
|
|
337
|
+
return self.identity_package.agent_did.did
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Dict, Optional, Type
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, create_model
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
|
|
7
|
+
from agentfield.agent_utils import AgentUtils
|
|
8
|
+
from agentfield.execution_context import ExecutionContext
|
|
9
|
+
from agentfield.logger import log_debug, log_error, log_info, log_warn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DynamicMCPSkillManager:
|
|
13
|
+
"""
|
|
14
|
+
Dynamic MCP Skill Generator that converts MCP tools into AgentField skills.
|
|
15
|
+
|
|
16
|
+
This class discovers MCP servers, lists their tools, and dynamically
|
|
17
|
+
registers each tool as a AgentField skill with proper schema generation
|
|
18
|
+
and execution context handling.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, agent, dev_mode: bool = False):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the Dynamic MCP Skill Manager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
agent: The AgentField agent instance
|
|
27
|
+
dev_mode: Enable development mode logging
|
|
28
|
+
"""
|
|
29
|
+
self.agent = agent
|
|
30
|
+
self.dev_mode = dev_mode
|
|
31
|
+
self.registered_skills: Dict[str, Dict] = {}
|
|
32
|
+
|
|
33
|
+
async def discover_and_register_all_skills(self) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Discover and register all MCP tools as AgentField skills.
|
|
36
|
+
|
|
37
|
+
This method:
|
|
38
|
+
1. Checks for MCP client registry availability
|
|
39
|
+
2. Iterates through all connected MCP servers
|
|
40
|
+
3. Waits for server readiness
|
|
41
|
+
4. Performs health checks on each server
|
|
42
|
+
5. Lists tools from healthy servers
|
|
43
|
+
6. Registers each tool as a AgentField skill
|
|
44
|
+
"""
|
|
45
|
+
if not self.agent.mcp_client_registry:
|
|
46
|
+
if self.dev_mode:
|
|
47
|
+
log_warn("MCP client registry not available")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if self.dev_mode:
|
|
51
|
+
log_info("Starting MCP skill discovery...")
|
|
52
|
+
|
|
53
|
+
# Get all registered MCP clients
|
|
54
|
+
clients = self.agent.mcp_client_registry.clients
|
|
55
|
+
|
|
56
|
+
if not clients:
|
|
57
|
+
if self.dev_mode:
|
|
58
|
+
log_info("No MCP servers found in registry")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Wait for server readiness
|
|
62
|
+
await asyncio.sleep(1)
|
|
63
|
+
|
|
64
|
+
for server_alias, client in clients.items():
|
|
65
|
+
try:
|
|
66
|
+
if self.dev_mode:
|
|
67
|
+
log_debug(f"Processing MCP server: {server_alias}")
|
|
68
|
+
|
|
69
|
+
# Perform health check
|
|
70
|
+
is_healthy = await client.health_check()
|
|
71
|
+
if not is_healthy:
|
|
72
|
+
if self.dev_mode:
|
|
73
|
+
log_warn(
|
|
74
|
+
f"MCP server {server_alias} failed health check, skipping"
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# List tools from the server
|
|
79
|
+
tools = await client.list_tools()
|
|
80
|
+
if not tools:
|
|
81
|
+
if self.dev_mode:
|
|
82
|
+
log_info(f"No tools found in MCP server {server_alias}")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if self.dev_mode:
|
|
86
|
+
log_debug(f"Found {len(tools)} tools in {server_alias}")
|
|
87
|
+
|
|
88
|
+
# Register each tool as a skill
|
|
89
|
+
for tool in tools:
|
|
90
|
+
try:
|
|
91
|
+
skill_name = AgentUtils.generate_skill_name(
|
|
92
|
+
server_alias, tool.get("name", "")
|
|
93
|
+
)
|
|
94
|
+
await self._register_mcp_tool_as_skill(
|
|
95
|
+
server_alias, tool, skill_name
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.dev_mode:
|
|
99
|
+
log_info(f"Registered skill: {skill_name}")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
if self.dev_mode:
|
|
103
|
+
log_error(
|
|
104
|
+
f"Failed to register tool {tool.get('name', 'unknown')} from {server_alias}: {e}"
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
if self.dev_mode:
|
|
110
|
+
log_error(f"Error processing MCP server {server_alias}: {e}")
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if self.dev_mode:
|
|
114
|
+
log_info(
|
|
115
|
+
f"MCP skill discovery complete. Registered {len(self.registered_skills)} skills"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def _register_mcp_tool_as_skill(
|
|
119
|
+
self, server_alias: str, tool: Dict[str, Any], skill_name: str
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Register an MCP tool as a AgentField skill.
|
|
123
|
+
|
|
124
|
+
This method:
|
|
125
|
+
1. Extracts tool metadata (name, description)
|
|
126
|
+
2. Generates Pydantic input schema from tool definition
|
|
127
|
+
3. Creates async wrapper function for MCP tool calls
|
|
128
|
+
4. Sets function metadata
|
|
129
|
+
5. Creates FastAPI endpoint
|
|
130
|
+
6. Handles execution context from request headers
|
|
131
|
+
7. Stores and clears execution context appropriately
|
|
132
|
+
8. Registers skill metadata with agent
|
|
133
|
+
9. Adds to internal skill registry
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
server_alias: MCP server alias
|
|
137
|
+
tool: Tool definition from MCP server
|
|
138
|
+
skill_name: Generated skill name
|
|
139
|
+
"""
|
|
140
|
+
tool_name = tool.get("name", "")
|
|
141
|
+
description = tool.get(
|
|
142
|
+
"description", f"MCP tool {tool_name} from {server_alias}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Generate Pydantic input schema
|
|
146
|
+
input_schema = self._create_input_schema_from_tool(skill_name, tool)
|
|
147
|
+
|
|
148
|
+
# Create async wrapper function for MCP tool calls
|
|
149
|
+
async def mcp_skill_wrapper(**kwargs):
|
|
150
|
+
"""Dynamically created MCP skill function"""
|
|
151
|
+
try:
|
|
152
|
+
# Get MCP client for this server
|
|
153
|
+
client = self.agent.mcp_client_registry.get_client(server_alias)
|
|
154
|
+
if not client:
|
|
155
|
+
return {
|
|
156
|
+
"status": "error",
|
|
157
|
+
"error": f"MCP client for server '{server_alias}' not available",
|
|
158
|
+
"server": server_alias,
|
|
159
|
+
"tool": tool_name,
|
|
160
|
+
"args": kwargs,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Call the MCP tool
|
|
164
|
+
result = await client.call_tool(tool_name, kwargs)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"status": "success",
|
|
168
|
+
"result": result,
|
|
169
|
+
"server": server_alias,
|
|
170
|
+
"tool": tool_name,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return {
|
|
175
|
+
"status": "error",
|
|
176
|
+
"error": str(e),
|
|
177
|
+
"server": server_alias,
|
|
178
|
+
"tool": tool_name,
|
|
179
|
+
"args": kwargs,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Set function metadata
|
|
183
|
+
mcp_skill_wrapper.__name__ = skill_name
|
|
184
|
+
mcp_skill_wrapper.__doc__ = description
|
|
185
|
+
|
|
186
|
+
# Create FastAPI endpoint
|
|
187
|
+
endpoint_path = f"/skills/{skill_name}"
|
|
188
|
+
|
|
189
|
+
# Create the endpoint function dynamically
|
|
190
|
+
async def mcp_skill_endpoint(input_data: Any, request: Request):
|
|
191
|
+
"""Dynamically created MCP skill endpoint"""
|
|
192
|
+
# Validate input data against the schema
|
|
193
|
+
validated_data = (
|
|
194
|
+
input_schema(**input_data)
|
|
195
|
+
if isinstance(input_data, dict)
|
|
196
|
+
else input_data
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Handle execution context from request headers
|
|
200
|
+
execution_context = ExecutionContext.from_request(
|
|
201
|
+
request, self.agent.node_id
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Store execution context in agent
|
|
205
|
+
self.agent._current_execution_context = execution_context
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Convert input to function arguments
|
|
209
|
+
if hasattr(validated_data, "dict"):
|
|
210
|
+
kwargs = validated_data.model_dump()
|
|
211
|
+
elif isinstance(validated_data, dict):
|
|
212
|
+
kwargs = validated_data
|
|
213
|
+
else:
|
|
214
|
+
kwargs = {}
|
|
215
|
+
|
|
216
|
+
# Call the MCP skill wrapper
|
|
217
|
+
result = await mcp_skill_wrapper(**kwargs)
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
finally:
|
|
222
|
+
# Clear execution context after completion
|
|
223
|
+
self.agent._current_execution_context = None
|
|
224
|
+
|
|
225
|
+
# Set the correct parameter annotation for FastAPI
|
|
226
|
+
mcp_skill_endpoint.__annotations__ = {
|
|
227
|
+
"input_data": input_schema,
|
|
228
|
+
"request": Request,
|
|
229
|
+
"return": dict,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Register the endpoint
|
|
233
|
+
self.agent.post(endpoint_path, response_model=dict)(mcp_skill_endpoint)
|
|
234
|
+
|
|
235
|
+
# Register skill metadata with agent
|
|
236
|
+
skill_metadata = {
|
|
237
|
+
"id": skill_name,
|
|
238
|
+
"input_schema": input_schema.model_json_schema(),
|
|
239
|
+
"tags": ["mcp", server_alias],
|
|
240
|
+
"description": description,
|
|
241
|
+
"server_alias": server_alias,
|
|
242
|
+
"tool_name": tool_name,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
self.agent.skills.append(skill_metadata)
|
|
246
|
+
|
|
247
|
+
# Add to internal skill registry
|
|
248
|
+
self.registered_skills[skill_name] = skill_metadata
|
|
249
|
+
|
|
250
|
+
def _create_input_schema_from_tool(
|
|
251
|
+
self, skill_name: str, tool: Dict[str, Any]
|
|
252
|
+
) -> Type[BaseModel]:
|
|
253
|
+
"""
|
|
254
|
+
Create Pydantic input schema from MCP tool definition.
|
|
255
|
+
|
|
256
|
+
Schema Generation Rules:
|
|
257
|
+
- Extract inputSchema.properties and required fields
|
|
258
|
+
- Map JSON Schema types to Python types
|
|
259
|
+
- Handle required vs optional fields appropriately
|
|
260
|
+
- Set default values when specified
|
|
261
|
+
- Use Optional[Type] for non-required fields without defaults
|
|
262
|
+
- Fallback to generic {"data": Optional[Dict[str, Any]]} if no properties
|
|
263
|
+
- Create model with name pattern: {skill_name}Input
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
skill_name: Name of the skill
|
|
267
|
+
tool: Tool definition from MCP server
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Pydantic BaseModel class for input validation
|
|
271
|
+
"""
|
|
272
|
+
input_schema = tool.get("inputSchema", {})
|
|
273
|
+
properties = input_schema.get("properties", {})
|
|
274
|
+
required_fields = set(input_schema.get("required", []))
|
|
275
|
+
|
|
276
|
+
# If no properties defined, use generic schema
|
|
277
|
+
if not properties:
|
|
278
|
+
return create_model(
|
|
279
|
+
f"{skill_name}Input", data=(Optional[Dict[str, Any]], None)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Build field definitions for Pydantic model
|
|
283
|
+
field_definitions = {}
|
|
284
|
+
|
|
285
|
+
for field_name, field_def in properties.items():
|
|
286
|
+
field_type = AgentUtils.map_json_type_to_python(
|
|
287
|
+
field_def.get("type", "string")
|
|
288
|
+
)
|
|
289
|
+
default_value = field_def.get("default")
|
|
290
|
+
is_required = field_name in required_fields
|
|
291
|
+
|
|
292
|
+
if is_required and default_value is None:
|
|
293
|
+
# Required field without default
|
|
294
|
+
field_definitions[field_name] = (field_type, ...)
|
|
295
|
+
elif default_value is not None:
|
|
296
|
+
# Field with default value
|
|
297
|
+
field_definitions[field_name] = (field_type, default_value)
|
|
298
|
+
else:
|
|
299
|
+
# Optional field without default
|
|
300
|
+
field_definitions[field_name] = (Optional[field_type], None)
|
|
301
|
+
|
|
302
|
+
# Create and return the Pydantic model
|
|
303
|
+
model_name = f"{skill_name}Input"
|
|
304
|
+
return create_model(model_name, **field_definitions)
|