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.
Files changed (42) hide show
  1. agentfield/__init__.py +66 -0
  2. agentfield/agent.py +3569 -0
  3. agentfield/agent_ai.py +1125 -0
  4. agentfield/agent_cli.py +386 -0
  5. agentfield/agent_field_handler.py +494 -0
  6. agentfield/agent_mcp.py +534 -0
  7. agentfield/agent_registry.py +29 -0
  8. agentfield/agent_server.py +1185 -0
  9. agentfield/agent_utils.py +269 -0
  10. agentfield/agent_workflow.py +323 -0
  11. agentfield/async_config.py +278 -0
  12. agentfield/async_execution_manager.py +1227 -0
  13. agentfield/client.py +1447 -0
  14. agentfield/connection_manager.py +280 -0
  15. agentfield/decorators.py +527 -0
  16. agentfield/did_manager.py +337 -0
  17. agentfield/dynamic_skills.py +304 -0
  18. agentfield/execution_context.py +255 -0
  19. agentfield/execution_state.py +453 -0
  20. agentfield/http_connection_manager.py +429 -0
  21. agentfield/litellm_adapters.py +140 -0
  22. agentfield/logger.py +249 -0
  23. agentfield/mcp_client.py +204 -0
  24. agentfield/mcp_manager.py +340 -0
  25. agentfield/mcp_stdio_bridge.py +550 -0
  26. agentfield/memory.py +723 -0
  27. agentfield/memory_events.py +489 -0
  28. agentfield/multimodal.py +173 -0
  29. agentfield/multimodal_response.py +403 -0
  30. agentfield/pydantic_utils.py +227 -0
  31. agentfield/rate_limiter.py +280 -0
  32. agentfield/result_cache.py +441 -0
  33. agentfield/router.py +190 -0
  34. agentfield/status.py +70 -0
  35. agentfield/types.py +710 -0
  36. agentfield/utils.py +26 -0
  37. agentfield/vc_generator.py +464 -0
  38. agentfield/vision.py +198 -0
  39. agentfield-0.1.22rc2.dist-info/METADATA +102 -0
  40. agentfield-0.1.22rc2.dist-info/RECORD +42 -0
  41. agentfield-0.1.22rc2.dist-info/WHEEL +5 -0
  42. 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)