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.
- flock/core/flock.py +24 -7
- flock/core/flock_agent.py +219 -75
- flock/core/flock_factory.py +8 -6
- flock/core/flock_registry.py +140 -60
- flock/core/flock_router.py +19 -6
- flock/core/serialization/serialization_utils.py +85 -19
- flock/core/util/input_resolver.py +5 -0
- flock/evaluators/declarative/declarative_evaluator.py +18 -10
- flock/evaluators/memory/memory_evaluator.py +2 -0
- flock/evaluators/test/test_case_evaluator.py +2 -0
- flock/evaluators/zep/zep_evaluator.py +2 -0
- flock/modules/assertion/assertion_module.py +286 -0
- flock/modules/callback/callback_module.py +2 -0
- flock/modules/memory/memory_module.py +2 -0
- flock/modules/output/output_module.py +2 -0
- flock/modules/performance/metrics_module.py +2 -0
- flock/modules/zep/zep_module.py +2 -0
- flock/routers/agent/agent_router.py +7 -5
- flock/routers/conditional/conditional_router.py +482 -0
- flock/routers/default/default_router.py +5 -1
- flock/routers/feedback/feedback_router.py +114 -0
- flock/routers/list_generator/list_generator_router.py +166 -0
- flock/routers/llm/llm_router.py +3 -1
- flock/workflow/activities.py +20 -1
- {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/METADATA +2 -1
- {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/RECORD +29 -28
- flock/evaluators/memory/azure_search_evaluator.py +0 -0
- flock/evaluators/natural_language/natural_language_evaluator.py +0 -66
- flock/modules/azure-search/azure_search_module.py +0 -0
- {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b22.dist-info → flock_core-0.4.0b24.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_registry.py
CHANGED
|
@@ -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,
|
|
176
|
-
"""Retrieves a callable by its
|
|
177
|
-
if
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
211
|
-
|
|
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"
|
|
252
|
+
f"Ambiguous callable name '{name_or_path}'. Found matches: {found_paths}. Use full path string for lookup."
|
|
214
253
|
)
|
|
215
|
-
raise
|
|
216
|
-
f"
|
|
254
|
+
raise KeyError(
|
|
255
|
+
f"Ambiguous callable name '{name_or_path}'. Use full path string."
|
|
217
256
|
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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
|
flock/core/flock_router.py
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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
|
-
|
|
287
|
+
ref_name = item["__callable_ref__"]
|
|
244
288
|
try:
|
|
245
|
-
|
|
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 '{
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
119
|
-
self.
|
|
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.
|
|
135
|
-
self.
|
|
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
|
|