flock-core 0.4.0b22__py3-none-any.whl → 0.4.0b24__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.

Files changed (32) hide show
  1. flock/core/flock.py +24 -7
  2. flock/core/flock_agent.py +219 -75
  3. flock/core/flock_factory.py +8 -6
  4. flock/core/flock_registry.py +140 -60
  5. flock/core/flock_router.py +19 -6
  6. flock/core/serialization/serialization_utils.py +85 -19
  7. flock/core/util/input_resolver.py +5 -0
  8. flock/evaluators/declarative/declarative_evaluator.py +18 -10
  9. flock/evaluators/memory/memory_evaluator.py +2 -0
  10. flock/evaluators/test/test_case_evaluator.py +2 -0
  11. flock/evaluators/zep/zep_evaluator.py +2 -0
  12. flock/modules/assertion/assertion_module.py +286 -0
  13. flock/modules/callback/callback_module.py +2 -0
  14. flock/modules/memory/memory_module.py +2 -0
  15. flock/modules/output/output_module.py +2 -0
  16. flock/modules/performance/metrics_module.py +2 -0
  17. flock/modules/zep/zep_module.py +2 -0
  18. flock/routers/agent/agent_router.py +7 -5
  19. flock/routers/conditional/conditional_router.py +482 -0
  20. flock/routers/default/default_router.py +5 -1
  21. flock/routers/feedback/feedback_router.py +114 -0
  22. flock/routers/list_generator/list_generator_router.py +166 -0
  23. flock/routers/llm/llm_router.py +3 -1
  24. flock/workflow/activities.py +20 -1
  25. {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/METADATA +2 -1
  26. {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/RECORD +29 -28
  27. flock/evaluators/memory/azure_search_evaluator.py +0 -0
  28. flock/evaluators/natural_language/natural_language_evaluator.py +0 -66
  29. flock/modules/azure-search/azure_search_module.py +0 -0
  30. {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/WHEEL +0 -0
  31. {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/entry_points.txt +0 -0
  32. {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ within the Flock framework to support dynamic lookup and serialization.
5
5
 
6
6
  from __future__ import annotations # Add this at the very top
7
7
 
8
+ import builtins
8
9
  import importlib
9
10
  import inspect
10
11
  import sys
@@ -31,6 +32,7 @@ if TYPE_CHECKING:
31
32
  from flock.core.flock_router import FlockRouter
32
33
 
33
34
  COMPONENT_BASE_TYPES = (FlockModule, FlockEvaluator, FlockRouter)
35
+
34
36
  IS_COMPONENT_CHECK_ENABLED = True
35
37
  else:
36
38
  # Define dummy types or skip check if not type checking
@@ -40,12 +42,15 @@ else:
40
42
 
41
43
  # Fallback if core types aren't available during setup
42
44
 
45
+ from flock.core.flock_module import FlockModuleConfig
43
46
  from flock.core.logging.logging import get_logger
44
47
 
45
48
  logger = get_logger("registry")
46
49
  T = TypeVar("T")
47
50
  ClassType = TypeVar("ClassType", bound=type)
48
51
  FuncType = TypeVar("FuncType", bound=Callable)
52
+ ConfigType = TypeVar("ConfigType", bound=BaseModel)
53
+ _COMPONENT_CONFIG_MAP: dict[type[BaseModel], type[any]] = {}
49
54
 
50
55
 
51
56
  class FlockRegistry:
@@ -104,6 +109,45 @@ class FlockRegistry:
104
109
  except Exception as e:
105
110
  logger.error(f"Failed to auto-register core type {t}: {e}")
106
111
 
112
+ @staticmethod
113
+ def register_config_component_pair(
114
+ config_cls: type[ConfigType], component_cls: type[ClassType]
115
+ ):
116
+ """Explicitly registers the mapping between a config and component class."""
117
+ from flock.core.flock_evaluator import (
118
+ FlockEvaluatorConfig,
119
+ )
120
+ from flock.core.flock_router import FlockRouterConfig
121
+
122
+ if not issubclass(
123
+ config_cls,
124
+ FlockModuleConfig | FlockRouterConfig | FlockEvaluatorConfig,
125
+ ):
126
+ logger.warning(
127
+ f"Config class {config_cls.__name__} does not inherit from a standard Flock config base."
128
+ )
129
+ # Add more checks if needed (e.g., component_cls inherits from Module/Router/Evaluator)
130
+
131
+ if (
132
+ config_cls in _COMPONENT_CONFIG_MAP
133
+ and _COMPONENT_CONFIG_MAP[config_cls] != component_cls
134
+ ):
135
+ logger.warning(
136
+ f"Config class {config_cls.__name__} already mapped to {_COMPONENT_CONFIG_MAP[config_cls].__name__}. Overwriting with {component_cls.__name__}."
137
+ )
138
+
139
+ _COMPONENT_CONFIG_MAP[config_cls] = component_cls
140
+ logger.debug(
141
+ f"Registered config mapping: {config_cls.__name__} -> {component_cls.__name__}"
142
+ )
143
+
144
+ @staticmethod
145
+ def get_component_class_for_config(
146
+ config_cls: type[ConfigType],
147
+ ) -> type[ClassType] | None:
148
+ """Looks up the Component Class associated with a Config Class."""
149
+ return _COMPONENT_CONFIG_MAP.get(config_cls)
150
+
107
151
  # --- Path String Generation ---
108
152
  @staticmethod
109
153
  def _get_path_string(obj: Callable | type) -> str | None:
@@ -172,57 +216,88 @@ class FlockRegistry:
172
216
  )
173
217
  return None
174
218
 
175
- def get_callable(self, path_str: str) -> Callable:
176
- """Retrieves a callable by its path string, attempting dynamic import if not found."""
177
- if path_str in self._callables:
178
- logger.debug(f"Found callable '{path_str}' in registry")
179
- return self._callables[path_str]
219
+ def get_callable(self, name_or_path: str) -> Callable:
220
+ """Retrieves a callable by its registered name or full path string.
221
+ Attempts dynamic import if not found directly. Prioritizes exact match,
222
+ then searches for matches ending with '.{name}'.
223
+ """
224
+ # 1. Try exact match first (covers full paths and simple names if registered that way)
225
+ if name_or_path in self._callables:
226
+ logger.debug(
227
+ f"Found callable '{name_or_path}' directly in registry."
228
+ )
229
+ return self._callables[name_or_path]
230
+
231
+ # 2. If not found, and it looks like a simple name, search registered paths
232
+ if "." not in name_or_path:
233
+ matches = []
234
+ for path_str, func in self._callables.items():
235
+ # Check if path ends with ".{simple_name}" or exactly matches simple_name
236
+ if path_str == name_or_path or path_str.endswith(
237
+ f".{name_or_path}"
238
+ ):
239
+ matches.append(func)
180
240
 
181
- logger.debug(
182
- f"Callable '{path_str}' not in registry, attempting dynamic import."
183
- )
184
- try:
185
- if "." not in path_str: # Built-ins
186
- logger.debug(f"Trying to import built-in callable '{path_str}'")
187
- builtins_module = importlib.import_module("builtins")
188
- if hasattr(builtins_module, path_str):
189
- func = getattr(builtins_module, path_str)
190
- if callable(func):
191
- self.register_callable(func, path_str) # Cache it
192
- logger.info(
193
- f"Successfully imported built-in callable '{path_str}'"
194
- )
195
- return func
196
- logger.error(f"Built-in callable '{path_str}' not found.")
197
- raise KeyError(f"Built-in callable '{path_str}' not found.")
198
-
199
- logger.debug(f"Trying to import module callable '{path_str}'")
200
- module_name, func_name = path_str.rsplit(".", 1)
201
- module = importlib.import_module(module_name)
202
- func = getattr(module, func_name)
203
- if callable(func):
204
- self.register_callable(
205
- func, path_str
206
- ) # Cache dynamically imported
207
- logger.info(
208
- f"Successfully imported module callable '{path_str}'"
241
+ if len(matches) == 1:
242
+ logger.debug(
243
+ f"Found unique callable for simple name '{name_or_path}' via path '{self.get_callable_path_string(matches[0])}'."
209
244
  )
210
- return func
211
- else:
245
+ return matches[0]
246
+ elif len(matches) > 1:
247
+ # Ambiguous simple name - require full path
248
+ found_paths = [
249
+ self.get_callable_path_string(f) for f in matches
250
+ ]
212
251
  logger.error(
213
- f"Dynamically imported object '{path_str}' is not callable."
252
+ f"Ambiguous callable name '{name_or_path}'. Found matches: {found_paths}. Use full path string for lookup."
214
253
  )
215
- raise TypeError(
216
- f"Dynamically imported object '{path_str}' is not callable."
254
+ raise KeyError(
255
+ f"Ambiguous callable name '{name_or_path}'. Use full path string."
217
256
  )
218
- except (ImportError, AttributeError, KeyError, TypeError) as e:
219
- logger.error(
220
- f"Failed to dynamically load/find callable '{path_str}': {e}",
221
- exc_info=True,
257
+ # else: Not found by simple name search in registry, proceed to dynamic import
258
+
259
+ # 3. Attempt dynamic import if it looks like a full path
260
+ if "." in name_or_path:
261
+ logger.debug(
262
+ f"Callable '{name_or_path}' not in registry cache, attempting dynamic import."
222
263
  )
223
- raise KeyError(
224
- f"Callable '{path_str}' not found or failed to load: {e}"
225
- ) from e
264
+ try:
265
+ module_name, func_name = name_or_path.rsplit(".", 1)
266
+ module = importlib.import_module(module_name)
267
+ func = getattr(module, func_name)
268
+ if callable(func):
269
+ self.register_callable(
270
+ func, name_or_path
271
+ ) # Cache dynamically imported
272
+ logger.info(
273
+ f"Successfully imported and registered module callable '{name_or_path}'"
274
+ )
275
+ return func
276
+ else:
277
+ raise TypeError(
278
+ f"Dynamically imported object '{name_or_path}' is not callable."
279
+ )
280
+ except (ImportError, AttributeError, TypeError) as e:
281
+ logger.error(
282
+ f"Failed to dynamically load/find callable '{name_or_path}': {e}",
283
+ exc_info=False,
284
+ )
285
+ # Fall through to raise KeyError
286
+ # 4. Handle built-ins if not found yet (might be redundant if simple name check worked)
287
+ elif name_or_path in builtins.__dict__:
288
+ func = builtins.__dict__[name_or_path]
289
+ if callable(func):
290
+ self.register_callable(func, name_or_path) # Cache it
291
+ logger.info(
292
+ f"Found and registered built-in callable '{name_or_path}'"
293
+ )
294
+ return func
295
+
296
+ # 5. Final failure
297
+ logger.error(
298
+ f"Callable '{name_or_path}' not found in registry or via import."
299
+ )
300
+ raise KeyError(f"Callable '{name_or_path}' not found.")
226
301
 
227
302
  def get_callable_path_string(self, func: Callable) -> str | None:
228
303
  """Gets the path string for a callable, registering it if necessary."""
@@ -407,36 +482,41 @@ def get_registry() -> FlockRegistry:
407
482
 
408
483
  # Type hinting for decorators to preserve signature
409
484
  @overload
410
- def flock_component(cls: ClassType) -> ClassType: ...
485
+ def flock_component(cls: ClassType) -> ClassType: ... # Basic registration
411
486
  @overload
412
487
  def flock_component(
413
- *, name: str | None = None
414
- ) -> Callable[[ClassType], ClassType]: ...
488
+ *, name: str | None = None, config_class: type[ConfigType] | None = None
489
+ ) -> Callable[[ClassType], ClassType]: ... # With options
415
490
 
416
491
 
417
492
  def flock_component(
418
- cls: ClassType | None = None, *, name: str | None = None
493
+ cls: ClassType | None = None,
494
+ *,
495
+ name: str | None = None,
496
+ config_class: type[ConfigType] | None = None,
419
497
  ) -> Any:
420
- """Decorator to register a Flock Component class (Module, Evaluator, Router).
421
-
422
- Usage:
423
- @flock_component
424
- class MyModule(FlockModule): ...
425
-
426
- @flock_component(name="CustomRouterAlias")
427
- class MyRouter(FlockRouter): ...
428
- """
498
+ """Decorator to register a Flock Component class and optionally link its config class."""
429
499
  registry = get_registry()
430
500
 
431
501
  def decorator(inner_cls: ClassType) -> ClassType:
432
502
  if not inspect.isclass(inner_cls):
433
503
  raise TypeError("@flock_component can only decorate classes.")
504
+
434
505
  component_name = name or inner_cls.__name__
435
- registry.register_component(inner_cls, name=component_name)
506
+ registry.register_component(
507
+ inner_cls, name=component_name
508
+ ) # Register component by name
509
+
510
+ # If config_class is provided, register the mapping
511
+ if config_class:
512
+ FlockRegistry.register_config_component_pair(
513
+ config_class, inner_cls
514
+ )
515
+
436
516
  return inner_cls
437
517
 
438
518
  if cls is None:
439
- # Called as @flock_component(name="...")
519
+ # Called as @flock_component(name="...", config_class=...)
440
520
  return decorator
441
521
  else:
442
522
  # Called as @flock_component
@@ -14,13 +14,26 @@ class HandOffRequest(BaseModel):
14
14
  next_agent: str = Field(default="", description="Next agent to invoke")
15
15
  # match = use the output fields of the current agent that also exists as input field of the next agent
16
16
  # add = add the output of the current agent to the input of the next agent
17
- hand_off_mode: Literal["match", "add"] = Field(default="match")
17
+ output_to_input_merge_strategy: Literal["match", "add"] = Field(
18
+ default="match"
19
+ )
20
+ add_input_fields: list[str] | None = Field(
21
+ default=None,
22
+ description="List of input fields to add to the next agent",
23
+ )
24
+ add_output_fields: list[str] | None = Field(
25
+ default=None,
26
+ description="List of output fields to add to the next agent",
27
+ )
28
+ add_description: str | None = Field(
29
+ default=None, description="Add this description to the next agent"
30
+ )
18
31
  override_next_agent: Any | None = Field(
19
32
  default=None,
20
33
  description="Override the next agent to hand off to",
21
34
  )
22
35
  override_context: FlockContext | None = Field(
23
- default=None, descrio="Override context parameters"
36
+ default=None, description="Override context parameters"
24
37
  )
25
38
 
26
39
 
@@ -34,10 +47,10 @@ class FlockRouterConfig(BaseModel):
34
47
  enabled: bool = Field(
35
48
  default=True, description="Whether the router is enabled"
36
49
  )
37
- agents: list[str] | None = Field(
38
- default=None,
39
- description="List of agents to choose from",
40
- )
50
+ # agents: list[str] | None = Field(
51
+ # default=None,
52
+ # description="List of agents to choose from",
53
+ # )
41
54
 
42
55
 
43
56
  class FlockRouter(BaseModel, ABC):
@@ -8,6 +8,7 @@ import sys
8
8
  import types
9
9
  import typing
10
10
  from collections.abc import Mapping, Sequence
11
+ from enum import Enum
11
12
  from typing import (
12
13
  TYPE_CHECKING,
13
14
  Any,
@@ -57,7 +58,9 @@ def _format_type_to_string(type_hint: type) -> str:
57
58
  inner_type = next(t for t in args if t is not type(None))
58
59
  return _format_type_to_string(inner_type)
59
60
  # return f"Optional[{_format_type_to_string(inner_type)}]"
60
- return f"Union[{', '.join(_format_type_to_string(arg) for arg in args)}]"
61
+ return (
62
+ f"Union[{', '.join(_format_type_to_string(arg) for arg in args)}]"
63
+ )
61
64
  elif origin is Literal:
62
65
  formatted_args = []
63
66
  for arg in args:
@@ -66,7 +69,9 @@ def _format_type_to_string(type_hint: type) -> str:
66
69
  else:
67
70
  formatted_args.append(str(arg))
68
71
  return f"Literal[{', '.join(formatted_args)}]"
69
- elif hasattr(type_hint, "__forward_arg__"): # Handle ForwardRefs if necessary
72
+ elif hasattr(
73
+ type_hint, "__forward_arg__"
74
+ ): # Handle ForwardRefs if necessary
70
75
  return type_hint.__forward_arg__
71
76
  elif hasattr(type_hint, "__name__"):
72
77
  # Handle custom types registered in registry (get preferred name)
@@ -83,7 +88,9 @@ def _format_type_to_string(type_hint: type) -> str:
83
88
  type_repr = str(type_hint).replace("typing.", "") # Basic cleanup
84
89
  type_repr = str(type_hint).replace("| None", "")
85
90
  type_repr = type_repr.strip()
86
- logger.debug(f"Using fallback string representation for type: {type_repr}")
91
+ logger.debug(
92
+ f"Using fallback string representation for type: {type_repr}"
93
+ )
87
94
  return type_repr
88
95
 
89
96
 
@@ -189,18 +196,51 @@ def serialize_item(item: Any) -> Any:
189
196
 
190
197
  FlockRegistry = get_registry()
191
198
 
192
- if isinstance(item, BaseModel):
193
- dumped = item.model_dump(mode="json", exclude_none=True)
194
- return serialize_item(dumped)
195
- elif callable(item) and not isinstance(item, type):
196
- path_str = FlockRegistry.get_callable_path_string(item) # Use registry helper
199
+ if callable(item) and not isinstance(item, type):
200
+ path_str = FlockRegistry.get_callable_path_string(
201
+ item
202
+ ) # Use registry helper
197
203
  if path_str:
198
- return {"__callable_ref__": path_str}
204
+ # Store the simple name (last part of the path) for cleaner YAML/JSON
205
+ simple_name = path_str.split(".")[-1]
206
+ logger.debug(
207
+ f"Serializing callable '{getattr(item, '__name__', 'unknown')}' as reference: '{simple_name}' (from path '{path_str}')"
208
+ )
209
+ return {
210
+ "__callable_ref__": simple_name
211
+ } # Use simple name convention
199
212
  else:
213
+ # Handle unregistered callables (e.g., lambdas defined inline)
214
+ # Option 1: Raise error (stricter)
215
+ # raise ValueError(f"Cannot serialize unregistered callable: {getattr(item, '__name__', item)}")
216
+ # Option 2: Store string representation with warning (more lenient)
200
217
  logger.warning(
201
- f"Could not get path string for callable {item}, storing as string."
218
+ f"Cannot serialize unregistered callable {getattr(item, '__name__', item)}, storing as string."
202
219
  )
203
220
  return str(item)
221
+ elif isinstance(item, BaseModel):
222
+ logger.debug(
223
+ f"Serializing Pydantic model instance: {item.__class__.__name__}"
224
+ )
225
+ serialized_dict = {}
226
+ # Iterate through defined fields in the model
227
+ fields_to_iterate = {}
228
+ if hasattr(item, "model_fields"): # Pydantic v2
229
+ fields_to_iterate = item.model_fields
230
+
231
+ for field_name in fields_to_iterate:
232
+ # Get the value *from the instance*
233
+ try:
234
+ value = getattr(item, field_name)
235
+ if value is not None: # Exclude None values
236
+ # Recursively serialize the field's value
237
+ serialized_dict[field_name] = serialize_item(value)
238
+ except AttributeError:
239
+ # Should not happen if iterating model_fields/__fields__ but handle defensively
240
+ logger.warning(
241
+ f"Attribute '{field_name}' not found on instance of {item.__class__.__name__} during serialization."
242
+ )
243
+ return serialized_dict
204
244
  elif isinstance(item, Mapping):
205
245
  return {key: serialize_item(value) for key, value in item.items()}
206
246
  elif isinstance(item, Sequence) and not isinstance(item, str):
@@ -218,8 +258,12 @@ def serialize_item(item: Any) -> Any:
218
258
  ) # Check regular types/classes by path
219
259
  if type_name:
220
260
  return {"__type_ref__": type_name}
221
- logger.warning(f"Could not serialize type object {item}, storing as string.")
261
+ logger.warning(
262
+ f"Could not serialize type object {item}, storing as string."
263
+ )
222
264
  return str(item)
265
+ elif isinstance(item, Enum):
266
+ return item.value
223
267
  else:
224
268
  # Return basic types as is
225
269
  return item
@@ -240,12 +284,24 @@ def deserialize_item(item: Any) -> Any:
240
284
 
241
285
  if isinstance(item, Mapping):
242
286
  if "__callable_ref__" in item and len(item) == 1:
243
- path_str = item["__callable_ref__"]
287
+ ref_name = item["__callable_ref__"]
244
288
  try:
245
- return FlockRegistry.get_callable(path_str)
289
+ # The registry's get_callable needs to handle lookup by simple name OR full path
290
+ # Or we assume get_callable handles finding the right function from the simple name
291
+ resolved_callable = FlockRegistry.get_callable(ref_name)
292
+ logger.debug(
293
+ f"Deserialized callable reference '{ref_name}' to {resolved_callable}"
294
+ )
295
+ return resolved_callable
246
296
  except KeyError:
247
297
  logger.error(
248
- f"Callable reference '{path_str}' not found during deserialization."
298
+ f"Callable reference '{ref_name}' not found during deserialization."
299
+ )
300
+ return None # Or raise?
301
+ except Exception as e:
302
+ logger.error(
303
+ f"Error resolving callable reference '{ref_name}': {e}",
304
+ exc_info=True,
249
305
  )
250
306
  return None
251
307
  elif "__component_ref__" in item and len(item) == 1:
@@ -273,7 +329,9 @@ def deserialize_item(item: Any) -> Any:
273
329
  mod = importlib.import_module(module_name)
274
330
  type_obj = getattr(mod, class_name)
275
331
  if isinstance(type_obj, type):
276
- FlockRegistry.register_type(type_obj, type_name) # Cache it
332
+ FlockRegistry.register_type(
333
+ type_obj, type_name
334
+ ) # Cache it
277
335
  return type_obj
278
336
  else:
279
337
  raise TypeError()
@@ -294,7 +352,9 @@ def deserialize_item(item: Any) -> Any:
294
352
 
295
353
 
296
354
  # --- Component Deserialization Helper ---
297
- def deserialize_component(data: dict | None, expected_base_type: type) -> Any | None:
355
+ def deserialize_component(
356
+ data: dict | None, expected_base_type: type
357
+ ) -> Any | None:
298
358
  """Deserializes a component (Module, Evaluator, Router) from its dict representation.
299
359
  Uses the 'type' field to find the correct class via FlockRegistry.
300
360
  """
@@ -306,10 +366,14 @@ def deserialize_component(data: dict | None, expected_base_type: type) -> Any |
306
366
  if data is None:
307
367
  return None
308
368
  if not isinstance(data, dict):
309
- logger.error(f"Expected dict for component deserialization, got {type(data)}")
369
+ logger.error(
370
+ f"Expected dict for component deserialization, got {type(data)}"
371
+ )
310
372
  return None
311
373
 
312
- type_name = data.get("type") # Assuming 'type' key holds the class name string
374
+ type_name = data.get(
375
+ "type"
376
+ ) # Assuming 'type' key holds the class name string
313
377
  if not type_name:
314
378
  logger.error(f"Component data missing 'type' field: {data}")
315
379
  return None
@@ -317,7 +381,9 @@ def deserialize_component(data: dict | None, expected_base_type: type) -> Any |
317
381
  try:
318
382
  ComponentClass = FlockRegistry.get_component(type_name) # Use registry
319
383
  # Optional: Keep the base type check
320
- if COMPONENT_BASE_TYPES and not issubclass(ComponentClass, expected_base_type):
384
+ if COMPONENT_BASE_TYPES and not issubclass(
385
+ ComponentClass, expected_base_type
386
+ ):
321
387
  raise TypeError(
322
388
  f"Deserialized class {type_name} is not a subclass of {expected_base_type.__name__}"
323
389
  )
@@ -152,6 +152,11 @@ def resolve_inputs(
152
152
  continue
153
153
 
154
154
  # Fallback to the initial input
155
+ var_value = context.get_variable(key)
156
+ if var_value is not None:
157
+ inputs[key] = var_value
158
+ continue
159
+
155
160
  inputs[key] = context.get_variable("flock." + key)
156
161
 
157
162
  # Case 2: A compound key (e.g., "agent_name.property" or "context.property")
@@ -2,11 +2,12 @@ from collections.abc import Generator
2
2
  from typing import Any
3
3
 
4
4
  import dspy
5
- from pydantic import Field
5
+ from pydantic import Field, PrivateAttr
6
6
  from rich.console import Console
7
7
 
8
8
  from flock.core.flock_agent import FlockAgent
9
9
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
10
+ from flock.core.flock_registry import flock_component
10
11
  from flock.core.logging.logging import get_logger
11
12
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
12
13
  from flock.core.mixin.prompt_parser import PromptParserMixin
@@ -35,7 +36,10 @@ class DeclarativeEvaluatorConfig(FlockEvaluatorConfig):
35
36
  kwargs: dict[str, Any] = Field(default_factory=dict)
36
37
 
37
38
 
38
- class DeclarativeEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
39
+ @flock_component(config_class=DeclarativeEvaluatorConfig)
40
+ class DeclarativeEvaluator(
41
+ FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin
42
+ ):
39
43
  """Evaluator that uses DSPy for generation."""
40
44
 
41
45
  config: DeclarativeEvaluatorConfig = Field(
@@ -43,8 +47,8 @@ class DeclarativeEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMix
43
47
  description="Evaluator configuration",
44
48
  )
45
49
 
46
- cost: float = 0.0
47
- lm_history: list = Field(default_factory=list)
50
+ _cost: float = PrivateAttr(default=0.0)
51
+ _lm_history: list = PrivateAttr(default_factory=list)
48
52
 
49
53
  async def evaluate(
50
54
  self, agent: FlockAgent, inputs: dict[str, Any], tools: list[Any]
@@ -89,10 +93,14 @@ class DeclarativeEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMix
89
93
 
90
94
  # --- Conditional Evaluation (Stream vs No Stream) ---
91
95
  if self.config.stream:
92
- logger.info(f"Evaluating agent '{agent.name}' with async streaming.")
96
+ logger.info(
97
+ f"Evaluating agent '{agent.name}' with async streaming."
98
+ )
93
99
  if not callable(agent_task):
94
100
  logger.error("agent_task is not callable, cannot stream.")
95
- raise TypeError("DSPy task could not be created or is not callable.")
101
+ raise TypeError(
102
+ "DSPy task could not be created or is not callable."
103
+ )
96
104
 
97
105
  streaming_task = dspy.streamify(agent_task)
98
106
  stream_generator: Generator = streaming_task(**inputs)
@@ -115,8 +123,8 @@ class DeclarativeEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMix
115
123
  result_dict, cost, lm_history = self._process_result(
116
124
  chunk, inputs
117
125
  )
118
- self.cost = cost
119
- self.lm_history = lm_history
126
+ self._cost = cost
127
+ self._lm_history = lm_history
120
128
 
121
129
  console.print("\n")
122
130
  return self.filter_thought_process(
@@ -131,8 +139,8 @@ class DeclarativeEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMix
131
139
  result_dict, cost, lm_history = self._process_result(
132
140
  result_obj, inputs
133
141
  )
134
- self.cost = cost
135
- self.lm_history = lm_history
142
+ self._cost = cost
143
+ self._lm_history = lm_history
136
144
  return self.filter_thought_process(
137
145
  result_dict, self.config.include_thought_process
138
146
  )
@@ -4,6 +4,7 @@ from pydantic import Field
4
4
 
5
5
  from flock.core.flock_agent import FlockAgent
6
6
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
+ from flock.core.flock_registry import flock_component
7
8
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
8
9
  from flock.core.mixin.prompt_parser import PromptParserMixin
9
10
  from flock.modules.memory.memory_module import MemoryModule, MemoryModuleConfig
@@ -45,6 +46,7 @@ class MemoryEvaluatorConfig(FlockEvaluatorConfig):
45
46
  )
46
47
 
47
48
 
49
+ @flock_component(config_class=MemoryEvaluatorConfig)
48
50
  class MemoryEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
49
51
  """Evaluator that uses DSPy for generation."""
50
52
 
@@ -4,6 +4,7 @@ from pydantic import Field
4
4
 
5
5
  from flock.core.flock_agent import FlockAgent
6
6
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
+ from flock.core.flock_registry import flock_component
7
8
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
8
9
 
9
10
 
@@ -13,6 +14,7 @@ class TestCaseEvaluatorConfig(FlockEvaluatorConfig):
13
14
  pass
14
15
 
15
16
 
17
+ @flock_component(config_class=TestCaseEvaluatorConfig)
16
18
  class TestCaseEvaluator(FlockEvaluator, DSPyIntegrationMixin):
17
19
  """Evaluator for test cases."""
18
20
 
@@ -4,6 +4,7 @@ from pydantic import Field
4
4
 
5
5
  from flock.core.flock_agent import FlockAgent
6
6
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
+ from flock.core.flock_registry import flock_component
7
8
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
8
9
  from flock.core.mixin.prompt_parser import PromptParserMixin
9
10
  from flock.modules.zep.zep_module import ZepModule, ZepModuleConfig
@@ -17,6 +18,7 @@ class ZepEvaluatorConfig(FlockEvaluatorConfig):
17
18
  )
18
19
 
19
20
 
21
+ @flock_component(config_class=ZepEvaluatorConfig)
20
22
  class ZepEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
21
23
  """Evaluator that uses DSPy for generation."""
22
24