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,340 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import subprocess
5
+ from typing import Any, Dict, List, Optional
6
+ from dataclasses import dataclass
7
+
8
+ from .logger import get_logger
9
+ from .mcp_stdio_bridge import StdioMCPBridge
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class MCPServerConfig:
16
+ alias: str
17
+ run_command: str
18
+ working_dir: str
19
+ environment: Dict[str, str]
20
+ health_check: Optional[str] = None
21
+ port: Optional[int] = None
22
+ transport: str = "http"
23
+
24
+
25
+ @dataclass
26
+ class MCPServerProcess:
27
+ config: MCPServerConfig
28
+ process: Optional[subprocess.Popen] = None
29
+ port: Optional[int] = None
30
+ status: str = "stopped" # stopped, starting, running, failed
31
+
32
+
33
+ class MCPManager:
34
+ def __init__(self, agent_directory: str, dev_mode: bool = False):
35
+ self.agent_directory = agent_directory
36
+ self.dev_mode = dev_mode
37
+ self.servers: Dict[str, MCPServerProcess] = {}
38
+ self.stdio_bridges: Dict[str, StdioMCPBridge] = {}
39
+ self.port_range_start = 8100 # Start assigning ports from 8100
40
+ self.used_ports = set()
41
+
42
+ def discover_mcp_servers(self) -> List[MCPServerConfig]:
43
+ """Discover MCP servers from packages/mcp/ directory"""
44
+ mcp_dir = os.path.join(self.agent_directory, "packages", "mcp")
45
+ servers = []
46
+
47
+ if not os.path.exists(mcp_dir):
48
+ if self.dev_mode:
49
+ logger.debug(f"No MCP directory found at {mcp_dir}")
50
+ return servers
51
+
52
+ for item in os.listdir(mcp_dir):
53
+ server_dir = os.path.join(mcp_dir, item)
54
+ config_file = os.path.join(server_dir, "config.json")
55
+
56
+ if os.path.isdir(server_dir) and os.path.exists(config_file):
57
+ try:
58
+ with open(config_file, "r") as f:
59
+ config_data = json.load(f)
60
+
61
+ config = MCPServerConfig(
62
+ alias=config_data.get("alias", item),
63
+ run_command=config_data.get("run", ""),
64
+ working_dir=server_dir,
65
+ environment=config_data.get("environment", {}),
66
+ health_check=config_data.get("health_check"),
67
+ transport=config_data.get("transport", "http"),
68
+ )
69
+ servers.append(config)
70
+
71
+ if self.dev_mode:
72
+ logger.debug(f"Discovered MCP server: {config.alias}")
73
+
74
+ except Exception as e:
75
+ if self.dev_mode:
76
+ logger.warning(f"Failed to load config for {item}: {e}")
77
+
78
+ return servers
79
+
80
+ def _get_next_available_port(self) -> int:
81
+ """Get next available port for MCP server"""
82
+ import socket
83
+
84
+ for port in range(self.port_range_start, self.port_range_start + 1000):
85
+ if port not in self.used_ports:
86
+ # Test if port is actually available
87
+ try:
88
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
89
+ s.bind(("localhost", port))
90
+ self.used_ports.add(port)
91
+ return port
92
+ except OSError:
93
+ continue
94
+
95
+ raise RuntimeError("No available ports for MCP servers")
96
+
97
+ def _detect_transport(self, config: MCPServerConfig) -> str:
98
+ """Detect transport type from config"""
99
+ return config.transport
100
+
101
+ async def _start_stdio_server(self, config: MCPServerConfig) -> bool:
102
+ """Start stdio MCP server using bridge"""
103
+ try:
104
+ # Assign port for the bridge
105
+ port = self._get_next_available_port()
106
+ config.port = port
107
+
108
+ if self.dev_mode:
109
+ logger.info(f"Starting stdio MCP server: {config.alias} on port {port}")
110
+ logger.debug(f"Command: {config.run_command}")
111
+
112
+ # Prepare server config for bridge
113
+ server_config = {
114
+ "run": config.run_command,
115
+ "working_dir": config.working_dir,
116
+ "environment": config.environment,
117
+ }
118
+
119
+ # Create and start stdio bridge
120
+ bridge = StdioMCPBridge(
121
+ server_config=server_config, port=port, dev_mode=self.dev_mode
122
+ )
123
+
124
+ success = await bridge.start()
125
+ if success:
126
+ self.stdio_bridges[config.alias] = bridge
127
+ if self.dev_mode:
128
+ logger.info(f"Stdio MCP server {config.alias} started successfully")
129
+ return True
130
+ else:
131
+ if self.dev_mode:
132
+ logger.error(f"Stdio MCP server {config.alias} failed to start")
133
+ return False
134
+
135
+ except Exception as e:
136
+ if self.dev_mode:
137
+ logger.error(f"Error starting stdio MCP server {config.alias}: {e}")
138
+ return False
139
+
140
+ async def _start_http_server(self, config: MCPServerConfig) -> bool:
141
+ """Start HTTP MCP server (original implementation)"""
142
+ try:
143
+ # Assign port
144
+ port = self._get_next_available_port()
145
+ config.port = port
146
+
147
+ # Prepare command with port substitution
148
+ run_command = config.run_command.replace("{{port}}", str(port))
149
+
150
+ # Prepare environment
151
+ env = os.environ.copy()
152
+ env.update(config.environment)
153
+
154
+ if self.dev_mode:
155
+ logger.info(f"Starting HTTP MCP server: {config.alias} on port {port}")
156
+ logger.debug(f"Command: {run_command}")
157
+
158
+ # Start process
159
+ process = subprocess.Popen(
160
+ run_command.split(),
161
+ cwd=config.working_dir,
162
+ env=env,
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE,
165
+ text=True,
166
+ )
167
+
168
+ # Create server process object
169
+ server_process = MCPServerProcess(
170
+ config=config, process=process, port=port, status="starting"
171
+ )
172
+
173
+ self.servers[config.alias] = server_process
174
+
175
+ # Wait a moment for startup
176
+ await asyncio.sleep(2)
177
+
178
+ # Check if process is still running
179
+ if process.poll() is None:
180
+ server_process.status = "running"
181
+ if self.dev_mode:
182
+ logger.info(f"HTTP MCP server {config.alias} started successfully")
183
+ return True
184
+ else:
185
+ server_process.status = "failed"
186
+ if self.dev_mode:
187
+ logger.error(f"HTTP MCP server {config.alias} failed to start")
188
+ return False
189
+
190
+ except Exception as e:
191
+ if self.dev_mode:
192
+ logger.error(f"Error starting HTTP MCP server {config.alias}: {e}")
193
+ if config.alias in self.servers:
194
+ self.servers[config.alias].status = "failed"
195
+ return False
196
+
197
+ async def start_server(self, config: MCPServerConfig) -> bool:
198
+ """Start individual MCP server"""
199
+ transport = self._detect_transport(config)
200
+ if transport == "stdio":
201
+ return await self._start_stdio_server(config)
202
+ else:
203
+ return await self._start_http_server(config)
204
+
205
+ async def start_all_servers(self) -> Dict[str, bool]:
206
+ """Start all discovered MCP servers"""
207
+ configs = self.discover_mcp_servers()
208
+ results = {}
209
+
210
+ if self.dev_mode:
211
+ logger.info(f"Starting {len(configs)} MCP servers...")
212
+
213
+ for config in configs:
214
+ success = await self.start_server(config)
215
+ results[config.alias] = success
216
+
217
+ return results
218
+
219
+ def get_server_status(self, alias: str) -> Optional[Dict[str, Any]]:
220
+ """Get status of specific MCP server"""
221
+ # Check stdio bridges first
222
+ if alias in self.stdio_bridges:
223
+ bridge = self.stdio_bridges[alias]
224
+ return {
225
+ "alias": alias,
226
+ "transport": "stdio",
227
+ "port": bridge.port,
228
+ "status": "running" if bridge.running else "stopped",
229
+ "initialized": bridge.initialized,
230
+ }
231
+
232
+ # Check HTTP servers
233
+ if alias in self.servers:
234
+ server_process = self.servers[alias]
235
+ return {
236
+ "alias": alias,
237
+ "transport": "http",
238
+ "port": server_process.port,
239
+ "status": server_process.status,
240
+ "config": server_process.config,
241
+ }
242
+
243
+ return None
244
+
245
+ def get_all_status(self) -> Dict[str, Dict[str, Any]]:
246
+ """Get status of all MCP servers"""
247
+ all_status = {}
248
+
249
+ # Add stdio bridges
250
+ for alias, bridge in self.stdio_bridges.items():
251
+ all_status[alias] = {
252
+ "alias": alias,
253
+ "transport": "stdio",
254
+ "port": bridge.port,
255
+ "status": "running" if bridge.running else "stopped",
256
+ "initialized": bridge.initialized,
257
+ }
258
+
259
+ # Add HTTP servers
260
+ for alias, server_process in self.servers.items():
261
+ all_status[alias] = {
262
+ "alias": alias,
263
+ "transport": "http",
264
+ "port": server_process.port,
265
+ "status": server_process.status,
266
+ "config": server_process.config,
267
+ }
268
+
269
+ return all_status
270
+
271
+ async def stop_server(self, alias: str) -> bool:
272
+ """Stop specific MCP server"""
273
+ # Check if it's a stdio bridge
274
+ if alias in self.stdio_bridges:
275
+ bridge = self.stdio_bridges[alias]
276
+ await bridge.stop()
277
+ if bridge.port:
278
+ self.used_ports.discard(bridge.port)
279
+ del self.stdio_bridges[alias]
280
+ if self.dev_mode:
281
+ logger.info(f"Stopped stdio MCP server: {alias}")
282
+ return True
283
+
284
+ # Check if it's an HTTP server
285
+ if alias in self.servers:
286
+ server_process = self.servers[alias]
287
+ if server_process.process and server_process.process.poll() is None:
288
+ server_process.process.terminate()
289
+ try:
290
+ server_process.process.wait(timeout=5)
291
+ except subprocess.TimeoutExpired:
292
+ server_process.process.kill()
293
+
294
+ server_process.status = "stopped"
295
+ if server_process.port:
296
+ self.used_ports.discard(server_process.port)
297
+
298
+ if self.dev_mode:
299
+ logger.info(f"Stopped HTTP MCP server: {alias}")
300
+ return True
301
+
302
+ return False
303
+
304
+ async def start_server_by_alias(self, alias: str) -> bool:
305
+ """Start MCP server by alias"""
306
+ # Find the config for this alias
307
+ configs = self.discover_mcp_servers()
308
+ for config in configs:
309
+ if config.alias == alias:
310
+ return await self.start_server(config)
311
+
312
+ if self.dev_mode:
313
+ logger.warning(f"No configuration found for MCP server: {alias}")
314
+ return False
315
+
316
+ async def restart_server(self, alias: str) -> bool:
317
+ """Restart MCP server by alias"""
318
+ # Stop first
319
+ stop_success = await self.stop_server(alias)
320
+ if self.dev_mode:
321
+ logger.info(f"Stopped '{alias}' for restart: {stop_success}")
322
+
323
+ # Wait a moment for cleanup
324
+ await asyncio.sleep(1)
325
+
326
+ # Start again
327
+ return await self.start_server_by_alias(alias)
328
+
329
+ async def shutdown_all(self) -> None:
330
+ """Stop all MCP servers"""
331
+ if self.dev_mode:
332
+ logger.info("Shutting down all MCP servers...")
333
+
334
+ # Stop all stdio bridges
335
+ for alias in list(self.stdio_bridges.keys()):
336
+ await self.stop_server(alias)
337
+
338
+ # Stop all HTTP servers
339
+ for alias in list(self.servers.keys()):
340
+ await self.stop_server(alias)