nitro_intelligence 3.1.0 → 4.1.0

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/Rakefile +21 -1
  4. data/docs/ASSISTANTS.md +594 -0
  5. data/docs/CHANGELOG.md +243 -0
  6. data/docs/README.md +190 -12
  7. data/lib/nitro_intelligence/assistant.rb +82 -3
  8. data/lib/nitro_intelligence/assistant_registry.rb +18 -0
  9. data/lib/nitro_intelligence/assistants.rb +340 -102
  10. data/lib/nitro_intelligence/client/base.rb +88 -8
  11. data/lib/nitro_intelligence/client/client.rb +22 -2
  12. data/lib/nitro_intelligence/client/factory.rb +12 -3
  13. data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +3 -1
  14. data/lib/nitro_intelligence/client/handlers/base_handler.rb +17 -2
  15. data/lib/nitro_intelligence/client/handlers/chat_handler.rb +3 -1
  16. data/lib/nitro_intelligence/client/handlers/decision_handler.rb +75 -0
  17. data/lib/nitro_intelligence/client/handlers/image_handler.rb +6 -2
  18. data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +30 -12
  19. data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +16 -3
  20. data/lib/nitro_intelligence/client/handlers/observed/decision_handler.rb +72 -0
  21. data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +20 -5
  22. data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +27 -9
  23. data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +3 -1
  24. data/lib/nitro_intelligence/client/observed.rb +8 -0
  25. data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +35 -7
  26. data/lib/nitro_intelligence/configuration.rb +48 -0
  27. data/lib/nitro_intelligence/decision.rb +105 -0
  28. data/lib/nitro_intelligence/deprecator.rb +10 -0
  29. data/lib/nitro_intelligence/langfuse_extension.rb +1 -0
  30. data/lib/nitro_intelligence/langfuse_tracer_provider.rb +7 -6
  31. data/lib/nitro_intelligence/media/audio.rb +6 -0
  32. data/lib/nitro_intelligence/media/image.rb +14 -1
  33. data/lib/nitro_intelligence/media/image_generation.rb +14 -2
  34. data/lib/nitro_intelligence/media/media.rb +4 -0
  35. data/lib/nitro_intelligence/models/model.rb +7 -0
  36. data/lib/nitro_intelligence/models/model_catalog.rb +3 -0
  37. data/lib/nitro_intelligence/models/model_factory.rb +2 -0
  38. data/lib/nitro_intelligence/null_cache.rb +2 -0
  39. data/lib/nitro_intelligence/observability/project.rb +41 -2
  40. data/lib/nitro_intelligence/observability/project_client.rb +10 -1
  41. data/lib/nitro_intelligence/observability/project_client_registry.rb +18 -0
  42. data/lib/nitro_intelligence/observability/prompt.rb +4 -3
  43. data/lib/nitro_intelligence/observability/prompt_resolver.rb +2 -0
  44. data/lib/nitro_intelligence/observability/prompt_store.rb +9 -0
  45. data/lib/nitro_intelligence/observability/upload_handler.rb +3 -1
  46. data/lib/nitro_intelligence/pending_review.rb +257 -0
  47. data/lib/nitro_intelligence/railtie.rb +10 -0
  48. data/lib/nitro_intelligence/reporter.rb +29 -0
  49. data/lib/nitro_intelligence/review_decision.rb +36 -0
  50. data/lib/nitro_intelligence/run_result.rb +160 -0
  51. data/lib/nitro_intelligence/tool_call_review_interrupt.rb +9 -13
  52. data/lib/nitro_intelligence/tool_call_review_validator.rb +6 -8
  53. data/lib/nitro_intelligence/trace.rb +8 -0
  54. data/lib/nitro_intelligence/version.rb +2 -1
  55. data/lib/nitro_intelligence.rb +40 -0
  56. metadata +12 -1
@@ -5,6 +5,7 @@ module NitroIntelligence
5
5
  module Client
6
6
  module Handlers
7
7
  module Observed
8
+ # @api private
8
9
  class ImageHandler
9
10
  def initialize(base_handler:, observer:)
10
11
  @base_handler = base_handler
@@ -18,7 +19,7 @@ module NitroIntelligence
18
19
 
