1414import abc
1515import inspect
1616import logging
17- from functools import lru_cache , partial , wraps
17+ from functools import lru_cache , wraps
1818from inspect import Signature , isclass , signature
1919from typing import Any , Callable , ClassVar , Dict , Generic , List , Optional , Tuple , Type , TypeVar , Union , get_args , get_origin
2020
4646 "EvaluatorBase" ,
4747 "Evaluator" ,
4848 "WrapperModel" ,
49- "dynamic_context" ,
5049)
5150
5251log = logging .getLogger (__name__ )
@@ -272,22 +271,22 @@ def wrapper(model, context=Signature.empty, *, _options: Optional[FlowOptions] =
272271 if not isinstance (model , CallableModel ):
273272 raise TypeError (f"Can only decorate methods on CallableModels (not { type (model )} ) with the flow decorator." )
274273
275- # Check if this is a dynamic_context decorated method
276- has_dynamic_context = hasattr (fn , "__dynamic_context__ " )
277- if has_dynamic_context :
278- method_context_type = fn .__dynamic_context__
274+ # Check if this is an auto_context decorated method
275+ has_auto_context = hasattr (fn , "__auto_context__ " )
276+ if has_auto_context :
277+ method_context_type = fn .__auto_context__
279278 else :
280279 method_context_type = model .context_type
281280
282- # Validate context type (skip for dynamic contexts which are always valid ContextBase subclasses)
283- if not has_dynamic_context :
281+ # Validate context type (skip for auto contexts which are always valid ContextBase subclasses)
282+ if not has_auto_context :
284283 if (not isclass (model .context_type ) or not issubclass (model .context_type , ContextBase )) and not (
285284 get_origin (model .context_type ) is Union and type (None ) in get_args (model .context_type )
286285 ):
287286 raise TypeError (f"Context type { model .context_type } must be a subclass of ContextBase" )
288287
289- # Validate result type - use __result_type__ for dynamic contexts if available
290- if has_dynamic_context and hasattr (fn , "__result_type__" ):
288+ # Validate result type - use __result_type__ for auto contexts if available
289+ if has_auto_context and hasattr (fn , "__result_type__" ):
291290 method_result_type = fn .__result_type__
292291 else :
293292 method_result_type = model .result_type
@@ -334,9 +333,9 @@ def wrapper(model, context=Signature.empty, *, _options: Optional[FlowOptions] =
334333 wrap .get_options = self .get_options
335334 wrap .get_evaluation_context = get_evaluation_context
336335
337- # Preserve dynamic context attributes for introspection
338- if hasattr (fn , "__dynamic_context__ " ):
339- wrap .__dynamic_context__ = fn .__dynamic_context__
336+ # Preserve auto context attributes for introspection
337+ if hasattr (fn , "__auto_context__ " ):
338+ wrap .__auto_context__ = fn .__auto_context__
340339 if hasattr (fn , "__result_type__" ):
341340 wrap .__result_type__ = fn .__result_type__
342341
@@ -418,7 +417,58 @@ def __exit__(self, exc_type, exc_value, exc_tb):
418417class Flow (PydanticBaseModel ):
419418 @staticmethod
420419 def call (* args , ** kwargs ):
421- """Decorator for methods on callable models"""
420+ """Decorator for methods on callable models.
421+
422+ Args:
423+ auto_context: Controls automatic context class generation from the function
424+ signature. Accepts three types of values:
425+ - False (default): No auto-generation, use traditional context parameter
426+ - True: Auto-generate context class with no parent
427+ - ContextBase subclass: Auto-generate context class inheriting from this parent
428+ **kwargs: Additional FlowOptions parameters (log_level, verbose, validate_result,
429+ cacheable, evaluator, volatile).
430+
431+ Basic Example:
432+ class MyModel(CallableModel):
433+ @Flow.call
434+ def __call__(self, context: MyContext) -> MyResult:
435+ return MyResult(value=context.x)
436+
437+ Auto Context Example:
438+ class MyModel(CallableModel):
439+ @Flow.call(auto_context=True)
440+ def __call__(self, *, x: int, y: str = "default") -> MyResult:
441+ return MyResult(value=f"{x}-{y}")
442+
443+ model = MyModel()
444+ model(x=42) # Call with kwargs directly
445+
446+ With Parent Context:
447+ class MyModel(CallableModel):
448+ @Flow.call(auto_context=DateContext)
449+ def __call__(self, *, date: date, extra: int = 0) -> MyResult:
450+ return MyResult(value=date.day + extra)
451+
452+ # The generated context inherits from DateContext, so it's compatible
453+ # with infrastructure expecting DateContext instances.
454+ """
455+ # Extract auto_context option (not part of FlowOptions)
456+ # Can be: False, True, or a ContextBase subclass
457+ auto_context = kwargs .pop ("auto_context" , False )
458+
459+ # Determine if auto_context is enabled and extract parent class if provided
460+ if auto_context is False :
461+ auto_context_enabled = False
462+ context_parent = None
463+ elif auto_context is True :
464+ auto_context_enabled = True
465+ context_parent = None
466+ elif isclass (auto_context ) and issubclass (auto_context , ContextBase ):
467+ auto_context_enabled = True
468+ context_parent = auto_context
469+ else :
470+ raise TypeError (f"auto_context must be False, True, or a ContextBase subclass, got { auto_context !r} " )
471+
422472 if len (args ) == 1 and callable (args [0 ]):
423473 # No arguments to decorator, this is the decorator
424474 fn = args [0 ]
@@ -427,6 +477,14 @@ def call(*args, **kwargs):
427477 else :
428478 # Arguments to decorator, this is just returning the decorator
429479 # Note that the code below is executed only once
480+ if auto_context_enabled :
481+ # Return a decorator that first applies auto_context, then FlowOptions
482+ def auto_context_decorator (fn ):
483+ wrapped = _apply_auto_context (fn , parent = context_parent )
484+ # FlowOptions.__call__ already applies wraps, so we just return its result
485+ return FlowOptions (** kwargs )(wrapped )
486+
487+ return auto_context_decorator
430488 return FlowOptions (** kwargs )
431489
432490 @staticmethod
@@ -444,81 +502,6 @@ def deps(*args, **kwargs):
444502 # Note that the code below is executed only once
445503 return FlowOptionsDeps (** kwargs )
446504
447- @staticmethod
448- def dynamic_call (* args , ** kwargs ):
449- """Decorator that combines @Flow.call with dynamic context creation.
450-
451- Instead of defining a separate context class, this decorator creates one
452- automatically from the function signature. The method can then be called
453- with keyword arguments directly.
454-
455- Basic Example:
456- class MyModel(CallableModel):
457- @Flow.dynamic_call
458- def __call__(self, *, date: date, region: str = "US") -> MyResult:
459- return MyResult(value=f"{date}-{region}")
460-
461- model = MyModel()
462- model(date=date.today()) # Uses default region="US"
463- model(date=date.today(), region="EU") # Override default
464-
465- With Parent Context:
466- class MyModel(CallableModel):
467- @Flow.dynamic_call(parent=DateContext)
468- def __call__(self, *, date: date, extra: int = 0) -> MyResult:
469- return MyResult(value=date.day + extra)
470-
471- # Parent fields (date) must be included in the function signature.
472- # This is useful for integrating with existing infrastructure that
473- # expects specific context types.
474-
475- Args:
476- *args: The decorated function when used without parentheses
477- **kwargs: Combined options for FlowOptions and dynamic_context:
478-
479- Dynamic context options:
480- parent: Parent context class to inherit from. All parent fields
481- must appear in the function signature.
482-
483- FlowOptions (passed through to @Flow.call):
484- log_level: Logging level for evaluation (default: DEBUG)
485- verbose: Use verbose logging (default: True)
486- validate_result: Validate return against result_type (default: True)
487- cacheable: Allow result caching (default: False)
488- evaluator: Custom evaluator instance
489-
490- Returns:
491- A decorated method that accepts keyword arguments matching the signature.
492-
493- Notes:
494- - All parameters (except 'self') must have type annotations
495- - Use keyword-only parameters (after *) for cleaner signatures
496- - The generated context class is accessible via method.__dynamic_context__
497- - The return type is accessible via method.__result_type__
498-
499- See Also:
500- dynamic_context: The underlying decorator for context creation
501- Flow.call: The underlying decorator for flow evaluation
502- """
503- # Import here to avoid circular import at module level
504- from ccflow .callable import dynamic_context
505-
506- # Extract dynamic_context-specific options
507- parent = kwargs .pop ("parent" , None )
508-
509- if len (args ) == 1 and callable (args [0 ]):
510- # No arguments to decorator (@Flow.dynamic_call)
511- fn = args [0 ]
512- wrapped = dynamic_context (fn , parent = parent )
513- return Flow .call (wrapped )
514- else :
515- # Arguments to decorator (@Flow.dynamic_call(...))
516- def decorator (fn ):
517- wrapped = dynamic_context (fn , parent = parent )
518- return Flow .call (** kwargs )(wrapped )
519-
520- return decorator
521-
522505
523506# *****************************************************************************
524507# Define "Evaluators" and associated types
@@ -859,30 +842,29 @@ def _validate_callable_model_generic_type(cls, m, handler, info):
859842
860843
861844# *****************************************************************************
862- # Dynamic Context Decorator
845+ # Auto Context (internal helper for Flow.call(auto_context=True))
863846# *****************************************************************************
864847
865848
866- def dynamic_context (func : Callable = None , * , parent : Type [ContextBase ] = None ) -> Callable :
867- """Decorator that creates a dynamic context class from function parameters.
849+ def _apply_auto_context (func : Callable , * , parent : Type [ContextBase ] = None ) -> Callable :
850+ """Internal function that creates an auto context class from function parameters.
868851
869- This decorator extracts the parameters from a function signature and creates
870- a dynamic ContextBase subclass whose fields correspond to those parameters.
852+ This function extracts the parameters from a function signature and creates
853+ a ContextBase subclass whose fields correspond to those parameters.
871854 The decorated function is then wrapped to accept the context object and
872855 unpack it into keyword arguments.
873856
857+ Used internally by Flow.call(auto_context=...).
858+
874859 Example:
875860 class MyCallable(CallableModel):
876- @Flow.dynamic_call # or @Flow. call @dynamic_context
861+ @Flow.call(auto_context=True)
877862 def __call__(self, *, x: int, y: str = "default") -> GenericResult:
878863 return GenericResult(value=f"{x}-{y}")
879864
880865 model = MyCallable()
881866 model(x=42, y="hello") # Works with kwargs
882867 """
883- if func is None :
884- return partial (dynamic_context , parent = parent )
885-
886868 sig = signature (func )
887869 base_class = parent or ContextBase
888870
@@ -902,8 +884,8 @@ def __call__(self, *, x: int, y: str = "default") -> GenericResult:
902884 default = ... if param .default is inspect .Parameter .empty else param .default
903885 fields [name ] = (param .annotation , default )
904886
905- # Create dynamic context class
906- dyn_context = create_ccflow_model (f"{ func .__qualname__ } _DynamicContext " , __base__ = base_class , ** fields )
887+ # Create auto context class
888+ auto_context_class = create_ccflow_model (f"{ func .__qualname__ } _AutoContext " , __base__ = base_class , ** fields )
907889
908890 @wraps (func )
909891 def wrapper (self , context ):
@@ -914,10 +896,10 @@ def wrapper(self, context):
914896 wrapper .__signature__ = inspect .Signature (
915897 parameters = [
916898 inspect .Parameter ("self" , inspect .Parameter .POSITIONAL_OR_KEYWORD ),
917- inspect .Parameter ("context" , inspect .Parameter .POSITIONAL_OR_KEYWORD , annotation = dyn_context ),
899+ inspect .Parameter ("context" , inspect .Parameter .POSITIONAL_OR_KEYWORD , annotation = auto_context_class ),
918900 ],
919901 return_annotation = sig .return_annotation ,
920902 )
921- wrapper .__dynamic_context__ = dyn_context
903+ wrapper .__auto_context__ = auto_context_class
922904 wrapper .__result_type__ = sig .return_annotation
923905 return wrapper
0 commit comments