Skip to content

Commit ab0931d

Browse files
committed
refactor: optimize code quality following Credo guidelines\n\n- Rename is_recoverable_error to recoverable_error? to follow Elixir naming conventions\n- Reduce function nesting in OpenAI provider by extracting helper functions\n- Replace cond with if where appropriate for better readability\n- Fix Logger.warn deprecation warnings
1 parent 86e019e commit ab0931d

File tree

3 files changed

+980
-170
lines changed

3 files changed

+980
-170
lines changed

lib/llm_agent/handlers.ex

+250-11
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ defmodule LLMAgent.Handlers do
155155
Handles tool call signals.
156156
157157
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.
158159
159160
## Parameters
160161
@@ -177,35 +178,273 @@ defmodule LLMAgent.Handlers do
177178
tool_name = signal.data.name
178179
tool_args = signal.data.args
179180

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
184322
try do
185323
result = tool_fn.(tool_args)
186-
187-
# Create tool result signal
188324
result_signal = Signals.tool_result(tool_name, result)
189-
190-
# Update state with tool call and result
191325
new_state = Store.add_tool_call(state, tool_name, tool_args, result)
192326

193327
{{:emit, result_signal}, new_state}
194328
rescue
195329
e ->
196330
error_message = Exception.message(e)
197331
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}
199346
end
200347

201348
{: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}
204374
end
205375
end
206376

207377
def tool_handler(_signal, state), do: {:skip, state}
208378

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+
209448
@doc """
210449
Handles tool result signals.
211450

0 commit comments

Comments
 (0)