openlayer 0.8.0 → 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: d27be2afe4632dccf14077f8fecaf96b1e67d6cf81fb4bca4917d9a31a6bd66b
4
- data.tar.gz: ba4f55cf2470fcad21b26531a3adfc0d59cdfae106cdd7ca65ad0d072093d618
3
+ metadata.gz: e2012084e7121e1610abd934759b02dea258adbf82bb08fed4975e793793b0bf
4
+ data.tar.gz: 2326a997d8b2a24a01e80007e002f5141a6b0ce5b06e769350172689f68197ef
5
5
  SHA512:
6
- metadata.gz: 0313bd3321521fb207a7aa14844cb4e5f91131457baaff2b8451b5b0ec750f646e747694301c9b284d4ca86e49dc61bac63e63db0bcbe62a58abbf7e4d97e2d9
7
- data.tar.gz: 1da5a6642fb3493d2484c67314f6ded8d619edab534b6d455a382caba85fdd6ec61839dabfabaff2c8be2c5ba38643680d4196fbc24316f685be460ba0430598
6
+ metadata.gz: 7d39d0ebbd0068ff855e3071021c6d8ab30d01c0c76c12ae61cdd3419eef0400b33f68249aeb682768e96b7f9c5a0bd72dfb6607cff34830d2c616c04ac83be5
7
+ data.tar.gz: 2a766e20dcbf8f279658818a25a2c5fcd6739ca208ef9a91313e90943434e4256326f00bb699adfa8e21269c36f363a6709bfac2bff79d7d5f7a8e10a5555c1e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 0.8.0 (2026-01-05)
4
13
 
5
14
  Full Changelog: [v0.7.1...v0.8.0](https://github.com/openlayer-ai/openlayer-ruby/compare/v0.7.1...v0.8.0)
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.8.0"
20
+ gem "openlayer", "~> 0.8.1"
21
21
  ```
22
22
 
23
23
  <!-- x-release-please-end -->
@@ -113,6 +113,51 @@ module Openlayer
113
113
  prompt_tokens = (query_text.length / 4.0).ceil
114
114
  completion_tokens = (answer_data[:answer_text].length / 4.0).ceil
115
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
+
116
161
  # Build trace data in Openlayer format
117
162
  trace_data = {
118
163
  config: {
@@ -128,31 +173,17 @@ module Openlayer
128
173
  latency_ms: latency_ms,
129
174
  timestamp: start_time.to_i,
130
175
  metadata: metadata,
131
- steps: [
132
- {
133
- name: "Conversational Search answer_query",
134
- type: "chat_completion",
135
- provider: "Google",
136
- startTime: start_time.to_i,
137
- endTime: end_time.to_i,
138
- latency: latency_ms,
139
- metadata: metadata,
140
- inputs: {
141
- prompt: [
142
- {role: "user", content: query_text}
143
- ]
144
- },
145
- output: answer_data[:answer_text],
146
- promptTokens: prompt_tokens,
147
- completionTokens: completion_tokens,
148
- tokens: prompt_tokens + completion_tokens,
149
- model: "google-discovery-engine"
150
- }
151
- ]
176
+ steps: [step]
152
177
  }
153
178
  ]
154
179
  }
155
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
+
156
187
  # Determine which session to use (kwarg takes precedence over auto-extracted)
157
188
  final_session = session_id || metadata[:session]
158
189
  if final_session
@@ -160,9 +191,11 @@ module Openlayer
160
191
  trace_data[:config][:sessionIdColumnName] = "session_id"
161
192
  end
162
193
 
163
- # Add user_id if provided
164
- if user_id
165
- trace_data[:rows][0][:user_id] = user_id
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
166
199
  trace_data[:config][:userIdColumnName] = "user_id"
167
200
  end
168
201
 
@@ -223,20 +256,208 @@ module Openlayer
223
256
  answer = response.answer
224
257
  return {answer_text: nil} if answer.nil?
225
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
+
226
265
  {
227
266
  answer_text: safe_extract(answer, :answer_text),
267
+ answer_name: safe_extract(answer, :name),
228
268
  state: safe_extract(answer, :state)&.to_s,
229
269
  grounding_score: safe_extract(answer, :grounding_score),
230
270
  create_time: extract_timestamp(answer, :create_time),
231
271
  complete_time: extract_timestamp(answer, :complete_time),
232
272
  citations_count: safe_count(answer, :citations),
233
- references_count: safe_count(answer, :references)
273
+ references_count: safe_count(answer, :references),
274
+ answer_skipped_reasons: answer_skipped_reasons
234
275
  }
235
276
  rescue StandardError => e
236
277
  warn_if_debug("[Openlayer] Failed to extract answer data: #{e.message}")
237
278
  {answer_text: nil}
238
279
  end
239
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
+
240
461
  # Extract metadata from request and response
241
462
  #
242
463
  # @param args [Array] Positional arguments
@@ -246,6 +467,7 @@ module Openlayer
246
467
  # @return [Hash] Metadata hash
247
468
  def self.extract_metadata(args, kwargs, response, latency_ms)
248
469
  answer_data = extract_answer_data(response)
470
+ answer = response.respond_to?(:answer) ? response.answer : nil
249
471
 
250
472
  metadata = {
251
473
  provider: "google",
@@ -254,15 +476,36 @@ module Openlayer
254
476
  }
255
477
 
256
478
  # Add answer metadata
479
+ metadata[:answer_name] = answer_data[:answer_name] if answer_data[:answer_name]
257
480
  metadata[:grounding_score] = answer_data[:grounding_score] if answer_data[:grounding_score]
258
481
  metadata[:state] = answer_data[:state] if answer_data[:state]
259
482
  metadata[:citations_count] = answer_data[:citations_count] if answer_data[:citations_count]
260
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
261
500
 
262
501
  # Add request metadata
263
502
  metadata[:serving_config] = extract_serving_config(args, kwargs)
264
503
  metadata[:session] = extract_session(args, kwargs)
265
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
+
266
509
  # Add timing metadata
267
510
  if answer_data[:create_time] && answer_data[:complete_time]
268
511
  generation_time_ms = ((answer_data[:complete_time] - answer_data[:create_time]) * 1000).round(2)
@@ -309,6 +552,48 @@ module Openlayer
309
552
  nil
310
553
  end
311
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
+
312
597
  # Safely extract a field from an object
313
598
  #
314
599
  # @param obj [Object] Object to extract from
@@ -362,9 +647,18 @@ module Openlayer
362
647
  # from the singleton method context
363
648
  private_class_method :extract_query,
364
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,
365
657
  :extract_metadata,
366
658
  :extract_serving_config,
367
659
  :extract_session,
660
+ :extract_user_pseudo_id,
661
+ :extract_query_understanding_info,
368
662
  :safe_extract,
369
663
  :safe_count,
370
664
  :extract_timestamp
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Openlayer
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  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.8.0
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-05 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