14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# File 'lib/legion/llm/inference/steps/tool_calls.rb', line 14
def step_tool_calls
unless @raw_response.respond_to?(:tool_calls) && @raw_response.tool_calls&.any?
log_step_debug(:tool_calls, :skipped, reason: :no_tool_calls)
return
end
tool_calls = @raw_response.tool_calls
log_step_debug(:tool_calls, :start, tool_call_count: tool_calls.size)
log.info(
"[llm][tools] detected request_id=#{@request.id} " \
"conversation_id=#{@request.conversation_id || 'none'} count=#{tool_calls.size}"
)
tool_calls.each do |tc|
tool_name = tc[:name] || tc['name']
tool_call_id = tc[:id] || tc['id']
source = find_tool_source(tool_name)
next unless source
if client_passthrough_source?(source)
log.info(
"[llm][tools] client_passthrough request_id=#{@request.id} " \
"tool_call_id=#{tool_call_id || 'none'} name=#{tool_name}"
)
log_step_debug(
:tool_calls,
:client_passthrough,
tool_call_id: tool_call_id || 'none',
tool_name: tool_name
)
next
end
if source[:type] == :builtin
log.info(
"[llm][tools] builtin_passthrough request_id=#{@request.id} " \
"tool_call_id=#{tool_call_id || 'none'} name=#{tool_name}"
)
next
end
tool_exchange_id = Tracing.exchange_id
log_tool_call_dispatch(tool_call_id, tool_name, source, tc[:arguments] || tc['arguments'])
result = ToolDispatcher.dispatch(
tool_call: tc,
source: source,
exchange_id: tool_exchange_id
)
if @pending_tool_history
lex_normalized = (source[:lex] || source[:extension] || '').delete_prefix('lex-').tr('-', '_')
runner_key = source[:type] == :extension ? "#{lex_normalized}_#{source[:runner]}" : nil
result_string = result[:result].is_a?(String) ? result[:result] : Legion::JSON.dump(result[:result] || {})
@pending_tool_history_mutex.synchronize do
@pending_tool_history << {
tool_call_id: tool_call_id,
pending_index: @pending_tool_history.size,
tool_name: tool_name,
args: tc[:arguments] || tc['arguments'] || {},
result: result_string,
error: result[:status] == :error,
runner_key: runner_key
}
end
end
@timeline.record(
category: :tool, key: "tool:execute:#{tc[:name] || tc['name']}",
exchange_id: tool_exchange_id, direction: :outbound,
detail: "#{result[:status]} via #{source[:type]}",
from: 'pipeline', to: "tool:#{tc[:name] || tc['name']}",
duration_ms: result[:duration_ms],
data: {
tool_call_id: tool_call_id,
arguments: tc[:arguments] || tc['arguments'] || {},
source: describe_tool_source(source),
status: result[:status]
}
)
@timeline.record(
category: :tool, key: "tool:result:#{tc[:name] || tc['name']}",
exchange_id: tool_exchange_id, direction: :inbound,
detail: result[:result].to_s[0..100].to_s,
from: "tool:#{tc[:name] || tc['name']}", to: 'pipeline',
data: {
tool_call_id: tool_call_id,
status: result[:status],
result: result[:result]
}
)
log_tool_call_result(tool_call_id, tool_name, result)
end
rescue StandardError => e
@warnings << "Tool call handling error: #{e.message}"
handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_calls')
end
|