verica-observability 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/lib/verica/ruby_openai_wrapper.rb +74 -12
- data/lib/verica/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 816c57a94a8a82efc43e464165b84611981c76dc6a456eb6bd359804621beddc
|
|
4
|
+
data.tar.gz: 929106f12d8e32c03de0bd9427b9babe4cf5639c5159e31d6a7f34d3d1a32d89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7242e9480405617939a9d4ab4109f5f1eb104046372009d100c458627b7f846987a34f59b0439a766d5b297244d4a11b46400062faf3cf72ea09b0f571e9f71
|
|
7
|
+
data.tar.gz: 832738007de7cab2d6ea750369609e0d94e08c5578477038018b7086df8cddcf93de574a0f7cabdff00f13d26d736855af5a61678f3bdbc7d9655c75eb16363b
|
data/README.md
CHANGED
|
@@ -108,8 +108,12 @@ true)` and the wrapper injects `stream_options: { include_usage: true }` for you
|
|
|
108
108
|
default because the extra usage chunk arrives with an empty `choices` array that
|
|
109
109
|
some caller procs may not expect.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
Tool calls are captured on both paths: a non-streamed response's
|
|
112
|
+
`message.tool_calls` land on the span's output message, and streamed
|
|
113
|
+
`delta.tool_calls` fragments are reassembled per index (arguments concatenated
|
|
114
|
+
in arrival order). Tool-call-only turns (content null, `tool_calls` present),
|
|
115
|
+
the shape of a tool-looping agent's intermediate turns, produce a span output
|
|
116
|
+
too, so those turns never show an empty output in Verica.
|
|
113
117
|
|
|
114
118
|
## Use (RubyLLM)
|
|
115
119
|
|
|
@@ -158,10 +158,12 @@ module Verica
|
|
|
158
158
|
message = response.dig('choices', 0, 'message')
|
|
159
159
|
return unless message.is_a?(Hash)
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
161
|
+
# A tool-call-only turn has content null: still emitted, with tool_calls,
|
|
162
|
+
# so the trace's output is never empty when the model DID respond.
|
|
163
|
+
output = { role: message['role'].to_s, content: message['content'] }
|
|
164
|
+
tool_calls = pinned_tool_calls(message['tool_calls'])
|
|
165
|
+
output[:tool_calls] = tool_calls unless tool_calls.empty?
|
|
166
|
+
span.set_attribute('gen_ai.output.messages', JSON.generate([output]))
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
def annotate_from_stream(span, accumulator, cfg)
|
|
@@ -176,12 +178,32 @@ module Verica
|
|
|
176
178
|
return unless cfg&.capture_content
|
|
177
179
|
|
|
178
180
|
text = accumulator.content
|
|
179
|
-
|
|
181
|
+
tool_calls = accumulator.tool_calls
|
|
182
|
+
return if (text.nil? || text.empty?) && tool_calls.empty?
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
# Tool-call-only turn: no content key (matches the n8n mapper; the
|
|
185
|
+
# normalizer's flattener drops empty text either way).
|
|
186
|
+
output = { role: (accumulator.role || 'assistant').to_s }
|
|
187
|
+
output[:content] = text unless text.nil? || text.empty?
|
|
188
|
+
output[:tool_calls] = tool_calls unless tool_calls.empty?
|
|
189
|
+
span.set_attribute('gen_ai.output.messages', JSON.generate([output]))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Reduces the API's tool_calls entries to the pinned semconv shape the
|
|
193
|
+
# Verica normalizer accepts (engine `toolCallOf`): `{function: {name,
|
|
194
|
+
# arguments}}` with `arguments` passed through as the API returned it (a
|
|
195
|
+
# JSON string). Same shape the TS/n8n producers emit. Junk entries are
|
|
196
|
+
# skipped; provider ids are not part of the pinned shape.
|
|
197
|
+
def pinned_tool_calls(raw)
|
|
198
|
+
return [] unless raw.is_a?(Array)
|
|
199
|
+
|
|
200
|
+
raw.filter_map do |entry|
|
|
201
|
+
fn = entry.is_a?(Hash) ? entry['function'] : nil
|
|
202
|
+
name = fn.is_a?(Hash) ? fn['name'] : nil
|
|
203
|
+
next unless name.is_a?(String) && !name.empty?
|
|
204
|
+
|
|
205
|
+
{ function: { name: name, arguments: fn['arguments'] } }
|
|
206
|
+
end
|
|
185
207
|
end
|
|
186
208
|
|
|
187
209
|
def safely
|
|
@@ -192,9 +214,11 @@ module Verica
|
|
|
192
214
|
|
|
193
215
|
# Drains a ruby-openai stream (parsed Hashes, string keys) into the pieces
|
|
194
216
|
# the span needs: the joined assistant text, the first non-nil model, the
|
|
195
|
-
# assistant role from the first delta that carries one,
|
|
196
|
-
#
|
|
197
|
-
#
|
|
217
|
+
# assistant role from the first delta that carries one, the usage Hash from
|
|
218
|
+
# the final chunk (present only when the caller asks for it), and the
|
|
219
|
+
# reassembled tool calls. Tool-call deltas carry an `index`: id/type/
|
|
220
|
+
# function.name come from the first fragment that has them; the argument
|
|
221
|
+
# fragments concatenate in arrival order into one JSON string per index.
|
|
198
222
|
class StreamAccumulator
|
|
199
223
|
attr_reader :content, :model, :role, :usage
|
|
200
224
|
|
|
@@ -203,6 +227,7 @@ module Verica
|
|
|
203
227
|
@model = nil
|
|
204
228
|
@role = nil
|
|
205
229
|
@usage = nil
|
|
230
|
+
@tool_call_parts = {}
|
|
206
231
|
end
|
|
207
232
|
|
|
208
233
|
def add(chunk)
|
|
@@ -214,10 +239,47 @@ module Verica
|
|
|
214
239
|
@role ||= delta['role']
|
|
215
240
|
fragment = delta['content']
|
|
216
241
|
@content << fragment if fragment.is_a?(String)
|
|
242
|
+
add_tool_call_fragments(delta['tool_calls'])
|
|
217
243
|
end
|
|
218
244
|
usage = chunk['usage']
|
|
219
245
|
@usage = usage if usage.is_a?(Hash)
|
|
220
246
|
end
|
|
247
|
+
|
|
248
|
+
# The reassembled tool calls, ordered by index, in the pinned semconv
|
|
249
|
+
# shape (`{function: {name, arguments}}`, arguments as one JSON string).
|
|
250
|
+
# Indexes that never produced a function name are dropped.
|
|
251
|
+
def tool_calls
|
|
252
|
+
@tool_call_parts.keys.sort.filter_map do |index|
|
|
253
|
+
part = @tool_call_parts[index]
|
|
254
|
+
name = part['name']
|
|
255
|
+
next unless name.is_a?(String) && !name.empty?
|
|
256
|
+
|
|
257
|
+
{ function: { name: name, arguments: part['arguments'] } }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def add_tool_call_fragments(fragments)
|
|
264
|
+
return unless fragments.is_a?(Array)
|
|
265
|
+
|
|
266
|
+
fragments.each do |fragment|
|
|
267
|
+
next unless fragment.is_a?(Hash)
|
|
268
|
+
|
|
269
|
+
index = fragment['index']
|
|
270
|
+
next unless index.is_a?(Integer)
|
|
271
|
+
|
|
272
|
+
part = (@tool_call_parts[index] ||= { 'arguments' => +'' })
|
|
273
|
+
part['id'] ||= fragment['id']
|
|
274
|
+
part['type'] ||= fragment['type']
|
|
275
|
+
fn = fragment['function']
|
|
276
|
+
next unless fn.is_a?(Hash)
|
|
277
|
+
|
|
278
|
+
part['name'] ||= fn['name']
|
|
279
|
+
args = fn['arguments']
|
|
280
|
+
part['arguments'] << args if args.is_a?(String)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
221
283
|
end
|
|
222
284
|
end
|
|
223
285
|
end
|
data/lib/verica/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: verica-observability
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Verica
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-exporter-otlp
|