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
agentfield/logger.py ADDED
@@ -0,0 +1,249 @@
1
+ """
2
+ AgentField SDK Logging Utility
3
+
4
+ This module provides a centralized logging system for the AgentField SDK that:
5
+ - Replaces print statements with proper logging
6
+ - Provides configurable log levels
7
+ - Truncates long messages and payloads
8
+ - Supports environment variable configuration
9
+ - Maintains emoji-based visual indicators for different message types
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ from enum import Enum
16
+ from typing import Any, Optional
17
+
18
+
19
+ class LogLevel(Enum):
20
+ """Log levels for AgentField SDK"""
21
+
22
+ DEBUG = "DEBUG"
23
+ INFO = "INFO"
24
+ WARN = "WARN"
25
+ WARNING = "WARNING"
26
+ ERROR = "ERROR"
27
+
28
+
29
+ class AgentFieldLogger:
30
+ """
31
+ Centralized logger for AgentField SDK with configurable verbosity and payload truncation.
32
+
33
+ Supports runtime log level changes (e.g., for dev_mode).
34
+ """
35
+
36
+ def __init__(self, name: str = "agentfield"):
37
+ self.logger = logging.getLogger(name)
38
+ self._setup_logger()
39
+
40
+ # Configuration from environment variables - default to WARNING (only important events)
41
+ self.log_level = os.getenv("AGENTFIELD_LOG_LEVEL", "WARNING").upper()
42
+ self.truncate_length = int(os.getenv("AGENTFIELD_LOG_TRUNCATE", "200"))
43
+ self.show_payloads = (
44
+ os.getenv("AGENTFIELD_LOG_PAYLOADS", "false").lower() == "true"
45
+ )
46
+ self.show_tracking = (
47
+ os.getenv("AGENTFIELD_LOG_TRACKING", "false").lower() == "true"
48
+ )
49
+ self.show_fire = os.getenv("AGENTFIELD_LOG_FIRE", "false").lower() == "true"
50
+
51
+ # Set logger level based on configuration
52
+ level_map = {
53
+ "DEBUG": logging.DEBUG,
54
+ "INFO": logging.INFO,
55
+ "WARN": logging.WARNING,
56
+ "WARNING": logging.WARNING,
57
+ "ERROR": logging.ERROR,
58
+ "SILENT": logging.CRITICAL + 1, # Effectively silent
59
+ }
60
+ self.logger.setLevel(level_map.get(self.log_level, logging.WARNING))
61
+
62
+ def set_level(self, level: str):
63
+ """Set log level at runtime (e.g., 'DEBUG', 'INFO', 'WARN', 'ERROR')"""
64
+ level_map = {
65
+ "DEBUG": logging.DEBUG,
66
+ "INFO": logging.INFO,
67
+ "WARN": logging.WARNING,
68
+ "WARNING": logging.WARNING,
69
+ "ERROR": logging.ERROR,
70
+ }
71
+ self.logger.setLevel(level_map.get(level.upper(), logging.INFO))
72
+
73
+ def _setup_logger(self):
74
+ """Setup logger with console handler if not already configured"""
75
+ if not self.logger.handlers:
76
+ handler = logging.StreamHandler()
77
+ formatter = logging.Formatter("%(message)s")
78
+ handler.setFormatter(formatter)
79
+ self.logger.addHandler(handler)
80
+ self.logger.propagate = False
81
+
82
+ def _truncate_message(self, message: str) -> str:
83
+ """Truncate message if it exceeds the configured length"""
84
+ if len(message) <= self.truncate_length:
85
+ return message
86
+ return message[: self.truncate_length] + "..."
87
+
88
+ def _format_payload(self, payload: Any) -> str:
89
+ """Format payload for logging with truncation"""
90
+ if not self.show_payloads:
91
+ return "[payload hidden - set AGENTFIELD_LOG_PAYLOADS=true to show]"
92
+
93
+ try:
94
+ if isinstance(payload, dict):
95
+ payload_str = json.dumps(payload, indent=2, default=str)
96
+ else:
97
+ payload_str = str(payload)
98
+
99
+ return self._truncate_message(payload_str)
100
+ except Exception:
101
+ return self._truncate_message(str(payload))
102
+
103
+ def heartbeat(self, message: str, **kwargs):
104
+ """Log heartbeat messages (only shown in debug mode to avoid spam)"""
105
+ self.logger.debug(f"💓 {message}")
106
+
107
+ def track(self, message: str, **kwargs):
108
+ """Log tracking messages (controlled by AGENTFIELD_LOG_TRACKING)"""
109
+ if self.show_tracking:
110
+ self.logger.debug(f"🔍 TRACK: {self._truncate_message(message)}")
111
+
112
+ def fire(self, message: str, payload: Optional[Any] = None, **kwargs):
113
+ """Log fire-and-forget workflow messages (controlled by AGENTFIELD_LOG_FIRE)"""
114
+ if self.show_fire:
115
+ if payload is not None:
116
+ formatted_payload = self._format_payload(payload)
117
+ self.logger.debug(
118
+ f"🔥 FIRE: {self._truncate_message(message)}\n{formatted_payload}"
119
+ )
120
+ else:
121
+ self.logger.debug(f"🔥 FIRE: {self._truncate_message(message)}")
122
+
123
+ def debug(self, message: str, payload: Optional[Any] = None, **kwargs):
124
+ """Log debug messages"""
125
+ if payload is not None:
126
+ formatted_payload = self._format_payload(payload)
127
+ self.logger.debug(
128
+ f"🔍 DEBUG: {self._truncate_message(message)}\n{formatted_payload}"
129
+ )
130
+ else:
131
+ self.logger.debug(f"🔍 DEBUG: {self._truncate_message(message)}")
132
+
133
+ def info(self, message: str, **kwargs):
134
+ """Log info messages"""
135
+ self.logger.info(f"ℹ️ {self._truncate_message(message)}")
136
+
137
+ def warn(self, message: str, **kwargs):
138
+ """Log warning messages"""
139
+ self.logger.warning(f"⚠️ {self._truncate_message(message)}")
140
+
141
+ def warning(self, message: str, **kwargs):
142
+ """Alias for warn to match logging.Logger API"""
143
+ self.warn(message, **kwargs)
144
+
145
+ def error(self, message: str, **kwargs):
146
+ """Log error messages"""
147
+ self.logger.error(f"❌ {self._truncate_message(message)}")
148
+
149
+ def critical(self, message: str, **kwargs):
150
+ """Log critical messages"""
151
+ self.logger.critical(f"🚨 {self._truncate_message(message)}")
152
+
153
+ def success(self, message: str, **kwargs):
154
+ """Log success messages"""
155
+ self.logger.info(f"✅ {self._truncate_message(message)}")
156
+
157
+ def setup(self, message: str, **kwargs):
158
+ """Log setup/initialization messages"""
159
+ self.logger.info(f"🔧 {self._truncate_message(message)}")
160
+
161
+ def network(self, message: str, **kwargs):
162
+ """Log network-related messages"""
163
+ self.logger.info(f"🌐 {self._truncate_message(message)}")
164
+
165
+ def mcp(self, message: str, **kwargs):
166
+ """Log MCP-related messages"""
167
+ self.logger.info(f"🔌 {self._truncate_message(message)}")
168
+
169
+ def security(self, message: str, **kwargs):
170
+ """Log security/DID-related messages"""
171
+ self.logger.info(f"🔐 {self._truncate_message(message)}")
172
+
173
+
174
+ # Global logger instance
175
+ _global_logger = None
176
+
177
+
178
+ def get_logger(name: str = "agentfield") -> AgentFieldLogger:
179
+ """Get or create a AgentField SDK logger instance"""
180
+ global _global_logger
181
+ if _global_logger is None:
182
+ _global_logger = AgentFieldLogger(name)
183
+ return _global_logger
184
+
185
+
186
+ def set_log_level(level: str):
187
+ """Set log level for the global logger at runtime (e.g., 'DEBUG', 'INFO', 'WARN', 'ERROR')"""
188
+ get_logger().set_level(level)
189
+
190
+
191
+ # Convenience functions for common logging patterns
192
+ def log_heartbeat(message: str, **kwargs):
193
+ """Log heartbeat message"""
194
+ get_logger().heartbeat(message, **kwargs)
195
+
196
+
197
+ def log_track(message: str, **kwargs):
198
+ """Log tracking message"""
199
+ get_logger().track(message, **kwargs)
200
+
201
+
202
+ def log_fire(message: str, payload: Optional[Any] = None, **kwargs):
203
+ """Log fire-and-forget message"""
204
+ get_logger().fire(message, payload, **kwargs)
205
+
206
+
207
+ def log_debug(message: str, payload: Optional[Any] = None, **kwargs):
208
+ """Log debug message"""
209
+ get_logger().debug(message, payload, **kwargs)
210
+
211
+
212
+ def log_info(message: str, **kwargs):
213
+ """Log info message"""
214
+ get_logger().info(message, **kwargs)
215
+
216
+
217
+ def log_warn(message: str, **kwargs):
218
+ """Log warning message"""
219
+ get_logger().warn(message, **kwargs)
220
+
221
+
222
+ def log_error(message: str, **kwargs):
223
+ """Log error message"""
224
+ get_logger().error(message, **kwargs)
225
+
226
+
227
+ def log_success(message: str, **kwargs):
228
+ """Log success message"""
229
+ get_logger().success(message, **kwargs)
230
+
231
+
232
+ def log_setup(message: str, **kwargs):
233
+ """Log setup message"""
234
+ get_logger().setup(message, **kwargs)
235
+
236
+
237
+ def log_network(message: str, **kwargs):
238
+ """Log network message"""
239
+ get_logger().network(message, **kwargs)
240
+
241
+
242
+ def log_mcp(message: str, **kwargs):
243
+ """Log MCP message"""
244
+ get_logger().mcp(message, **kwargs)
245
+
246
+
247
+ def log_security(message: str, **kwargs):
248
+ """Log security message"""
249
+ get_logger().security(message, **kwargs)
@@ -0,0 +1,204 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ import aiohttp
4
+ from aiohttp import ClientTimeout
5
+
6
+ from agentfield.logger import log_debug, log_error, log_info, log_warn
7
+
8
+
9
+ class MCPClient:
10
+ def __init__(self, base_url: str, alias: str, dev_mode: bool = False):
11
+ self.server_alias = alias
12
+ self.base_url = base_url
13
+ self.dev_mode = dev_mode
14
+ self.session: Optional[aiohttp.ClientSession] = None
15
+ self._is_stdio_bridge = False # Default to direct HTTP
16
+
17
+ # Legacy constructor support for backward compatibility
18
+ @classmethod
19
+ def from_port(cls, server_alias: str, port: int, dev_mode: bool = False):
20
+ """Create MCPClient from port (legacy method for backward compatibility)"""
21
+ base_url = f"http://localhost:{port}"
22
+ return cls(base_url, server_alias, dev_mode)
23
+
24
+ async def _ensure_session(self) -> None:
25
+ """Ensure aiohttp session exists"""
26
+ if self.session is None or self.session.closed:
27
+ self.session = aiohttp.ClientSession()
28
+
29
+ async def close(self):
30
+ """Close the client session"""
31
+ if self.session and not self.session.closed:
32
+ await self.session.close()
33
+
34
+ async def health_check(self) -> bool:
35
+ """Check if MCP server is healthy"""
36
+ try:
37
+ await self._ensure_session()
38
+ if self.session is None:
39
+ return False
40
+ timeout = ClientTimeout(total=5)
41
+
42
+ # Use /health endpoint for both bridge and direct HTTP
43
+ async with self.session.get(
44
+ f"{self.base_url}/health", timeout=timeout
45
+ ) as response:
46
+ return response.status == 200
47
+ except Exception as e:
48
+ if self.dev_mode:
49
+ log_warn(f"Health check failed for {self.server_alias}: {e}")
50
+ return False
51
+
52
+ async def list_tools(self) -> List[Dict[str, Any]]:
53
+ """Get available tools from MCP server"""
54
+ try:
55
+ await self._ensure_session()
56
+ if self.session is None:
57
+ return []
58
+
59
+ timeout = ClientTimeout(total=10)
60
+
61
+ if getattr(self, "_is_stdio_bridge", False):
62
+ # Use bridge endpoint
63
+ endpoint = "/mcp/tools/list"
64
+ async with self.session.post(
65
+ f"{self.base_url}{endpoint}", timeout=timeout
66
+ ) as response:
67
+ if response.status == 200:
68
+ data = await response.json()
69
+ tools = data.get("tools", [])
70
+ if self.dev_mode:
71
+ log_debug(
72
+ f"Found {len(tools)} tools in {self.server_alias} (stdio bridge)"
73
+ )
74
+ return tools
75
+ else:
76
+ # Use direct HTTP endpoint
77
+ request_data = {
78
+ "jsonrpc": "2.0",
79
+ "id": 1,
80
+ "method": "tools/list",
81
+ "params": {},
82
+ }
83
+
84
+ async with self.session.post(
85
+ f"{self.base_url}/mcp/v1", json=request_data, timeout=timeout
86
+ ) as response:
87
+ if response.status == 200:
88
+ data = await response.json()
89
+ if "result" in data and "tools" in data["result"]:
90
+ tools = data["result"]["tools"]
91
+ if self.dev_mode:
92
+ log_debug(
93
+ f"Found {len(tools)} tools in {self.server_alias} (direct HTTP)"
94
+ )
95
+ return tools
96
+
97
+ except Exception as e:
98
+ if self.dev_mode:
99
+ log_error(f"Failed to list tools for {self.server_alias}: {e}")
100
+
101
+ return []
102
+
103
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
104
+ """Call specific tool on MCP server"""
105
+ try:
106
+ await self._ensure_session()
107
+ if self.session is None:
108
+ raise Exception("Session not available")
109
+
110
+ if self.dev_mode:
111
+ transport_type = (
112
+ "stdio bridge"
113
+ if getattr(self, "_is_stdio_bridge", False)
114
+ else "direct HTTP"
115
+ )
116
+ log_debug(
117
+ f"Calling {self.server_alias}.{tool_name} with args: {arguments} ({transport_type})"
118
+ )
119
+
120
+ timeout = ClientTimeout(total=30)
121
+
122
+ if getattr(self, "_is_stdio_bridge", False):
123
+ # Use bridge endpoint
124
+ request_data = {"tool_name": tool_name, "arguments": arguments}
125
+
126
+ async with self.session.post(
127
+ f"{self.base_url}/mcp/tools/call",
128
+ json=request_data,
129
+ timeout=timeout,
130
+ ) as response:
131
+ if response.status == 200:
132
+ data = await response.json()
133
+ return data
134
+ else:
135
+ raise Exception(
136
+ f"HTTP {response.status}: {await response.text()}"
137
+ )
138
+ else:
139
+ # Use direct HTTP endpoint
140
+ request_data = {
141
+ "jsonrpc": "2.0",
142
+ "id": 1,
143
+ "method": "tools/call",
144
+ "params": {"name": tool_name, "arguments": arguments},
145
+ }
146
+
147
+ async with self.session.post(
148
+ f"{self.base_url}/mcp/v1", json=request_data, timeout=timeout
149
+ ) as response:
150
+ if response.status == 200:
151
+ data = await response.json()
152
+ if "result" in data:
153
+ return data["result"]
154
+ elif "error" in data:
155
+ raise Exception(f"MCP tool error: {data['error']}")
156
+ else:
157
+ raise Exception(
158
+ f"HTTP {response.status}: {await response.text()}"
159
+ )
160
+
161
+ except Exception as e:
162
+ if self.dev_mode:
163
+ log_error(f"Tool call failed {self.server_alias}.{tool_name}: {e}")
164
+ raise Exception(
165
+ f"MCP tool '{self.server_alias}.{tool_name}' failed: {str(e)}"
166
+ )
167
+
168
+
169
+ class MCPClientRegistry:
170
+ """Registry to manage MCP clients for all servers"""
171
+
172
+ def __init__(self, dev_mode: bool = False):
173
+ self.clients: Dict[str, MCPClient] = {}
174
+ self.dev_mode = dev_mode
175
+
176
+ def register_client(self, alias: str, port: int):
177
+ """Register MCP client for server"""
178
+ base_url = f"http://localhost:{port}"
179
+ client = MCPClient(base_url, alias, self.dev_mode)
180
+ self.clients[alias] = client
181
+
182
+ if self.dev_mode:
183
+ log_info(f"Registered MCP client for {alias} on port {port}")
184
+
185
+ def register_stdio_bridge_client(self, alias: str, bridge_port: int) -> None:
186
+ """Register a client for a stdio bridge server"""
187
+ base_url = f"http://localhost:{bridge_port}"
188
+ client = MCPClient(base_url, alias, self.dev_mode)
189
+ client._is_stdio_bridge = True # Mark as bridge client
190
+ self.clients[alias] = client
191
+ if self.dev_mode:
192
+ log_info(
193
+ f"Registered stdio bridge client for {alias} on port {bridge_port}"
194
+ )
195
+
196
+ def get_client(self, alias: str) -> Optional[MCPClient]:
197
+ """Get MCP client by server alias"""
198
+ return self.clients.get(alias)
199
+
200
+ async def close_all(self):
201
+ """Close all MCP clients"""
202
+ for client in self.clients.values():
203
+ await client.close()
204
+ self.clients.clear()