openlayer 0.7.1 → 0.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4940bcba0e8de54b1f4196a6d416f18fd905196e22756ad008b268349cf58985
4
- data.tar.gz: a262d23d26f708c6bdf98acbb4d4e8ed3e4b819943ffd19464e16387be9f3779
3
+ metadata.gz: e2012084e7121e1610abd934759b02dea258adbf82bb08fed4975e793793b0bf
4
+ data.tar.gz: 2326a997d8b2a24a01e80007e002f5141a6b0ce5b06e769350172689f68197ef
5
5
  SHA512:
6
- metadata.gz: 28efc94d5e0d938417ce4bfba8091570594eccb71e9274ffdcfeabf64177e1f107a032dcc66166256660112536177ad1e43cc06c22ef84e9a0c3cf34649060d7
7
- data.tar.gz: 6d2be20e5677b5d7f2642b989e954e288385170f86451579bc6535f5232615705a7b1eac6ecef0ea66a6dfe1a09871990527d7460559a7130bfa2601db23f50f
6
+ metadata.gz: 7d39d0ebbd0068ff855e3071021c6d8ab30d01c0c76c12ae61cdd3419eef0400b33f68249aeb682768e96b7f9c5a0bd72dfb6607cff34830d2c616c04ac83be5
7
+ data.tar.gz: 2a766e20dcbf8f279658818a25a2c5fcd6739ca208ef9a91313e90943434e4256326f00bb699adfa8e21269c36f363a6709bfac2bff79d7d5f7a8e10a5555c1e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1 (2026-01-06)
4
+
5
+ Full Changelog: [v0.8.0...v0.8.1](https://github.com/openlayer-ai/openlayer-ruby/compare/v0.8.0...v0.8.1)
6
+
7
+ ### Chores
8
+
9
+ * **closes OPEN-8602:** parse document chunks from ConversationalSearchService response ([afb7d2e](https://github.com/openlayer-ai/openlayer-ruby/commit/afb7d2e7ea00f809c27e382bc410bc4ea1571c7f))
10
+ * **closes OPEN-8603:** represent nested steps for ConversationalSearchService traces ([e5026c7](https://github.com/openlayer-ai/openlayer-ruby/commit/e5026c72738e77610da528eff1fa826a67ffa5fe))
11
+
12
+ ## 0.8.0 (2026-01-05)
13
+
14
+ Full Changelog: [v0.7.1...v0.8.0](https://github.com/openlayer-ai/openlayer-ruby/compare/v0.7.1...v0.8.0)
15
+
16
+ ### Features
17
+
18
+ * **closes OPEN-8574:** session and user id support for the ConversationalSearchService tracer ([9157c52](https://github.com/openlayer-ai/openlayer-ruby/commit/9157c528195ae1a3c56ba652b8b598a2c2e53eab))
19
+
3
20
  ## 0.7.1 (2025-12-19)
4
21
 
5
22
  Full Changelog: [v0.7.0...v0.7.1](https://github.com/openlayer-ai/openlayer-ruby/compare/v0.7.0...v0.7.1)
data/README.md CHANGED
@@ -17,7 +17,7 @@ To use this gem, install via Bundler by adding the following to your application
17
17
  <!-- x-release-please-start-version -->
18
18
 
19
19
  ```ruby
20
- gem "openlayer", "~> 0.7.1"
20
+ gem "openlayer", "~> 0.8.1"
21
21
  ```
22
22
 
23
23
  <!-- x-release-please-end -->
@@ -38,8 +38,12 @@ module Openlayer
38
38
  # The Openlayer client instance for sending traces
39
39
  # @param inference_pipeline_id [String]
40
40
  # The Openlayer inference pipeline ID to send traces to
41
+ # @param session_id [String, nil]
42
+ # Optional session ID to use for all traces. Takes precedence over auto-extracted sessions.
43
+ # @param user_id [String, nil]
44
+ # Optional user ID to use for all traces.
41
45
  # @return [void]
42
- def self.trace_client(client, openlayer_client:, inference_pipeline_id:)
46
+ def self.trace_client(client, openlayer_client:, inference_pipeline_id:, session_id: nil, user_id: nil)
43
47
  # Store original method reference
44
48
  original_answer_query = client.method(:answer_query)
45
49
 
@@ -63,7 +67,9 @@ module Openlayer
63
67
  start_time: start_time,
64
68
  end_time: end_time,
65
69
  openlayer_client: openlayer_client,
66
- inference_pipeline_id: inference_pipeline_id
70
+ inference_pipeline_id: inference_pipeline_id,
71
+ session_id: session_id,
72
+ user_id: user_id
67
73
  )
68
74
  rescue StandardError => e
69
75
  # Never break the user's application due to tracing errors
@@ -87,8 +93,10 @@ module Openlayer
87
93
  # @param end_time [Time] Request end time
88
94
  # @param openlayer_client [Openlayer::Client] Openlayer client instance
89
95
  # @param inference_pipeline_id [String] Pipeline ID
96
+ # @param session_id [String, nil] Optional session ID (takes precedence over auto-extracted)
97
+ # @param user_id [String, nil] Optional user ID
90
98
  # @return [void]
91
- def self.send_trace(args:, kwargs:, response:, start_time:, end_time:, openlayer_client:, inference_pipeline_id:)
99
+ def self.send_trace(args:, kwargs:, response:, start_time:, end_time:, openlayer_client:, inference_pipeline_id:, session_id: nil, user_id: nil)
92
100
  # Calculate latency
93
101
  latency_ms = ((end_time - start_time) * 1000).round(2)
94
102
 
@@ -105,6 +113,51 @@ module Openlayer
105
113
  prompt_tokens = (query_text.length / 4.0).ceil
106
114
  completion_tokens = (answer_data[:answer_text].length / 4.0).ceil
107
115
 
116
+ # Extract grounding information from metadata for step root level
117
+ citations = metadata.delete(:citations)
118
+ references = metadata.delete(:references)
119
+ related_questions = metadata.delete(:relatedQuestions)
120
+
121
+ # Extract context from references (array of content strings)
122
+ context = if references && references.is_a?(Array)
123
+ references.map { |ref| ref[:content] }.compact
124
+ else
125
+ nil
126
+ end
127
+
128
+ # Extract nested steps (Google's execution steps)
129
+ answer = response.respond_to?(:answer) ? response.answer : nil
130
+ nested_steps = answer ? extract_steps(answer, query_text) : nil
131
+
132
+ # Build step object
133
+ step = {
134
+ name: "Conversational Search answer_query",
135
+ type: "chat_completion",
136
+ provider: "Google",
137
+ startTime: start_time.to_i,
138
+ endTime: end_time.to_i,
139
+ latency: latency_ms,
140
+ metadata: metadata,
141
+ inputs: {
142
+ prompt: [
143
+ {role: "user", content: query_text}
144
+ ]
145
+ },
146
+ output: answer_data[:answer_text],
147
+ promptTokens: prompt_tokens,
148
+ completionTokens: completion_tokens,
149
+ tokens: prompt_tokens + completion_tokens,
150
+ model: "google-discovery-engine"
151
+ }
152
+
153
+ # Add grounding information at step root level
154
+ step[:citations] = citations if citations
155
+ step[:references] = references if references
156
+ step[:relatedQuestions] = related_questions if related_questions
157
+
158
+ # Add nested steps (Google's execution steps as RetrieverSteps)
159
+ step[:steps] = nested_steps if nested_steps && !nested_steps.empty?
160
+
108
161
  # Build trace data in Openlayer format
109
162
  trace_data = {
110
163
  config: {
@@ -120,31 +173,32 @@ module Openlayer
120
173
  latency_ms: latency_ms,
121
174
  timestamp: start_time.to_i,
122
175
  metadata: metadata,
123
- steps: [
124
- {
125
- name: "Conversational Search answer_query",
126
- type: "chat_completion",
127
- provider: "Google",
128
- startTime: start_time.to_i,
129
- endTime: end_time.to_i,
130
- latency: latency_ms,
131
- metadata: metadata,
132
- inputs: {
133
- prompt: [
134
- {role: "user", content: query_text}
135
- ]
136
- },
137
- output: answer_data[:answer_text],
138
- promptTokens: prompt_tokens,
139
- completionTokens: completion_tokens,
140
- tokens: prompt_tokens + completion_tokens,
141
- model: "google-discovery-engine"
142
- }
143
- ]
176
+ steps: [step]
144
177
  }
145
178
  ]
146
179
  }
147
180
 
181
+ # Add context column if available
182
+ if context && !context.empty?
183
+ trace_data[:rows][0][:context] = context
184
+ trace_data[:config][:contextColumnName] = "context"
185
+ end
186
+
187
+ # Determine which session to use (kwarg takes precedence over auto-extracted)
188
+ final_session = session_id || metadata[:session]
189
+ if final_session
190
+ trace_data[:rows][0][:session_id] = final_session
191
+ trace_data[:config][:sessionIdColumnName] = "session_id"
192
+ end
193
+
194
+ # Determine which user_id to use (kwarg takes precedence over auto-extracted)
195
+ user_pseudo_id = extract_user_pseudo_id(response)
196
+ final_user_id = user_id || user_pseudo_id
197
+ if final_user_id
198
+ trace_data[:rows][0][:user_id] = final_user_id
199
+ trace_data[:config][:userIdColumnName] = "user_id"
200
+ end
201
+
148
202
  # Send to Openlayer
149
203
  openlayer_client
150
204
  .inference_pipelines
@@ -202,20 +256,208 @@ module Openlayer
202
256
  answer = response.answer
203
257
  return {answer_text: nil} if answer.nil?
204
258
 
259
+ # Extract answer_skipped_reasons if present
260
+ answer_skipped_reasons = safe_extract(answer, :answer_skipped_reasons)
261
+ answer_skipped_reasons = if answer_skipped_reasons && answer_skipped_reasons.respond_to?(:to_a)
262
+ answer_skipped_reasons.to_a.map(&:to_s).compact
263
+ end
264
+
205
265
  {
206
266
  answer_text: safe_extract(answer, :answer_text),
267
+ answer_name: safe_extract(answer, :name),
207
268
  state: safe_extract(answer, :state)&.to_s,
208
269
  grounding_score: safe_extract(answer, :grounding_score),
209
270
  create_time: extract_timestamp(answer, :create_time),
210
271
  complete_time: extract_timestamp(answer, :complete_time),
211
272
  citations_count: safe_count(answer, :citations),
212
- references_count: safe_count(answer, :references)
273
+ references_count: safe_count(answer, :references),
274
+ answer_skipped_reasons: answer_skipped_reasons
213
275
  }
214
276
  rescue StandardError => e
215
277
  warn_if_debug("[Openlayer] Failed to extract answer data: #{e.message}")
216
278
  {answer_text: nil}
217
279
  end
218
280
 
281
+ # Extract citations from answer
282
+ #
283
+ # @param answer [Object] Answer object from response
284
+ # @return [Array<Hash>, nil] Array of citation hashes or nil
285
+ def self.extract_citations(answer)
286
+ return nil unless answer && answer.respond_to?(:citations)
287
+
288
+ citations = safe_extract(answer, :citations)
289
+ return nil if citations.nil? || !citations.respond_to?(:map)
290
+
291
+ citations.map do |citation|
292
+ {
293
+ start_index: safe_extract(citation, :start_index)&.to_i,
294
+ end_index: safe_extract(citation, :end_index)&.to_i,
295
+ sources: extract_citation_sources(citation)
296
+ }.compact
297
+ end
298
+ rescue StandardError => e
299
+ warn_if_debug("[Openlayer] Failed to extract citations: #{e.message}")
300
+ nil
301
+ end
302
+
303
+ # Extract sources from a citation
304
+ #
305
+ # @param citation [Object] Citation object
306
+ # @return [Array<Hash>, nil] Array of source hashes or nil
307
+ def self.extract_citation_sources(citation)
308
+ sources = safe_extract(citation, :sources)
309
+ return nil if sources.nil? || !sources.respond_to?(:map)
310
+
311
+ sources.map do |source|
312
+ {reference_id: safe_extract(source, :reference_id)}.compact
313
+ end
314
+ rescue StandardError
315
+ nil
316
+ end
317
+
318
+ # Extract references from answer
319
+ #
320
+ # @param answer [Object] Answer object from response
321
+ # @return [Array<Hash>, nil] Array of reference hashes or nil
322
+ def self.extract_references(answer)
323
+ return nil unless answer && answer.respond_to?(:references)
324
+
325
+ references = safe_extract(answer, :references)
326
+ return nil if references.nil? || !references.respond_to?(:each_with_index)
327
+
328
+ references.each_with_index.map do |reference, index|
329
+ chunk_info = safe_extract(reference, :chunk_info)
330
+ next nil if chunk_info.nil?
331
+
332
+ doc_metadata = safe_extract(chunk_info, :document_metadata)
333
+
334
+ {
335
+ reference_id: index.to_s,
336
+ content: safe_extract(chunk_info, :content),
337
+ relevance_score: safe_extract(chunk_info, :relevance_score)&.to_f,
338
+ document_id: doc_metadata ? safe_extract(doc_metadata, :document) : nil,
339
+ uri: doc_metadata ? safe_extract(doc_metadata, :uri) : nil,
340
+ title: doc_metadata ? safe_extract(doc_metadata, :title) : nil
341
+ }.compact
342
+ end.compact
343
+ rescue StandardError => e
344
+ warn_if_debug("[Openlayer] Failed to extract references: #{e.message}")
345
+ nil
346
+ end
347
+
348
+ # Extract related questions from answer
349
+ #
350
+ # @param answer [Object] Answer object from response
351
+ # @return [Array<String>, nil] Array of related questions or nil
352
+ def self.extract_related_questions(answer)
353
+ return nil unless answer && answer.respond_to?(:related_questions)
354
+
355
+ questions = safe_extract(answer, :related_questions)
356
+ return nil if questions.nil? || !questions.respond_to?(:to_a)
357
+
358
+ questions.to_a.map(&:to_s).compact
359
+ rescue StandardError => e
360
+ warn_if_debug("[Openlayer] Failed to extract related questions: #{e.message}")
361
+ nil
362
+ end
363
+
364
+ # Extract execution steps from answer
365
+ #
366
+ # @param answer [Object] Answer object from response
367
+ # @param original_query [String, nil] Original user query for comparison
368
+ # @return [Array<Hash>, nil] Array of step hashes or nil
369
+ def self.extract_steps(answer, original_query = nil)
370
+ return nil unless answer && answer.respond_to?(:steps)
371
+
372
+ steps = safe_extract(answer, :steps)
373
+ return nil if steps.nil? || !steps.respond_to?(:map)
374
+
375
+ steps.map do |step|
376
+ extract_step_data(step, original_query)
377
+ end.compact
378
+ rescue StandardError => e
379
+ warn_if_debug("[Openlayer] Failed to extract steps: #{e.message}")
380
+ nil
381
+ end
382
+
383
+ # Extract data from a single step
384
+ #
385
+ # @param step [Object] Step object
386
+ # @param original_query [String, nil] Original user query for comparison
387
+ # @return [Hash, nil] Step hash or nil
388
+ def self.extract_step_data(step, original_query = nil)
389
+ return nil if step.nil?
390
+
391
+ # Extract basic step info
392
+ description = safe_extract(step, :description)
393
+ state = safe_extract(step, :state)&.to_s
394
+ actions = safe_extract(step, :actions)
395
+
396
+ # Extract search action and results from first action
397
+ search_query = nil
398
+ search_results = nil
399
+
400
+ if actions && actions.respond_to?(:first)
401
+ first_action = actions.first
402
+ if first_action
403
+ search_action = safe_extract(first_action, :search_action)
404
+ observation = safe_extract(first_action, :observation)
405
+
406
+ search_query = safe_extract(search_action, :query) if search_action
407
+ search_results = safe_extract(observation, :search_results) if observation
408
+ end
409
+ end
410
+
411
+ # Build inputs showing both original and rephrased queries
412
+ inputs = if search_query
413
+ result = {rephrased_query: search_query}
414
+ result[:original_query] = original_query if original_query && original_query != search_query
415
+ result
416
+ end
417
+
418
+ # Map to OpenLayer RetrieverStep format
419
+ {
420
+ name: (description ? description.gsub(/\.\z/, "") : "Search and retrieve documents"),
421
+ type: "retriever",
422
+ inputs: inputs,
423
+ output: search_results ? {num_results: search_results.length} : nil,
424
+ documents: extract_search_results(search_results),
425
+ metadata: {
426
+ state: state,
427
+ action_type: "searchAction"
428
+ }.compact
429
+ }.compact
430
+ rescue StandardError => e
431
+ warn_if_debug("[Openlayer] Failed to extract step data: #{e.message}")
432
+ nil
433
+ end
434
+
435
+ # Extract search results as documents
436
+ #
437
+ # @param search_results [Array] Array of search result objects
438
+ # @return [Array<Hash>, nil] Array of document hashes or nil
439
+ def self.extract_search_results(search_results)
440
+ return nil if search_results.nil? || !search_results.respond_to?(:map)
441
+
442
+ search_results.map do |result|
443
+ snippet_info = safe_extract(result, :snippet_info)
444
+ snippet = if snippet_info && snippet_info.respond_to?(:first)
445
+ first_snippet = snippet_info.first
446
+ safe_extract(first_snippet, :snippet) if first_snippet
447
+ end
448
+
449
+ {
450
+ id: safe_extract(result, :document),
451
+ uri: safe_extract(result, :uri),
452
+ title: safe_extract(result, :title),
453
+ snippet: snippet
454
+ }.compact
455
+ end.compact
456
+ rescue StandardError => e
457
+ warn_if_debug("[Openlayer] Failed to extract search results: #{e.message}")
458
+ nil
459
+ end
460
+
219
461
  # Extract metadata from request and response
220
462
  #
221
463
  # @param args [Array] Positional arguments
@@ -225,6 +467,7 @@ module Openlayer
225
467
  # @return [Hash] Metadata hash
226
468
  def self.extract_metadata(args, kwargs, response, latency_ms)
227
469
  answer_data = extract_answer_data(response)
470
+ answer = response.respond_to?(:answer) ? response.answer : nil
228
471
 
229
472
  metadata = {
230
473
  provider: "google",
@@ -233,15 +476,36 @@ module Openlayer
233
476
  }
234
477
 
235
478
  # Add answer metadata
479
+ metadata[:answer_name] = answer_data[:answer_name] if answer_data[:answer_name]
236
480
  metadata[:grounding_score] = answer_data[:grounding_score] if answer_data[:grounding_score]
237
481
  metadata[:state] = answer_data[:state] if answer_data[:state]
238
482
  metadata[:citations_count] = answer_data[:citations_count] if answer_data[:citations_count]
239
483
  metadata[:references_count] = answer_data[:references_count] if answer_data[:references_count]
484
+ metadata[:answer_skipped_reasons] = answer_data[:answer_skipped_reasons] if answer_data[:answer_skipped_reasons] && !answer_data[:answer_skipped_reasons].empty?
485
+
486
+ # Add grounding information (citations, references, related questions)
487
+ if answer
488
+ citations = extract_citations(answer)
489
+ references = extract_references(answer)
490
+ related_questions = extract_related_questions(answer)
491
+
492
+ metadata[:citations] = citations if citations && !citations.empty?
493
+ metadata[:references] = references if references && !references.empty?
494
+ metadata[:relatedQuestions] = related_questions if related_questions && !related_questions.empty?
495
+
496
+ # Add query understanding info
497
+ query_understanding_info = extract_query_understanding_info(answer)
498
+ metadata[:query_understanding_info] = query_understanding_info if query_understanding_info
499
+ end
240
500
 
241
501
  # Add request metadata
242
502
  metadata[:serving_config] = extract_serving_config(args, kwargs)
243
503
  metadata[:session] = extract_session(args, kwargs)
244
504
 
505
+ # Add answer query token
506
+ answer_query_token = safe_extract(response, :answer_query_token)
507
+ metadata[:answer_query_token] = answer_query_token if answer_query_token
508
+
245
509
  # Add timing metadata
246
510
  if answer_data[:create_time] && answer_data[:complete_time]
247
511
  generation_time_ms = ((answer_data[:complete_time] - answer_data[:create_time]) * 1000).round(2)
@@ -288,6 +552,48 @@ module Openlayer
288
552
  nil
289
553
  end
290
554
 
555
+ # Extract user pseudo ID from response session
556
+ #
557
+ # @param response [Object] Response object
558
+ # @return [String, nil] User pseudo ID or nil
559
+ def self.extract_user_pseudo_id(response)
560
+ return nil unless response && response.respond_to?(:session)
561
+
562
+ session = safe_extract(response, :session)
563
+ return nil unless session
564
+
565
+ safe_extract(session, :user_pseudo_id)
566
+ rescue StandardError
567
+ nil
568
+ end
569
+
570
+ # Extract query understanding info from answer
571
+ #
572
+ # @param answer [Object] Answer object
573
+ # @return [Hash, nil] Query understanding info or nil
574
+ def self.extract_query_understanding_info(answer)
575
+ return nil unless answer && answer.respond_to?(:query_understanding_info)
576
+
577
+ query_understanding_info = safe_extract(answer, :query_understanding_info)
578
+ return nil unless query_understanding_info
579
+
580
+ result = {}
581
+
582
+ # Extract query classification info
583
+ classification_info = safe_extract(query_understanding_info, :query_classification_info)
584
+ if classification_info && classification_info.respond_to?(:map)
585
+ result[:query_classification_info] = classification_info.map do |info|
586
+ type = safe_extract(info, :type)
587
+ type ? {type: type.to_s} : nil
588
+ end.compact
589
+ end
590
+
591
+ result.empty? ? nil : result
592
+ rescue StandardError => e
593
+ warn_if_debug("[Openlayer] Failed to extract query understanding info: #{e.message}")
594
+ nil
595
+ end
596
+
291
597
  # Safely extract a field from an object
292
598
  #
293
599
  # @param obj [Object] Object to extract from
@@ -341,9 +647,18 @@ module Openlayer
341
647
  # from the singleton method context
342
648
  private_class_method :extract_query,
343
649
  :extract_answer_data,
650
+ :extract_citations,
651
+ :extract_citation_sources,
652
+ :extract_references,
653
+ :extract_related_questions,
654
+ :extract_steps,
655
+ :extract_step_data,
656
+ :extract_search_results,
344
657
  :extract_metadata,
345
658
  :extract_serving_config,
346
659
  :extract_session,
660
+ :extract_user_pseudo_id,
661
+ :extract_query_understanding_info,
347
662
  :safe_extract,
348
663
  :safe_count,
349
664
  :extract_timestamp
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Openlayer
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.1"
5
5
  end
@@ -8,13 +8,17 @@ module Openlayer
8
8
  params(
9
9
  client: T.untyped,
10
10
  openlayer_client: Openlayer::Client,
11
- inference_pipeline_id: String
11
+ inference_pipeline_id: String,
12
+ session_id: T.nilable(String),
13
+ user_id: T.nilable(String)
12
14
  ).void
13
15
  end
14
16
  def self.trace_client(
15
17
  client,
16
18
  openlayer_client:,
17
- inference_pipeline_id:
19
+ inference_pipeline_id:,
20
+ session_id: nil,
21
+ user_id: nil
18
22
  )
19
23
  end
20
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openlayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Openlayer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-02 00:00:00.000000000 Z
11
+ date: 2026-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool