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.

@@ -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 inspect
4
- import sys
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
- logger = get_logger("flock")
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
- AgentType = (
13
- Literal["ReAct"] | Literal["Completion"] | Literal["ChainOfThought"] | None
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
- """Trying to create a dynamic class using dspy library."""
24
- # ---------------------------
25
- # 1. Parse the class specification.
26
- # ---------------------------
27
- import dspy
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 = fields_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
- """Parser.
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() # contains name and (optionally) type
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" # default type
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
- # TODO: We have to find a way to avoid using eval here.
70
- # This is a security risk, as it allows arbitrary code execution.
71
- # Figure out why the following code doesn't work as well as the eval.
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
- try:
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 a comma-separated list of field definitions.
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] = field_type
124
-
125
- # Use the proper Field constructor.
126
- if field_kind == "input":
127
- if desc is not None:
128
- class_dict[name] = dspy.InputField(desc=desc)
129
- else:
130
- class_dict[name] = dspy.InputField()
131
- elif field_kind == "output":
132
- if desc is not None:
133
- class_dict[name] = dspy.OutputField(desc=desc)
134
- else:
135
- class_dict[name] = dspy.OutputField()
136
- else:
137
- raise ValueError("Unknown field kind: " + field_kind)
138
-
139
- # Process input fields (to be used with my.InputField)
140
- process_fields(inputs_spec, "input")
141
- # Process output fields (to be used with my.OutputField)
142
- process_fields(outputs_spec, "output")
143
-
144
- return type("dspy_" + agent_name, (base_class,), class_dict)
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, model, use_cache, temperature, max_tokens
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
- lm = dspy.LM(
153
- model,
154
- cache=use_cache,
155
- temperature=temperature,
156
- max_tokens=max_tokens,
157
- )
158
- dspy.configure(lm=lm)
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 task based on tool availability.
167
-
168
- Args:
169
- prompt: The detailed prompt string.
170
- input_desc: Dictionary of input key descriptions.
171
- output_desc: Dictionary of output key descriptions.
172
-
173
- Returns:
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 inspect.ismodule(tool) or inspect.isclass(tool):
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
- dspy_solver = None
359
+ dspy_program = None
360
+ selected_type = agent_type_override
187
361
 
188
- if agent_type_override:
189
- if agent_type_override == "ChainOfThought":
190
- dspy_solver = dspy.ChainOfThought(
191
- signature,
192
- )
193
- if agent_type_override == "ReAct":
194
- dspy.ReAct(
195
- signature,
196
- tools=processed_tools,
197
- max_iters=10,
198
- )
199
- if agent_type_override == "Completion":
200
- dspy_solver = dspy.Predict(
201
- signature,
202
- )
203
- else:
204
- if tools:
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
- else:
211
- dspy_solver = dspy.Predict(
212
- signature,
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
- return dspy_solver
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 and add the inputs for an unified result object.
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
- Args:
223
- result: The raw result from the dspy task.
224
- inputs: The original inputs provided to the agent.
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.warning(
235
- f"Warning: Failed to convert result to dict in agent '{self.name}': {conv_error}"
432
+ logger.error(
433
+ f"Failed to process DSPy result into dictionary: {conv_error}",
434
+ exc_info=True,
236
435
  )
237
- return result
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__ = ["FlockJSONEncoder", "SecureSerializer", "Serializable"]
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