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.

@@ -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
- self, agent_name, description_spec, fields_spec
150
- ) -> Any:
151
- """Creates a dynamic DSPy Signature class from string specifications,
152
- resolving types using the registry.
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
- "DSPy library is not installed. Cannot create DSPy signature. "
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
- field_type = _resolve_type_string(type_str)
192
- except Exception as e: # Catch resolution errors
193
- logger.error(
194
- f"Failed to resolve type '{type_str}' for field '{name}': {e}. Defaulting to str."
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
- field_type = str
197
-
198
- return name, field_type, desc
199
-
200
- def process_fields(fields_string, field_kind):
201
- """Process fields and add to class_dict."""
202
- if not fields_string or not fields_string.strip():
203
- return
204
-
205
- split_fields = split_top_level(fields_string)
206
- for field in split_fields:
207
- if field.strip():
208
- parsed = parse_field(field)
209
- if not parsed:
210
- continue
211
- name, field_type, desc = parsed
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
- process_fields(inputs_spec, "input")
230
- process_fields(outputs_spec, "output")
231
- except Exception as e:
232
- logger.error(
233
- f"Error processing fields for DSPy signature '{agent_name}': {e}",
234
- exc_info=True,
235
- )
236
- raise ValueError(
237
- f"Could not process fields for signature: {e}"
238
- ) from e
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
- "DSPy library is not installed. Cannot configure language model."
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
- dspy.settings.configure(lm=lm_instance)
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
- f"DSPy LM configured with model: {model}, temp: {temperature}, max_tokens: {max_tokens}"
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
- "ReAct" if processed_tools or processed_mcp_tools else "Predict"
354
- ) # Default logic
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 selected_type == "ChainOfThought":
334
+ if sel == "chain_of_thought":
372
335
  dspy_program = dspy.ChainOfThought(signature, **kwargs)
373
- elif selected_type == "ReAct":
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 selected_type == "Predict": # Default or explicitly Completion
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
- self, result: Any, inputs: dict[str, Any]
400
- ) -> tuple[dict[str, Any], float, list]:
401
- """Convert the DSPy result object to a dictionary."""
402
- import dspy
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
- # DSPy Prediction objects often behave like dicts or have .keys() / items()
409
- if hasattr(result, "items") and callable(result.items):
410
- output_dict = dict(result.items())
411
- elif hasattr(result, "__dict__"): # Fallback for other object types
412
- output_dict = {
413
- k: v
414
- for k, v in result.__dict__.items()
415
- if not k.startswith("_")
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
- # If it's already a dict (less common for DSPy results directly)
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
- lm = dspy.settings.get("lm")
432
- cost = sum([x["cost"] for x in lm.history if x["cost"] is not None])
433
- lm_history = lm.history
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, cost, lm_history
399
+ return final_result, 0.0, lm_history
436
400
 
437
- except Exception as conv_error:
438
- logger.error(
439
- f"Failed to process DSPy result into dictionary: {conv_error}",
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
- for (
80
- name,
81
- reg_type,
82
- ) in registry._types.items(): # Access internal for lookup
83
- if reg_type == type_hint:
84
- return name # Return registered name
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
- item, type
250
- ): # Handle type objects themselves (e.g. if stored directly)
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
- type_name = registry._get_path_string(
257
- item
258
- ) # Check regular types/classes by path
259
- if type_name:
260
- return {"__type_ref__": type_name}
261
- logger.warning(
262
- f"Could not serialize type object {item}, storing as string."
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