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,269 @@
1
+ import os
2
+ import re
3
+ import socket
4
+ import time
5
+ from typing import Any, Dict, List, Optional, Type
6
+
7
+ from pydantic import BaseModel, create_model
8
+
9
+
10
+ class AgentUtils:
11
+ """Utility functions extracted from Agent class for better code organization."""
12
+
13
+ @staticmethod
14
+ def detect_input_type(input_data: Any) -> str:
15
+ """Intelligently detect input type without explicit declarations"""
16
+
17
+ if isinstance(input_data, str):
18
+ # Smart string detection
19
+ if input_data.startswith(("http://", "https://")):
20
+ return "image_url" if AgentUtils.is_image_url(input_data) else "url"
21
+ elif input_data.startswith("data:image"):
22
+ return "image_base64"
23
+ elif input_data.startswith("data:audio"):
24
+ return "audio_base64"
25
+ elif os.path.isfile(input_data):
26
+ ext = os.path.splitext(input_data)[1].lower()
27
+ if ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"]:
28
+ return "image_file"
29
+ elif ext in [".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac"]:
30
+ return "audio_file"
31
+ elif ext in [".pdf", ".doc", ".docx", ".txt", ".rtf", ".md"]:
32
+ return "document_file"
33
+ elif ext in [".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm"]:
34
+ return "video_file"
35
+ else:
36
+ return "file"
37
+ return "text"
38
+
39
+ elif isinstance(input_data, bytes):
40
+ # Detect file type from bytes
41
+ if input_data.startswith(b"\xff\xd8\xff"): # JPEG
42
+ return "image_bytes"
43
+ elif input_data.startswith(b"\x89PNG"): # PNG
44
+ return "image_bytes"
45
+ elif input_data.startswith(b"GIF8"): # GIF
46
+ return "image_bytes"
47
+ elif input_data.startswith(b"RIFF") and b"WAVE" in input_data[:12]: # WAV
48
+ return "audio_bytes"
49
+ elif input_data.startswith(b"ID3") or input_data.startswith(
50
+ b"\xff\xfb"
51
+ ): # MP3
52
+ return "audio_bytes"
53
+ elif b"ftyp" in input_data[:20]: # MP4/M4A
54
+ return "audio_bytes"
55
+ elif input_data.startswith(b"%PDF"): # PDF
56
+ return "document_bytes"
57
+ return "binary_data"
58
+
59
+ elif isinstance(input_data, dict):
60
+ # Check for structured input patterns
61
+ if any(
62
+ key in input_data for key in ["system", "user", "assistant", "role"]
63
+ ):
64
+ return "message_dict"
65
+ elif any(
66
+ key in input_data
67
+ for key in ["image", "image_url", "audio", "file", "text"]
68
+ ):
69
+ return "structured_input"
70
+ return "dict"
71
+
72
+ elif isinstance(input_data, list):
73
+ if len(input_data) > 0:
74
+ # Check if it's a conversation format
75
+ if isinstance(input_data[0], dict) and "role" in input_data[0]:
76
+ return "conversation_list"
77
+ # Check if it's multimodal content
78
+ elif any(isinstance(item, (str, dict)) for item in input_data):
79
+ return "multimodal_list"
80
+ return "list"
81
+
82
+ return "unknown"
83
+
84
+ @staticmethod
85
+ def is_image_url(url: str) -> bool:
86
+ """Check if URL points to an image based on extension or content type"""
87
+ image_extensions = [
88
+ ".jpg",
89
+ ".jpeg",
90
+ ".png",
91
+ ".gif",
92
+ ".webp",
93
+ ".bmp",
94
+ ".tiff",
95
+ ".svg",
96
+ ]
97
+ return any(url.lower().endswith(ext) for ext in image_extensions)
98
+
99
+ @staticmethod
100
+ def is_audio_url(url: str) -> bool:
101
+ """Check if URL points to audio based on extension"""
102
+ audio_extensions = [".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac"]
103
+ return any(url.lower().endswith(ext) for ext in audio_extensions)
104
+
105
+ @staticmethod
106
+ def get_mime_type(extension: str) -> str:
107
+ """Get MIME type from file extension"""
108
+ mime_types = {
109
+ ".jpg": "image/jpeg",
110
+ ".jpeg": "image/jpeg",
111
+ ".png": "image/png",
112
+ ".gif": "image/gif",
113
+ ".webp": "image/webp",
114
+ ".bmp": "image/bmp",
115
+ ".tiff": "image/tiff",
116
+ ".svg": "image/svg+xml",
117
+ ".mp3": "audio/mpeg",
118
+ ".wav": "audio/wav",
119
+ ".m4a": "audio/mp4",
120
+ ".ogg": "audio/ogg",
121
+ ".flac": "audio/flac",
122
+ ".aac": "audio/aac",
123
+ ".pdf": "application/pdf",
124
+ ".doc": "application/msword",
125
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
126
+ ".txt": "text/plain",
127
+ ".md": "text/markdown",
128
+ ".rtf": "application/rtf",
129
+ }
130
+ return mime_types.get(extension.lower(), "application/octet-stream")
131
+
132
+ @staticmethod
133
+ def map_json_type_to_python(json_type: str) -> Type:
134
+ """
135
+ Map JSON Schema types to Python types.
136
+
137
+ Args:
138
+ json_type: JSON Schema type string
139
+
140
+ Returns:
141
+ Python type
142
+ """
143
+ type_mapping = {
144
+ "string": str,
145
+ "integer": int,
146
+ "number": float,
147
+ "boolean": bool,
148
+ "array": List[Any],
149
+ "object": Dict[str, Any],
150
+ "null": type(None),
151
+ }
152
+
153
+ return type_mapping.get(json_type, str)
154
+
155
+ @staticmethod
156
+ def generate_skill_name(server_alias: str, tool_name: str) -> str:
157
+ """
158
+ Generate a valid Python function name for the MCP skill.
159
+
160
+ Args:
161
+ server_alias: MCP server alias
162
+ tool_name: MCP tool name
163
+
164
+ Returns:
165
+ Valid Python function name
166
+ """
167
+ # Convert to snake_case and ensure it's a valid Python identifier
168
+ name = f"{server_alias}_{tool_name}"
169
+ name = re.sub(
170
+ r"[^a-zA-Z0-9_]", "_", name
171
+ ) # Replace invalid chars with underscore
172
+ name = re.sub(r"_+", "_", name) # Replace multiple underscores with single
173
+ name = name.strip("_") # Remove leading/trailing underscores
174
+
175
+ # Ensure it starts with a letter or underscore
176
+ if name and name[0].isdigit():
177
+ name = "_" + name
178
+
179
+ # Ensure it's not empty
180
+ if not name:
181
+ name = f"mcp_tool_{int(time.time())}"
182
+
183
+ return name
184
+
185
+ @staticmethod
186
+ def create_input_schema_from_mcp_tool(
187
+ skill_name: str, tool: Dict[str, Any]
188
+ ) -> Type[BaseModel]:
189
+ """
190
+ Create a Pydantic input schema from MCP tool definition.
191
+
192
+ Args:
193
+ skill_name: Name of the skill function
194
+ tool: MCP tool definition
195
+
196
+ Returns:
197
+ Pydantic model class for input validation
198
+ """
199
+ input_schema = tool.get("input_schema", {})
200
+ properties = input_schema.get("properties", {})
201
+ required = input_schema.get("required", [])
202
+
203
+ # Create fields for Pydantic model
204
+ fields = {}
205
+
206
+ for prop_name, prop_def in properties.items():
207
+ prop_type = AgentUtils.map_json_type_to_python(
208
+ prop_def.get("type", "string")
209
+ )
210
+ is_required = prop_name in required
211
+
212
+ if is_required:
213
+ fields[prop_name] = (prop_type, ...)
214
+ else:
215
+ default_value = prop_def.get("default")
216
+ if default_value is not None:
217
+ fields[prop_name] = (prop_type, default_value)
218
+ else:
219
+ fields[prop_name] = (Optional[prop_type], None)
220
+
221
+ # If no fields defined, create a generic schema
222
+ if not fields:
223
+ fields["data"] = (Optional[Dict[str, Any]], None)
224
+
225
+ # Create the Pydantic model
226
+ InputModel = create_model(f"{skill_name}Input", **fields)
227
+ return InputModel
228
+
229
+ @staticmethod
230
+ def is_port_available(port: int) -> bool:
231
+ """
232
+ Check if a port is available for use.
233
+
234
+ Args:
235
+ port: Port number to check
236
+
237
+ Returns:
238
+ True if port is available, False otherwise
239
+ """
240
+ try:
241
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
242
+ s.bind(("localhost", port))
243
+ return True
244
+ except OSError:
245
+ return False
246
+
247
+ @staticmethod
248
+ def serialize_result(result: Any) -> Any:
249
+ """Convert complex objects to JSON-serializable format"""
250
+ try:
251
+ if hasattr(result, "model_dump"): # Pydantic v2
252
+ return result.model_dump()
253
+ elif hasattr(result, "dict"): # Pydantic v1
254
+ return result.model_dump()
255
+ elif hasattr(result, "__dict__"): # Regular objects with attributes
256
+ return {
257
+ k: AgentUtils.serialize_result(v)
258
+ for k, v in result.__dict__.items()
259
+ }
260
+ elif isinstance(result, (list, tuple)):
261
+ return [AgentUtils.serialize_result(item) for item in result]
262
+ elif isinstance(result, dict):
263
+ return {k: AgentUtils.serialize_result(v) for k, v in result.items()}
264
+ else:
265
+ # Primitive types (str, int, float, bool, None) are already JSON-serializable
266
+ return result
267
+ except Exception:
268
+ # Fallback: convert to string if serialization fails
269
+ return str(result)
@@ -0,0 +1,323 @@
1
+ import inspect
2
+ import time
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from agentfield.logger import log_debug, log_warn
6
+
7
+ from .execution_context import (
8
+ ExecutionContext,
9
+ get_current_context,
10
+ set_execution_context,
11
+ reset_execution_context,
12
+ )
13
+ from fastapi.encoders import jsonable_encoder
14
+
15
+
16
+ class AgentWorkflow:
17
+ """Workflow helper that keeps local execution metadata in sync with AgentField."""
18
+
19
+ def __init__(self, agent_instance):
20
+ self.agent = agent_instance
21
+
22
+ # --------------------------------------------------------------------- #
23
+ # Public API #
24
+ # --------------------------------------------------------------------- #
25
+
26
+ def replace_function_references(
27
+ self, original_func: Callable, tracked_func: Callable, func_name: str
28
+ ) -> None:
29
+ """Replace the agent attribute with the tracked wrapper."""
30
+ setattr(self.agent, func_name, tracked_func)
31
+
32
+ async def execute_with_tracking(
33
+ self, original_func: Callable, args: tuple, kwargs: dict
34
+ ) -> Any:
35
+ """
36
+ Execute the wrapped function with automatic workflow instrumentation.
37
+ """
38
+
39
+ reasoner_name = getattr(original_func, "__name__", "reasoner")
40
+
41
+ parent_context = self._get_parent_context()
42
+ execution_context = self._build_execution_context(reasoner_name, parent_context)
43
+
44
+ # Ensure this execution is registered when running under an existing workflow
45
+ execution_context = await self._ensure_execution_registered(
46
+ execution_context, reasoner_name, parent_context
47
+ )
48
+
49
+ call_args = args
50
+ call_kwargs = dict(kwargs or {})
51
+ signature = self._safe_signature(original_func)
52
+
53
+ if "execution_context" in signature.parameters:
54
+ call_kwargs.setdefault("execution_context", execution_context)
55
+
56
+ input_data = self._build_input_payload(signature, call_args, call_kwargs)
57
+
58
+ previous_agent_context = getattr(self.agent, "_current_execution_context", None)
59
+ client_context = getattr(self.agent, "client", None)
60
+ previous_client_context = None
61
+ if client_context is not None:
62
+ previous_client_context = getattr(
63
+ client_context, "_current_workflow_context", None
64
+ )
65
+
66
+ token = set_execution_context(execution_context)
67
+ self.agent._current_execution_context = execution_context
68
+ if client_context is not None:
69
+ client_context._current_workflow_context = execution_context
70
+
71
+ start_time = time.time()
72
+ parent_execution_id = parent_context.execution_id if parent_context else None
73
+
74
+ await self.notify_call_start(
75
+ execution_context.execution_id,
76
+ execution_context,
77
+ reasoner_name,
78
+ input_data,
79
+ parent_execution_id=parent_execution_id,
80
+ )
81
+
82
+ try:
83
+ result = original_func(*call_args, **call_kwargs)
84
+ if inspect.isawaitable(result):
85
+ result = await result
86
+ duration_ms = int((time.time() - start_time) * 1000)
87
+ await self.notify_call_complete(
88
+ execution_context.execution_id,
89
+ execution_context.workflow_id,
90
+ result,
91
+ duration_ms,
92
+ execution_context,
93
+ input_data=input_data,
94
+ parent_execution_id=parent_execution_id,
95
+ )
96
+ return result
97
+ except Exception as exc: # pragma: no cover - re-raised
98
+ duration_ms = int((time.time() - start_time) * 1000)
99
+ await self.notify_call_error(
100
+ execution_context.execution_id,
101
+ execution_context.workflow_id,
102
+ str(exc),
103
+ duration_ms,
104
+ execution_context,
105
+ input_data=input_data,
106
+ parent_execution_id=parent_execution_id,
107
+ )
108
+ raise
109
+ finally:
110
+ reset_execution_context(token)
111
+ self.agent._current_execution_context = previous_agent_context
112
+ if client_context is not None:
113
+ client_context._current_workflow_context = previous_client_context
114
+
115
+ async def notify_call_start(
116
+ self,
117
+ execution_id: str,
118
+ context: ExecutionContext,
119
+ reasoner_name: str,
120
+ input_data: Dict[str, Any],
121
+ *,
122
+ parent_execution_id: Optional[str] = None,
123
+ ) -> None:
124
+ payload = self._build_event_payload(
125
+ context,
126
+ reasoner_name,
127
+ status="running",
128
+ parent_execution_id=parent_execution_id,
129
+ input_data=input_data,
130
+ )
131
+ await self.fire_and_forget_update(payload)
132
+
133
+ async def notify_call_complete(
134
+ self,
135
+ execution_id: str,
136
+ workflow_id: str,
137
+ result: Any,
138
+ duration_ms: int,
139
+ context: ExecutionContext,
140
+ *,
141
+ input_data: Optional[Dict[str, Any]] = None,
142
+ parent_execution_id: Optional[str] = None,
143
+ ) -> None:
144
+ payload = self._build_event_payload(
145
+ context,
146
+ context.reasoner_name,
147
+ status="succeeded",
148
+ parent_execution_id=parent_execution_id,
149
+ input_data=input_data,
150
+ )
151
+ payload["result"] = result
152
+ payload["duration_ms"] = duration_ms
153
+ await self.fire_and_forget_update(payload)
154
+
155
+ async def notify_call_error(
156
+ self,
157
+ execution_id: str,
158
+ workflow_id: str,
159
+ error: str,
160
+ duration_ms: int,
161
+ context: ExecutionContext,
162
+ *,
163
+ input_data: Optional[Dict[str, Any]] = None,
164
+ parent_execution_id: Optional[str] = None,
165
+ ) -> None:
166
+ payload = self._build_event_payload(
167
+ context,
168
+ context.reasoner_name,
169
+ status="failed",
170
+ parent_execution_id=parent_execution_id,
171
+ input_data=input_data,
172
+ )
173
+ payload["error"] = error
174
+ payload["duration_ms"] = duration_ms
175
+ await self.fire_and_forget_update(payload)
176
+
177
+ async def fire_and_forget_update(self, payload: Dict[str, Any]) -> None:
178
+ """Send workflow update to AgentField when a client is available."""
179
+
180
+ client = getattr(self.agent, "client", None)
181
+ base_url = getattr(self.agent, "agentfield_server", None)
182
+ if not client or not hasattr(client, "_async_request") or not base_url:
183
+ return
184
+
185
+ url = base_url.rstrip("/") + "/api/v1/workflow/executions/events"
186
+ try:
187
+ safe_payload = jsonable_encoder(payload)
188
+ await client._async_request("POST", url, json=safe_payload)
189
+ except Exception: # pragma: no cover - best effort logging
190
+ if getattr(self.agent, "dev_mode", False):
191
+ log_debug("Failed to publish workflow update", exc_info=True)
192
+
193
+ # --------------------------------------------------------------------- #
194
+ # Internal helpers #
195
+ # --------------------------------------------------------------------- #
196
+
197
+ def _get_parent_context(self) -> Optional[ExecutionContext]:
198
+ return (
199
+ getattr(self.agent, "_current_execution_context", None)
200
+ or get_current_context()
201
+ )
202
+
203
+ def _build_execution_context(
204
+ self,
205
+ reasoner_name: str,
206
+ parent_context: Optional[ExecutionContext],
207
+ ) -> ExecutionContext:
208
+ if parent_context:
209
+ context = parent_context.create_child_context()
210
+ context.reasoner_name = reasoner_name
211
+ else:
212
+ context = ExecutionContext.create_new(
213
+ getattr(self.agent, "node_id", "agent"), reasoner_name
214
+ )
215
+ context.reasoner_name = reasoner_name
216
+ context.agent_instance = self.agent
217
+ return context
218
+
219
+ async def _ensure_execution_registered(
220
+ self,
221
+ context: ExecutionContext,
222
+ reasoner_name: str,
223
+ parent_context: Optional[ExecutionContext],
224
+ ) -> ExecutionContext:
225
+ if context.registered:
226
+ return context
227
+
228
+ client = getattr(self.agent, "client", None)
229
+ base_url = getattr(self.agent, "agentfield_server", None)
230
+ if not client or not hasattr(client, "_async_request") or not base_url:
231
+ context.registered = True
232
+ return context
233
+
234
+ payload = {
235
+ "execution_id": context.execution_id,
236
+ "run_id": context.run_id,
237
+ "workflow_id": context.workflow_id,
238
+ "reasoner_name": reasoner_name,
239
+ "node_id": getattr(self.agent, "node_id", None),
240
+ "parent_execution_id": (
241
+ parent_context.execution_id if parent_context else None
242
+ ),
243
+ "parent_workflow_id": (
244
+ parent_context.workflow_id if parent_context else None
245
+ ),
246
+ "session_id": context.session_id,
247
+ "caller_did": context.caller_did,
248
+ "target_did": context.target_did,
249
+ "agent_node_did": context.agent_node_did,
250
+ }
251
+
252
+ url = base_url.rstrip("/") + "/api/v1/workflow/executions"
253
+ try:
254
+ response = await client._async_request("POST", url, json=payload)
255
+ body = response.json() if hasattr(response, "json") else response
256
+ if isinstance(body, dict):
257
+ context.execution_id = body.get("execution_id", context.execution_id)
258
+ context.workflow_id = body.get("workflow_id", context.workflow_id)
259
+ context.run_id = body.get("run_id", context.run_id)
260
+ except Exception as exc: # pragma: no cover - network failure path
261
+ if getattr(self.agent, "dev_mode", False):
262
+ log_warn(f"Workflow registration failed: {exc}")
263
+ finally:
264
+ context.registered = True
265
+
266
+ return context
267
+
268
+ @staticmethod
269
+ def _safe_signature(func: Callable) -> inspect.Signature:
270
+ try:
271
+ return inspect.signature(func)
272
+ except (TypeError, ValueError):
273
+ return inspect.Signature()
274
+
275
+ def _build_event_payload(
276
+ self,
277
+ context: ExecutionContext,
278
+ reasoner_name: str,
279
+ *,
280
+ status: str,
281
+ parent_execution_id: Optional[str],
282
+ input_data: Optional[Dict[str, Any]] = None,
283
+ ) -> Dict[str, Any]:
284
+ payload: Dict[str, Any] = {
285
+ "execution_id": context.execution_id,
286
+ "workflow_id": context.workflow_id,
287
+ "run_id": context.run_id,
288
+ "reasoner_id": reasoner_name,
289
+ "agent_node_id": getattr(self.agent, "node_id", None),
290
+ "status": status,
291
+ "type": reasoner_name,
292
+ "parent_execution_id": parent_execution_id,
293
+ "parent_workflow_id": context.parent_workflow_id,
294
+ }
295
+ if input_data is not None:
296
+ payload["input_data"] = input_data
297
+ return payload
298
+
299
+ @staticmethod
300
+ def _build_input_payload(
301
+ signature: inspect.Signature, args: tuple, kwargs: Dict[str, Any]
302
+ ) -> Dict[str, Any]:
303
+ if not signature.parameters:
304
+ return dict(kwargs)
305
+
306
+ try:
307
+ bound = signature.bind_partial(*args, **kwargs)
308
+ bound.apply_defaults()
309
+ except Exception:
310
+ # Fallback when binding fails (e.g., C extensions)
311
+ payload = {f"arg_{idx}": value for idx, value in enumerate(args)}
312
+ payload.update(kwargs)
313
+ return payload
314
+
315
+ payload = {}
316
+ for name, value in bound.arguments.items():
317
+ if name == "self":
318
+ continue
319
+ payload[name] = value
320
+ return payload
321
+
322
+
323
+ __all__ = ["AgentWorkflow"]