@@ -155,6 +155,7 @@ defmodule LLMAgent.Handlers do
155
155
Handles tool call signals.
156
156
157
157
Executes the specified tool with the provided arguments and generates a tool result signal.
158
+ Includes parameter validation based on tool schema and detailed error handling.
158
159
159
160
## Parameters
160
161
@@ -177,35 +178,273 @@ defmodule LLMAgent.Handlers do
177
178
tool_name = signal . data . name
178
179
tool_args = signal . data . args
179
180
180
- # Get tool from AgentForge Tools registry
181
- case AgentForge.Tools . get ( tool_name ) do
182
- { :ok , tool_fn } ->
183
- # Execute tool
181
+ Logger . info ( "Tool call: #{ tool_name } with args: #{ inspect ( tool_args ) } " )
182
+
183
+ # Get tool registry from state or use default
184
+ tool_registry = Map . get ( state , :tool_registry , & AgentForge.Tools . get / 1 )
185
+
186
+ # Get tool from registry
187
+ case tool_registry . ( tool_name ) do
188
+ { :ok , % { execute: tool_fn , parameters: params_schema } = tool } ->
189
+ # Validate parameters against schema if schema exists
190
+ validation_result =
191
+ if params_schema do
192
+ validate_tool_parameters ( tool_args , params_schema )
193
+ else
194
+ { :ok , tool_args }
195
+ end
196
+
197
+ case validation_result do
198
+ { :ok , validated_args } ->
199
+ # Execute tool with validated args
200
+ try do
201
+ # Track start time for performance metrics
202
+ start_time = System . monotonic_time ( :millisecond )
203
+
204
+ # Execute tool function
205
+ result = tool_fn . ( validated_args )
206
+
207
+ # Calculate execution time
208
+ execution_time = System . monotonic_time ( :millisecond ) - start_time
209
+
210
+ # Create tool result signal with execution metadata
211
+ result_signal =
212
+ Signals . tool_result (
213
+ tool_name ,
214
+ result ,
215
+ % {
216
+ execution_time_ms: execution_time ,
217
+ tool_type: Map . get ( tool , :type , "unknown" )
218
+ }
219
+ )
220
+
221
+ # Update state with detailed tool call information
222
+ new_state =
223
+ state
224
+ |> Store . add_tool_call (
225
+ tool_name ,
226
+ validated_args ,
227
+ % {
228
+ result: result ,
229
+ execution_time_ms: execution_time ,
230
+ status: "success" ,
231
+ timestamp: DateTime . utc_now ( )
232
+ }
233
+ )
234
+ |> Map . update (
235
+ :execution_stats ,
236
+ % { tool_calls: 1 , total_execution_time: execution_time } ,
237
+ fn stats ->
238
+ stats
239
+ |> Map . update ( :tool_calls , 1 , & ( & 1 + 1 ) )
240
+ |> Map . update ( :total_execution_time , execution_time , & ( & 1 + execution_time ) )
241
+ end
242
+ )
243
+
244
+ { { :emit , result_signal } , new_state }
245
+ rescue
246
+ e ->
247
+ stack = __STACKTRACE__
248
+ error_message = Exception . message ( e )
249
+
250
+ error_data = % {
251
+ message: error_message ,
252
+ type: "execution_error" ,
253
+ stacktrace: Enum . take ( stack , 3 ) ,
254
+ tool: tool_name ,
255
+ args: validated_args
256
+ }
257
+
258
+ # Log detailed error for debugging
259
+ Logger . error ( "Tool execution error: #{ inspect ( error_data ) } " )
260
+
261
+ # Create error signal with context
262
+ error_signal =
263
+ Signals . error (
264
+ error_message ,
265
+ tool_name ,
266
+ % { error_type: "execution_error" , args: validated_args }
267
+ )
268
+
269
+ # Update state with error information
270
+ new_state =
271
+ Store . add_tool_call (
272
+ state ,
273
+ tool_name ,
274
+ validated_args ,
275
+ % {
276
+ error: error_message ,
277
+ status: "error" ,
278
+ error_type: "execution_error"
279
+ }
280
+ )
281
+
282
+ { { :emit , error_signal } , new_state }
283
+ end
284
+
285
+ { :error , validation_errors } ->
286
+ # Parameter validation failed
287
+ error_message = "Invalid tool parameters: #{ inspect ( validation_errors ) } "
288
+ Logger . warning ( error_message )
289
+
290
+ # Create detailed error signal
291
+ error_signal =
292
+ Signals . error (
293
+ error_message ,
294
+ tool_name ,
295
+ % {
296
+ error_type: "validation_error" ,
297
+ args: tool_args ,
298
+ validation_errors: validation_errors ,
299
+ expected_schema: params_schema
300
+ }
301
+ )
302
+
303
+ # Update state with validation error
304
+ new_state =
305
+ Store . add_tool_call (
306
+ state ,
307
+ tool_name ,
308
+ tool_args ,
309
+ % {
310
+ error: error_message ,
311
+ status: "error" ,
312
+ error_type: "validation_error" ,
313
+ validation_errors: validation_errors
314
+ }
315
+ )
316
+
317
+ { { :emit , error_signal } , new_state }
318
+ end
319
+
320
+ { :ok , tool_fn } when is_function ( tool_fn ) ->
321
+ # Simple tool without schema, execute directly
184
322
try do
185
323
result = tool_fn . ( tool_args )
186
-
187
- # Create tool result signal
188
324
result_signal = Signals . tool_result ( tool_name , result )
189
-
190
- # Update state with tool call and result
191
325
new_state = Store . add_tool_call ( state , tool_name , tool_args , result )
192
326
193
327
{ { :emit , result_signal } , new_state }
194
328
rescue
195
329
e ->
196
330
error_message = Exception . message ( e )
197
331
error_signal = Signals . error ( error_message , tool_name )
198
- { { :emit , error_signal } , state }
332
+
333
+ # Update state with error
334
+ new_state =
335
+ Store . add_tool_call (
336
+ state ,
337
+ tool_name ,
338
+ tool_args ,
339
+ % {
340
+ error: error_message ,
341
+ status: "error"
342
+ }
343
+ )
344
+
345
+ { { :emit , error_signal } , new_state }
199
346
end
200
347
201
348
{ :error , reason } ->
202
- error_signal = Signals . error ( "Tool not found: #{ reason } " , tool_name )
203
- { { :emit , error_signal } , state }
349
+ # Tool not found in registry
350
+ error_message = "Tool not found: #{ reason } "
351
+ Logger . warning ( error_message )
352
+
353
+ error_signal =
354
+ Signals . error (
355
+ error_message ,
356
+ tool_name ,
357
+ % { error_type: "not_found" , available_tools: list_available_tools ( state ) }
358
+ )
359
+
360
+ # Update state with not found error
361
+ new_state =
362
+ Store . add_tool_call (
363
+ state ,
364
+ tool_name ,
365
+ tool_args ,
366
+ % {
367
+ error: error_message ,
368
+ status: "error" ,
369
+ error_type: "not_found"
370
+ }
371
+ )
372
+
373
+ { { :emit , error_signal } , new_state }
204
374
end
205
375
end
206
376
207
377
def tool_handler ( _signal , state ) , do: { :skip , state }
208
378
379
+ # Helper function to validate tool parameters against schema
380
+ defp validate_tool_parameters ( args , schema ) do
381
+ # Implementation of JSON Schema validation
382
+ # This is a simplified version, in production would use a proper validator
383
+ try do
384
+ # Check required fields
385
+ required = Map . get ( schema , "required" , [ ] )
386
+ missing_fields = Enum . filter ( required , fn field -> is_nil ( Map . get ( args , field ) ) end )
387
+
388
+ if length ( missing_fields ) > 0 do
389
+ { :error , % { missing_required: missing_fields } }
390
+ else
391
+ # Validate types if properties defined
392
+ properties = Map . get ( schema , "properties" , % { } )
393
+
394
+ validation_errors =
395
+ Enum . reduce ( properties , % { } , fn { field , field_schema } , errors ->
396
+ value = Map . get ( args , field )
397
+
398
+ if is_nil ( value ) do
399
+ # Skip validation for optional fields that are not provided
400
+ errors
401
+ else
402
+ # Validate field type
403
+ expected_type = Map . get ( field_schema , "type" )
404
+ actual_type = determine_json_type ( value )
405
+
406
+ if expected_type != actual_type do
407
+ Map . put ( errors , field , "expected #{ expected_type } , got #{ actual_type } " )
408
+ else
409
+ errors
410
+ end
411
+ end
412
+ end )
413
+
414
+ if map_size ( validation_errors ) > 0 do
415
+ { :error , % { type_mismatch: validation_errors } }
416
+ else
417
+ { :ok , args }
418
+ end
419
+ end
420
+ rescue
421
+ e -> { :error , % { validation_error: Exception . message ( e ) } }
422
+ end
423
+ end
424
+
425
+ # Helper to determine JSON Schema type of a value
426
+ defp determine_json_type ( value ) when is_binary ( value ) , do: "string"
427
+ defp determine_json_type ( value ) when is_integer ( value ) , do: "integer"
428
+ defp determine_json_type ( value ) when is_float ( value ) , do: "number"
429
+ defp determine_json_type ( value ) when is_boolean ( value ) , do: "boolean"
430
+ defp determine_json_type ( value ) when is_nil ( value ) , do: "null"
431
+ defp determine_json_type ( value ) when is_map ( value ) , do: "object"
432
+ defp determine_json_type ( value ) when is_list ( value ) , do: "array"
433
+
434
+ # Get list of available tools from state
435
+ defp list_available_tools ( state ) do
436
+ available_tools = Map . get ( state , :available_tools , [ ] )
437
+
438
+ Enum . map ( available_tools , fn tool ->
439
+ case tool do
440
+ % { name: name } -> name
441
+ name when is_binary ( name ) -> name
442
+ _ -> nil
443
+ end
444
+ end )
445
+ |> Enum . reject ( & is_nil / 1 )
446
+ end
447
+
209
448
@ doc """
210
449
Handles tool result signals.
211
450
0 commit comments