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.
- checksums.yaml +4 -4
- data/.yardopts +9 -0
- data/Rakefile +21 -1
- data/docs/ASSISTANTS.md +594 -0
- data/docs/CHANGELOG.md +243 -0
- data/docs/README.md +190 -12
- data/lib/nitro_intelligence/assistant.rb +82 -3
- data/lib/nitro_intelligence/assistant_registry.rb +18 -0
- data/lib/nitro_intelligence/assistants.rb +340 -102
- data/lib/nitro_intelligence/client/base.rb +88 -8
- data/lib/nitro_intelligence/client/client.rb +22 -2
- data/lib/nitro_intelligence/client/factory.rb +12 -3
- data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +3 -1
- data/lib/nitro_intelligence/client/handlers/base_handler.rb +17 -2
- data/lib/nitro_intelligence/client/handlers/chat_handler.rb +3 -1
- data/lib/nitro_intelligence/client/handlers/decision_handler.rb +75 -0
- data/lib/nitro_intelligence/client/handlers/image_handler.rb +6 -2
- data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +30 -12
- data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +16 -3
- data/lib/nitro_intelligence/client/handlers/observed/decision_handler.rb +72 -0
- data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +20 -5
- data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +27 -9
- data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +3 -1
- data/lib/nitro_intelligence/client/observed.rb +8 -0
- data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +35 -7
- data/lib/nitro_intelligence/configuration.rb +48 -0
- data/lib/nitro_intelligence/decision.rb +105 -0
- data/lib/nitro_intelligence/deprecator.rb +10 -0
- data/lib/nitro_intelligence/langfuse_extension.rb +1 -0
- data/lib/nitro_intelligence/langfuse_tracer_provider.rb +7 -6
- data/lib/nitro_intelligence/media/audio.rb +6 -0
- data/lib/nitro_intelligence/media/image.rb +14 -1
- data/lib/nitro_intelligence/media/image_generation.rb +14 -2
- data/lib/nitro_intelligence/media/media.rb +4 -0
- data/lib/nitro_intelligence/models/model.rb +7 -0
- data/lib/nitro_intelligence/models/model_catalog.rb +3 -0
- data/lib/nitro_intelligence/models/model_factory.rb +2 -0
- data/lib/nitro_intelligence/null_cache.rb +2 -0
- data/lib/nitro_intelligence/observability/project.rb +41 -2
- data/lib/nitro_intelligence/observability/project_client.rb +10 -1
- data/lib/nitro_intelligence/observability/project_client_registry.rb +18 -0
- data/lib/nitro_intelligence/observability/prompt.rb +4 -3
- data/lib/nitro_intelligence/observability/prompt_resolver.rb +2 -0
- data/lib/nitro_intelligence/observability/prompt_store.rb +9 -0
- data/lib/nitro_intelligence/observability/upload_handler.rb +3 -1
- data/lib/nitro_intelligence/pending_review.rb +257 -0
- data/lib/nitro_intelligence/railtie.rb +10 -0
- data/lib/nitro_intelligence/reporter.rb +29 -0
- data/lib/nitro_intelligence/review_decision.rb +36 -0
- data/lib/nitro_intelligence/run_result.rb +160 -0
- data/lib/nitro_intelligence/tool_call_review_interrupt.rb +9 -13
- data/lib/nitro_intelligence/tool_call_review_validator.rb +6 -8
- data/lib/nitro_intelligence/trace.rb +8 -0
- data/lib/nitro_intelligence/version.rb +2 -1
- data/lib/nitro_intelligence.rb +40 -0
- 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 ||
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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[:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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,
|