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.
- agentfield/__init__.py +66 -0
- agentfield/agent.py +3569 -0
- agentfield/agent_ai.py +1125 -0
- agentfield/agent_cli.py +386 -0
- agentfield/agent_field_handler.py +494 -0
- agentfield/agent_mcp.py +534 -0
- agentfield/agent_registry.py +29 -0
- agentfield/agent_server.py +1185 -0
- agentfield/agent_utils.py +269 -0
- agentfield/agent_workflow.py +323 -0
- agentfield/async_config.py +278 -0
- agentfield/async_execution_manager.py +1227 -0
- agentfield/client.py +1447 -0
- agentfield/connection_manager.py +280 -0
- agentfield/decorators.py +527 -0
- agentfield/did_manager.py +337 -0
- agentfield/dynamic_skills.py +304 -0
- agentfield/execution_context.py +255 -0
- agentfield/execution_state.py +453 -0
- agentfield/http_connection_manager.py +429 -0
- agentfield/litellm_adapters.py +140 -0
- agentfield/logger.py +249 -0
- agentfield/mcp_client.py +204 -0
- agentfield/mcp_manager.py +340 -0
- agentfield/mcp_stdio_bridge.py +550 -0
- agentfield/memory.py +723 -0
- agentfield/memory_events.py +489 -0
- agentfield/multimodal.py +173 -0
- agentfield/multimodal_response.py +403 -0
- agentfield/pydantic_utils.py +227 -0
- agentfield/rate_limiter.py +280 -0
- agentfield/result_cache.py +441 -0
- agentfield/router.py +190 -0
- agentfield/status.py +70 -0
- agentfield/types.py +710 -0
- agentfield/utils.py +26 -0
- agentfield/vc_generator.py +464 -0
- agentfield/vision.py +198 -0
- agentfield-0.1.22rc2.dist-info/METADATA +102 -0
- agentfield-0.1.22rc2.dist-info/RECORD +42 -0
- agentfield-0.1.22rc2.dist-info/WHEEL +5 -0
- 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"]
|