@@ -142,7 +142,151 @@ def parse_tools(llm_response: str) -> list[tuple[str, Mapping]]:
142142 if tool_name is not None and tool_arguments is not None :
143143 tools .append ((tool_name , tool_arguments ))
144144
145- return tools
145+
146+ def validate_tool_arguments (
147+ func : Callable ,
148+ args : Mapping [str , Any ],
149+ * ,
150+ coerce_types : bool = True ,
151+ strict : bool = False ,
152+ ) -> dict [str , Any ]:
153+ """Validate and optionally coerce tool arguments against function signature.
154+
155+ This function validates tool call arguments extracted from LLM responses against
156+ the expected function signature. It can automatically coerce common type mismatches
157+ (e.g., string "30" to int 30) and provides detailed error messages.
158+
159+ Args:
160+ func: The tool function to validate against
161+ args: Raw arguments from model (post-JSON parsing)
162+ coerce_types: If True, attempt type coercion for common cases (default: True)
163+ strict: If True, raise ValidationError on failures; if False, log warnings
164+ and return original args (default: False)
165+
166+ Returns:
167+ Validated and optionally coerced arguments dict
168+
169+ Raises:
170+ ValidationError: If strict=True and validation fails
171+
172+ Examples:
173+ >>> def get_weather(location: str, days: int = 1) -> dict:
174+ ... return {"location": location, "days": days}
175+
176+ >>> # LLM returns days as string
177+ >>> args = {"location": "Boston", "days": "3"}
178+ >>> validated = validate_tool_arguments(get_weather, args)
179+ >>> validated
180+ {'location': 'Boston', 'days': 3}
181+
182+ >>> # Strict mode raises on validation errors
183+ >>> bad_args = {"location": "Boston", "days": "not_a_number"}
184+ >>> validate_tool_arguments(get_weather, bad_args, strict=True)
185+ Traceback (most recent call last):
186+ ...
187+ pydantic.ValidationError: ...
188+ """
189+ from pydantic import ValidationError , create_model
190+
191+ from ..core import FancyLogger
192+
193+ # Get function signature
194+ sig = inspect .signature (func )
195+
196+ # Build Pydantic model from function signature
197+ # This reuses the logic from convert_function_to_tool
198+ field_definitions : dict [str , Any ] = {}
199+
200+ for param_name , param in sig .parameters .items ():
201+ # Skip *args and **kwargs
202+ if param .kind in (
203+ inspect .Parameter .VAR_POSITIONAL ,
204+ inspect .Parameter .VAR_KEYWORD ,
205+ ):
206+ continue
207+
208+ # Get type annotation
209+ param_type = param .annotation
210+ if param_type == inspect .Parameter .empty :
211+ # No type hint, default to Any
212+ param_type = Any
213+
214+ # Handle default values
215+ if param .default == inspect .Parameter .empty :
216+ # Required parameter
217+ field_definitions [param_name ] = (param_type , ...)
218+ else :
219+ # Optional parameter with default
220+ field_definitions [param_name ] = (param_type , param .default )
221+
222+ # Create dynamic Pydantic model for validation
223+ ValidatorModel = create_model (f"{ func .__name__ } _Validator" , ** field_definitions )
224+
225+ # Configure model for type coercion if requested
226+ if coerce_types :
227+ # Pydantic v2 uses model_config
228+ ValidatorModel .model_config = ConfigDict (
229+ str_strip_whitespace = True # Strip whitespace from strings
230+ # Pydantic automatically coerces compatible types
231+ )
232+
233+ try :
234+ # Validate using Pydantic
235+ validated_model = ValidatorModel (** args )
236+ validated_args = validated_model .model_dump ()
237+
238+ # Log successful validation with coercion details
239+ coerced_fields = []
240+ for key , original_value in args .items ():
241+ validated_value = validated_args .get (key )
242+ if type (original_value ) is not type (validated_value ):
243+ coerced_fields .append (
244+ f"{ key } : { type (original_value ).__name__ } → { type (validated_value ).__name__ } "
245+ )
246+
247+ if coerced_fields and coerce_types :
248+ FancyLogger .get_logger ().debug (
249+ f"Tool '{ func .__name__ } ' arguments coerced: { ', ' .join (coerced_fields )} "
250+ )
251+
252+ return validated_args
253+
254+ except ValidationError as e :
255+ # Format error message
256+ error_details = []
257+ for error in e .errors ():
258+ field = "." .join (str (loc ) for loc in error ["loc" ])
259+ msg = error ["msg" ]
260+ error_details .append (f" - { field } : { msg } " )
261+
262+ error_msg = (
263+ f"Tool argument validation failed for '{ func .__name__ } ':\n "
264+ + "\n " .join (error_details )
265+ )
266+
267+ if strict :
268+ # Re-raise with enhanced message
269+ FancyLogger .get_logger ().error (error_msg )
270+ raise
271+ else :
272+ # Log warning and return original args
273+ FancyLogger .get_logger ().warning (
274+ error_msg + "\n Returning original arguments without validation."
275+ )
276+ return dict (args )
277+
278+ except Exception as e :
279+ # Catch any other errors during validation
280+ error_msg = f"Unexpected error validating tool '{ func .__name__ } ' arguments: { e } "
281+
282+ if strict :
283+ FancyLogger .get_logger ().error (error_msg )
284+ raise
285+ else :
286+ FancyLogger .get_logger ().warning (
287+ error_msg + "\n Returning original arguments without validation."
288+ )
289+ return dict (args )
146290
147291
148292# Below functions and classes extracted from Ollama Python SDK (v0.6.1)
0 commit comments