flock-core 0.5.0b16__py3-none-any.whl → 0.5.0b18__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/components/evaluation/declarative_evaluation_component.py +99 -40
- flock/components/utility/memory_utility_component.py +5 -3
- flock/core/__init__.py +5 -0
- flock/core/agent/default_agent.py +170 -0
- flock/core/agent/flock_agent_integration.py +54 -0
- flock/core/agent/flock_agent_lifecycle.py +9 -0
- flock/core/context/context.py +2 -3
- flock/core/execution/local_executor.py +1 -1
- flock/core/execution/temporal_executor.py +4 -6
- flock/core/flock_agent.py +17 -2
- flock/core/flock_factory.py +92 -80
- flock/core/logging/telemetry.py +7 -2
- flock/core/mcp/flock_mcp_server.py +19 -0
- flock/core/mcp/flock_mcp_tool.py +9 -53
- flock/core/mcp/mcp_config.py +22 -4
- flock/core/mixin/dspy_integration.py +107 -149
- flock/core/orchestration/flock_execution.py +7 -0
- flock/core/orchestration/flock_initialization.py +24 -0
- flock/core/serialization/serialization_utils.py +20 -20
- flock/workflow/agent_execution_activity.py +57 -83
- flock/workflow/flock_workflow.py +39 -50
- flock/workflow/temporal_setup.py +11 -3
- {flock_core-0.5.0b16.dist-info → flock_core-0.5.0b18.dist-info}/METADATA +7 -7
- {flock_core-0.5.0b16.dist-info → flock_core-0.5.0b18.dist-info}/RECORD +27 -26
- {flock_core-0.5.0b16.dist-info → flock_core-0.5.0b18.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b16.dist-info → flock_core-0.5.0b18.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b16.dist-info → flock_core-0.5.0b18.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# src/flock/core/mixin/dspy_integration.py
|
|
2
|
-
"""Mixin class for integrating with the dspy library.
|
|
2
|
+
"""Mixin class for integrating with the dspy library.
|
|
3
|
+
|
|
4
|
+
This mixin centralizes Flock ↔ DSPy interop. It intentionally
|
|
5
|
+
delegates more to DSPy’s native builders (Signature, settings.context,
|
|
6
|
+
modules) to reduce custom glue and stay aligned with DSPy updates.
|
|
7
|
+
"""
|
|
3
8
|
|
|
4
9
|
import ast
|
|
5
10
|
import re # Import re for parsing
|
|
@@ -145,114 +150,70 @@ def _resolve_type_string(type_str: str) -> type:
|
|
|
145
150
|
class DSPyIntegrationMixin:
|
|
146
151
|
"""Mixin class for integrating with the dspy library."""
|
|
147
152
|
|
|
148
|
-
def create_dspy_signature_class(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
def create_dspy_signature_class(self, agent_name: str, description_spec: str, fields_spec: str) -> Any:
|
|
154
|
+
"""Create a DSPy Signature using DSPy's native builder.
|
|
155
|
+
|
|
156
|
+
We support the Flock spec format: "field: type | description, ... -> ...".
|
|
157
|
+
This converts to the dict-based make_signature format with
|
|
158
|
+
InputField/OutputField and resolved Python types.
|
|
153
159
|
"""
|
|
154
160
|
try:
|
|
155
161
|
import dspy
|
|
156
|
-
except ImportError:
|
|
157
|
-
logger.error(
|
|
158
|
-
|
|
159
|
-
"Install with: pip install dspy-ai"
|
|
160
|
-
)
|
|
161
|
-
raise ImportError("DSPy is required for this functionality.")
|
|
162
|
-
|
|
163
|
-
base_class = dspy.Signature
|
|
164
|
-
class_dict = {"__doc__": description_spec, "__annotations__": {}}
|
|
162
|
+
except ImportError as exc:
|
|
163
|
+
logger.error("DSPy is not installed. Install with: pip install dspy-ai")
|
|
164
|
+
raise
|
|
165
165
|
|
|
166
|
+
# Split input/output part
|
|
166
167
|
if "->" in fields_spec:
|
|
167
168
|
inputs_spec, outputs_spec = fields_spec.split("->", 1)
|
|
168
169
|
else:
|
|
169
|
-
inputs_spec, outputs_spec =
|
|
170
|
-
fields_spec,
|
|
171
|
-
"",
|
|
172
|
-
) # Assume only inputs if no '->'
|
|
170
|
+
inputs_spec, outputs_spec = fields_spec, ""
|
|
173
171
|
|
|
174
|
-
def parse_field(field_str):
|
|
175
|
-
"""Parses 'name: type_str | description' using _resolve_type_string."""
|
|
172
|
+
def parse_field(field_str: str) -> tuple[str, type, str | None] | None:
|
|
176
173
|
field_str = field_str.strip()
|
|
177
174
|
if not field_str:
|
|
178
175
|
return None
|
|
179
|
-
|
|
180
176
|
parts = field_str.split("|", 1)
|
|
181
177
|
main_part = parts[0].strip()
|
|
182
178
|
desc = parts[1].strip() if len(parts) > 1 else None
|
|
183
|
-
|
|
184
179
|
if ":" in main_part:
|
|
185
180
|
name, type_str = [s.strip() for s in main_part.split(":", 1)]
|
|
186
181
|
else:
|
|
187
|
-
name = main_part
|
|
188
|
-
type_str = "str" # Default type
|
|
189
|
-
|
|
182
|
+
name, type_str = main_part, "str"
|
|
190
183
|
try:
|
|
191
|
-
|
|
192
|
-
except Exception as e:
|
|
193
|
-
logger.
|
|
194
|
-
f"
|
|
184
|
+
py_type = _resolve_type_string(type_str)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(
|
|
187
|
+
f"Type resolution failed for '{type_str}' in field '{name}': {e}. Falling back to str."
|
|
195
188
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class_dict["__annotations__"][name] = (
|
|
213
|
-
field_type # Use resolved type
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
FieldClass = (
|
|
217
|
-
dspy.InputField
|
|
218
|
-
if field_kind == "input"
|
|
219
|
-
else dspy.OutputField
|
|
220
|
-
)
|
|
221
|
-
# DSPy Fields use 'desc' for description
|
|
222
|
-
class_dict[name] = (
|
|
223
|
-
FieldClass(desc=desc)
|
|
224
|
-
if desc is not None
|
|
225
|
-
else FieldClass()
|
|
226
|
-
)
|
|
189
|
+
py_type = str
|
|
190
|
+
return name, py_type, desc
|
|
191
|
+
|
|
192
|
+
def to_field_tuples(spec: str, kind: str) -> dict[str, tuple[type, Any]]:
|
|
193
|
+
mapping: dict[str, tuple[type, Any]] = {}
|
|
194
|
+
if not spec.strip():
|
|
195
|
+
return mapping
|
|
196
|
+
for raw in split_top_level(spec):
|
|
197
|
+
parsed = parse_field(raw)
|
|
198
|
+
if not parsed:
|
|
199
|
+
continue
|
|
200
|
+
fname, ftype, fdesc = parsed
|
|
201
|
+
FieldClass = dspy.InputField if kind == "input" else dspy.OutputField
|
|
202
|
+
finfo = FieldClass(desc=fdesc) if fdesc is not None else FieldClass()
|
|
203
|
+
mapping[fname] = (ftype, finfo)
|
|
204
|
+
return mapping
|
|
227
205
|
|
|
228
206
|
try:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Create and return the dynamic class
|
|
241
|
-
try:
|
|
242
|
-
DynamicSignature = type(
|
|
243
|
-
"dspy_" + agent_name, (base_class,), class_dict
|
|
244
|
-
)
|
|
245
|
-
logger.info(
|
|
246
|
-
f"Successfully created DSPy Signature: {DynamicSignature.__name__} "
|
|
247
|
-
f"with fields: {DynamicSignature.__annotations__}"
|
|
248
|
-
)
|
|
249
|
-
return DynamicSignature
|
|
250
|
-
except Exception as e:
|
|
251
|
-
logger.error(
|
|
252
|
-
f"Failed to create dynamic type 'dspy_{agent_name}': {e}",
|
|
253
|
-
exc_info=True,
|
|
254
|
-
)
|
|
255
|
-
raise TypeError(f"Could not create DSPy signature type: {e}") from e
|
|
207
|
+
fields: dict[str, tuple[type, Any]] = {
|
|
208
|
+
**to_field_tuples(inputs_spec, "input"),
|
|
209
|
+
**to_field_tuples(outputs_spec, "output"),
|
|
210
|
+
}
|
|
211
|
+
sig = dspy.Signature(fields, description_spec or None, signature_name=f"dspy_{agent_name}")
|
|
212
|
+
logger.info("Created DSPy Signature %s", sig.__name__)
|
|
213
|
+
return sig
|
|
214
|
+
except Exception as e: # pragma: no cover - defensive
|
|
215
|
+
logger.error("Failed to create DSPy Signature for %s: %s", agent_name, e, exc_info=True)
|
|
216
|
+
raise
|
|
256
217
|
|
|
257
218
|
def _configure_language_model(
|
|
258
219
|
self,
|
|
@@ -275,34 +236,28 @@ class DSPyIntegrationMixin:
|
|
|
275
236
|
try:
|
|
276
237
|
import dspy
|
|
277
238
|
except ImportError:
|
|
278
|
-
logger.error(
|
|
279
|
-
|
|
280
|
-
)
|
|
281
|
-
return # Or raise
|
|
239
|
+
logger.error("DSPy is not installed; cannot configure LM.")
|
|
240
|
+
return
|
|
282
241
|
|
|
242
|
+
# Build an LM instance for per-call usage; prefer settings.context over global configure.
|
|
283
243
|
try:
|
|
284
|
-
# Ensure 'cache' parameter is handled correctly (might not exist on dspy.LM directly)
|
|
285
|
-
# DSPy handles caching globally or via specific optimizers typically.
|
|
286
|
-
# We'll configure the LM without explicit cache control here.
|
|
287
244
|
lm_instance = dspy.LM(
|
|
288
245
|
model=model,
|
|
289
246
|
temperature=temperature,
|
|
290
247
|
max_tokens=max_tokens,
|
|
291
248
|
cache=use_cache,
|
|
292
|
-
# Add other relevant parameters if needed, e.g., API keys via dspy.settings
|
|
293
249
|
)
|
|
294
|
-
|
|
250
|
+
# Do not call settings.configure() here to avoid cross-task/thread conflicts.
|
|
251
|
+
# Callers should pass this LM via dspy.settings.context(lm=...) or program.acall(lm=...)
|
|
252
|
+
dspy.settings # touch to ensure settings is importable
|
|
295
253
|
logger.info(
|
|
296
|
-
|
|
254
|
+
"Prepared DSPy LM (defer install to settings.context): model=%s temp=%s max_tokens=%s",
|
|
255
|
+
model,
|
|
256
|
+
temperature,
|
|
257
|
+
max_tokens,
|
|
297
258
|
)
|
|
298
|
-
# Note: DSPy caching is usually configured globally, e.g., dspy.settings.configure(cache=...)
|
|
299
|
-
# or handled by optimizers. Setting `cache=use_cache` on dspy.LM might not be standard.
|
|
300
259
|
except Exception as e:
|
|
301
|
-
logger.error(
|
|
302
|
-
f"Failed to configure DSPy language model '{model}': {e}",
|
|
303
|
-
exc_info=True,
|
|
304
|
-
)
|
|
305
|
-
# We need to raise this exception, otherwise Flock will trundle on until it needs dspy.settings.lm and can't find it.
|
|
260
|
+
logger.error("Failed to prepare DSPy LM '%s': %s", model, e, exc_info=True)
|
|
306
261
|
raise
|
|
307
262
|
|
|
308
263
|
def _select_task(
|
|
@@ -349,9 +304,17 @@ class DSPyIntegrationMixin:
|
|
|
349
304
|
|
|
350
305
|
# Determine type if not overridden
|
|
351
306
|
if not selected_type:
|
|
352
|
-
selected_type =
|
|
353
|
-
|
|
354
|
-
|
|
307
|
+
selected_type = "ReAct" if processed_tools or processed_mcp_tools else "Predict"
|
|
308
|
+
|
|
309
|
+
# Normalize common aliases/casing
|
|
310
|
+
sel = selected_type.lower() if isinstance(selected_type, str) else selected_type
|
|
311
|
+
if isinstance(sel, str):
|
|
312
|
+
if sel in {"completion", "predict"}:
|
|
313
|
+
sel = "predict"
|
|
314
|
+
elif sel in {"react"}:
|
|
315
|
+
sel = "react"
|
|
316
|
+
elif sel in {"chainofthought", "cot", "chain_of_thought"}:
|
|
317
|
+
sel = "chain_of_thought"
|
|
355
318
|
|
|
356
319
|
logger.debug(
|
|
357
320
|
f"Selecting DSPy program type: {selected_type} (Tools provided: {bool(processed_tools)}) (MCP Tools: {bool(processed_mcp_tools)}"
|
|
@@ -368,15 +331,15 @@ class DSPyIntegrationMixin:
|
|
|
368
331
|
merged_tools = merged_tools + processed_mcp_tools
|
|
369
332
|
|
|
370
333
|
try:
|
|
371
|
-
if
|
|
334
|
+
if sel == "chain_of_thought":
|
|
372
335
|
dspy_program = dspy.ChainOfThought(signature, **kwargs)
|
|
373
|
-
elif
|
|
336
|
+
elif sel == "react":
|
|
374
337
|
if not kwargs:
|
|
375
338
|
kwargs = {"max_iters": max_tool_calls}
|
|
376
339
|
dspy_program = dspy.ReAct(
|
|
377
340
|
signature, tools=merged_tools or [], **kwargs
|
|
378
341
|
)
|
|
379
|
-
elif
|
|
342
|
+
elif sel == "predict":
|
|
380
343
|
dspy_program = dspy.Predict(signature)
|
|
381
344
|
else: # Fallback or handle unknown type
|
|
382
345
|
logger.warning(
|
|
@@ -395,51 +358,46 @@ class DSPyIntegrationMixin:
|
|
|
395
358
|
)
|
|
396
359
|
raise RuntimeError(f"Could not create DSPy program: {e}") from e
|
|
397
360
|
|
|
398
|
-
def _process_result(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
361
|
+
def _process_result(self, result: Any, inputs: dict[str, Any]) -> tuple[dict[str, Any], float, list]:
|
|
362
|
+
"""Convert a DSPy Prediction or mapping to a plain dict and attach LM history.
|
|
363
|
+
|
|
364
|
+
Returns (result_dict, cost_placeholder, lm_history). The cost is set to 0.0;
|
|
365
|
+
use token usage trackers elsewhere for accurate accounting.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
import dspy
|
|
369
|
+
except ImportError:
|
|
370
|
+
dspy = None
|
|
403
371
|
|
|
404
372
|
if result is None:
|
|
405
373
|
logger.warning("DSPy program returned None result.")
|
|
406
|
-
return {}
|
|
374
|
+
return {}, 0.0, []
|
|
375
|
+
|
|
407
376
|
try:
|
|
408
|
-
#
|
|
409
|
-
if
|
|
410
|
-
output_dict = dict(result.items())
|
|
411
|
-
elif
|
|
412
|
-
output_dict =
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
377
|
+
# Best-effort extraction from DSPy Prediction
|
|
378
|
+
if dspy and isinstance(result, dspy.Prediction):
|
|
379
|
+
output_dict = dict(result.items(include_dspy=False))
|
|
380
|
+
elif isinstance(result, dict):
|
|
381
|
+
output_dict = result
|
|
382
|
+
elif hasattr(result, "items") and callable(result.items):
|
|
383
|
+
try:
|
|
384
|
+
output_dict = dict(result.items())
|
|
385
|
+
except Exception:
|
|
386
|
+
output_dict = {"raw_result": str(result)}
|
|
417
387
|
else:
|
|
418
|
-
|
|
419
|
-
if isinstance(result, dict):
|
|
420
|
-
output_dict = result
|
|
421
|
-
else: # Final fallback
|
|
422
|
-
logger.warning(
|
|
423
|
-
f"Could not reliably convert DSPy result of type {type(result)} to dict. Returning as is."
|
|
424
|
-
)
|
|
425
|
-
output_dict = {"raw_result": result}
|
|
388
|
+
output_dict = {"raw_result": str(result)}
|
|
426
389
|
|
|
427
|
-
logger.debug(f"Processed DSPy result to dict: {output_dict}")
|
|
428
|
-
# Optionally merge inputs back if desired (can make result dict large)
|
|
429
390
|
final_result = {**inputs, **output_dict}
|
|
430
391
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
392
|
+
lm_history = []
|
|
393
|
+
try:
|
|
394
|
+
if dspy and dspy.settings.lm is not None and hasattr(dspy.settings.lm, "history"):
|
|
395
|
+
lm_history = dspy.settings.lm.history
|
|
396
|
+
except Exception:
|
|
397
|
+
lm_history = []
|
|
434
398
|
|
|
435
|
-
return final_result,
|
|
399
|
+
return final_result, 0.0, lm_history
|
|
436
400
|
|
|
437
|
-
except Exception as conv_error:
|
|
438
|
-
logger.error(
|
|
439
|
-
|
|
440
|
-
exc_info=True,
|
|
441
|
-
)
|
|
442
|
-
return {
|
|
443
|
-
"error": "Failed to process result",
|
|
444
|
-
"raw_result": str(result),
|
|
445
|
-
}
|
|
401
|
+
except Exception as conv_error: # pragma: no cover - defensive
|
|
402
|
+
logger.error("Failed to process DSPy result into dictionary: %s", conv_error, exc_info=True)
|
|
403
|
+
return {"error": "Failed to process result", "raw_result": str(result)}, 0.0, []
|
|
@@ -124,6 +124,13 @@ class FlockExecution:
|
|
|
124
124
|
|
|
125
125
|
# Setup execution context and input
|
|
126
126
|
run_input = input if input is not None else self.flock._start_input
|
|
127
|
+
# Accept Pydantic BaseModel instances as input by converting to dict
|
|
128
|
+
try:
|
|
129
|
+
from pydantic import BaseModel as _BM
|
|
130
|
+
if not isinstance(run_input, dict) and isinstance(run_input, _BM):
|
|
131
|
+
run_input = run_input.model_dump(exclude_none=True) # type: ignore[attr-defined]
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
127
134
|
effective_run_id = run_id or f"flockrun_{uuid.uuid4().hex[:8]}"
|
|
128
135
|
|
|
129
136
|
# Set span attributes
|
|
@@ -31,6 +31,10 @@ class FlockInitialization:
|
|
|
31
31
|
servers: list["FlockMCPServer"] | None = None,
|
|
32
32
|
) -> None:
|
|
33
33
|
"""Handle all initialization side effects and setup."""
|
|
34
|
+
# Workaround: newer litellm logging tries to import proxy dependencies (apscheduler)
|
|
35
|
+
# via cold storage logging even for non-proxy usage. Avoid hard dependency by
|
|
36
|
+
# pre-stubbing the `litellm.proxy.proxy_server` module with a minimal object.
|
|
37
|
+
self._patch_litellm_proxy_imports()
|
|
34
38
|
# Register passed servers first (agents may depend on them)
|
|
35
39
|
if servers:
|
|
36
40
|
self._register_servers(servers)
|
|
@@ -64,6 +68,26 @@ class FlockInitialization:
|
|
|
64
68
|
enable_temporal=self.flock.enable_temporal,
|
|
65
69
|
)
|
|
66
70
|
|
|
71
|
+
def _patch_litellm_proxy_imports(self) -> None:
|
|
72
|
+
"""Stub litellm proxy_server to avoid optional proxy deps when not used.
|
|
73
|
+
|
|
74
|
+
Some litellm versions import `litellm.proxy.proxy_server` during standard logging
|
|
75
|
+
to read `general_settings`, which pulls in optional dependencies like `apscheduler`.
|
|
76
|
+
We provide a stub so imports succeed but cold storage remains disabled.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
import sys
|
|
80
|
+
import types
|
|
81
|
+
|
|
82
|
+
if "litellm.proxy.proxy_server" not in sys.modules:
|
|
83
|
+
stub = types.ModuleType("litellm.proxy.proxy_server")
|
|
84
|
+
# Minimal surface that cold_storage_handler accesses
|
|
85
|
+
setattr(stub, "general_settings", {})
|
|
86
|
+
sys.modules["litellm.proxy.proxy_server"] = stub
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Safe to ignore; worst case litellm will log a warning
|
|
89
|
+
logger.debug(f"Failed to stub litellm proxy_server: {e}")
|
|
90
|
+
|
|
67
91
|
def _register_servers(self, servers: list["FlockMCPServer"]) -> None:
|
|
68
92
|
"""Register servers with the Flock instance."""
|
|
69
93
|
from flock.core.mcp.flock_mcp_server import (
|
|
@@ -76,12 +76,14 @@ def _format_type_to_string(type_hint: type) -> str:
|
|
|
76
76
|
elif hasattr(type_hint, "__name__"):
|
|
77
77
|
# Handle custom types registered in registry (get preferred name)
|
|
78
78
|
registry = get_registry()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
reg_type
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
try:
|
|
80
|
+
# Prefer explicit type registration names when available
|
|
81
|
+
for name, reg_type in registry.types.get_all_types().items():
|
|
82
|
+
if reg_type == type_hint:
|
|
83
|
+
return name
|
|
84
|
+
except Exception:
|
|
85
|
+
# Defensive: if registry helper is unavailable, fall back below
|
|
86
|
+
pass
|
|
85
87
|
return type_hint.__name__ # Fallback to class name if not registered
|
|
86
88
|
else:
|
|
87
89
|
# Fallback for complex types or types not handled above
|
|
@@ -245,22 +247,20 @@ def serialize_item(item: Any) -> Any:
|
|
|
245
247
|
return {key: serialize_item(value) for key, value in item.items()}
|
|
246
248
|
elif isinstance(item, Sequence) and not isinstance(item, str):
|
|
247
249
|
return [serialize_item(sub_item) for sub_item in item]
|
|
248
|
-
elif isinstance(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
type_name = registry.get_component_type_name(
|
|
252
|
-
item
|
|
253
|
-
) # Check components first
|
|
250
|
+
elif isinstance(item, type): # Handle type objects themselves (e.g. if stored directly)
|
|
251
|
+
# Prefer registered component/type names when possible
|
|
252
|
+
type_name = registry.get_component_type_name(item)
|
|
254
253
|
if type_name:
|
|
255
254
|
return {"__component_ref__": type_name}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
# Fall back to module-qualified path for general types
|
|
256
|
+
try:
|
|
257
|
+
module = getattr(item, "__module__", None)
|
|
258
|
+
name = getattr(item, "__name__", None)
|
|
259
|
+
if module and name:
|
|
260
|
+
return {"__type_ref__": f"{module}.{name}"}
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
logger.warning(f"Could not serialize type object {item}, storing as string.")
|
|
264
264
|
return str(item)
|
|
265
265
|
elif isinstance(item, Enum):
|
|
266
266
|
return item.value
|
|
@@ -23,7 +23,7 @@ registry = get_registry() # Get registry instance once
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@activity.defn
|
|
26
|
-
async def execute_single_agent(agent_name: str,
|
|
26
|
+
async def execute_single_agent(agent_name: str, context_dict: dict) -> dict:
|
|
27
27
|
"""Executes a single specified agent and returns its result.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
@@ -47,7 +47,8 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
|
|
|
47
47
|
# Raise error for Temporal to potentially retry/fail the activity
|
|
48
48
|
raise ValueError(f"Agent '{agent_name}' not found in registry.")
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Rehydrate context from dict and set on agent (transient for this execution)
|
|
51
|
+
context = FlockContext.from_dict(context_dict)
|
|
51
52
|
agent.context = context
|
|
52
53
|
|
|
53
54
|
# Ensure model is set (using context value if needed)
|
|
@@ -66,13 +67,39 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
|
|
|
66
67
|
# agent.resolve_callables(context=context)
|
|
67
68
|
|
|
68
69
|
# Resolve inputs for this specific agent run
|
|
69
|
-
previous_agent_name = (
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
previous_agent_name = context.get_last_agent_name() # May be None on first agent
|
|
71
|
+
prev_def = (
|
|
72
|
+
context.get_agent_definition(previous_agent_name)
|
|
73
|
+
if previous_agent_name
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
prev_out_spec = (
|
|
77
|
+
(prev_def.agent_data.get("output_spec") if isinstance(prev_def, type(prev_def)) else None)
|
|
78
|
+
if prev_def and isinstance(prev_def.agent_data, dict)
|
|
79
|
+
else None
|
|
80
|
+
)
|
|
81
|
+
prev_cfg = (
|
|
82
|
+
prev_def.agent_data.get("config")
|
|
83
|
+
if prev_def and isinstance(prev_def.agent_data, dict)
|
|
84
|
+
else {}
|
|
85
|
+
)
|
|
86
|
+
prev_strategy = (
|
|
87
|
+
prev_cfg.get("handoff_strategy") if isinstance(prev_cfg, dict) else None
|
|
88
|
+
) or "static"
|
|
89
|
+
prev_map = (
|
|
90
|
+
prev_cfg.get("handoff_map") if isinstance(prev_cfg, dict) else None
|
|
91
|
+
) or {}
|
|
72
92
|
logger.debug(
|
|
73
93
|
f"Resolving inputs for {agent_name} with previous agent {previous_agent_name}"
|
|
74
94
|
)
|
|
75
|
-
agent_inputs = resolve_inputs(
|
|
95
|
+
agent_inputs = resolve_inputs(
|
|
96
|
+
agent.input,
|
|
97
|
+
context,
|
|
98
|
+
previous_agent_name or "",
|
|
99
|
+
prev_out_spec or "",
|
|
100
|
+
prev_strategy,
|
|
101
|
+
prev_map,
|
|
102
|
+
)
|
|
76
103
|
span.add_event(
|
|
77
104
|
"resolved inputs", attributes={"inputs": str(agent_inputs)}
|
|
78
105
|
)
|
|
@@ -96,6 +123,8 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
|
|
|
96
123
|
error=str(e),
|
|
97
124
|
exc_info=True,
|
|
98
125
|
)
|
|
126
|
+
# Debug aid: ensure exception prints even if logger is muted in environment
|
|
127
|
+
print(f"[agent_activity] Single agent execution failed for {agent_name}: {e!r}")
|
|
99
128
|
span.record_exception(e)
|
|
100
129
|
# Re-raise the exception for Temporal to handle based on retry policy
|
|
101
130
|
raise
|
|
@@ -103,22 +132,11 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
|
|
|
103
132
|
|
|
104
133
|
@activity.defn
|
|
105
134
|
async def determine_next_agent(
|
|
106
|
-
current_agent_name: str, result: dict,
|
|
107
|
-
) ->
|
|
108
|
-
"""
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
current_agent_name: The name of the agent that just ran.
|
|
112
|
-
result: The result produced by the current agent.
|
|
113
|
-
context: The current FlockContext.
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
A dictionary representing the HandOffRequest (serialized via model_dump),
|
|
117
|
-
or None if no handoff occurs or router doesn't specify a next agent.
|
|
135
|
+
current_agent_name: str, result: dict, context_dict: dict
|
|
136
|
+
) -> str | None:
|
|
137
|
+
"""Determine the next agent using the agent's routing component.
|
|
118
138
|
|
|
119
|
-
|
|
120
|
-
ValueError: If the current agent cannot be found.
|
|
121
|
-
Exception: Propagates exceptions from router execution for Temporal retries.
|
|
139
|
+
Returns the next agent's name or None if the workflow should terminate.
|
|
122
140
|
"""
|
|
123
141
|
with tracer.start_as_current_span("determine_next_agent") as span:
|
|
124
142
|
span.set_attribute("agent.name", current_agent_name)
|
|
@@ -145,75 +163,30 @@ async def determine_next_agent(
|
|
|
145
163
|
agent=agent.name,
|
|
146
164
|
)
|
|
147
165
|
try:
|
|
148
|
-
# Execute the
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
agent=agent.name,
|
|
162
|
-
)
|
|
163
|
-
# Ensure context is available if the callable needs it
|
|
164
|
-
try:
|
|
165
|
-
handoff_data = handoff_data(
|
|
166
|
-
context, result
|
|
167
|
-
) # Potential side effects
|
|
168
|
-
if not isinstance(handoff_data, HandOffRequest):
|
|
169
|
-
logger.error(
|
|
170
|
-
"Handoff function did not return a HandOffRequest object.",
|
|
171
|
-
agent=agent.name,
|
|
172
|
-
)
|
|
173
|
-
raise TypeError(
|
|
174
|
-
"Handoff function must return a HandOffRequest object."
|
|
175
|
-
)
|
|
176
|
-
except Exception as e:
|
|
177
|
-
logger.error(
|
|
178
|
-
"Handoff function execution failed",
|
|
179
|
-
agent=agent.name,
|
|
180
|
-
error=str(e),
|
|
181
|
-
exc_info=True,
|
|
182
|
-
)
|
|
183
|
-
span.record_exception(e)
|
|
184
|
-
raise # Propagate error
|
|
185
|
-
|
|
186
|
-
# Ensure we have a HandOffRequest object after potentially calling function
|
|
187
|
-
if not isinstance(handoff_data, HandOffRequest):
|
|
188
|
-
logger.error(
|
|
189
|
-
"Router returned unexpected type",
|
|
190
|
-
type=type(handoff_data).__name__,
|
|
191
|
-
agent=agent.name,
|
|
192
|
-
)
|
|
193
|
-
raise TypeError(
|
|
194
|
-
f"Router for agent '{agent.name}' did not return a HandOffRequest object."
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
# Ensure agent instance is converted to name for serialization across boundaries
|
|
198
|
-
if isinstance(handoff_data.next_agent, FlockAgent):
|
|
199
|
-
handoff_data.next_agent = handoff_data.next_agent.name
|
|
200
|
-
|
|
201
|
-
# If router logic determines no further agent, return None
|
|
202
|
-
if not handoff_data.next_agent:
|
|
166
|
+
# Execute routing logic on the router component (unified architecture)
|
|
167
|
+
context = FlockContext.from_dict(context_dict)
|
|
168
|
+
next_val = await agent.router.determine_next_step(agent, result, context)
|
|
169
|
+
|
|
170
|
+
# Convert to a simple agent name if needed
|
|
171
|
+
if isinstance(next_val, FlockAgent):
|
|
172
|
+
next_name = next_val.name
|
|
173
|
+
elif isinstance(next_val, str):
|
|
174
|
+
next_name = next_val
|
|
175
|
+
else:
|
|
176
|
+
next_name = None
|
|
177
|
+
|
|
178
|
+
if not next_name:
|
|
203
179
|
logger.info("Router determined no next agent", agent=agent.name)
|
|
204
180
|
span.add_event("no_next_agent_from_router")
|
|
205
181
|
return None
|
|
206
182
|
|
|
207
183
|
logger.info(
|
|
208
|
-
"
|
|
209
|
-
next_agent=
|
|
184
|
+
"Next agent determined",
|
|
185
|
+
next_agent=next_name,
|
|
210
186
|
agent=agent.name,
|
|
211
187
|
)
|
|
212
|
-
span.set_attribute("next_agent",
|
|
213
|
-
|
|
214
|
-
return handoff_data.model_dump(
|
|
215
|
-
mode="json"
|
|
216
|
-
) # Ensure JSON-serializable
|
|
188
|
+
span.set_attribute("next_agent", next_name)
|
|
189
|
+
return next_name
|
|
217
190
|
|
|
218
191
|
except Exception as e:
|
|
219
192
|
# Catch potential errors during routing execution
|
|
@@ -223,6 +196,7 @@ async def determine_next_agent(
|
|
|
223
196
|
error=str(e),
|
|
224
197
|
exc_info=True,
|
|
225
198
|
)
|
|
199
|
+
print(f"[agent_activity] Router execution failed for {agent.name}: {e!r}")
|
|
226
200
|
span.record_exception(e)
|
|
227
201
|
# Let Temporal handle the activity failure based on retry policy
|
|
228
202
|
raise
|