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,494 @@
1
+ import asyncio
2
+ import os
3
+ import signal
4
+ import threading
5
+ from datetime import datetime
6
+
7
+ import requests
8
+ from agentfield.types import AgentStatus, HeartbeatData
9
+ from agentfield.logger import (
10
+ log_heartbeat,
11
+ log_debug,
12
+ log_warn,
13
+ log_error,
14
+ log_success,
15
+ log_setup,
16
+ log_info,
17
+ )
18
+
19
+
20
+ class AgentFieldHandler:
21
+ """
22
+ AgentField Server Communication handler for Agent class.
23
+
24
+ This class encapsulates all AgentField server communication functionality including:
25
+ - Agent registration with AgentField server
26
+ - Heartbeat management (both simple and enhanced)
27
+ - Fast lifecycle management
28
+ - Graceful shutdown notifications
29
+ - Signal handling for fast shutdown
30
+ """
31
+
32
+ def __init__(self, agent_instance):
33
+ """
34
+ Initialize the AgentField handler with a reference to the agent instance.
35
+
36
+ Args:
37
+ agent_instance: The Agent instance this handler belongs to
38
+ """
39
+ self.agent = agent_instance
40
+
41
+ async def register_with_agentfield_server(self, port: int):
42
+ """Register this agent node with AgentField server"""
43
+ # Import the callback URL resolution function
44
+ from agentfield.agent import (
45
+ _build_callback_candidates,
46
+ _resolve_callback_url,
47
+ _is_running_in_container,
48
+ )
49
+
50
+ # Enhanced debugging for callback URL resolution
51
+ log_debug("Starting callback URL resolution")
52
+ log_debug(f"Original callback_url parameter: {self.agent.callback_url}")
53
+ log_debug(
54
+ f"AGENT_CALLBACK_URL env var: {os.environ.get('AGENT_CALLBACK_URL', 'NOT_SET')}"
55
+ )
56
+ log_debug(f"Port: {port}")
57
+ log_debug(f"Running in container: {_is_running_in_container()}")
58
+ log_debug(
59
+ f"All env vars containing 'AGENT': {[k for k in os.environ.keys() if 'AGENT' in k.upper()]}"
60
+ )
61
+
62
+ # 🔥 FIX: Only resolve callback URL if not already set
63
+ # This prevents overwriting the URL resolved in Agent.__init__()
64
+ if not self.agent.base_url:
65
+ self.agent.callback_candidates = _build_callback_candidates(
66
+ self.agent.callback_url, port
67
+ )
68
+ if self.agent.callback_candidates:
69
+ self.agent.base_url = self.agent.callback_candidates[0]
70
+ log_debug(
71
+ f"Resolved callback URL during registration: {self.agent.base_url}"
72
+ )
73
+ else:
74
+ self.agent.base_url = _resolve_callback_url(
75
+ self.agent.callback_url, port
76
+ )
77
+ log_debug(
78
+ f"Resolved callback URL during registration: {self.agent.base_url}"
79
+ )
80
+ else:
81
+ # Update port in existing base_url if needed, but preserve Railway internal URLs
82
+ import urllib.parse
83
+
84
+ parsed = urllib.parse.urlparse(self.agent.base_url)
85
+
86
+ # Don't modify Railway internal URLs or other container-specific URLs
87
+ if "railway.internal" in parsed.netloc or "internal" in parsed.netloc:
88
+ log_debug(
89
+ f"Preserving container-specific callback URL: {self.agent.base_url}"
90
+ )
91
+ elif parsed.port != port:
92
+ # Update the port in the existing URL
93
+ self.agent.base_url = f"{parsed.scheme}://{parsed.hostname}:{port}"
94
+ log_debug(
95
+ f"Updated port in existing callback URL: {self.agent.base_url}"
96
+ )
97
+ else:
98
+ log_debug(f"Using existing callback URL: {self.agent.base_url}")
99
+
100
+ if not self.agent.callback_candidates:
101
+ self.agent.callback_candidates = _build_callback_candidates(
102
+ self.agent.base_url, port
103
+ )
104
+ elif (
105
+ self.agent.base_url
106
+ and self.agent.callback_candidates[0] != self.agent.base_url
107
+ ):
108
+ # Keep resolved base URL at front for clarity
109
+ if self.agent.base_url in self.agent.callback_candidates:
110
+ self.agent.callback_candidates.remove(self.agent.base_url)
111
+ self.agent.callback_candidates.insert(0, self.agent.base_url)
112
+
113
+ # Always log the resolved callback URL for debugging
114
+ log_info(f"Final callback URL: {self.agent.base_url}")
115
+
116
+ if self.agent.dev_mode:
117
+ log_debug(f"Final callback URL: {self.agent.base_url}")
118
+
119
+ try:
120
+ log_debug(
121
+ f"Attempting to register with AgentField server at {self.agent.agentfield_server}"
122
+ )
123
+ discovery_payload = self.agent._build_callback_discovery_payload()
124
+
125
+ success, payload = await self.agent.client.register_agent(
126
+ node_id=self.agent.node_id,
127
+ reasoners=self.agent.reasoners,
128
+ skills=self.agent.skills,
129
+ base_url=self.agent.base_url,
130
+ discovery=discovery_payload,
131
+ vc_metadata=self.agent._build_vc_metadata(),
132
+ )
133
+ if success:
134
+ if payload:
135
+ self.agent._apply_discovery_response(payload)
136
+ log_success(
137
+ f"Registered node '{self.agent.node_id}' with AgentField server"
138
+ )
139
+ self.agent.agentfield_connected = True
140
+
141
+ # Attempt DID registration after successful AgentField registration
142
+ if self.agent.did_manager:
143
+ did_success = self.agent._register_agent_with_did()
144
+ if not did_success and self.agent.dev_mode:
145
+ log_warn(
146
+ "DID registration failed, continuing without DID functionality"
147
+ )
148
+ else:
149
+ log_error("Registration failed")
150
+ self.agent.agentfield_connected = False
151
+
152
+ except Exception as e:
153
+ self.agent.agentfield_connected = False
154
+ if self.agent.dev_mode:
155
+ log_warn(f"AgentField server not available: {e}")
156
+ log_setup("Running in development mode - agent will work standalone")
157
+ log_info(
158
+ f"To connect to AgentField server, start it at {self.agent.agentfield_server}"
159
+ )
160
+ else:
161
+ log_error(f"Failed to register with AgentField server: {e}")
162
+ if (
163
+ isinstance(e, requests.exceptions.RequestException)
164
+ and e.response is not None
165
+ ):
166
+ log_warn(f"Response status: {e.response.status_code}")
167
+ log_warn(f"Response text: {e.response.text}")
168
+ raise
169
+
170
+ def send_heartbeat(self):
171
+ """Send heartbeat to AgentField server"""
172
+ if not self.agent.agentfield_connected:
173
+ return # Skip heartbeat if not connected to AgentField
174
+
175
+ try:
176
+ headers = {"Content-Type": "application/json"}
177
+ if self.agent.api_key:
178
+ headers["X-API-Key"] = self.agent.api_key
179
+ response = requests.post(
180
+ f"{self.agent.agentfield_server}/api/v1/nodes/{self.agent.node_id}/heartbeat",
181
+ headers=headers,
182
+ timeout=5,
183
+ )
184
+ if response.status_code == 200:
185
+ log_heartbeat("Heartbeat sent successfully")
186
+ else:
187
+ log_warn(
188
+ f"Heartbeat failed with status {response.status_code}: {response.text}"
189
+ )
190
+ except Exception as e:
191
+ log_error(f"Failed to send heartbeat: {e}")
192
+
193
+ def heartbeat_worker(
194
+ self, interval: int = 30
195
+ ): # pragma: no cover - long-running thread loop
196
+ """Background worker that sends periodic heartbeats"""
197
+ if not self.agent.agentfield_connected:
198
+ log_heartbeat(
199
+ "Heartbeat worker skipped - not connected to AgentField server"
200
+ )
201
+ return
202
+
203
+ log_heartbeat(f"Starting heartbeat worker (interval: {interval}s)")
204
+ while not self.agent._heartbeat_stop_event.wait(interval):
205
+ self.send_heartbeat()
206
+ log_heartbeat("Heartbeat worker stopped")
207
+
208
+ def start_heartbeat(self, interval: int = 30):
209
+ """Start the heartbeat background thread"""
210
+ if not self.agent.agentfield_connected:
211
+ return # Skip heartbeat if not connected to AgentField
212
+
213
+ if (
214
+ self.agent._heartbeat_thread is None
215
+ or not self.agent._heartbeat_thread.is_alive()
216
+ ):
217
+ self.agent._heartbeat_stop_event.clear()
218
+ self.agent._heartbeat_thread = threading.Thread(
219
+ target=self.heartbeat_worker, args=(interval,), daemon=True
220
+ )
221
+ self.agent._heartbeat_thread.start()
222
+
223
+ def stop_heartbeat(self):
224
+ """Stop the heartbeat background thread"""
225
+ if self.agent._heartbeat_thread and self.agent._heartbeat_thread.is_alive():
226
+ log_debug("Stopping heartbeat worker...")
227
+ self.agent._heartbeat_stop_event.set()
228
+ self.agent._heartbeat_thread.join(timeout=5)
229
+
230
+ async def send_enhanced_heartbeat(self) -> bool:
231
+ """
232
+ Send enhanced heartbeat with current status and MCP information.
233
+
234
+ Returns:
235
+ True if heartbeat was successful, False otherwise
236
+ """
237
+ if not self.agent.agentfield_connected:
238
+ return False
239
+
240
+ try:
241
+ # Get MCP server health information
242
+ mcp_servers = self.agent.mcp_handler._get_mcp_server_health()
243
+
244
+ # Create heartbeat data
245
+ heartbeat_data = HeartbeatData(
246
+ status=self.agent._current_status,
247
+ mcp_servers=mcp_servers,
248
+ timestamp=datetime.now().isoformat(),
249
+ )
250
+
251
+ # Send enhanced heartbeat
252
+ success = await self.agent.client.send_enhanced_heartbeat(
253
+ self.agent.node_id, heartbeat_data
254
+ )
255
+
256
+ if success:
257
+ log_heartbeat(
258
+ f"Enhanced heartbeat sent - Status: {self.agent._current_status.value}"
259
+ )
260
+
261
+ return success
262
+
263
+ except Exception as e:
264
+ if self.agent.dev_mode:
265
+ log_error(f"Enhanced heartbeat failed: {e}")
266
+ return False
267
+
268
+ async def notify_shutdown(self) -> bool:
269
+ """
270
+ Notify AgentField server of graceful shutdown.
271
+
272
+ Returns:
273
+ True if notification was successful, False otherwise
274
+ """
275
+ if not self.agent.agentfield_connected:
276
+ return False
277
+
278
+ try:
279
+ success = await self.agent.client.notify_graceful_shutdown(
280
+ self.agent.node_id
281
+ )
282
+ if self.agent.dev_mode and success:
283
+ log_success("Graceful shutdown notification sent")
284
+ return success
285
+ except Exception as e:
286
+ if self.agent.dev_mode:
287
+ log_error(f"Shutdown notification failed: {e}")
288
+ return False
289
+
290
+ def setup_fast_lifecycle_signal_handlers(
291
+ self,
292
+ ) -> None: # pragma: no cover - requires OS signal integration
293
+ """
294
+ Setup signal handler for fast lifecycle status while allowing uvicorn to perform graceful shutdown.
295
+
296
+ - Only intercepts SIGTERM to mark the agent offline and notify AgentField immediately.
297
+ - Leaves SIGINT (Ctrl+C) to uvicorn so its shutdown hooks run and resources are cleaned up.
298
+ """
299
+
300
+ def signal_handler(signum: int, frame) -> None:
301
+ """Handle SIGTERM: mark offline, notify AgentField, then re-emit the signal for default handling."""
302
+ signal_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT"
303
+
304
+ if self.agent.dev_mode:
305
+ log_warn(
306
+ f"{signal_name} received - initiating graceful shutdown via uvicorn"
307
+ )
308
+
309
+ # Set shutdown flag
310
+ self.agent._shutdown_requested = True
311
+ self.agent._current_status = AgentStatus.OFFLINE
312
+
313
+ # Best-effort immediate notification to AgentField
314
+ try:
315
+ success = self.agent.client.notify_graceful_shutdown_sync(
316
+ self.agent.node_id
317
+ )
318
+ if self.agent.dev_mode:
319
+ state = "sent" if success else "failed"
320
+ log_info(f"Shutdown notification {state}")
321
+ except Exception as e:
322
+ if self.agent.dev_mode:
323
+ log_error(f"Shutdown notification error: {e}")
324
+
325
+ # IMPORTANT: Do not perform heavy cleanup here. Let FastAPI/uvicorn shutdown events handle it.
326
+ # Re-install default handler and re-emit the same signal so uvicorn orchestrates cleanup.
327
+ try:
328
+ signal.signal(signum, signal.SIG_DFL)
329
+ os.kill(os.getpid(), signum)
330
+ except Exception:
331
+ # Fallback: polite exit (still allows finally blocks/atexit to run)
332
+ import sys
333
+
334
+ sys.exit(0)
335
+
336
+ try:
337
+ # Only register for SIGTERM; leave SIGINT (Ctrl+C) to uvicorn
338
+ signal.signal(signal.SIGTERM, signal_handler)
339
+
340
+ if self.agent.dev_mode:
341
+ log_debug("Fast lifecycle signal handler registered (SIGTERM only)")
342
+ except Exception as e:
343
+ if self.agent.dev_mode:
344
+ log_error(f"Failed to setup signal handlers: {e}")
345
+
346
+ async def register_with_fast_lifecycle(
347
+ self, port: int
348
+ ) -> bool: # pragma: no cover - fast-path relies on external coordination
349
+ """
350
+ Register agent with immediate status reporting for fast lifecycle.
351
+
352
+ Args:
353
+ port: The port the agent is running on
354
+
355
+ Returns:
356
+ True if registration was successful, False otherwise
357
+ """
358
+ from agentfield.agent import _build_callback_candidates, _resolve_callback_url
359
+
360
+ if not self.agent.base_url:
361
+ self.agent.callback_candidates = _build_callback_candidates(
362
+ self.agent.callback_url, port
363
+ )
364
+ if self.agent.callback_candidates:
365
+ self.agent.base_url = self.agent.callback_candidates[0]
366
+ log_debug(
367
+ f"Fast lifecycle - Resolved callback URL during registration: {self.agent.base_url}"
368
+ )
369
+ else:
370
+ self.agent.base_url = _resolve_callback_url(
371
+ self.agent.callback_url, port
372
+ )
373
+ log_debug(
374
+ f"Fast lifecycle - Resolved callback URL during registration: {self.agent.base_url}"
375
+ )
376
+ else:
377
+ import urllib.parse
378
+
379
+ parsed = urllib.parse.urlparse(self.agent.base_url)
380
+ if parsed.port != port:
381
+ self.agent.base_url = f"{parsed.scheme}://{parsed.hostname}:{port}"
382
+ log_debug(
383
+ f"Fast lifecycle - Updated port in existing callback URL: {self.agent.base_url}"
384
+ )
385
+ else:
386
+ log_debug(
387
+ f"Fast lifecycle - Using existing callback URL: {self.agent.base_url}"
388
+ )
389
+
390
+ if not self.agent.callback_candidates:
391
+ self.agent.callback_candidates = _build_callback_candidates(
392
+ self.agent.base_url, port
393
+ )
394
+ elif (
395
+ self.agent.base_url
396
+ and self.agent.callback_candidates
397
+ and self.agent.callback_candidates[0] != self.agent.base_url
398
+ ):
399
+ if self.agent.base_url in self.agent.callback_candidates:
400
+ self.agent.callback_candidates.remove(self.agent.base_url)
401
+ self.agent.callback_candidates.insert(0, self.agent.base_url)
402
+
403
+ log_debug(f"Fast lifecycle - Final callback URL: {self.agent.base_url}")
404
+ log_debug(
405
+ f"Fast lifecycle - Original callback_url parameter: {self.agent.callback_url}"
406
+ )
407
+ log_debug(
408
+ f"Fast lifecycle - AGENT_CALLBACK_URL env var: {os.environ.get('AGENT_CALLBACK_URL', 'NOT_SET')}"
409
+ )
410
+ log_debug(f"Fast lifecycle - Port: {port}")
411
+
412
+ try:
413
+ if self.agent.dev_mode:
414
+ log_info(
415
+ f"Fast registration with AgentField server at {self.agent.agentfield_server}"
416
+ )
417
+ log_info(f"Using callback URL: {self.agent.base_url}")
418
+
419
+ # Register with STARTING status for immediate visibility
420
+ discovery_payload = self.agent._build_callback_discovery_payload()
421
+
422
+ success, payload = await self.agent.client.register_agent_with_status(
423
+ node_id=self.agent.node_id,
424
+ reasoners=self.agent.reasoners,
425
+ skills=self.agent.skills,
426
+ base_url=self.agent.base_url,
427
+ status=AgentStatus.STARTING,
428
+ discovery=discovery_payload,
429
+ vc_metadata=self.agent._build_vc_metadata(),
430
+ )
431
+
432
+ if success:
433
+ if payload:
434
+ self.agent._apply_discovery_response(payload)
435
+ if self.agent.dev_mode:
436
+ log_success(
437
+ f"Fast registration successful - Status: {AgentStatus.STARTING.value}"
438
+ )
439
+ self.agent.agentfield_connected = True
440
+
441
+ # Attempt DID registration after successful AgentField registration
442
+ if self.agent.did_manager:
443
+ did_success = self.agent._register_agent_with_did()
444
+ if not did_success and self.agent.dev_mode:
445
+ log_warn(
446
+ "DID registration failed, continuing without DID functionality"
447
+ )
448
+
449
+ return True
450
+ else:
451
+ if self.agent.dev_mode:
452
+ log_error("Fast registration failed")
453
+ self.agent.agentfield_connected = False
454
+ return False
455
+
456
+ except Exception as e:
457
+ self.agent.agentfield_connected = False
458
+ if self.agent.dev_mode:
459
+ log_warn(f"Fast registration error: {e}")
460
+ return False
461
+
462
+ async def enhanced_heartbeat_loop(self, interval: int) -> None:
463
+ """
464
+ Background loop for sending enhanced heartbeats with status and MCP information.
465
+
466
+ Args:
467
+ interval: Heartbeat interval in seconds
468
+ """
469
+ if self.agent.dev_mode:
470
+ log_debug(f"Enhanced heartbeat loop started (interval: {interval}s)")
471
+
472
+ while not self.agent._shutdown_requested:
473
+ try:
474
+ # Send enhanced heartbeat
475
+ success = await self.send_enhanced_heartbeat()
476
+
477
+ if not success and self.agent.dev_mode:
478
+ log_warn("Enhanced heartbeat failed - retrying next cycle")
479
+
480
+ # Wait for next heartbeat interval
481
+ await asyncio.sleep(interval)
482
+
483
+ except asyncio.CancelledError:
484
+ if self.agent.dev_mode:
485
+ log_debug("Enhanced heartbeat loop cancelled")
486
+ break
487
+ except Exception as e:
488
+ if self.agent.dev_mode:
489
+ log_error(f"Enhanced heartbeat loop error: {e}")
490
+ # Continue loop even on errors
491
+ await asyncio.sleep(interval)
492
+
493
+ if self.agent.dev_mode:
494
+ log_debug("Enhanced heartbeat loop stopped")