19
20
  # Modifies parameters in place
20
21
  prompt = handle_prompt(parameters:)
21
- trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
22
+ trace_name = parameters[:trace_name] || prompt&.name || project.slug
22
23
 
23
24
  @observer.observe(
24
25
  "image-generation",
@@ -35,6 +36,10 @@ module NitroIntelligence
35
36
 
36
37
  private
37
38
 
39
+ def project
40
+ @observer.project_client.project
41
+ end
42
+
38
43
  def build_image_generation(message:, target_image:, reference_images:, parameters:)
39
44
  NitroIntelligence::ImageGeneration.new(message:, target_image:, reference_images:) do |config|
40
45
  config.aspect_ratio = parameters[:aspect_ratio] if parameters.key?(:aspect_ratio)
@@ -44,12 +49,17 @@ module NitroIntelligence
44
49
  end
45
50
 
46
51
  def handle_image_generation_uploads(input, output, image_generation)
52
+ # Uploads go to Cerebro's media API directly, past any masking, and a masked
53
+ # project records no input or output for a media reference to sit in.
54
+ return if project.mask_content?
55
+
47
56
  # If we are doing image generation we should upload the media to observability manually
48
57
  upload_handler = NitroIntelligence::Observability::UploadHandler.new(
49
- auth_token: @observer.project_client.project.auth_token
58
+ auth_token: project.auth_token
50
59
  )
51
60
  upload_handler.upload(
52
61
  image_generation.trace_id,
62
+ observation_id: image_generation.observation_id,
53
63
  upload_queue: Queue.new(image_generation.files)
54
64
  )
55
65
 
@@ -60,7 +70,7 @@ module NitroIntelligence
60
70
 
61
71
  def handle_prompt(parameters:)
62
72
  prompt = NitroIntelligence::Observability::PromptResolver.for(
63
- store: @observer.project_client.project.prompt_store,
73
+ store: project.prompt_store,
64
74
  parameters:
65
75
  )
66
76
  return nil if prompt.blank?
@@ -76,12 +86,17 @@ module NitroIntelligence
76
86
  end
77
87
 
78
88
  def workflow(generation:, image_generation:, parameters:)
79
- chat_completion = @base_handler.perform_request(parameters:, correlation_trace_id: generation.trace_id)
89
+ chat_completion = @base_handler.perform_request(
90
+ parameters:, correlation_trace_id: generation.trace_id, mask_content: project.mask_content?
91
+ )
80
92
 
81
93
  image_generation.trace_id = generation.trace_id
94
+ image_generation.observation_id = generation.id
82
95
  image_generation.parse_file(chat_completion)
83
96
 
84
- input = parameters[:messages]
97
+ # A copy, because recording replaces its image URLs and the caller may send the same
98
+ # messages again.
99
+ input = parameters[:messages].deep_dup
85
100
  output = chat_completion.choices.first.message.to_h
86
101
  handle_image_generation_uploads(input, output, image_generation)
87
102
 
@@ -5,7 +5,12 @@ module NitroIntelligence
5
5
  module Client
6
6
  module Handlers
7
7
  module Observed
8
+ # @api private
8
9
  class TextToSpeechHandler
10
+ # Raised by {Client::Base#text_to_speech} on an observed client when the prompt it names
11
+ # is not a text prompt.
12
+ #
13
+ # @api public
9
14
  class ObservedTextToSpeechPromptError < StandardError; end
10
15
 
11
16
  def initialize(base_handler:, observer:)
@@ -18,7 +23,7 @@ module NitroIntelligence
18
23
 
19
24
  @base_handler.validate_and_resolve!(parameters)
20
25
 
21
- trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
26
+ trace_name = parameters[:trace_name] || prompt&.name || project.slug
22
27
 
23
28
  @observer.observe(
24
29
  "text-to-speech",
@@ -28,15 +33,19 @@ module NitroIntelligence
28
33
  prompt:,
29
34
  input: message
30
35
  ) do |generation|
31
- workflow(message:, parameters:, trace_id: generation.trace_id)
36
+ workflow(generation:, message:, parameters:)
32
37
  end
33
38
  end
34
39
 
35
40
  private
36
41
 
42
+ def project
43
+ @observer.project_client.project
44
+ end
45
+
37
46
  def handle_prompt(parameters:)
38
47
  prompt = NitroIntelligence::Observability::PromptResolver.for(
39
- store: @observer.project_client.project.prompt_store,
48
+ store: project.prompt_store,
40
49
  parameters:
41
50
  )
42
51
  return nil if prompt.blank?
@@ -54,21 +63,30 @@ module NitroIntelligence
54
63
  prompt
55
64
  end
56
65
 
57
- def handle_text_to_speech_upload(tts_file, trace_id)
66
+ def handle_text_to_speech_upload(tts_file, trace_id, observation_id:)
67
+ # Uploads go to Cerebro's media API directly, past any masking. A masked
68
+ # project has no media reference to record; the observer masks the output.
69
+ return "" if project.mask_content?
70
+
58
71
  upload_handler = NitroIntelligence::Observability::UploadHandler.new(
59
- auth_token: @observer.project_client.project.auth_token
72
+ auth_token: project.auth_token
60
73
  )
74
+ audio = Audio.new(tts_file)
75
+ audio.direction = "output"
76
+
61
77
  upload_handler.upload(
62
78
  trace_id,
63
- upload_queue: Queue.new([Audio.new(tts_file)])
79
+ observation_id:,
80
+ upload_queue: Queue.new([audio])
64
81
  )
65
82
 
66
83
  uploaded_media = upload_handler.uploaded_media.first
67
84
  "@@@langfuseMedia:type=#{uploaded_media.mime_type}|id=#{uploaded_media.reference_id}|source=bytes@@@"
68
85
  end
69
86
 
70
- def workflow(message:, parameters:, trace_id:)
71
- tts = @base_handler.perform_request(message:, parameters:, correlation_trace_id: trace_id)
87
+ def workflow(generation:, message:, parameters:)
88
+ tts = @base_handler.perform_request(message:, parameters:, correlation_trace_id: generation.trace_id,
89
+ mask_content: project.mask_content?)
72
90
  output = ""
73
91
 
74
92
  Tempfile.create(["tts", ".#{parameters[:response_format]}"]) do |tempfile|
@@ -76,7 +94,7 @@ module NitroIntelligence
76
94
  tempfile.write(tts.string)
77
95
  tempfile.rewind
78
96
 
79
- output = handle_text_to_speech_upload(tempfile, trace_id)
97
+ output = handle_text_to_speech_upload(tempfile, generation.trace_id, observation_id: generation.id)
80
98
  end
81
99
 
82
100
  # Usage details and the resolved model are read off a response body, and
@@ -4,6 +4,7 @@ require "nitro_intelligence/client/handlers/base_handler"
4
4
  module NitroIntelligence
5
5
  module Client
6
6
  module Handlers
7
+ # @api private
7
8
  class TextToSpeechHandler < BaseHandler
8
9
  ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::SpeechCreateParams.fields.keys.uniq.freeze
9
10
 
@@ -12,9 +13,10 @@ module NitroIntelligence
12
13
  perform_request(message:, parameters:)
13
14
  end
14
15
 
15
- def perform_request(message: "", parameters: {}, correlation_trace_id: nil)
16
+ def perform_request(message: "", parameters: {}, correlation_trace_id: nil, mask_content: false)
16
17
  add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
17
18
  add_correlation_headers(parameters, trace_id: correlation_trace_id)
19
+ add_message_redaction_header(parameters, mask_content:)
18
20
  @client.audio.speech.create(
19
21
  input: message,
20
22
  **parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
@@ -1,5 +1,6 @@
1
1
  require "nitro_intelligence/client/handlers/observed/audio_transcription_handler"
2
2
  require "nitro_intelligence/client/handlers/observed/chat_handler"
3
+ require "nitro_intelligence/client/handlers/observed/decision_handler"
3
4
  require "nitro_intelligence/client/handlers/observed/image_handler"
4
5
  require "nitro_intelligence/client/handlers/observed/text_to_speech_handler"
5
6
  require "nitro_intelligence/observability/prompt_store"
@@ -8,6 +9,7 @@ require "nitro_intelligence/trace"
8
9
 
9
10
  module NitroIntelligence
10
11
  module Client
12
+ # @api private
11
13
  class Observed < Base
12
14
  def initialize(client:, observer:)
13
15
  super(client:)
@@ -29,6 +31,12 @@ module NitroIntelligence
29
31
  )
30
32
  end
31
33
 
34
+ def decision_handler
35
+ @decision_handler ||= Handlers::Observed::DecisionHandler.new(
36
+ base_handler: Handlers::DecisionHandler.new(client: @client), observer: @observer
37
+ )
38
+ end
39
+
32
40
  def image_handler
33
41
  @image_handler ||= Handlers::Observed::ImageHandler.new(
34
42
  base_handler: Handlers::ImageHandler.new(client: @client), observer: @observer
@@ -1,6 +1,8 @@
1
1
  module NitroIntelligence
2
2
  module Client
3
+ # @api private
3
4
  module Observers
5
+ # @api private
4
6
  class LangfuseObserver
5
7
  # The inference gateway returns its own request identifier on every
6
8
  # response, including error responses. Recording it on the observation is
@@ -25,9 +27,10 @@ module NitroIntelligence
25
27
  seed = parameters[:trace_seed]
26
28
  trace_id = NitroIntelligence::Trace.create_id(seed:) if seed.present?
27
29
 
30
+ # Into the request's own parameters, so the gateway's spend metadata names the prompt too,
31
+ # without writing into a metadata hash the caller may pass again.
28
32
  if prompt
29
- metadata[:prompt_name] = prompt.name
30
- metadata[:prompt_version] = prompt.version
33
+ metadata = parameters[:metadata] = metadata.merge(prompt_name: prompt.name, prompt_version: prompt.version)
31
34
  end
32
35
 
33
36
  metadata = metadata.transform_values(&:to_s)
@@ -55,6 +58,10 @@ module NitroIntelligence
55
58
 
56
59
  private
57
60
 
61
+ def project
62
+ @project_client.project
63
+ end
64
+
58
65
  # Applied once the response is in hand, so the observation reflects what came
59
66
  # back rather than what was asked for. Each attribute is set only when the
60
67
  # handler supplied it: an observation that records nothing is better than one
@@ -65,7 +72,12 @@ module NitroIntelligence
65
72
  def record_result(generation, trace_attributes)
66
73
  return unless trace_attributes
67
74
 
68
- handle_truncation(trace_attributes[:input], trace_attributes[:output], trace_attributes[:model])
75
+ trace_attributes = masked(trace_attributes)
76
+
77
+ # Truncation trims fields out of an output that a masked project does not record.
78
+ unless project.mask_content?
79
+ handle_truncation(trace_attributes[:input], trace_attributes[:output], trace_attributes[:model])
80
+ end
69
81
 
70
82
  RECORDED_ATTRIBUTES.each do |name|
71
83
  value = trace_attributes[name]
@@ -77,6 +89,15 @@ module NitroIntelligence
77
89
  generation.update_trace(input: trace_attributes[:input], output: trace_attributes[:output])
78
90
  end
79
91
 
92
+ def masked(trace_attributes)
93
+ return trace_attributes unless project.mask_content?
94
+
95
+ trace_attributes.merge(
96
+ input: project.mask(trace_attributes[:input]),
97
+ output: project.mask(trace_attributes[:output])
98
+ )
99
+ end
100
+
80
101
  # A response the endpoint answered but did not finish -- a generation cut off at its
81
102
  # token ceiling, say -- is neither a success worth nothing being said about nor a
82
103
  # failure the request raised on. Recording it at WARNING leaves it findable without
@@ -98,6 +119,8 @@ module NitroIntelligence
98
119
  def record_input(generation, input)
99
120
  return if input.blank?
100
121
 
122
+ input = project.mask(input)
123
+
101
124
  generation.input = input
102
125
  generation.update_trace(input:)
103
126
  end
@@ -126,15 +149,20 @@ module NitroIntelligence
126
149
  end
127
150
 
128
151
  def record_failure(generation, error)
129
- generation.update(
130
- level: "ERROR",
131
- status_message: "#{error.class}: #{error.message}".truncate(MAX_STATUS_MESSAGE_LENGTH)
132
- )
152
+ generation.update(level: "ERROR", status_message: failure_message(error))
133
153
 
134
154
  call_id = litellm_call_id(error)
135
155
  generation.metadata = { litellm_call_id: call_id } if call_id
136
156
  end
137
157
 
158
+ # The message of a gateway error can quote the request or the response, so a
159
+ # masked project records which error occurred and nothing it said.
160
+ def failure_message(error)
161
+ return error.class.to_s if project.mask_content?
162
+
163
+ "#{error.class}: #{error.message}".truncate(MAX_STATUS_MESSAGE_LENGTH)
164
+ end
165
+
138
166
  def litellm_call_id(error)
139
167
  return nil unless error.respond_to?(:headers)
140
168
 
@@ -3,22 +3,70 @@ require "logger"
3
3
  require "nitro_intelligence/null_cache"
4
4
 
5
5
  module NitroIntelligence
6
+ # The settings {NitroIntelligence.configure} yields. Each is set on the yielded object, never on
7
+ # this class.
8
+ #
9
+ # @example
10
+ # NitroIntelligence.configure do |config|
11
+ # config.logger = Rails.logger
12
+ # config.inference_api_key = "..."
13
+ # end
6
14
  class Configuration
7
15
  include ActiveSupport::Configurable
8
16
 
17
+ # @!attribute [rw] logger
18
+ # @return [Logger] where diagnostic output goes. Defaults to standard output.
9
19
  config_accessor :logger, default: Logger.new($stdout)
20
+ # @!attribute [rw] cache_provider
21
+ # @return [ActiveSupport::Cache::Store] the cache prompts are kept in, normally `Rails.cache`.
22
+ # Defaults to one that caches nothing.
10
23
  config_accessor :cache_provider, default: NitroIntelligence::NullCache.new
24
+ # @!attribute [rw] current_revision
25
+ # @return [String] the host application's revision, such as its commit, recorded on each trace
26
+ # in Cerebro as its `release`
11
27
  config_accessor :current_revision, default: ""
28
+ # @!attribute [rw] environment
29
+ # @return [String] the runtime environment, such as `Rails.env`, recorded on traces and scores.
30
+ # Defaults to `"test"`.
12
31
  config_accessor :environment, default: "test"
32
+ # @!attribute [rw] assistants_config
33
+ # @return [Hash] the assistants {NitroIntelligence.assistants} makes addressable by key: shared
34
+ # `base_url` and `user_id`, and one entry per assistant under `definitions`. Without
35
+ # `definitions` it is read as the keyword arguments of a single {Assistants} client. See the
36
+ # README's "Assistants" section.
13
37
  config_accessor :assistants_config, default: {}
38
+ # @!attribute [rw] inference_api_key
39
+ # @return [String] the API key for the inference gateway
14
40
  config_accessor :inference_api_key, default: ""
41
+ # @!attribute [rw] inference_base_url
42
+ # @return [String] the inference gateway's base URL. Defaults to the shared gateway.
15
43
  config_accessor :inference_base_url, default: "https://inference.powerhome.ai"
44
+ # @!attribute [rw] inference_max_retries
45
+ # @return [Integer] how many times the client retries a failed request itself. Defaults to 0:
46
+ # see the README's "Retries and Timeouts".
47
+ config_accessor :inference_max_retries, default: 0
48
+ # @!attribute [rw] inference_timeout
49
+ # @return [Numeric, nil] the request timeout in seconds. Nil keeps openai-ruby's default of 600.
50
+ config_accessor :inference_timeout, default: nil
51
+ # @!attribute [rw] model_config
52
+ # @return [Hash] the default model for each modality (`default_text_model`,
53
+ # `default_audio_transcription_model`, `default_image_model`,
54
+ # `default_text_to_speech_model`) and the per-model settings under `models`. See the README's
55
+ # "Configuration".
16
56
  config_accessor :model_config, default: {}
57
+ # @!attribute [rw] observability_base_url
58
+ # @return [String] Cerebro's base URL. Defaults to the shared deployment.
17
59
  config_accessor :observability_base_url, default: "https://cerebro.powerhome.ai"
60
+ # @!attribute [rw] observability_projects
61
+ # @return [Array<Hash>] the credentials of each Cerebro project the host reports to, each with
62
+ # `slug`, `id`, `public_key` and `secret_key`
18
63
  config_accessor :observability_projects, default: []
64
+ # @!attribute [rw] observability_user_id
65
+ # @return [String] the user recorded on observed traces when a request passes no `user_id`
19
66
  config_accessor :observability_user_id, default: ""
20
67
 
21
68
  class << self
69
+ # @api private
22
70
  def configure
23
71
  yield config
24
72
  end
@@ -0,0 +1,105 @@
1
+ module NitroIntelligence
2
+ # What {Client::Base#decide} returns: a decision model's answers to the questions asked of it,
3
+ # keyed by the names the questions were given. See https://docs.typesafe.ai/api/system-one.md
4
+ class Decision
5
+ # The answer to a `noul` question.
6
+ #
7
+ # @!attribute [r] noul
8
+ # @return [Float] the probability, from 0 to 1, that the answer is yes
9
+ NoulAnswer = Data.define(:noul)
10
+
11
+ # The answer to a `choice` question.
12
+ #
13
+ # @!attribute [r] choice
14
+ # @return [String] the most probable option's name
15
+ # @!attribute [r] confidence
16
+ # @return [Float] how concentrated the probabilities are, from 0 to 1
17
+ # @!attribute [r] probabilities
18
+ # @return [Hash{String => Float}] the probability of every option, keyed by option name
19
+ ChoiceAnswer = Data.define(:choice, :confidence, :probabilities)
20
+
21
+ # The answer to a `score` question, whose levels are numbered from 0 in the order the
22
+ # question's `criteria` listed them.
23
+ #
24
+ # @!attribute [r] score
25
+ # @return [Float] the expected level, which may fall between levels
26
+ # @!attribute [r] confidence
27
+ # @return [Float] how concentrated the probabilities are, from 0 to 1
28
+ # @!attribute [r] probabilities
29
+ # @return [Hash{Integer, String => Float}] the probability of every level, keyed by level
30
+ # number, or by the key the gateway sent where that is not a number
31
+ # @!attribute [r] legend
32
+ # @return [Hash{Integer, String => String, Hash, Array}] the description of every level, as the
33
+ # question's `criteria` gave it, keyed like `probabilities`
34
+ ScoreAnswer = Data.define(:score, :confidence, :probabilities, :legend)
35
+
36
+ # @return [String, nil] the model that answered, as the gateway named it; nil when the
37
+ # response did not name one
38
+ attr_reader :model
39
+ # @return [ActiveSupport::HashWithIndifferentAccess] the answers keyed by question name, each a
40
+ # {NoulAnswer}, {ChoiceAnswer} or {ScoreAnswer}, or, for an answer of any other type, what the
41
+ # gateway sent
42
+ attr_reader :answers
43
+ # @return [Hash] the answers as the gateway sent them, keyed by question name
44
+ attr_reader :raw_answers
45
+ # @return [ActiveSupport::HashWithIndifferentAccess] the token counts the gateway reported
46
+ attr_reader :usage
47
+ # @api private
48
+ attr_reader :response
49
+
50
+ # @!method last_response
51
+ # @return [OpenAI::ResponseMetadata, nil] the HTTP response, including the
52
+ # gateway's headers
53
+ delegate :last_response, to: :response
54
+
55
+ # @api private
56
+ def initialize(response)
57
+ @response = response
58
+ @model = response.model
59
+ @raw_answers = response[:answers].to_h
60
+ @answers = raw_answers.to_h { |name, answer| [name, build_answer(answer)] }.with_indifferent_access
61
+ @usage = response[:usage].to_h.with_indifferent_access
62
+ end
63
+
64
+ private
65
+
66
+ # A `box` answer, and any answer type added after this client, is returned as the hash it
67
+ # arrived as rather than dropped. An answer that is not a hash at all is returned as it came
68
+ # too, so it does not cost the caller every other answer in a response already paid for. Every
69
+ # answer is built from a copy, so changing one leaves `raw_answers` as the gateway sent it.
70
+ def build_answer(answer)
71
+ answer = answer.deep_dup
72
+ return answer unless answer.is_a?(Hash)
73
+
74
+ case answer[:type]
75
+ when "noul"
76
+ NoulAnswer.new(noul: answer[:noul])
77
+ when "choice"
78
+ ChoiceAnswer.new(
79
+ choice: answer[:choice],
80
+ confidence: answer[:confidence],
81
+ probabilities: answer[:probabilities].to_h.transform_keys(&:to_s)
82
+ )
83
+ when "score"
84
+ ScoreAnswer.new(
85
+ score: answer[:score],
86
+ confidence: answer[:confidence],
87
+ probabilities: levels(answer[:probabilities]),
88
+ legend: levels(answer[:legend])
89
+ )
90
+ else
91
+ answer
92
+ end
93
+ end
94
+
95
+ # Levels arrive as JSON object keys, so "0", "1" and so on. A key that is not a number is
96
+ # kept as it came rather than failing every other answer in the response.
97
+ def levels(hash)
98
+ hash.to_h.transform_keys do |level|
99
+ Integer(level.to_s, 10)
100
+ rescue ArgumentError
101
+ level.to_s
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,10 @@
1
+ require "active_support/deprecation"
2
+
3
+ module NitroIntelligence
4
+ # Removed in the next major release. A Rails host has it registered with its own deprecators by
5
+ # the Railtie, so its `config.active_support` deprecation settings (log, raise, silence) apply;
6
+ # elsewhere it warns on stderr.
7
+ def self.deprecator
8
+ @deprecator ||= ActiveSupport::Deprecation.new("5.0", "nitro_intelligence")
9
+ end
10
+ end
@@ -11,6 +11,7 @@
11
11
  require "nitro_intelligence/langfuse_tracer_provider"
12
12
 
13
13
  module NitroIntelligence
14
+ # @api private
14
15
  class LangfuseExtension
15
16
  attr_reader :config
16
17
 
@@ -1,10 +1,11 @@
1
- # This is an adaptation of https://github.com/simplepractice/langfuse-rb/blob/main/lib/langfuse/otel_setup.rb
2
- # that lets users create multiple Langfuse clients in the same process.
3
- # See also components/nitro_intelligence/lib/nitro_intelligence/langfuse_extension.rb
4
- #
5
- # The content of this file should eventually make its way upstream.
6
- #
7
1
  module NitroIntelligence
2
+ # This is an adaptation of https://github.com/simplepractice/langfuse-rb/blob/main/lib/langfuse/otel_setup.rb
3
+ # that lets users create multiple Langfuse clients in the same process.
4
+ # See also components/nitro_intelligence/lib/nitro_intelligence/langfuse_extension.rb
5
+ #
6
+ # The content of this file should eventually make its way upstream.
7
+ #
8
+ # @api private
8
9
  class LangfuseTracerProvider
9
10
  def initialize(config)
10
11
  @trace_id_key = OpenTelemetry::Context.create_key("nitro-intelligence-trace-id")
@@ -1,7 +1,13 @@
1
1
  require "nitro_intelligence/media/media"
2
2
 
3
3
  module NitroIntelligence
4
+ # @api private
4
5
  class Audio < Media
6
+ # Raised by {Client::Base#transcribe_audio} on an observed client whose project does not mask
7
+ # content, when `audio_file` has no path. It is raised while uploading the audio to Cerebro,
8
+ # after the gateway has transcribed it, so the transcription is lost.
9
+ #
10
+ # @api public
5
11
  class AudioFileFormatError < StandardError; end
6
12
 
7
13
  def initialize(file)
@@ -4,9 +4,21 @@ require "mini_magick"
4
4
  require "nitro_intelligence/media/media"
5
5
 
6
6
  module NitroIntelligence
7
+ # An image, such as {ImageGeneration#generated_image}.
8
+ #
9
+ # @!attribute [r] byte_string
10
+ # @return [String] the image's contents, as a byte string
11
+ # @!attribute [r] file_extension
12
+ # @return [String] the image's extension, such as `"png"`, without a dot
13
+ # @!attribute [r] mime_type
14
+ # @return [String] the image's MIME type, such as `"image/png"`
7
15
  class Image < Media
8
- attr_reader :height, :width
16
+ # @return [Integer] in pixels
17
+ attr_reader :height
18
+ # @return [Integer] in pixels
19
+ attr_reader :width
9
20
 
21
+ # @api private
10
22
  def self.from_base64(base64_string)
11
23
  # Strip data_uri from string
12
24
  base64_string = base64_string.sub(/^data:[^;]*;base64,/, "")
@@ -15,6 +27,7 @@ module NitroIntelligence
15
27
  new(byte_string)
16
28
  end
17
29
 
30
+ # @api private
18
31
  def initialize(file)
19
32
  super
20
33
 
@@ -7,7 +7,9 @@ require "time"
7
7
  require "nitro_intelligence/media/image"
8
8
 
9
9
  module NitroIntelligence
10
+ # What {Client::Base#generate_image} returns.
10
11
  class ImageGeneration
12
+ # @api private
11
13
  class Config
12
14
  DEFAULT_ASPECT_RATIO = "1:1".freeze
13
15
  DEFAULT_RESOLUTION = "1K".freeze
@@ -21,10 +23,17 @@ module NitroIntelligence
21
23
  end
22
24
  end
23
25
 
24
- attr_reader :byte_string, :config, :file_type, :file_extension, :generated_image, :messages, :reference_images,
25
- :target_image
26
+ # @api private
27
+ attr_reader :byte_string, :config, :file_type, :file_extension, :messages, :reference_images, :target_image
28
+ # @return [Image, nil] the generated image; nil when the model returned none
29
+ attr_reader :generated_image
30
+ # @return [String, nil] the trace the request was recorded under; nil without observability
26
31
  attr_accessor :trace_id
32
+ # @return [String, nil] the generation observation the images are attached to; nil without
33
+ # observability
34
+ attr_accessor :observation_id
27
35
 
36
+ # @api private
28
37
  def initialize(
29
38
  message: "",
30
39
  target_image: nil,
@@ -35,6 +44,7 @@ module NitroIntelligence
35
44
  @model = NitroIntelligence.model_catalog.lookup_by_name(@config.model)
36
45
  @reference_images = reference_images.map { |img| Image.new(img) }
37
46
  @trace_id = nil
47
+ @observation_id = nil
38
48
 
39
49
  # Overrides
40
50
  yield(@config) if block_given?
@@ -48,10 +58,12 @@ module NitroIntelligence
48
58
  build_messages(message, @target_image, @reference_images)
49
59
  end
50
60
 
61
+ # @api private
51
62
  def files
52
63
  [target_image, reference_images, generated_image].flatten.compact
53
64
  end
54
65
 
66
+ # @api private
55
67
  def parse_file(chat_completion)
56
68
  base64_string = chat_completion.choices.first&.message.to_h.fetch(:images, {})&.first&.dig(:image_url, :url)
57
69
 
@@ -1,6 +1,10 @@
1
1
  require "base64"
2
2
 
3
3
  module NitroIntelligence
4
+ # A file sent to or returned by a model. Callers see it as an {Image}, which documents the readers
5
+ # they use.
6
+ #
7
+ # @api private
4
8
  class Media
5
9
  attr_accessor :direction, :reference_id
6
10
  attr_reader :base64, :byte_string, :file_extension, :file_type, :mime_type
@@ -1,4 +1,5 @@
1
1
  module NitroIntelligence
2
+ # @api private
2
3
  class Model
3
4
  attr_reader :name, :omit_output_fields
4
5
 
@@ -8,6 +9,7 @@ module NitroIntelligence
8
9
  end
9
10
  end
10
11
 
12
+ # @api private
11
13
  class ImageModel < Model
12
14
  attr_reader :aspect_ratios, :resolutions
13
15
 
@@ -18,8 +20,13 @@ module NitroIntelligence
18
20
  end
19
21
  end
20
22
 
23
+ # @api private
24
+ class DecisionModel < Model; end
25
+
26
+ # @api private
21
27
  class TextModel < Model; end
22
28
 
29
+ # @api private
23
30
  class TextToSpeechModel < Model
24
31
  attr_reader :default_voice,
25
32
  :default_response_format,