4646import hashlib
4747import inspect
4848import logging
49+ import sys
4950import tempfile
5051from collections .abc import Callable
5152from pathlib import Path
@@ -555,12 +556,25 @@ async def _execute_function(
555556 try :
556557 from ..context import reset_execution_context , set_execution_context
557558
559+ # Convert parameters based on function signature
560+ converted_params = self ._convert_parameters (func , params )
561+
562+ # Handle validation errors
563+ if isinstance (converted_params , dict ) and "__validation_errors" in converted_params :
564+ # Convert structured errors to string for backward compatibility
565+ # TODO: In future, return structured errors directly for better UI
566+ error_parts = []
567+ for param_name , param_errors in converted_params ["__validation_errors" ].items ():
568+ error_details = [f"{ err ['field' ]} : { err ['message' ]} " for err in param_errors ]
569+ error_parts .append (f"Invalid { param_name } : { ', ' .join (error_details )} " )
570+ return "; " .join (error_parts )
571+
558572 # Check if function is async
559573 if asyncio .iscoroutinefunction (func ):
560574 # For async functions, set context and let contextvars propagate it
561575 context_token = set_execution_context (context )
562576 try :
563- result = await func (** params )
577+ result = await func (** converted_params )
564578 finally :
565579 reset_execution_context (context_token )
566580 else :
@@ -570,7 +584,7 @@ def sync_function_wrapper() -> Any:
570584
571585 thread_token = set_execution_context (context )
572586 try :
573- return func (** params )
587+ return func (** converted_params )
574588 finally :
575589 reset_execution_context (thread_token )
576590
@@ -586,3 +600,155 @@ def sync_function_wrapper() -> Any:
586600 except Exception as e :
587601 logger .error (f"Function execution failed: { e } " )
588602 raise
603+
604+ def _convert_parameters (
605+ self , func : Callable [..., Any ], params : dict [str , Any ]
606+ ) -> dict [str , Any ]:
607+ """Convert parameters based on function signature with comprehensive type support.
608+
609+ Features:
610+ - Uses TypeAdapter for robust type conversion and validation
611+ - Supports forward references with proper module namespace context
612+ - Collects structured validation errors per parameter for better UI
613+ - Uses signature binding for proper parameter handling
614+ - Handles **kwargs functions specially to maintain compatibility
615+
616+ Args:
617+ func: The function to call
618+ params: Raw parameter dictionary
619+
620+ Returns:
621+ Converted parameters dictionary, or dict with __validation_errors on failure.
622+ The __validation_errors contains structured per-parameter error details.
623+ """
624+ import typing
625+
626+ from pydantic import ValidationError
627+
628+ # Get function signature
629+ sig = inspect .signature (func )
630+
631+ # Resolve type hints with proper module context for forward references
632+ try :
633+ globalns = getattr (func , "__globals__" , {})
634+ # Use the defining module's namespace for localns (handles more forward-ref edge cases)
635+ mod = sys .modules .get (getattr (func , "__module__" , "" ), None )
636+ localns = vars (mod ) if mod else globalns
637+ type_hints = typing .get_type_hints (
638+ func , globalns = globalns , localns = localns , include_extras = True
639+ )
640+ except (NameError , AttributeError , TypeError ) as e :
641+ logger .debug (f"Failed to resolve type hints for { func .__name__ } : { e } " )
642+ type_hints = {}
643+
644+ # Use signature binding for proper parameter mapping, but handle **kwargs specially
645+ has_var_keyword = any (
646+ p .kind == inspect .Parameter .VAR_KEYWORD for p in sig .parameters .values ()
647+ )
648+
649+ if has_var_keyword :
650+ # For functions with **kwargs, use direct parameter mapping to maintain compatibility
651+ bound_params = params
652+ else :
653+ # For regular functions, use bind_partial for proper parameter handling
654+ try :
655+ bound_args = sig .bind_partial (** params )
656+ bound_args .apply_defaults ()
657+ bound_params = bound_args .arguments
658+ except TypeError as e :
659+ # If binding fails, fall back to direct parameter mapping
660+ logger .debug (f"Parameter binding failed for { func .__name__ } : { e } " )
661+ bound_params = params
662+
663+ converted_params = {}
664+ validation_errors = {} # Dict to store per-parameter error details
665+
666+ for param_name , param_value in bound_params .items ():
667+ try :
668+ # Get resolved type hint, fall back to raw annotation if available
669+ param_type = type_hints .get (param_name )
670+ if param_type is None and param_name in sig .parameters :
671+ param_type = sig .parameters [param_name ].annotation
672+
673+ # Skip if no type annotation or annotation is Any/object
674+ if (
675+ param_type is None
676+ or param_type == inspect .Parameter .empty
677+ or param_type in (typing .Any , object )
678+ ):
679+ converted_params [param_name ] = param_value
680+ continue
681+
682+ # Convert the parameter value based on its type
683+ converted_value = self ._convert_parameter_value (param_name , param_value , param_type )
684+ converted_params [param_name ] = converted_value
685+
686+ except ValidationError as ve :
687+ # Collect structured validation errors for this parameter
688+ param_errors = []
689+ for error in ve .errors ():
690+ param_errors .append (
691+ {
692+ "field" : (
693+ "." .join (str (loc ) for loc in error ["loc" ])
694+ if error ["loc" ]
695+ else param_name
696+ ),
697+ "message" : error ["msg" ],
698+ "type" : error ["type" ],
699+ }
700+ )
701+ validation_errors [param_name ] = param_errors
702+
703+ except Exception as e :
704+ # Log unexpected errors with stack trace but continue processing other parameters
705+ logger .exception (f"Unexpected error converting parameter '{ param_name } ': { e } " )
706+ converted_params [param_name ] = param_value
707+
708+ # Return structured validation errors if any occurred
709+ if validation_errors :
710+ logger .error (
711+ f"Parameter validation failed for parameters: { list (validation_errors .keys ())} "
712+ )
713+ return {"__validation_errors" : validation_errors }
714+
715+ return converted_params
716+
717+ def _convert_parameter_value (self , param_name : str , param_value : Any , param_type : Any ) -> Any :
718+ """Convert a single parameter value based on its type annotation.
719+
720+ Uses TypeAdapter to handle all type conversions uniformly:
721+ - Direct models: User
722+ - Optional models: Optional[User], User | None
723+ - Container types: list[User], dict[str, User], etc.
724+ - Complex nested types: dict[str, list[User]]
725+ - Non-model types: TypedDicts, dataclasses, primitives
726+
727+ Args:
728+ param_name: Parameter name (for logging/errors)
729+ param_value: Raw parameter value
730+ param_type: Resolved type annotation
731+
732+ Returns:
733+ Converted parameter value
734+
735+ Raises:
736+ ValidationError: If pydantic validation fails
737+ """
738+ from pydantic import BaseModel , TypeAdapter
739+
740+ # Fast path: if value is already a BaseModel instance, keep it as-is
741+ if isinstance (param_value , BaseModel ):
742+ logger .debug (f"Parameter '{ param_name } ' is already a BaseModel instance" )
743+ return param_value
744+
745+ # Try to create TypeAdapter for the parameter type
746+ try :
747+ adapter = TypeAdapter (param_type )
748+ logger .debug (f"Converting parameter '{ param_name } ' using TypeAdapter for { param_type } " )
749+ return adapter .validate_python (param_value )
750+ except (TypeError , ValueError ) as e :
751+ # TypeAdapter couldn't be created or type doesn't need validation
752+ # This happens for types that don't require special handling
753+ logger .debug (f"No TypeAdapter needed for parameter '{ param_name } ': { e } " )
754+ return param_value
0 commit comments