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,534 @@
1
+ import asyncio
2
+ from datetime import datetime
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from agentfield.agent_utils import AgentUtils
6
+ from agentfield.dynamic_skills import DynamicMCPSkillManager
7
+ from agentfield.execution_context import ExecutionContext
8
+ from agentfield.logger import log_debug, log_error, log_info, log_warn
9
+ from agentfield.mcp_client import MCPClientRegistry
10
+ from agentfield.mcp_manager import MCPManager
11
+ from agentfield.types import AgentStatus, MCPServerHealth
12
+ from fastapi import Request
13
+
14
+
15
+ class AgentMCP:
16
+ """
17
+ MCP Management handler for Agent class.
18
+
19
+ This class encapsulates all MCP-related functionality including:
20
+ - Agent directory detection
21
+ - MCP server lifecycle management
22
+ - MCP skill registration
23
+ - Health monitoring
24
+ """
25
+
26
+ def __init__(self, agent_instance):
27
+ """
28
+ Initialize the MCP handler with a reference to the agent instance.
29
+
30
+ Args:
31
+ agent_instance: The Agent instance this handler belongs to
32
+ """
33
+ self.agent = agent_instance
34
+
35
+ def _detect_agent_directory(self) -> str:
36
+ """Detect the correct agent directory for MCP config discovery"""
37
+ import os
38
+ from pathlib import Path
39
+
40
+ current_dir = Path(os.getcwd())
41
+
42
+ # Check if packages/mcp exists in current directory
43
+ if (current_dir / "packages" / "mcp").exists():
44
+ return str(current_dir)
45
+
46
+ # Look for agent directories in current directory
47
+ for item in current_dir.iterdir():
48
+ if item.is_dir() and (item / "packages" / "mcp").exists():
49
+ if self.agent.dev_mode:
50
+ log_debug(f"Found agent directory: {item}")
51
+ return str(item)
52
+
53
+ # Look in parent directories (up to 3 levels)
54
+ for i in range(3):
55
+ parent = current_dir.parents[i] if i < len(current_dir.parents) else None
56
+ if parent and (parent / "packages" / "mcp").exists():
57
+ if self.agent.dev_mode:
58
+ log_debug(f"Found agent directory in parent: {parent}")
59
+ return str(parent)
60
+
61
+ # Fallback to current directory
62
+ if self.agent.dev_mode:
63
+ log_warn(
64
+ f"No packages/mcp directory found, using current directory: {current_dir}"
65
+ )
66
+ return str(current_dir)
67
+
68
+ async def initialize_mcp(self):
69
+ """
70
+ Initialize MCP management components.
71
+
72
+ This method combines the MCP initialization logic that was previously
73
+ scattered in the Agent.__init__ method.
74
+ """
75
+ try:
76
+ agent_dir = self._detect_agent_directory()
77
+ self.agent.mcp_manager = MCPManager(agent_dir, self.agent.dev_mode)
78
+ self.agent.mcp_client_registry = MCPClientRegistry(self.agent.dev_mode)
79
+
80
+ if self.agent.dev_mode:
81
+ log_info(f"Initialized MCP Manager in {agent_dir}")
82
+
83
+ # Initialize Dynamic Skill Manager when both MCP components are available
84
+ if self.agent.mcp_manager and self.agent.mcp_client_registry:
85
+ self.agent.dynamic_skill_manager = DynamicMCPSkillManager(
86
+ self.agent, self.agent.dev_mode
87
+ )
88
+ if self.agent.dev_mode:
89
+ log_info("Dynamic MCP skill manager initialized")
90
+
91
+ except Exception as e:
92
+ if self.agent.dev_mode:
93
+ log_error(f"Failed to initialize MCP Manager: {e}")
94
+ self.agent.mcp_manager = None
95
+ self.agent.mcp_client_registry = None
96
+ self.agent.dynamic_skill_manager = None
97
+
98
+ async def _start_mcp_servers(self) -> None:
99
+ """Start all configured MCP servers using SimpleMCPManager."""
100
+ if not self.agent.mcp_manager:
101
+ if self.agent.dev_mode:
102
+ log_info("No MCP Manager available - skipping server startup")
103
+ return
104
+
105
+ try:
106
+ if self.agent.dev_mode:
107
+ log_info("Starting MCP servers...")
108
+
109
+ # Start all servers
110
+ started_servers = await self.agent.mcp_manager.start_all_servers()
111
+
112
+ if started_servers:
113
+ successful = sum(1 for success in started_servers.values() if success)
114
+ if self.agent.dev_mode:
115
+ log_info(f"Started {successful}/{len(started_servers)} MCP servers")
116
+ elif self.agent.dev_mode:
117
+ log_info("No MCP servers configured to start")
118
+
119
+ except Exception as e:
120
+ if self.agent.dev_mode:
121
+ log_error(f"MCP server startup error: {e}")
122
+
123
+ def _cleanup_mcp_servers(self) -> None:
124
+ """
125
+ Stop all MCP servers during agent shutdown.
126
+
127
+ This method is called during graceful shutdown to ensure all
128
+ MCP server processes are properly terminated.
129
+ """
130
+ if not self.agent.mcp_manager:
131
+ if self.agent.dev_mode:
132
+ log_info("No MCP Manager available - skipping cleanup")
133
+ return
134
+
135
+ async def async_cleanup():
136
+ try:
137
+ if self.agent.dev_mode:
138
+ log_info("Stopping MCP servers...")
139
+
140
+ # Check if mcp_manager is still available
141
+ if not self.agent.mcp_manager:
142
+ if self.agent.dev_mode:
143
+ log_info("MCP Manager not available during cleanup")
144
+ return
145
+
146
+ # Get current server status before stopping
147
+ all_status = self.agent.mcp_manager.get_all_status()
148
+
149
+ if all_status:
150
+ running_servers = [
151
+ alias
152
+ for alias, health in all_status.items()
153
+ if health.get("status") == "running"
154
+ ]
155
+
156
+ if running_servers:
157
+ # Stop all running servers
158
+ for alias in running_servers:
159
+ try:
160
+ if (
161
+ self.agent.mcp_manager
162
+ ): # Double-check before each call
163
+ await self.agent.mcp_manager.stop_server(alias)
164
+ if self.agent.dev_mode:
165
+ health = all_status.get(alias, {})
166
+ pid = health.get("pid") or "N/A"
167
+ log_info(
168
+ f"Stopped MCP server: {alias} (PID: {pid})"
169
+ )
170
+ except Exception as e:
171
+ if self.agent.dev_mode:
172
+ log_error(f"Failed to stop MCP server {alias}: {e}")
173
+
174
+ if self.agent.dev_mode:
175
+ log_info(f"Stopped {len(running_servers)} MCP servers")
176
+ elif self.agent.dev_mode:
177
+ log_info("No running MCP servers to stop")
178
+ except Exception as e:
179
+ if self.agent.dev_mode:
180
+ log_error(f"Error during MCP server cleanup: {e}")
181
+ # Continue with shutdown even if cleanup fails
182
+
183
+ # Run the async cleanup properly
184
+ try:
185
+ # Check if we're already in an event loop
186
+ try:
187
+ loop = asyncio.get_running_loop()
188
+ # If we're in a loop, create a task and store reference to prevent warning
189
+ task = loop.create_task(async_cleanup())
190
+
191
+ # Add a done callback to handle any exceptions and suppress warnings
192
+ def handle_task_completion(t):
193
+ try:
194
+ if t.exception() is not None and self.agent.dev_mode:
195
+ log_error(f"MCP cleanup task failed: {t.exception()}")
196
+ except Exception:
197
+ # Suppress any callback exceptions to prevent warnings
198
+ pass
199
+
200
+ task.add_done_callback(handle_task_completion)
201
+ # Store task reference to prevent garbage collection warning
202
+ if not hasattr(self, "_cleanup_tasks"):
203
+ self._cleanup_tasks = []
204
+ self._cleanup_tasks.append(task)
205
+ except RuntimeError:
206
+ # No event loop running, we can use asyncio.run()
207
+ try:
208
+ asyncio.run(async_cleanup())
209
+ except Exception as cleanup_error:
210
+ if self.agent.dev_mode:
211
+ log_error(f"MCP cleanup failed: {cleanup_error}")
212
+ except Exception as e:
213
+ if self.agent.dev_mode:
214
+ log_error(f"Failed to run MCP cleanup: {e}")
215
+
216
+ def _register_mcp_server_skills(self) -> None:
217
+ """
218
+ DEPRECATED: This method is replaced by DynamicMCPSkillManager.
219
+ The static file-based approach is broken after SimpleMCPManager refactor.
220
+ """
221
+ if self.agent.dev_mode:
222
+ log_warn("DEPRECATED: _register_mcp_server_skills() is no longer used")
223
+ return
224
+
225
+ def _register_mcp_tool_as_skill(
226
+ self, server_alias: str, tool: Dict[str, Any]
227
+ ) -> None:
228
+ """
229
+ Register an MCP tool as a proper FastAPI skill endpoint.
230
+
231
+ Args:
232
+ server_alias: The alias of the MCP server
233
+ tool: Tool definition from mcp.json
234
+ """
235
+ tool_name = tool.get("name", "")
236
+ if not tool_name:
237
+ if self.agent.dev_mode:
238
+ log_warn(f"Skipping tool with missing name: {tool}")
239
+ return
240
+
241
+ skill_name = f"{server_alias}_{tool_name}"
242
+ endpoint_path = f"/skills/{skill_name}"
243
+
244
+ # Create a simple input schema - use dict for flexibility
245
+ from pydantic import BaseModel
246
+
247
+ class InputSchema(BaseModel):
248
+ """Dynamic input schema for MCP tool"""
249
+
250
+ args: dict = {}
251
+
252
+ class Config:
253
+ extra = "allow" # Allow additional fields
254
+
255
+ # Create the MCP skill function
256
+ async def mcp_skill_function(**kwargs):
257
+ """Dynamically created MCP skill function"""
258
+ if self.agent.dev_mode:
259
+ log_debug(
260
+ f"MCP skill called: {server_alias}.{tool_name} with args: {kwargs}"
261
+ )
262
+
263
+ try:
264
+ # Get process-aware MCP client (reuses existing running processes)
265
+ if not self.agent.mcp_client_registry:
266
+ raise Exception("MCPClientRegistry not initialized")
267
+ mcp_client = self.agent.mcp_client_registry.get_client(server_alias)
268
+ if not mcp_client:
269
+ raise Exception(f"MCP client for {server_alias} not found")
270
+
271
+ # Call the MCP tool using existing process
272
+ result = await mcp_client.call_tool(tool_name, kwargs)
273
+
274
+ return {
275
+ "status": "success",
276
+ "result": result,
277
+ "server": server_alias,
278
+ "tool": tool_name,
279
+ }
280
+
281
+ except Exception as e:
282
+ if self.agent.dev_mode:
283
+ log_error(f"MCP skill error: {e}")
284
+ return {
285
+ "status": "error",
286
+ "error": str(e),
287
+ "server": server_alias,
288
+ "tool": tool_name,
289
+ "args": kwargs,
290
+ }
291
+
292
+ # Create FastAPI endpoint
293
+ @self.agent.post(endpoint_path, response_model=dict)
294
+ async def mcp_skill_endpoint(input_data: InputSchema, request: Request):
295
+ from agentfield.execution_context import ExecutionContext
296
+
297
+ # Extract execution context from request headers
298
+ execution_context = ExecutionContext.from_request(
299
+ request, self.agent.node_id
300
+ )
301
+
302
+ # Store current context for use in app.call()
303
+ self.agent._current_execution_context = execution_context
304
+
305
+ # Convert input to function arguments
306
+ kwargs = input_data.args
307
+
308
+ # Call the MCP skill function
309
+ result = await mcp_skill_function(**kwargs)
310
+
311
+ return result
312
+
313
+ # Register skill metadata
314
+ self.agent.skills.append(
315
+ {
316
+ "id": skill_name,
317
+ "input_schema": InputSchema.model_json_schema(),
318
+ "tags": ["mcp", server_alias],
319
+ "description": tool.get("description", f"MCP tool: {tool_name}"),
320
+ }
321
+ )
322
+
323
+ def _create_and_register_mcp_skill(
324
+ self, server_alias: str, tool: Dict[str, Any]
325
+ ) -> None:
326
+ """
327
+ Create and register a single MCP tool as a AgentField skill.
328
+
329
+ Args:
330
+ server_alias: The alias of the MCP server
331
+ tool: Tool definition from mcp.json
332
+ """
333
+ tool_name = tool.get("name", "")
334
+ if not tool_name:
335
+ raise ValueError("Tool missing 'name' field")
336
+
337
+ # Generate skill function name: server_alias + tool_name
338
+ skill_name = AgentUtils.generate_skill_name(server_alias, tool_name)
339
+
340
+ # Create the skill function dynamically
341
+ async def mcp_skill_function(
342
+ execution_context: Optional[ExecutionContext] = None, **kwargs
343
+ ) -> Any:
344
+ """
345
+ Auto-generated MCP skill function.
346
+
347
+ This function calls the corresponding MCP tool and returns the result.
348
+ """
349
+ try:
350
+ # Get MCP client
351
+ if not self.agent.mcp_client_registry:
352
+ raise Exception("MCPClientRegistry not initialized")
353
+ client = self.agent.mcp_client_registry.get_client(server_alias)
354
+ if not client:
355
+ raise Exception(f"MCP client for {server_alias} not found")
356
+
357
+ # Call the MCP tool
358
+ result = await client.call_tool(tool_name, kwargs)
359
+ return result
360
+
361
+ except Exception as e:
362
+ # Re-raise with helpful context
363
+ raise Exception(
364
+ f"MCP tool '{server_alias}.{tool_name}' failed: {str(e)}"
365
+ ) from e
366
+
367
+ # Set function metadata
368
+ mcp_skill_function.__name__ = skill_name
369
+ mcp_skill_function.__doc__ = f"""
370
+ {tool.get("description", f"MCP tool: {tool_name}")}
371
+
372
+ This is an auto-generated skill that wraps the '{tool_name}' tool from the '{server_alias}' MCP server.
373
+
374
+ Args:
375
+ execution_context (ExecutionContext, optional): AgentField execution context for workflow tracking
376
+ **kwargs: Arguments to pass to the MCP tool
377
+
378
+ Returns:
379
+ Any: The result from the MCP tool execution
380
+
381
+ Raises:
382
+ Exception: If the MCP server is unavailable or the tool execution fails
383
+ """
384
+
385
+ # Create input schema from tool's input schema
386
+ input_schema = AgentUtils.create_input_schema_from_mcp_tool(skill_name, tool)
387
+
388
+ # Create FastAPI endpoint
389
+ endpoint_path = f"/skills/{skill_name}"
390
+
391
+ @self.agent.post(endpoint_path, response_model=dict)
392
+ async def mcp_skill_endpoint(input_data: Dict[str, Any], request: Request):
393
+ # Extract execution context from request headers
394
+ execution_context = ExecutionContext.from_request(
395
+ request, self.agent.node_id
396
+ )
397
+
398
+ # Store current context for use in app.call()
399
+ self.agent._current_execution_context = execution_context
400
+
401
+ # Convert input to function arguments
402
+ kwargs = input_data
403
+
404
+ # Call the MCP skill function
405
+ result = await mcp_skill_function(
406
+ execution_context=execution_context, **kwargs
407
+ )
408
+ return result
409
+
410
+ # Register skill metadata
411
+ self.agent.skills.append(
412
+ {
413
+ "id": skill_name,
414
+ "input_schema": input_schema.model_json_schema(),
415
+ "tags": ["mcp", server_alias],
416
+ }
417
+ )
418
+
419
+ def _get_mcp_server_health(self) -> List[MCPServerHealth]:
420
+ """
421
+ Get health information for all MCP servers.
422
+
423
+ Returns:
424
+ List of MCPServerHealth objects
425
+ """
426
+ mcp_servers = []
427
+
428
+ if self.agent.mcp_manager:
429
+ try:
430
+ all_status = self.agent.mcp_manager.get_all_status()
431
+
432
+ for alias, server_info in all_status.items():
433
+ server_health = MCPServerHealth(
434
+ alias=alias,
435
+ status=server_info.get("status", "unknown"),
436
+ tool_count=0,
437
+ port=server_info.get("port"),
438
+ process_id=(
439
+ server_info.get("process", {}).get("pid")
440
+ if server_info.get("process")
441
+ else None
442
+ ),
443
+ started_at=datetime.now().isoformat(),
444
+ last_health_check=datetime.now().isoformat(),
445
+ )
446
+
447
+ # Try to get tool count if server is running
448
+ if (
449
+ server_health.status == "running"
450
+ and self.agent.mcp_client_registry
451
+ ):
452
+ try:
453
+ client = self.agent.mcp_client_registry.get_client(alias)
454
+ if client:
455
+ # This would need to be implemented properly
456
+ server_health.tool_count = 0 # Placeholder
457
+ except Exception:
458
+ pass
459
+
460
+ mcp_servers.append(server_health)
461
+
462
+ except Exception as e:
463
+ if self.agent.dev_mode:
464
+ log_error(f"Error getting MCP server health: {e}")
465
+
466
+ return mcp_servers
467
+
468
+ async def _background_mcp_initialization(self) -> None:
469
+ """
470
+ Initialize MCP servers in the background after registration.
471
+ """
472
+ try:
473
+ if self.agent.dev_mode:
474
+ log_info("Background MCP initialization started")
475
+
476
+ # Start MCP servers
477
+ if self.agent.mcp_manager:
478
+ results = await self.agent.mcp_manager.start_all_servers()
479
+
480
+ # Register clients for successfully started servers
481
+ for alias, success in results.items():
482
+ if success and self.agent.mcp_client_registry:
483
+ server_status = self.agent.mcp_manager.get_server_status(alias)
484
+ if server_status and server_status.get("port"):
485
+ self.agent.mcp_client_registry.register_client(
486
+ alias, server_status["port"]
487
+ )
488
+
489
+ successful = sum(1 for success in results.values() if success)
490
+ total = len(results)
491
+
492
+ if self.agent.dev_mode:
493
+ log_info(
494
+ f"MCP initialization: {successful}/{total} servers started"
495
+ )
496
+
497
+ # Update status based on MCP results
498
+ if successful == total and total > 0:
499
+ self.agent._current_status = AgentStatus.READY
500
+ elif successful > 0:
501
+ self.agent._current_status = AgentStatus.DEGRADED
502
+ else:
503
+ self.agent._current_status = (
504
+ AgentStatus.READY
505
+ ) # Still ready even without MCP
506
+ else:
507
+ # No MCP manager, agent is ready
508
+ self.agent._current_status = AgentStatus.READY
509
+ if self.agent.dev_mode:
510
+ log_info("No MCP servers to initialize - agent ready")
511
+
512
+ # Register dynamic skills if available
513
+ if self.agent.dynamic_skill_manager:
514
+ if self.agent.dev_mode:
515
+ log_info("Registering MCP tools as skills...")
516
+ await (
517
+ self.agent.dynamic_skill_manager.discover_and_register_all_skills()
518
+ )
519
+
520
+ self.agent._mcp_initialization_complete = True
521
+
522
+ # Send status update heartbeat
523
+ await self.agent.agentfield_handler.send_enhanced_heartbeat()
524
+
525
+ if self.agent.dev_mode:
526
+ log_info(
527
+ f"Background initialization complete - Status: {self.agent._current_status.value}"
528
+ )
529
+
530
+ except Exception as e:
531
+ if self.agent.dev_mode:
532
+ log_error(f"Background MCP initialization error: {e}")
533
+ self.agent._current_status = AgentStatus.DEGRADED
534
+ await self.agent.agentfield_handler.send_enhanced_heartbeat()
@@ -0,0 +1,29 @@
1
+ """
2
+ Agent registry for tracking the current agent instance in thread-local storage.
3
+ This allows reasoners to automatically find their parent agent for workflow tracking.
4
+ """
5
+
6
+ import threading
7
+ from typing import Optional, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .agent import Agent
11
+
12
+ # Thread-local storage for agent instances
13
+ _thread_local = threading.local()
14
+
15
+
16
+ def set_current_agent(agent_instance: "Agent"):
17
+ """Register the current agent instance for this thread."""
18
+ _thread_local.current_agent = agent_instance
19
+
20
+
21
+ def get_current_agent_instance() -> Optional["Agent"]:
22
+ """Get the current agent instance for this thread."""
23
+ return getattr(_thread_local, "current_agent", None)
24
+
25
+
26
+ def clear_current_agent():
27
+ """Clear the current agent instance."""
28
+ if hasattr(_thread_local, "current_agent"):
29
+ delattr(_thread_local, "current_agent")