nitro_intelligence 0.0.1 → 1.0.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/docs/README.md +83 -11
- data/lib/nitro_intelligence/agent_server.rb +119 -8
- data/lib/nitro_intelligence/client/base.rb +52 -0
- data/lib/nitro_intelligence/client/client.rb +13 -0
- data/lib/nitro_intelligence/client/factory.rb +53 -0
- data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +38 -0
- data/lib/nitro_intelligence/client/handlers/chat_handler.rb +41 -0
- data/lib/nitro_intelligence/client/handlers/image_handler.rb +61 -0
- data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +90 -0
- data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +74 -0
- data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +109 -0
- data/lib/nitro_intelligence/client/observed.rb +38 -0
- data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +75 -0
- data/lib/nitro_intelligence/configuration.rb +2 -0
- data/lib/nitro_intelligence/langfuse_extension.rb +10 -53
- data/lib/nitro_intelligence/media/audio.rb +50 -0
- data/lib/nitro_intelligence/media/image_generation.rb +4 -2
- data/lib/nitro_intelligence/models/model_catalog.rb +7 -2
- data/lib/nitro_intelligence/observability/project.rb +33 -0
- data/lib/nitro_intelligence/observability/project_client.rb +18 -0
- data/lib/nitro_intelligence/observability/project_client_registry.rb +44 -0
- data/lib/nitro_intelligence/observability/prompt.rb +58 -0
- data/lib/nitro_intelligence/observability/prompt_store.rb +112 -0
- data/lib/nitro_intelligence/observability/upload_handler.rb +138 -0
- data/lib/nitro_intelligence/reporter.rb +43 -0
- data/lib/nitro_intelligence/tool_call_review_validator.rb +69 -0
- data/lib/nitro_intelligence/trace.rb +2 -2
- data/lib/nitro_intelligence/version.rb +1 -1
- data/lib/nitro_intelligence.rb +10 -44
- metadata +26 -10
- data/lib/nitro_intelligence/client.rb +0 -337
- data/lib/nitro_intelligence/media/upload_handler.rb +0 -135
- data/lib/nitro_intelligence/prompt/prompt.rb +0 -56
- data/lib/nitro_intelligence/prompt/prompt_store.rb +0 -110
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module NitroIntelligence
|
|
2
|
+
module Client
|
|
3
|
+
module Handlers
|
|
4
|
+
module Observed
|
|
5
|
+
class ChatHandler
|
|
6
|
+
def initialize(base_handler:, observer:)
|
|
7
|
+
@base_handler = base_handler
|
|
8
|
+
@observer = observer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create(message: "", parameters: {})
|
|
12
|
+
@base_handler.validate_and_resolve!(parameters, message)
|
|
13
|
+
|
|
14
|
+
prompt = handle_prompt(parameters:)
|
|
15
|
+
trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
|
|
16
|
+
|
|
17
|
+
@observer.observe(
|
|
18
|
+
"chat-completion",
|
|
19
|
+
type: :generation,
|
|
20
|
+
parameters:,
|
|
21
|
+
trace_name:,
|
|
22
|
+
prompt:
|
|
23
|
+
) do |_generation|
|
|
24
|
+
workflow(parameters:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def handle_prompt(parameters:)
|
|
31
|
+
return nil if parameters[:prompt_name].blank?
|
|
32
|
+
|
|
33
|
+
prompt = @observer.project_client.project.prompt_store.get_prompt(
|
|
34
|
+
prompt_name: parameters[:prompt_name],
|
|
35
|
+
prompt_label: parameters[:prompt_label],
|
|
36
|
+
prompt_version: parameters[:prompt_version]
|
|
37
|
+
)
|
|
38
|
+
prompt_variables = parameters[:prompt_variables] || {}
|
|
39
|
+
|
|
40
|
+
if prompt.present?
|
|
41
|
+
parameters[:messages] = prompt.interpolate(
|
|
42
|
+
messages: parameters[:messages],
|
|
43
|
+
variables: prompt_variables
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
prompt
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def workflow(parameters:)
|
|
53
|
+
chat_completion = @base_handler.perform_request(parameters:)
|
|
54
|
+
input = parameters[:messages]
|
|
55
|
+
output = chat_completion.choices.first.message.to_h
|
|
56
|
+
|
|
57
|
+
trace_attributes = {
|
|
58
|
+
model: chat_completion.model,
|
|
59
|
+
input:,
|
|
60
|
+
output:,
|
|
61
|
+
usage_details: {
|
|
62
|
+
prompt_tokens: chat_completion.usage.prompt_tokens,
|
|
63
|
+
completion_tokens: chat_completion.usage.completion_tokens,
|
|
64
|
+
total_tokens: chat_completion.usage.total_tokens,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[chat_completion, trace_attributes]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require "nitro_intelligence/media/image_generation"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
module Handlers
|
|
6
|
+
module Observed
|
|
7
|
+
class ImageHandler
|
|
8
|
+
def initialize(base_handler:, observer:)
|
|
9
|
+
@base_handler = base_handler
|
|
10
|
+
@observer = observer
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(message: "", target_image: nil, reference_images: [], parameters: {})
|
|
14
|
+
image_generation = build_image_generation(message:, target_image:, reference_images:, parameters:)
|
|
15
|
+
|
|
16
|
+
@base_handler.validate_and_resolve!(parameters, image_generation)
|
|
17
|
+
|
|
18
|
+
# Modifies parameters in place
|
|
19
|
+
prompt = handle_prompt(parameters:)
|
|
20
|
+
trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
|
|
21
|
+
|
|
22
|
+
@observer.observe(
|
|
23
|
+
"image-generation",
|
|
24
|
+
type: :generation,
|
|
25
|
+
parameters:,
|
|
26
|
+
trace_name:,
|
|
27
|
+
prompt:
|
|
28
|
+
) do |generation|
|
|
29
|
+
workflow(generation:, image_generation:, parameters:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
image_generation
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_image_generation(message:, target_image:, reference_images:, parameters:)
|
|
38
|
+
NitroIntelligence::ImageGeneration.new(message:, target_image:, reference_images:) do |config|
|
|
39
|
+
config.aspect_ratio = parameters[:aspect_ratio] if parameters.key?(:aspect_ratio)
|
|
40
|
+
config.model = parameters[:model] if parameters.key?(:model)
|
|
41
|
+
config.resolution = parameters[:resolution] if parameters.key?(:resolution)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_image_generation_uploads(input, output, image_generation)
|
|
46
|
+
# If we are doing image generation we should upload the media to observability manually
|
|
47
|
+
upload_handler = NitroIntelligence::Observability::UploadHandler.new(
|
|
48
|
+
auth_token: @observer.project_client.project.auth_token
|
|
49
|
+
)
|
|
50
|
+
upload_handler.upload(
|
|
51
|
+
image_generation.trace_id,
|
|
52
|
+
upload_queue: Queue.new(image_generation.files)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Replace base64 strings with media references
|
|
56
|
+
upload_handler.replace_base64_with_media_references(input)
|
|
57
|
+
upload_handler.replace_base64_with_media_references(output)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_prompt(parameters:)
|
|
61
|
+
return nil if parameters[:prompt_name].blank?
|
|
62
|
+
|
|
63
|
+
prompt = @observer.project_client.project.prompt_store.get_prompt(
|
|
64
|
+
prompt_name: parameters[:prompt_name],
|
|
65
|
+
prompt_label: parameters[:prompt_label],
|
|
66
|
+
prompt_version: parameters[:prompt_version]
|
|
67
|
+
)
|
|
68
|
+
prompt_variables = parameters[:prompt_variables] || {}
|
|
69
|
+
|
|
70
|
+
if prompt.present?
|
|
71
|
+
parameters[:messages] = prompt.interpolate(
|
|
72
|
+
messages: parameters[:messages],
|
|
73
|
+
variables: prompt_variables
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
prompt
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def workflow(generation:, image_generation:, parameters:)
|
|
83
|
+
chat_completion = @base_handler.perform_request(parameters:)
|
|
84
|
+
|
|
85
|
+
image_generation.trace_id = generation.trace_id
|
|
86
|
+
image_generation.parse_file(chat_completion)
|
|
87
|
+
|
|
88
|
+
input = parameters[:messages]
|
|
89
|
+
output = chat_completion.choices.first.message.to_h
|
|
90
|
+
handle_image_generation_uploads(input, output, image_generation)
|
|
91
|
+
|
|
92
|
+
trace_attributes = {
|
|
93
|
+
model: chat_completion.model,
|
|
94
|
+
input:,
|
|
95
|
+
output:,
|
|
96
|
+
usage_details: {
|
|
97
|
+
prompt_tokens: chat_completion.usage.prompt_tokens,
|
|
98
|
+
completion_tokens: chat_completion.usage.completion_tokens,
|
|
99
|
+
total_tokens: chat_completion.usage.total_tokens,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[chat_completion, trace_attributes]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "nitro_intelligence/client/handlers/observed/audio_transcription_handler"
|
|
2
|
+
require "nitro_intelligence/client/handlers/observed/chat_handler"
|
|
3
|
+
require "nitro_intelligence/client/handlers/observed/image_handler"
|
|
4
|
+
require "nitro_intelligence/observability/prompt_store"
|
|
5
|
+
require "nitro_intelligence/observability/upload_handler"
|
|
6
|
+
require "nitro_intelligence/trace"
|
|
7
|
+
|
|
8
|
+
module NitroIntelligence
|
|
9
|
+
module Client
|
|
10
|
+
class Observed < Base
|
|
11
|
+
def initialize(client:, observer:)
|
|
12
|
+
super(client:)
|
|
13
|
+
@observer = observer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def chat_handler
|
|
19
|
+
@chat_handler ||= Handlers::Observed::ChatHandler.new(
|
|
20
|
+
base_handler: Handlers::ChatHandler.new(client: @client), observer: @observer
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def audio_transcription_handler
|
|
25
|
+
@audio_transcription_handler ||=
|
|
26
|
+
Handlers::Observed::AudioTranscriptionHandler.new(
|
|
27
|
+
base_handler: Handlers::AudioTranscriptionHandler.new(client: @client), observer: @observer
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def image_handler
|
|
32
|
+
@image_handler ||= Handlers::Observed::ImageHandler.new(
|
|
33
|
+
base_handler: Handlers::ImageHandler.new(client: @client), observer: @observer
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module NitroIntelligence
|
|
2
|
+
module Client
|
|
3
|
+
module Observers
|
|
4
|
+
class LangfuseObserver
|
|
5
|
+
attr_reader :project_client
|
|
6
|
+
|
|
7
|
+
def initialize(project_client:)
|
|
8
|
+
@project_client = project_client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def observe(operation_name, type:, parameters:, trace_name:, prompt: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
12
|
+
metadata = parameters[:metadata]
|
|
13
|
+
seed = parameters[:trace_seed]
|
|
14
|
+
user_id = parameters[:user_id] || NitroIntelligence.configuration.observability_user_id
|
|
15
|
+
trace_id = NitroIntelligence::Trace.create_id(seed:) if seed.present?
|
|
16
|
+
|
|
17
|
+
if prompt
|
|
18
|
+
metadata[:prompt_name] = prompt.name
|
|
19
|
+
metadata[:prompt_version] = prompt.version
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
metadata = metadata.transform_values(&:to_s)
|
|
23
|
+
|
|
24
|
+
Langfuse.propagate_attributes(
|
|
25
|
+
user_id:,
|
|
26
|
+
metadata:
|
|
27
|
+
) do
|
|
28
|
+
@project_client.observability_client.observe(
|
|
29
|
+
operation_name,
|
|
30
|
+
as_type: type,
|
|
31
|
+
trace_id:,
|
|
32
|
+
environment: NitroIntelligence.environment.to_s,
|
|
33
|
+
model: parameters[:model],
|
|
34
|
+
metadata:
|
|
35
|
+
) do |generation|
|
|
36
|
+
generation.update_trace(name: trace_name, release: NitroIntelligence.configuration.current_revision)
|
|
37
|
+
generation.update({ prompt: { name: prompt.name, version: prompt.version } }) if prompt
|
|
38
|
+
|
|
39
|
+
result, trace_attributes = yield(generation)
|
|
40
|
+
|
|
41
|
+
if trace_attributes
|
|
42
|
+
handle_truncation(trace_attributes[:input], trace_attributes[:output], trace_attributes[:model])
|
|
43
|
+
|
|
44
|
+
generation.model = trace_attributes[:model] if trace_attributes[:model]
|
|
45
|
+
generation.usage_details = trace_attributes[:usage_details] if trace_attributes[:usage_details]
|
|
46
|
+
generation.input = trace_attributes[:input] if trace_attributes[:input]
|
|
47
|
+
generation.output = trace_attributes[:output] if trace_attributes[:output]
|
|
48
|
+
|
|
49
|
+
generation.update_trace(input: trace_attributes[:input], output: trace_attributes[:output])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def handle_truncation(_input, output, model_name)
|
|
60
|
+
model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
|
|
61
|
+
|
|
62
|
+
return unless model&.omit_output_fields
|
|
63
|
+
|
|
64
|
+
model.omit_output_fields.each do |omit_output_field|
|
|
65
|
+
last_key = omit_output_field.last
|
|
66
|
+
parent_keys = omit_output_field[0...-1]
|
|
67
|
+
parent = parent_keys.empty? ? output : output.dig(*parent_keys)
|
|
68
|
+
|
|
69
|
+
parent[last_key] = "[Truncated...]" if parent.is_a?(Hash) && parent.key?(last_key)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -8,6 +8,7 @@ module NitroIntelligence
|
|
|
8
8
|
|
|
9
9
|
config_accessor :logger, default: Logger.new($stdout)
|
|
10
10
|
config_accessor :cache_provider, default: NitroIntelligence::NullCache.new
|
|
11
|
+
config_accessor :current_revision, default: ""
|
|
11
12
|
config_accessor :environment, default: "test"
|
|
12
13
|
config_accessor :agent_server_config, default: {}
|
|
13
14
|
config_accessor :inference_api_key, default: ""
|
|
@@ -15,6 +16,7 @@ module NitroIntelligence
|
|
|
15
16
|
config_accessor :model_config, default: {}
|
|
16
17
|
config_accessor :observability_base_url, default: ""
|
|
17
18
|
config_accessor :observability_projects, default: []
|
|
19
|
+
config_accessor :observability_user_id, default: ""
|
|
18
20
|
|
|
19
21
|
class << self
|
|
20
22
|
def configure
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
# The content of this file should eventually make its way upstream. Setting a custom trace ID is
|
|
5
5
|
# already awaiting approval in https://github.com/simplepractice/langfuse-rb/pull/69.
|
|
6
6
|
#
|
|
7
|
+
|
|
8
|
+
require "nitro_intelligence/langfuse_tracer_provider"
|
|
9
|
+
|
|
7
10
|
module NitroIntelligence
|
|
8
11
|
class LangfuseExtension
|
|
9
12
|
attr_reader :config
|
|
@@ -14,7 +17,7 @@ module NitroIntelligence
|
|
|
14
17
|
|
|
15
18
|
@config = config
|
|
16
19
|
@client = Langfuse::Client.new(config)
|
|
17
|
-
@tracer_provider = LangfuseTracerProvider.new(config)
|
|
20
|
+
@tracer_provider = NitroIntelligence::LangfuseTracerProvider.new(config)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def shutdown(timeout: 30)
|
|
@@ -40,14 +43,11 @@ module NitroIntelligence
|
|
|
40
43
|
)
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
def start_observation(name, attrs = {}, as_type: :span, parent_span_context: nil, start_time: nil,
|
|
46
|
+
def start_observation(name, attrs = {}, as_type: :span, trace_id: nil, parent_span_context: nil, start_time: nil, # rubocop:disable Metrics/ParameterLists
|
|
44
47
|
skip_validation: false)
|
|
48
|
+
parent_span_context = Langfuse.send(:resolve_trace_context, trace_id, parent_span_context)
|
|
45
49
|
type_str = as_type.to_s
|
|
46
|
-
|
|
47
|
-
unless skip_validation || Langfuse.send(:valid_observation_type?, as_type)
|
|
48
|
-
valid_types = Langfuse::OBSERVATION_TYPES.values.sort.join(", ")
|
|
49
|
-
raise ArgumentError, "Invalid observation type: #{type_str}. Valid types: #{valid_types}"
|
|
50
|
-
end
|
|
50
|
+
Langfuse.send(:validate_observation_type!, as_type, type_str) unless skip_validation
|
|
51
51
|
|
|
52
52
|
otel_tracer = @tracer_provider.tracer
|
|
53
53
|
otel_span = Langfuse.send(:create_otel_span,
|
|
@@ -73,54 +73,11 @@ module NitroIntelligence
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def observe(name, attrs = {}, as_type: :span, trace_id: nil, **kwargs, &block)
|
|
76
|
-
# Merge positional attrs and keyword kwargs
|
|
77
76
|
merged_attrs = attrs.to_h.merge(kwargs)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
raise ArgumentError, "#{trace_id} is not a valid 32 lowercase hex char Langfuse trace ID"
|
|
81
|
-
end
|
|
77
|
+
observation = start_observation(name, merged_attrs, as_type:, trace_id:)
|
|
78
|
+
return observation unless block
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
end
|
|
85
|
-
observation = start_observation(name, merged_attrs, as_type:, parent_span_context:)
|
|
86
|
-
|
|
87
|
-
if block
|
|
88
|
-
# Block-based API: auto-ends when block completes
|
|
89
|
-
# Set context and execute block
|
|
90
|
-
current_context = OpenTelemetry::Context.current
|
|
91
|
-
begin
|
|
92
|
-
result = OpenTelemetry::Context.with_current(
|
|
93
|
-
OpenTelemetry::Trace.context_with_span(observation.otel_span, parent_context: current_context)
|
|
94
|
-
) do
|
|
95
|
-
yield(observation)
|
|
96
|
-
end
|
|
97
|
-
ensure
|
|
98
|
-
# Only end if not already ended (events auto-end in start_observation)
|
|
99
|
-
observation.end unless as_type.to_s == Langfuse::OBSERVATION_TYPES[:event]
|
|
100
|
-
end
|
|
101
|
-
result
|
|
102
|
-
else
|
|
103
|
-
# Stateful API - return observation
|
|
104
|
-
# Events already auto-ended in start_observation
|
|
105
|
-
observation
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
def valid_trace_id?(trace_id)
|
|
112
|
-
!!(trace_id =~ /^[0-9a-f]{32}$/)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def create_span_context_with_trace_id(trace_id_as_hex_str)
|
|
116
|
-
trace_id_as_byte_str = [trace_id_as_hex_str].pack("H*")
|
|
117
|
-
# NOTE: trace_flags must be SAMPLED or the trace will not appear in Langfuse.
|
|
118
|
-
# The Python SDK does the same: https://github.com/langfuse/langfuse-python/blob/v4.0.0/langfuse/_client/client.py#L1568
|
|
119
|
-
trace_flags = OpenTelemetry::Trace::TraceFlags::SAMPLED
|
|
120
|
-
OpenTelemetry::Trace::SpanContext.new(
|
|
121
|
-
trace_id: trace_id_as_byte_str,
|
|
122
|
-
trace_flags:
|
|
123
|
-
)
|
|
80
|
+
observation.send(:run_in_context, &block)
|
|
124
81
|
end
|
|
125
82
|
end
|
|
126
83
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "nitro_intelligence/media/media"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
class Audio < Media
|
|
5
|
+
class AudioFileFormatError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def initialize(file)
|
|
8
|
+
# TODO: Consider a library for dealing with audio files. Dirty implementation
|
|
9
|
+
raise AudioFileFormatError unless file.respond_to?(:to_path)
|
|
10
|
+
|
|
11
|
+
file_extension = File.basename(file).split(".").last
|
|
12
|
+
file = file.read
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
@file_extension = file_extension
|
|
16
|
+
@file_type = "audio"
|
|
17
|
+
@mime_type = determine_mime_type
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# These are supported by Langfuse
|
|
23
|
+
def determine_mime_type
|
|
24
|
+
ext = @file_extension.downcase
|
|
25
|
+
|
|
26
|
+
case ext
|
|
27
|
+
when "m4a", "m4b", "m4r", "m4p", "mp4"
|
|
28
|
+
"audio/mp4"
|
|
29
|
+
when "mp3", "mpga", "mp2", "mp1"
|
|
30
|
+
"audio/mp3"
|
|
31
|
+
when "wav", "wave"
|
|
32
|
+
"audio/wav"
|
|
33
|
+
when "webm", "weba"
|
|
34
|
+
"audio/webm"
|
|
35
|
+
when "ogg", "spx"
|
|
36
|
+
"audio/ogg"
|
|
37
|
+
when "oga"
|
|
38
|
+
"audio/oga"
|
|
39
|
+
when "aac"
|
|
40
|
+
"audio/aac"
|
|
41
|
+
when "flac"
|
|
42
|
+
"audio/flac"
|
|
43
|
+
when "opus"
|
|
44
|
+
"audio/opus"
|
|
45
|
+
else
|
|
46
|
+
"audio/#{ext}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -9,7 +9,6 @@ require "nitro_intelligence/media/image"
|
|
|
9
9
|
module NitroIntelligence
|
|
10
10
|
class ImageGeneration
|
|
11
11
|
class Config
|
|
12
|
-
CUSTOM_PARAMS = %i[aspect_ratio image_generation resolution].freeze
|
|
13
12
|
DEFAULT_ASPECT_RATIO = "1:1".freeze
|
|
14
13
|
DEFAULT_RESOLUTION = "1K".freeze
|
|
15
14
|
|
|
@@ -17,7 +16,7 @@ module NitroIntelligence
|
|
|
17
16
|
|
|
18
17
|
def initialize
|
|
19
18
|
@aspect_ratio = DEFAULT_ASPECT_RATIO
|
|
20
|
-
@model = NitroIntelligence.model_catalog.default_image_model
|
|
19
|
+
@model = NitroIntelligence.model_catalog.default_image_model&.name
|
|
21
20
|
@resolution = DEFAULT_RESOLUTION
|
|
22
21
|
end
|
|
23
22
|
end
|
|
@@ -61,6 +60,9 @@ module NitroIntelligence
|
|
|
61
60
|
@generated_image = Image.from_base64(base64_string)
|
|
62
61
|
@generated_image.direction = "output"
|
|
63
62
|
@generated_image
|
|
63
|
+
rescue ArgumentError
|
|
64
|
+
NitroIntelligence.logger.info("Skipping image parse due to invalid base64; likely already parsed.")
|
|
65
|
+
nil
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
private
|
|
@@ -2,16 +2,21 @@ require "nitro_intelligence/models/model_factory"
|
|
|
2
2
|
|
|
3
3
|
module NitroIntelligence
|
|
4
4
|
class ModelCatalog
|
|
5
|
-
attr_reader :models, :
|
|
5
|
+
attr_reader :models, :default_audio_transcription_model, :default_image_model, :default_text_model
|
|
6
6
|
|
|
7
7
|
def initialize(model_config)
|
|
8
8
|
@models = (model_config[:models] || []).map { |model_metadata| ModelFactory.build(model_metadata) }
|
|
9
|
-
@
|
|
9
|
+
@default_audio_transcription_model = lookup_by_name(model_config[:default_audio_transcription_model])
|
|
10
10
|
@default_image_model = lookup_by_name(model_config[:default_image_model])
|
|
11
|
+
@default_text_model = lookup_by_name(model_config[:default_text_model])
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def lookup_by_name(name)
|
|
14
15
|
@models.find { |model| model.name == name }
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
def exists?(name)
|
|
19
|
+
lookup_by_name(name).present?
|
|
20
|
+
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "nitro_intelligence/observability/prompt_store"
|
|
3
|
+
|
|
4
|
+
module NitroIntelligence
|
|
5
|
+
module Observability
|
|
6
|
+
class Project
|
|
7
|
+
class NotFoundError < StandardError; end
|
|
8
|
+
|
|
9
|
+
attr_reader :slug, :id, :public_key, :secret_key, :auth_token, :prompt_store
|
|
10
|
+
|
|
11
|
+
def initialize(slug:, id:, public_key:, secret_key:, **_kwargs)
|
|
12
|
+
@slug = slug
|
|
13
|
+
@id = id
|
|
14
|
+
@public_key = public_key
|
|
15
|
+
@secret_key = secret_key
|
|
16
|
+
@auth_token = Base64.strict_encode64("#{public_key}:#{secret_key}")
|
|
17
|
+
@prompt_store = PromptStore.new(
|
|
18
|
+
observability_project_slug: slug,
|
|
19
|
+
observability_public_key: public_key,
|
|
20
|
+
observability_secret_key: secret_key
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.find_by_slug(slug:)
|
|
25
|
+
project_config = NitroIntelligence.config.observability_projects.find { |project| project["slug"] == slug }
|
|
26
|
+
|
|
27
|
+
return new(**project_config.to_h.transform_keys(&:to_sym)) if project_config
|
|
28
|
+
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module NitroIntelligence
|
|
2
|
+
module Observability
|
|
3
|
+
class ProjectClient
|
|
4
|
+
class NotFoundError < StandardError; end
|
|
5
|
+
|
|
6
|
+
attr_reader :project, :observability_client
|
|
7
|
+
|
|
8
|
+
def initialize(project:, observability_client:)
|
|
9
|
+
@project = project
|
|
10
|
+
@observability_client = observability_client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def shutdown
|
|
14
|
+
@observability_client.shutdown
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "nitro_intelligence/langfuse_extension"
|
|
2
|
+
require "nitro_intelligence/observability/project"
|
|
3
|
+
require "nitro_intelligence/observability/project_client"
|
|
4
|
+
|
|
5
|
+
module NitroIntelligence
|
|
6
|
+
module Observability
|
|
7
|
+
class ProjectClientRegistry
|
|
8
|
+
def initialize(base_url:)
|
|
9
|
+
@base_url = base_url
|
|
10
|
+
@project_clients = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch(slug)
|
|
14
|
+
@project_clients[slug] ||= build_project_client(slug)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def shutdown_all
|
|
18
|
+
@project_clients.values.each(&:shutdown)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_project_client(slug)
|
|
24
|
+
project = Observability::Project.find_by_slug(slug:)
|
|
25
|
+
|
|
26
|
+
unless project
|
|
27
|
+
raise NitroIntelligence::Observability::Project::NotFoundError,
|
|
28
|
+
"No observability project config found for slug: #{slug}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
observability_client = NitroIntelligence::LangfuseExtension.new do |config|
|
|
32
|
+
config.public_key = project.public_key
|
|
33
|
+
config.secret_key = project.secret_key
|
|
34
|
+
config.base_url = @base_url
|
|
35
|
+
# Default flush of 60 seconds can be too quick when
|
|
36
|
+
# dealing with longer responses like image gen
|
|
37
|
+
config.flush_interval = 120
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Observability::ProjectClient.new(project:, observability_client:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module NitroIntelligence
|
|
2
|
+
module Observability
|
|
3
|
+
class Prompt
|
|
4
|
+
attr_reader :name, :type, :prompt, :version, :config, :labels, :tags
|
|
5
|
+
|
|
6
|
+
VARIABLE_REGEX = /\{\{([a-zA-Z0-9_]+)\}\}/
|
|
7
|
+
|
|
8
|
+
def initialize(name:, type:, prompt:, version:, **extra_args)
|
|
9
|
+
@name = name
|
|
10
|
+
@type = type
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
@version = version
|
|
13
|
+
@config = extra_args[:config] || {}
|
|
14
|
+
@labels = extra_args[:labels] || []
|
|
15
|
+
@tags = extra_args[:tags] || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns prompt "content" from API with prompt variables replaced
|
|
19
|
+
# Prompt "content" will either be a string or an array of hashes
|
|
20
|
+
# based on prompt "type" ("text" or "chat")
|
|
21
|
+
def compile(**replacements)
|
|
22
|
+
return replace_variables(@prompt, **replacements) if @type == "text"
|
|
23
|
+
|
|
24
|
+
@prompt.map do |message|
|
|
25
|
+
message[:content] = replace_variables(message[:content], **replacements)
|
|
26
|
+
message
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Takes provided chat messages and inserts the compiled prompt
|
|
31
|
+
# into the correct position based on prompt "type" ("text" or "chat")
|
|
32
|
+
def interpolate(messages:, variables:)
|
|
33
|
+
if @type == "text"
|
|
34
|
+
messages.prepend({ role: "system", content: compile(**variables) })
|
|
35
|
+
elsif @type == "chat"
|
|
36
|
+
compile(**variables) + messages
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def variables
|
|
41
|
+
messages = @type == "text" ? [@prompt] : @prompt.pluck(:content)
|
|
42
|
+
|
|
43
|
+
messages.map do |message|
|
|
44
|
+
message.scan(VARIABLE_REGEX).flatten.map(&:to_sym)
|
|
45
|
+
end.flatten
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def replace_variables(input, **replacements)
|
|
51
|
+
input.gsub(VARIABLE_REGEX) do |match|
|
|
52
|
+
key = ::Regexp.last_match(1).to_sym
|
|
53
|
+
replacements.fetch(key, match)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|