flock-core 0.5.0b15__py3-none-any.whl → 0.5.0b17__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 +15 -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/core/util/input_resolver.py +16 -6
- 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.0b15.dist-info → flock_core-0.5.0b17.dist-info}/METADATA +7 -7
- {flock_core-0.5.0b15.dist-info → flock_core-0.5.0b17.dist-info}/RECORD +28 -27
- {flock_core-0.5.0b15.dist-info → flock_core-0.5.0b17.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b15.dist-info → flock_core-0.5.0b17.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b15.dist-info → flock_core-0.5.0b17.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
|
|
@@ -102,6 +102,16 @@ def resolve_inputs(
|
|
|
102
102
|
keys = _parse_keys(split_input)
|
|
103
103
|
inputs = {}
|
|
104
104
|
|
|
105
|
+
def _normalize_empty_string(val):
|
|
106
|
+
"""Treat empty string inputs as None to match None semantics.
|
|
107
|
+
|
|
108
|
+
This aligns behavior so passing "" behaves like passing None
|
|
109
|
+
for agent input properties.
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(val, str) and val == "":
|
|
112
|
+
return None
|
|
113
|
+
return val
|
|
114
|
+
|
|
105
115
|
for key in keys:
|
|
106
116
|
split_key = key.split(".")
|
|
107
117
|
|
|
@@ -122,16 +132,16 @@ def resolve_inputs(
|
|
|
122
132
|
# Fallback to the most recent value in the state
|
|
123
133
|
historic_value = context.get_most_recent_value(key)
|
|
124
134
|
if historic_value is not None:
|
|
125
|
-
inputs[key] = historic_value
|
|
135
|
+
inputs[key] = _normalize_empty_string(historic_value)
|
|
126
136
|
continue
|
|
127
137
|
|
|
128
138
|
# Fallback to the initial input
|
|
129
139
|
var_value = context.get_variable(key)
|
|
130
140
|
if var_value is not None:
|
|
131
|
-
inputs[key] = var_value
|
|
141
|
+
inputs[key] = _normalize_empty_string(var_value)
|
|
132
142
|
continue
|
|
133
143
|
|
|
134
|
-
inputs[key] = context.get_variable("flock." + key)
|
|
144
|
+
inputs[key] = _normalize_empty_string(context.get_variable("flock." + key))
|
|
135
145
|
|
|
136
146
|
# Case 2: A compound key (e.g., "agent_name.property" or "context.property")
|
|
137
147
|
elif len(split_key) == 2:
|
|
@@ -139,16 +149,16 @@ def resolve_inputs(
|
|
|
139
149
|
|
|
140
150
|
if entity_name.lower() == "context":
|
|
141
151
|
# Try to fetch the attribute from the context
|
|
142
|
-
inputs[key] = getattr(context, property_name, None)
|
|
152
|
+
inputs[key] = _normalize_empty_string(getattr(context, property_name, None))
|
|
143
153
|
continue
|
|
144
154
|
|
|
145
155
|
if entity_name.lower() == "def":
|
|
146
156
|
# Return the agent definition for the given property name
|
|
147
|
-
inputs[key] = context.get_agent_definition(property_name)
|
|
157
|
+
inputs[key] = _normalize_empty_string(context.get_agent_definition(property_name))
|
|
148
158
|
continue
|
|
149
159
|
|
|
150
160
|
# Otherwise, attempt to look up a state variable with the key "entity_name.property_name"
|
|
151
|
-
inputs[key] = context.get_variable(f"{entity_name}.{property_name}")
|
|
161
|
+
inputs[key] = _normalize_empty_string(context.get_variable(f"{entity_name}.{property_name}"))
|
|
152
162
|
continue
|
|
153
163
|
|
|
154
164
|
return inputs
|