flock-core 0.3.22__py3-none-any.whl → 0.3.30__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/__init__.py +2 -2
- flock/core/api/__init__.py +11 -0
- flock/core/api/endpoints.py +222 -0
- flock/core/api/main.py +237 -0
- flock/core/api/models.py +34 -0
- flock/core/api/run_store.py +72 -0
- flock/core/api/ui/__init__.py +0 -0
- flock/core/api/ui/routes.py +271 -0
- flock/core/api/ui/utils.py +119 -0
- flock/core/flock.py +475 -397
- flock/core/flock_agent.py +384 -121
- flock/core/flock_registry.py +614 -0
- flock/core/logging/logging.py +97 -23
- flock/core/mixin/dspy_integration.py +360 -158
- flock/core/serialization/__init__.py +7 -1
- flock/core/serialization/callable_registry.py +52 -0
- flock/core/serialization/serializable.py +259 -37
- flock/core/serialization/serialization_utils.py +184 -0
- flock/workflow/activities.py +2 -2
- {flock_core-0.3.22.dist-info → flock_core-0.3.30.dist-info}/METADATA +7 -3
- {flock_core-0.3.22.dist-info → flock_core-0.3.30.dist-info}/RECORD +24 -15
- flock/core/flock_api.py +0 -214
- flock/core/registry/agent_registry.py +0 -120
- {flock_core-0.3.22.dist-info → flock_core-0.3.30.dist-info}/WHEEL +0 -0
- {flock_core-0.3.22.dist-info → flock_core-0.3.30.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.22.dist-info → flock_core-0.3.30.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,173 @@
|
|
|
1
|
+
# src/flock/core/mixin/dspy_integration.py
|
|
1
2
|
"""Mixin class for integrating with the dspy library."""
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
4
|
+
import re # Import re for parsing
|
|
5
|
+
import typing
|
|
5
6
|
from typing import Any, Literal
|
|
6
7
|
|
|
8
|
+
from flock.core.flock_registry import get_registry # Use FlockRegistry
|
|
7
9
|
from flock.core.logging.logging import get_logger
|
|
8
|
-
from flock.core.util.input_resolver import get_callable_members, split_top_level
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
# Import split_top_level (assuming it's moved or copied appropriately)
|
|
12
|
+
# Option 1: If moved to a shared util
|
|
13
|
+
# from flock.core.util.parsing_utils import split_top_level
|
|
14
|
+
# Option 2: If kept within this file (as in previous example)
|
|
15
|
+
# Define split_top_level here or ensure it's imported
|
|
16
|
+
|
|
17
|
+
logger = get_logger("mixin.dspy")
|
|
18
|
+
FlockRegistry = get_registry() # Get singleton instance
|
|
19
|
+
|
|
20
|
+
# Type definition for agent type override
|
|
21
|
+
AgentType = Literal["ReAct", "Completion", "ChainOfThought"] | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Helper function needed by _resolve_type_string (copied from input_resolver.py/previous response)
|
|
25
|
+
def split_top_level(s: str) -> list[str]:
|
|
26
|
+
"""Split a string on commas that are not enclosed within brackets, parentheses, or quotes."""
|
|
27
|
+
parts = []
|
|
28
|
+
current = []
|
|
29
|
+
level = 0
|
|
30
|
+
in_quote = False
|
|
31
|
+
quote_char = ""
|
|
32
|
+
i = 0
|
|
33
|
+
while i < len(s):
|
|
34
|
+
char = s[i]
|
|
35
|
+
# Handle escapes within quotes
|
|
36
|
+
if in_quote and char == "\\" and i + 1 < len(s):
|
|
37
|
+
current.append(char)
|
|
38
|
+
current.append(s[i + 1])
|
|
39
|
+
i += 1 # Skip next char
|
|
40
|
+
elif in_quote:
|
|
41
|
+
current.append(char)
|
|
42
|
+
if char == quote_char:
|
|
43
|
+
in_quote = False
|
|
44
|
+
elif char in ('"', "'"):
|
|
45
|
+
in_quote = True
|
|
46
|
+
quote_char = char
|
|
47
|
+
current.append(char)
|
|
48
|
+
elif char in "([{":
|
|
49
|
+
level += 1
|
|
50
|
+
current.append(char)
|
|
51
|
+
elif char in ")]}":
|
|
52
|
+
level -= 1
|
|
53
|
+
current.append(char)
|
|
54
|
+
elif char == "," and level == 0:
|
|
55
|
+
parts.append("".join(current).strip())
|
|
56
|
+
current = []
|
|
57
|
+
else:
|
|
58
|
+
current.append(char)
|
|
59
|
+
i += 1
|
|
60
|
+
if current:
|
|
61
|
+
parts.append("".join(current).strip())
|
|
62
|
+
# Filter out empty strings that might result from trailing commas etc.
|
|
63
|
+
return [part for part in parts if part]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Helper function to resolve type strings (can be static or module-level)
|
|
67
|
+
def _resolve_type_string(type_str: str) -> type:
|
|
68
|
+
"""Resolves a type string into a Python type object.
|
|
69
|
+
Handles built-ins, registered types, and common typing generics like
|
|
70
|
+
List, Dict, Optional, Union, Literal.
|
|
71
|
+
"""
|
|
72
|
+
type_str = type_str.strip()
|
|
73
|
+
logger.debug(f"Attempting to resolve type string: '{type_str}'")
|
|
74
|
+
|
|
75
|
+
# 1. Check built-ins and registered types directly
|
|
76
|
+
try:
|
|
77
|
+
# This covers str, int, bool, Any, and types registered by name
|
|
78
|
+
resolved_type = FlockRegistry.get_type(type_str)
|
|
79
|
+
logger.debug(f"Resolved '{type_str}' via registry to: {resolved_type}")
|
|
80
|
+
return resolved_type
|
|
81
|
+
except KeyError:
|
|
82
|
+
logger.debug(
|
|
83
|
+
f"'{type_str}' not found directly in registry, attempting generic parsing."
|
|
84
|
+
)
|
|
85
|
+
pass # Not found, continue parsing generics
|
|
86
|
+
|
|
87
|
+
# 2. Handle typing generics (List, Dict, Optional, Union, Literal)
|
|
88
|
+
# Use regex to match pattern like Generic[InnerType1, InnerType2, ...]
|
|
89
|
+
generic_match = re.fullmatch(r"(\w+)\s*\[(.*)\]", type_str)
|
|
90
|
+
if generic_match:
|
|
91
|
+
base_name = generic_match.group(1).strip()
|
|
92
|
+
args_str = generic_match.group(2).strip()
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"Detected generic pattern: Base='{base_name}', Args='{args_str}'"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Get the base generic type (e.g., list, dict, Optional) from registry/builtins
|
|
99
|
+
BaseType = FlockRegistry.get_type(
|
|
100
|
+
base_name
|
|
101
|
+
) # Expects List, Dict etc. to be registered
|
|
102
|
+
logger.debug(
|
|
103
|
+
f"Resolved base generic type '{base_name}' to: {BaseType}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Special handling for Literal
|
|
107
|
+
if BaseType is typing.Literal:
|
|
108
|
+
# Split literal values, remove quotes, strip whitespace
|
|
109
|
+
literal_args_raw = split_top_level(args_str)
|
|
110
|
+
literal_args = tuple(
|
|
111
|
+
s.strip().strip("'\"") for s in literal_args_raw
|
|
112
|
+
)
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"Parsing Literal arguments: {literal_args_raw} -> {literal_args}"
|
|
115
|
+
)
|
|
116
|
+
resolved_type = typing.Literal[literal_args] # type: ignore
|
|
117
|
+
logger.debug(f"Constructed Literal type: {resolved_type}")
|
|
118
|
+
return resolved_type
|
|
119
|
+
|
|
120
|
+
# Recursively resolve arguments for other generics
|
|
121
|
+
logger.debug(f"Splitting generic arguments: '{args_str}'")
|
|
122
|
+
arg_strs = split_top_level(args_str)
|
|
123
|
+
logger.debug(f"Split arguments: {arg_strs}")
|
|
124
|
+
if not arg_strs:
|
|
125
|
+
raise ValueError("Generic type has no arguments.")
|
|
126
|
+
|
|
127
|
+
resolved_arg_types = tuple(
|
|
128
|
+
_resolve_type_string(arg) for arg in arg_strs
|
|
129
|
+
)
|
|
130
|
+
logger.debug(f"Resolved generic arguments: {resolved_arg_types}")
|
|
131
|
+
|
|
132
|
+
# Construct the generic type hint
|
|
133
|
+
if BaseType is typing.Optional:
|
|
134
|
+
if len(resolved_arg_types) != 1:
|
|
135
|
+
raise ValueError("Optional requires exactly one argument.")
|
|
136
|
+
resolved_type = typing.Union[resolved_arg_types[0], type(None)] # type: ignore
|
|
137
|
+
logger.debug(
|
|
138
|
+
f"Constructed Optional type as Union: {resolved_type}"
|
|
139
|
+
)
|
|
140
|
+
return resolved_type
|
|
141
|
+
elif BaseType is typing.Union:
|
|
142
|
+
if not resolved_arg_types:
|
|
143
|
+
raise ValueError("Union requires at least one argument.")
|
|
144
|
+
resolved_type = typing.Union[resolved_arg_types] # type: ignore
|
|
145
|
+
logger.debug(f"Constructed Union type: {resolved_type}")
|
|
146
|
+
return resolved_type
|
|
147
|
+
elif hasattr(
|
|
148
|
+
BaseType, "__getitem__"
|
|
149
|
+
): # Check if subscriptable (like list, dict, List, Dict)
|
|
150
|
+
resolved_type = BaseType[resolved_arg_types] # type: ignore
|
|
151
|
+
logger.debug(
|
|
152
|
+
f"Constructed subscripted generic type: {resolved_type}"
|
|
153
|
+
)
|
|
154
|
+
return resolved_type
|
|
155
|
+
else:
|
|
156
|
+
# Base type found but cannot be subscripted
|
|
157
|
+
logger.warning(
|
|
158
|
+
f"Base type '{base_name}' found but is not a standard subscriptable generic. Returning base type."
|
|
159
|
+
)
|
|
160
|
+
return BaseType
|
|
11
161
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
162
|
+
except (KeyError, ValueError, IndexError, TypeError) as e:
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"Failed to parse generic type '{type_str}': {e}. Falling back."
|
|
165
|
+
)
|
|
166
|
+
# Fall through to raise KeyError below if base type itself wasn't found or parsing failed
|
|
167
|
+
|
|
168
|
+
# 3. If not resolved by now, raise error
|
|
169
|
+
logger.error(f"Type string '{type_str}' could not be resolved.")
|
|
170
|
+
raise KeyError(f"Type '{type_str}' could not be resolved.")
|
|
15
171
|
|
|
16
172
|
|
|
17
173
|
class DSPyIntegrationMixin:
|
|
@@ -20,142 +176,159 @@ class DSPyIntegrationMixin:
|
|
|
20
176
|
def create_dspy_signature_class(
|
|
21
177
|
self, agent_name, description_spec, fields_spec
|
|
22
178
|
) -> Any:
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
179
|
+
"""Creates a dynamic DSPy Signature class from string specifications,
|
|
180
|
+
resolving types using the FlockRegistry.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
import dspy
|
|
184
|
+
except ImportError:
|
|
185
|
+
logger.error(
|
|
186
|
+
"DSPy library is not installed. Cannot create DSPy signature. "
|
|
187
|
+
"Install with: pip install dspy-ai"
|
|
188
|
+
)
|
|
189
|
+
raise ImportError("DSPy is required for this functionality.")
|
|
28
190
|
|
|
29
191
|
base_class = dspy.Signature
|
|
30
|
-
|
|
31
|
-
# Start building the class dictionary with a docstring and annotations dict.
|
|
32
192
|
class_dict = {"__doc__": description_spec, "__annotations__": {}}
|
|
33
193
|
|
|
34
|
-
# ---------------------------
|
|
35
|
-
# 2. Split the fields specification into inputs and outputs.
|
|
36
|
-
# ---------------------------
|
|
37
194
|
if "->" in fields_spec:
|
|
38
195
|
inputs_spec, outputs_spec = fields_spec.split("->", 1)
|
|
39
196
|
else:
|
|
40
|
-
inputs_spec, outputs_spec =
|
|
197
|
+
inputs_spec, outputs_spec = (
|
|
198
|
+
fields_spec,
|
|
199
|
+
"",
|
|
200
|
+
) # Assume only inputs if no '->'
|
|
41
201
|
|
|
42
|
-
# ---------------------------
|
|
43
|
-
# 3. Draw the rest of the owl.
|
|
44
|
-
# ---------------------------
|
|
45
202
|
def parse_field(field_str):
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
Parse a field of the form:
|
|
49
|
-
<name> [ : <type> ] [ | <desc> ]
|
|
50
|
-
Returns a tuple: (name, field_type, desc)
|
|
51
|
-
"""
|
|
203
|
+
"""Parses 'name: type_str | description' using _resolve_type_string."""
|
|
52
204
|
field_str = field_str.strip()
|
|
53
205
|
if not field_str:
|
|
54
206
|
return None
|
|
55
207
|
|
|
56
208
|
parts = field_str.split("|", 1)
|
|
57
|
-
main_part = parts[0].strip()
|
|
209
|
+
main_part = parts[0].strip()
|
|
58
210
|
desc = parts[1].strip() if len(parts) > 1 else None
|
|
59
211
|
|
|
60
212
|
if ":" in main_part:
|
|
61
213
|
name, type_str = [s.strip() for s in main_part.split(":", 1)]
|
|
62
214
|
else:
|
|
63
215
|
name = main_part
|
|
64
|
-
type_str = "str" #
|
|
216
|
+
type_str = "str" # Default type
|
|
65
217
|
|
|
66
|
-
# Evaluate the type. Since type can be any valid expression (including custom types),
|
|
67
|
-
# we use eval. (Be cautious if using eval with untrusted input.)
|
|
68
218
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# import dspy
|
|
74
|
-
|
|
75
|
-
# field_type = dspy.PythonInterpreter(
|
|
76
|
-
# sys.modules[__name__].__dict__ | sys.modules["__main__"].__dict__
|
|
77
|
-
# ).execute(type_str)
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
field_type = eval(type_str, sys.modules[__name__].__dict__)
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.warning(
|
|
83
|
-
"Failed to evaluate type_str in __name__" + str(e)
|
|
84
|
-
)
|
|
85
|
-
field_type = eval(
|
|
86
|
-
type_str, sys.modules["__main__"].__dict__
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
except Exception as ex:
|
|
90
|
-
# AREPL fix - var
|
|
91
|
-
logger.warning(
|
|
92
|
-
"Failed to evaluate type_str in __main__" + str(ex)
|
|
219
|
+
field_type = _resolve_type_string(type_str)
|
|
220
|
+
except Exception as e: # Catch resolution errors
|
|
221
|
+
logger.error(
|
|
222
|
+
f"Failed to resolve type '{type_str}' for field '{name}': {e}. Defaulting to str."
|
|
93
223
|
)
|
|
94
|
-
|
|
95
|
-
field_type = eval(
|
|
96
|
-
f"exec_locals.get('{type_str}')",
|
|
97
|
-
sys.modules["__main__"].__dict__,
|
|
98
|
-
)
|
|
99
|
-
except Exception as ex_arepl:
|
|
100
|
-
logger.warning(
|
|
101
|
-
"Failed to evaluate type_str in exec_locals"
|
|
102
|
-
+ str(ex_arepl)
|
|
103
|
-
)
|
|
104
|
-
field_type = str
|
|
224
|
+
field_type = str
|
|
105
225
|
|
|
106
226
|
return name, field_type, desc
|
|
107
227
|
|
|
108
228
|
def process_fields(fields_string, field_kind):
|
|
109
|
-
"""Process
|
|
110
|
-
|
|
111
|
-
field_kind: "input" or "output" determines which Field constructor to use.
|
|
112
|
-
"""
|
|
113
|
-
if not fields_string.strip():
|
|
229
|
+
"""Process fields and add to class_dict."""
|
|
230
|
+
if not fields_string or not fields_string.strip():
|
|
114
231
|
return
|
|
115
232
|
|
|
116
|
-
# Split on commas.
|
|
117
233
|
for field in split_top_level(fields_string):
|
|
118
234
|
if field.strip():
|
|
119
235
|
parsed = parse_field(field)
|
|
120
236
|
if not parsed:
|
|
121
237
|
continue
|
|
122
238
|
name, field_type, desc = parsed
|
|
123
|
-
class_dict["__annotations__"][name] =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
239
|
+
class_dict["__annotations__"][name] = (
|
|
240
|
+
field_type # Use resolved type
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
FieldClass = (
|
|
244
|
+
dspy.InputField
|
|
245
|
+
if field_kind == "input"
|
|
246
|
+
else dspy.OutputField
|
|
247
|
+
)
|
|
248
|
+
# DSPy Fields use 'desc' for description
|
|
249
|
+
class_dict[name] = (
|
|
250
|
+
FieldClass(desc=desc)
|
|
251
|
+
if desc is not None
|
|
252
|
+
else FieldClass()
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
process_fields(inputs_spec, "input")
|
|
257
|
+
process_fields(outputs_spec, "output")
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.error(
|
|
260
|
+
f"Error processing fields for DSPy signature '{agent_name}': {e}",
|
|
261
|
+
exc_info=True,
|
|
262
|
+
)
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Could not process fields for signature: {e}"
|
|
265
|
+
) from e
|
|
266
|
+
|
|
267
|
+
# Create and return the dynamic class
|
|
268
|
+
try:
|
|
269
|
+
DynamicSignature = type(
|
|
270
|
+
"dspy_" + agent_name, (base_class,), class_dict
|
|
271
|
+
)
|
|
272
|
+
logger.info(
|
|
273
|
+
f"Successfully created DSPy Signature: {DynamicSignature.__name__} "
|
|
274
|
+
f"with fields: {DynamicSignature.__annotations__}"
|
|
275
|
+
)
|
|
276
|
+
return DynamicSignature
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(
|
|
279
|
+
f"Failed to create dynamic type 'dspy_{agent_name}': {e}",
|
|
280
|
+
exc_info=True,
|
|
281
|
+
)
|
|
282
|
+
raise TypeError(f"Could not create DSPy signature type: {e}") from e
|
|
145
283
|
|
|
146
284
|
def _configure_language_model(
|
|
147
|
-
self,
|
|
285
|
+
self,
|
|
286
|
+
model: str | None,
|
|
287
|
+
use_cache: bool,
|
|
288
|
+
temperature: float,
|
|
289
|
+
max_tokens: int,
|
|
148
290
|
) -> None:
|
|
149
|
-
import dspy
|
|
150
|
-
|
|
151
291
|
"""Initialize and configure the language model using dspy."""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
292
|
+
if model is None:
|
|
293
|
+
logger.warning(
|
|
294
|
+
"No model specified for DSPy configuration. Using DSPy default."
|
|
295
|
+
)
|
|
296
|
+
# Rely on DSPy's global default or raise error if none configured
|
|
297
|
+
# import dspy
|
|
298
|
+
# if dspy.settings.lm is None:
|
|
299
|
+
# raise ValueError("No model specified for agent and no global DSPy LM configured.")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
import dspy
|
|
304
|
+
except ImportError:
|
|
305
|
+
logger.error(
|
|
306
|
+
"DSPy library is not installed. Cannot configure language model."
|
|
307
|
+
)
|
|
308
|
+
return # Or raise
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
# Ensure 'cache' parameter is handled correctly (might not exist on dspy.LM directly)
|
|
312
|
+
# DSPy handles caching globally or via specific optimizers typically.
|
|
313
|
+
# We'll configure the LM without explicit cache control here.
|
|
314
|
+
lm_instance = dspy.LM(
|
|
315
|
+
model=model,
|
|
316
|
+
temperature=temperature,
|
|
317
|
+
max_tokens=max_tokens,
|
|
318
|
+
cache=use_cache,
|
|
319
|
+
# Add other relevant parameters if needed, e.g., API keys via dspy.settings
|
|
320
|
+
)
|
|
321
|
+
dspy.settings.configure(lm=lm_instance)
|
|
322
|
+
logger.info(
|
|
323
|
+
f"DSPy LM configured with model: {model}, temp: {temperature}, max_tokens: {max_tokens}"
|
|
324
|
+
)
|
|
325
|
+
# Note: DSPy caching is usually configured globally, e.g., dspy.settings.configure(cache=...)
|
|
326
|
+
# or handled by optimizers. Setting `cache=use_cache` on dspy.LM might not be standard.
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(
|
|
329
|
+
f"Failed to configure DSPy language model '{model}': {e}",
|
|
330
|
+
exc_info=True,
|
|
331
|
+
)
|
|
159
332
|
|
|
160
333
|
def _select_task(
|
|
161
334
|
self,
|
|
@@ -163,75 +336,104 @@ class DSPyIntegrationMixin:
|
|
|
163
336
|
agent_type_override: AgentType,
|
|
164
337
|
tools: list[Any] | None = None,
|
|
165
338
|
) -> Any:
|
|
166
|
-
"""Select and instantiate the appropriate
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
An instance of a dspy task (either ReAct or Predict).
|
|
175
|
-
"""
|
|
176
|
-
import dspy
|
|
339
|
+
"""Select and instantiate the appropriate DSPy Program/Module."""
|
|
340
|
+
try:
|
|
341
|
+
import dspy
|
|
342
|
+
except ImportError:
|
|
343
|
+
logger.error(
|
|
344
|
+
"DSPy library is not installed. Cannot select DSPy task."
|
|
345
|
+
)
|
|
346
|
+
raise ImportError("DSPy is required for this functionality.")
|
|
177
347
|
|
|
178
348
|
processed_tools = []
|
|
179
349
|
if tools:
|
|
180
350
|
for tool in tools:
|
|
181
|
-
if
|
|
182
|
-
processed_tools.extend(get_callable_members(tool))
|
|
183
|
-
else:
|
|
351
|
+
if callable(tool): # Basic check
|
|
184
352
|
processed_tools.append(tool)
|
|
353
|
+
# Could add more sophisticated tool wrapping/validation here if needed
|
|
354
|
+
else:
|
|
355
|
+
logger.warning(
|
|
356
|
+
f"Item '{tool}' in tools list is not callable, skipping."
|
|
357
|
+
)
|
|
185
358
|
|
|
186
|
-
|
|
359
|
+
dspy_program = None
|
|
360
|
+
selected_type = agent_type_override
|
|
187
361
|
|
|
188
|
-
if
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
dspy_solver = dspy.ReAct(
|
|
206
|
-
signature,
|
|
207
|
-
tools=processed_tools,
|
|
208
|
-
max_iters=10,
|
|
362
|
+
# Determine type if not overridden
|
|
363
|
+
if not selected_type:
|
|
364
|
+
selected_type = (
|
|
365
|
+
"ReAct" if processed_tools else "Predict"
|
|
366
|
+
) # Default logic
|
|
367
|
+
|
|
368
|
+
logger.debug(
|
|
369
|
+
f"Selecting DSPy program type: {selected_type} (Tools provided: {bool(processed_tools)})"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
if selected_type == "ChainOfThought":
|
|
374
|
+
dspy_program = dspy.ChainOfThought(signature)
|
|
375
|
+
elif selected_type == "ReAct":
|
|
376
|
+
# ReAct requires tools, even if empty list
|
|
377
|
+
dspy_program = dspy.ReAct(
|
|
378
|
+
signature, tools=processed_tools or [], max_iters=10
|
|
209
379
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
380
|
+
elif selected_type == "Predict": # Default or explicitly Completion
|
|
381
|
+
dspy_program = dspy.Predict(signature)
|
|
382
|
+
else: # Fallback or handle unknown type
|
|
383
|
+
logger.warning(
|
|
384
|
+
f"Unknown or unsupported agent_type_override '{selected_type}'. Defaulting to dspy.Predict."
|
|
213
385
|
)
|
|
386
|
+
dspy_program = dspy.Predict(signature)
|
|
214
387
|
|
|
215
|
-
|
|
388
|
+
logger.info(
|
|
389
|
+
f"Instantiated DSPy program: {type(dspy_program).__name__}"
|
|
390
|
+
)
|
|
391
|
+
return dspy_program
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.error(
|
|
394
|
+
f"Failed to instantiate DSPy program of type '{selected_type}': {e}",
|
|
395
|
+
exc_info=True,
|
|
396
|
+
)
|
|
397
|
+
raise RuntimeError(f"Could not create DSPy program: {e}") from e
|
|
216
398
|
|
|
217
399
|
def _process_result(
|
|
218
400
|
self, result: Any, inputs: dict[str, Any]
|
|
219
401
|
) -> dict[str, Any]:
|
|
220
|
-
"""Convert the result to a dictionary
|
|
402
|
+
"""Convert the DSPy result object to a dictionary."""
|
|
403
|
+
if result is None:
|
|
404
|
+
logger.warning("DSPy program returned None result.")
|
|
405
|
+
return {}
|
|
406
|
+
try:
|
|
407
|
+
# DSPy Prediction objects often behave like dicts or have .keys() / items()
|
|
408
|
+
if hasattr(result, "items") and callable(result.items):
|
|
409
|
+
output_dict = dict(result.items())
|
|
410
|
+
elif hasattr(result, "__dict__"): # Fallback for other object types
|
|
411
|
+
output_dict = {
|
|
412
|
+
k: v
|
|
413
|
+
for k, v in result.__dict__.items()
|
|
414
|
+
if not k.startswith("_")
|
|
415
|
+
}
|
|
416
|
+
else:
|
|
417
|
+
# If it's already a dict (less common for DSPy results directly)
|
|
418
|
+
if isinstance(result, dict):
|
|
419
|
+
output_dict = result
|
|
420
|
+
else: # Final fallback
|
|
421
|
+
logger.warning(
|
|
422
|
+
f"Could not reliably convert DSPy result of type {type(result)} to dict. Returning as is."
|
|
423
|
+
)
|
|
424
|
+
output_dict = {"raw_result": result}
|
|
221
425
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
426
|
+
logger.debug(f"Processed DSPy result to dict: {output_dict}")
|
|
427
|
+
# Optionally merge inputs back if desired (can make result dict large)
|
|
428
|
+
final_result = {**inputs, **output_dict}
|
|
429
|
+
return final_result
|
|
225
430
|
|
|
226
|
-
Returns:
|
|
227
|
-
A dictionary containing the processed output.
|
|
228
|
-
"""
|
|
229
|
-
try:
|
|
230
|
-
result = result.toDict()
|
|
231
|
-
for key in inputs:
|
|
232
|
-
result.setdefault(key, inputs.get(key))
|
|
233
431
|
except Exception as conv_error:
|
|
234
|
-
logger.
|
|
235
|
-
f"
|
|
432
|
+
logger.error(
|
|
433
|
+
f"Failed to process DSPy result into dictionary: {conv_error}",
|
|
434
|
+
exc_info=True,
|
|
236
435
|
)
|
|
237
|
-
|
|
436
|
+
return {
|
|
437
|
+
"error": "Failed to process result",
|
|
438
|
+
"raw_result": str(result),
|
|
439
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"""Serialization utilities for Flock objects."""
|
|
2
2
|
|
|
3
|
+
from flock.core.serialization.callable_registry import CallableRegistry
|
|
3
4
|
from flock.core.serialization.json_encoder import FlockJSONEncoder
|
|
4
5
|
from flock.core.serialization.secure_serializer import SecureSerializer
|
|
5
6
|
from flock.core.serialization.serializable import Serializable
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CallableRegistry",
|
|
10
|
+
"FlockJSONEncoder",
|
|
11
|
+
"SecureSerializer",
|
|
12
|
+
"Serializable",
|
|
13
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Registry system for callable objects to support serialization."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CallableRegistry:
|
|
7
|
+
"""Registry for callable objects.
|
|
8
|
+
|
|
9
|
+
This class serves as a central registry for callable objects (functions, methods)
|
|
10
|
+
that can be referenced by name in serialized formats.
|
|
11
|
+
|
|
12
|
+
This is a placeholder implementation that will be fully implemented in task US007-T004.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_registry: dict[str, Callable] = {}
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def register(cls, name: str, callable_obj: Callable) -> None:
|
|
19
|
+
"""Register a callable object with the given name.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
name: Unique name for the callable
|
|
23
|
+
callable_obj: Function or method to register
|
|
24
|
+
"""
|
|
25
|
+
cls._registry[name] = callable_obj
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get(cls, name: str) -> Callable:
|
|
29
|
+
"""Get a callable object by name.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Name of the callable to retrieve
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The registered callable
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
KeyError: If no callable with the given name is registered
|
|
39
|
+
"""
|
|
40
|
+
return cls._registry[name]
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def contains(cls, name: str) -> bool:
|
|
44
|
+
"""Check if a callable with the given name is registered.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Name to check
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if registered, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
return name in cls._registry
|