Skip to content

Commit b829255

Browse files
committed
Add tool call instrumentation
Creates spans for RubyLLM::Chat#execute_tool with tool name, arguments, and result. Those spans appear as children of the chat span for waterfall visualization.
1 parent c9fc08b commit b829255

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ gemspec
66

77
gem "ruby_llm"
88
gem "opentelemetry-sdk"
9+
gem "opentelemetry-exporter-otlp"
910

1011
group :test do
1112
gem "minitest"

Gemfile.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ GEM
2626
net-http (~> 0.5)
2727
faraday-retry (2.4.0)
2828
faraday (~> 2.0)
29+
google-protobuf (4.33.4)
30+
bigdecimal
31+
rake (>= 13)
32+
google-protobuf (4.33.4-arm64-darwin)
33+
bigdecimal
34+
rake (>= 13)
35+
googleapis-common-protos-types (1.22.0)
36+
google-protobuf (~> 4.26)
2937
hashdiff (1.2.1)
3038
json (2.18.0)
3139
logger (1.7.0)
@@ -38,6 +46,13 @@ GEM
3846
opentelemetry-api (1.7.0)
3947
opentelemetry-common (0.23.0)
4048
opentelemetry-api (~> 1.0)
49+
opentelemetry-exporter-otlp (0.31.1)
50+
google-protobuf (>= 3.18)
51+
googleapis-common-protos-types (~> 1.3)
52+
opentelemetry-api (~> 1.1)
53+
opentelemetry-common (~> 0.20)
54+
opentelemetry-sdk (~> 1.10)
55+
opentelemetry-semantic_conventions
4156
opentelemetry-instrumentation-base (0.25.0)
4257
opentelemetry-api (~> 1.7)
4358
opentelemetry-common (~> 0.21)
@@ -79,6 +94,7 @@ PLATFORMS
7994

8095
DEPENDENCIES
8196
minitest
97+
opentelemetry-exporter-otlp
8298
opentelemetry-instrumentation-ruby_llm!
8399
opentelemetry-sdk
84100
rake

lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ def ask(message, &block)
3737
end
3838
end
3939

40+
def execute_tool(tool_call)
41+
attributes = {
42+
"gen_ai.tool.name" => tool_call.name,
43+
"gen_ai.tool.call.id" => tool_call.id,
44+
"gen_ai.tool.call.arguments" => tool_call.arguments.to_json,
45+
"gen_ai.tool.type" => "function"
46+
}
47+
48+
tracer.in_span("execute_tool #{tool_call.name}", attributes: attributes, kind: OpenTelemetry::Trace::SpanKind::INTERNAL) do |span|
49+
result = super
50+
result_str = result.is_a?(::RubyLLM::Tool::Halt) ? result.content.to_s : result.to_s
51+
span.set_attribute("gen_ai.tool.call.result", result_str[0..500])
52+
result
53+
end
54+
end
55+
4056
private
4157

4258
def tracer

test/instrumentation_test.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,79 @@ def test_records_error_on_api_failure
6666
assert span.attributes["error.type"]
6767
assert_equal OpenTelemetry::Trace::Status::ERROR, span.status.code
6868
end
69+
70+
def test_creates_span_for_tool_call
71+
calculator = Class.new(RubyLLM::Tool) do
72+
def self.name = "calculator"
73+
description "Performs math"
74+
param :expression, type: "string", desc: "Math expression"
75+
76+
def execute(expression:)
77+
eval(expression).to_s
78+
end
79+
end
80+
81+
stub_request(:post, "https://api.openai.com/v1/chat/completions")
82+
.to_return(
83+
{
84+
status: 200,
85+
headers: { "Content-Type" => "application/json" },
86+
body: {
87+
id: "chatcmpl-123",
88+
object: "chat.completion",
89+
model: "gpt-4o-mini",
90+
choices: [{
91+
index: 0,
92+
message: {
93+
role: "assistant",
94+
content: nil,
95+
tool_calls: [{
96+
id: "call_abc123",
97+
type: "function",
98+
function: { name: "calculator", arguments: '{"expression":"2+2"}' }
99+
}]
100+
},
101+
finish_reason: "tool_calls"
102+
}],
103+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
104+
}.to_json
105+
},
106+
{
107+
status: 200,
108+
headers: { "Content-Type" => "application/json" },
109+
body: {
110+
id: "chatcmpl-456",
111+
object: "chat.completion",
112+
model: "gpt-4o-mini",
113+
choices: [{
114+
index: 0,
115+
message: { role: "assistant", content: "The answer is 4" },
116+
finish_reason: "stop"
117+
}],
118+
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }
119+
}.to_json
120+
}
121+
)
122+
123+
chat = RubyLLM.chat(model: "gpt-4o-mini")
124+
chat.with_tool(calculator)
125+
chat.ask("What is 2+2?")
126+
127+
spans = EXPORTER.finished_spans
128+
129+
tool_spans = spans.select { |s| s.name.start_with?("execute_tool ") }
130+
chat_spans = spans.select { |s| s.name.include?("chat ") }
131+
132+
assert_equal 1, tool_spans.length
133+
assert_equal 1, chat_spans.length
134+
135+
tool_span = tool_spans.first
136+
assert_equal OpenTelemetry::Trace::SpanKind::INTERNAL, tool_span.kind
137+
assert_equal "execute_tool calculator", tool_span.name
138+
assert_equal "calculator", tool_span.attributes["gen_ai.tool.name"]
139+
assert_equal '{"expression":"2+2"}', tool_span.attributes["gen_ai.tool.call.arguments"]
140+
assert_equal "4", tool_span.attributes["gen_ai.tool.call.result"]
141+
assert_equal "call_abc123", tool_span.attributes["gen_ai.tool.call.id"]
142+
assert_equal "function", tool_span.attributes["gen_ai.tool.type"]
143+
end
69144
end

0 commit comments

Comments
 (0)