nitro_intelligence 1.0.1 → 2.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 +68 -0
- data/lib/nitro_intelligence/agent_server.rb +31 -19
- data/lib/nitro_intelligence/client/base.rb +10 -0
- data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +100 -0
- data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +58 -0
- data/lib/nitro_intelligence/client/observed.rb +7 -0
- data/lib/nitro_intelligence/models/model.rb +24 -2
- data/lib/nitro_intelligence/models/model_catalog.rb +7 -1
- data/lib/nitro_intelligence/models/model_factory.rb +10 -10
- data/lib/nitro_intelligence/observability/prompt_store.rb +12 -9
- data/lib/nitro_intelligence/observability/upload_handler.rb +38 -27
- data/lib/nitro_intelligence/reporter.rb +12 -7
- data/lib/nitro_intelligence/version.rb +1 -1
- metadata +3 -161
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c03a8a4c436205c01d926c024c1bcd12542553f36e3998b10462653cec4cdc1
|
|
4
|
+
data.tar.gz: 58f340818e47d7b6e36dd84f44b4072bf8636a83d4fd6931e65e62ec1ed29ab5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70eab830245ab3c7bd4a6835c080770ee324dca9c440ba200f0ac29fc528e9b4be5579091259a9abed7c35ce51372da82d4452d1df71ee26c51a61bdb342acaa
|
|
7
|
+
data.tar.gz: 549cd0d2477d1382cff28d866266797983b0ddd860396c10ff5edd8087f5538a4834b1194d1ae6d5d16d497825bf660308bcacfcb3da12846e2f69ef611264a4
|
data/docs/README.md
CHANGED
|
@@ -33,6 +33,39 @@ NitroIntelligence.configure do |config|
|
|
|
33
33
|
|
|
34
34
|
# Agent server settings (optional)
|
|
35
35
|
config.agent_server_config = {} # Hash of AgentServer keyword arguments
|
|
36
|
+
|
|
37
|
+
# Model configuration
|
|
38
|
+
config.model_config = {
|
|
39
|
+
"default_audio_transcription_model" => "gpt-4o-transcribe",
|
|
40
|
+
"default_text_model" => "gpt-4o-mini",
|
|
41
|
+
"default_image_model" => "nano-banana-2",
|
|
42
|
+
"default_text_to_speech_model" => "gpt-4o-mini-tts",
|
|
43
|
+
"models" => [
|
|
44
|
+
{
|
|
45
|
+
"name" => "gpt-4o-mini",
|
|
46
|
+
"type" => "text"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name" => "gpt-4o-transcribe",
|
|
50
|
+
"type" => "audio_transcription"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name" => "nano-banana-2",
|
|
54
|
+
"type" => "image",
|
|
55
|
+
"aspect_ratios" => ["1:1", "2:3", "3:2", "3:4", "4:3"],
|
|
56
|
+
"resolutions" => ["512", "1K", "2K"],
|
|
57
|
+
"omit_output_fields" => ["provider_specific_fields.thought_signatures"]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name" => "gpt-4o-mini-tts",
|
|
61
|
+
"type" => "text_to_speech",
|
|
62
|
+
"default_voice" => "marin",
|
|
63
|
+
"default_response_format" => "mp3",
|
|
64
|
+
"voices" => ["echo", "nova", "marin", "cedar"],
|
|
65
|
+
"response_formats" => ["mp3", "wav"]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
36
69
|
end
|
|
37
70
|
```
|
|
38
71
|
|
|
@@ -48,6 +81,7 @@ end
|
|
|
48
81
|
| `observability_base_url` | `String` | `""` | Base URL for the Langfuse observability service |
|
|
49
82
|
| `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key) |
|
|
50
83
|
| `agent_server_config` | `Hash` | `{}` | Credentials for `AgentServer.new`. Expected keys: `base_url` (String) — HTTP base URL of the agent server; `api_key` (String) — bearer token; `user_id` (String, default: `"default-user"`) — caller identity |
|
|
84
|
+
| `model_config` | `Hash` | `{}` | Model defaults and per-model settings. Top-level keys: `default_text_model`, `default_audio_transcription_model`, `default_image_model`, `default_text_to_speech_model`, and `models` (array of per-model hashes keyed by `name` and `type`, with type-specific options like `aspect_ratios`/`resolutions` for images or `voices`/`response_formats` for TTS) |
|
|
51
85
|
|
|
52
86
|
## Basic Usage
|
|
53
87
|
|
|
@@ -111,6 +145,40 @@ puts result
|
|
|
111
145
|
# <OpenAI::Models::Audio::Transcription:0x2fd8c {:text=>"Hola, ¿cómo estás hoy?", :usage=>{:input_tokens=>37, :output_tokens=>9, :total_tokens=>46, :type=>:tokens, :input_token_details=>{:audio_tokens=>28, :text_tokens=>9}}}>
|
|
112
146
|
```
|
|
113
147
|
|
|
148
|
+
### Text-to-Speech
|
|
149
|
+
|
|
150
|
+
Nitro Intelligence can be used to create spoken-word audio files from text.
|
|
151
|
+
|
|
152
|
+
Basic example of usage:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
client = NitroIntelligence::Client.new
|
|
156
|
+
tts = client.text_to_speech(message: 'Hello, this is Power Home Remodeling Group.')
|
|
157
|
+
|
|
158
|
+
# tts is a StringIO object, we can write the file to disk
|
|
159
|
+
File.binwrite('tts.mp3', tts.string)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Supplying custom parameters:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
client = NitroIntelligence::Client.new
|
|
166
|
+
tts = client.text_to_speech(
|
|
167
|
+
message: 'Hello, this is Power Home Remodeling Group.',
|
|
168
|
+
parameters: {
|
|
169
|
+
voice: 'marin',
|
|
170
|
+
response_format: 'mp3',
|
|
171
|
+
instructions: 'Translate the English into Spanish before speaking.',
|
|
172
|
+
speed: 1.25
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# tts is a StringIO object, we can write the file to disk
|
|
177
|
+
File.binwrite('tts.mp3', tts.string)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a full list of supported parameters, see the [API reference here](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create ). Note that `voice` and `response_format` are further constrained to the `voices` and `response_formats` listed for the chosen model in `config.model_config`.
|
|
181
|
+
|
|
114
182
|
### Image Editing and Generation
|
|
115
183
|
|
|
116
184
|
Nitro Intelligence can be used for image editing and generation
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
1
4
|
require "nitro_intelligence/tool_call_review_validator"
|
|
2
5
|
|
|
3
6
|
module NitroIntelligence
|
|
@@ -85,25 +88,25 @@ module NitroIntelligence
|
|
|
85
88
|
}
|
|
86
89
|
)
|
|
87
90
|
|
|
88
|
-
raise ThreadInitializationError, thread_response.body if thread_response.code != 200
|
|
91
|
+
raise ThreadInitializationError, thread_response.body if thread_response.code.to_i != 200
|
|
89
92
|
|
|
90
|
-
thread_response
|
|
93
|
+
JSON.parse(thread_response.body)
|
|
91
94
|
end
|
|
92
95
|
|
|
93
96
|
def get_thread_state(thread_id:)
|
|
94
97
|
state_response = get(path: "/threads/#{thread_id}/state")
|
|
95
98
|
|
|
96
|
-
raise ThreadResumptionError, state_response.body if state_response.code != 200
|
|
99
|
+
raise ThreadResumptionError, state_response.body if state_response.code.to_i != 200
|
|
97
100
|
|
|
98
|
-
state_response
|
|
101
|
+
JSON.parse(state_response.body)
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def get_thread(thread_id:)
|
|
102
105
|
thread_response = get(path: "/threads/#{thread_id}")
|
|
103
106
|
|
|
104
|
-
raise ThreadResumptionError, thread_response.body if thread_response.code != 200
|
|
107
|
+
raise ThreadResumptionError, thread_response.body if thread_response.code.to_i != 200
|
|
105
108
|
|
|
106
|
-
thread_response
|
|
109
|
+
JSON.parse(thread_response.body)
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def trigger_run(thread_id:, assistant_id:, last_message:, context: {})
|
|
@@ -118,9 +121,10 @@ module NitroIntelligence
|
|
|
118
121
|
}
|
|
119
122
|
)
|
|
120
123
|
|
|
121
|
-
raise RunError, run_response.body if run_response.code != 200
|
|
124
|
+
raise RunError, run_response.body if run_response.code.to_i != 200
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
run = JSON.parse(run_response.body)
|
|
127
|
+
Array(run["messages"]).last&.dig("content")
|
|
124
128
|
end
|
|
125
129
|
|
|
126
130
|
def resume_run(thread_id:, assistant_id:, resume:, context:)
|
|
@@ -135,9 +139,9 @@ module NitroIntelligence
|
|
|
135
139
|
}
|
|
136
140
|
)
|
|
137
141
|
|
|
138
|
-
raise ThreadResumptionError, run_response.body if run_response.code != 200
|
|
142
|
+
raise ThreadResumptionError, run_response.body if run_response.code.to_i != 200
|
|
139
143
|
|
|
140
|
-
run_response
|
|
144
|
+
JSON.parse(run_response.body)
|
|
141
145
|
end
|
|
142
146
|
|
|
143
147
|
def interrupted?(thread)
|
|
@@ -163,18 +167,26 @@ module NitroIntelligence
|
|
|
163
167
|
end
|
|
164
168
|
|
|
165
169
|
def get(path:)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
uri = URI("#{base_url}#{path}")
|
|
171
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
172
|
+
http.use_ssl = uri.scheme == "https"
|
|
173
|
+
|
|
174
|
+
request = Net::HTTP::Get.new(uri)
|
|
175
|
+
request_headers.each { |k, v| request[k] = v }
|
|
176
|
+
|
|
177
|
+
http.request(request)
|
|
170
178
|
end
|
|
171
179
|
|
|
172
180
|
def post(path:, body:)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
181
|
+
uri = URI("#{base_url}#{path}")
|
|
182
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
183
|
+
http.use_ssl = uri.scheme == "https"
|
|
184
|
+
|
|
185
|
+
request = Net::HTTP::Post.new(uri)
|
|
186
|
+
request_headers.each { |k, v| request[k] = v }
|
|
187
|
+
request.body = body.to_json
|
|
188
|
+
|
|
189
|
+
http.request(request)
|
|
178
190
|
end
|
|
179
191
|
|
|
180
192
|
def request_headers
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "nitro_intelligence/client/handlers/audio_transcription_handler"
|
|
2
2
|
require "nitro_intelligence/client/handlers/chat_handler"
|
|
3
3
|
require "nitro_intelligence/client/handlers/image_handler"
|
|
4
|
+
require "nitro_intelligence/client/handlers/text_to_speech_handler"
|
|
4
5
|
|
|
5
6
|
module NitroIntelligence
|
|
6
7
|
module Client
|
|
@@ -26,6 +27,11 @@ module NitroIntelligence
|
|
|
26
27
|
audio_transcription_handler.create(message:, audio_file:, parameters:)
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Returns StringIO
|
|
31
|
+
def text_to_speech(message: "", parameters: {})
|
|
32
|
+
text_to_speech_handler.create(message:, parameters:)
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
private
|
|
30
36
|
|
|
31
37
|
def audio_transcription_handler
|
|
@@ -47,6 +53,10 @@ module NitroIntelligence
|
|
|
47
53
|
def respond_to_missing?(method_name, include_private = false)
|
|
48
54
|
@client.respond_to?(method_name, include_private) || super
|
|
49
55
|
end
|
|
56
|
+
|
|
57
|
+
def text_to_speech_handler
|
|
58
|
+
@text_to_speech_handler ||= Handlers::TextToSpeechHandler.new(client: @client)
|
|
59
|
+
end
|
|
50
60
|
end
|
|
51
61
|
end
|
|
52
62
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "nitro_intelligence/media/audio"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
module Handlers
|
|
6
|
+
module Observed
|
|
7
|
+
class TextToSpeechHandler
|
|
8
|
+
class ObservedTextToSpeechPromptError < StandardError; end
|
|
9
|
+
|
|
10
|
+
def initialize(base_handler:, observer:)
|
|
11
|
+
@base_handler = base_handler
|
|
12
|
+
@observer = observer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create(message: "", parameters: {})
|
|
16
|
+
prompt = handle_prompt(parameters:)
|
|
17
|
+
|
|
18
|
+
@base_handler.validate_and_resolve!(parameters)
|
|
19
|
+
|
|
20
|
+
trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
|
|
21
|
+
|
|
22
|
+
@observer.observe(
|
|
23
|
+
"text-to-speech",
|
|
24
|
+
type: :generation,
|
|
25
|
+
parameters:,
|
|
26
|
+
trace_name:,
|
|
27
|
+
prompt:
|
|
28
|
+
) do |generation|
|
|
29
|
+
workflow(message:, parameters:, trace_id: generation.trace_id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def handle_prompt(parameters:)
|
|
36
|
+
return nil if parameters[:prompt_name].blank?
|
|
37
|
+
|
|
38
|
+
prompt = @observer.project_client.project.prompt_store.get_prompt(
|
|
39
|
+
prompt_name: parameters[:prompt_name],
|
|
40
|
+
prompt_label: parameters[:prompt_label],
|
|
41
|
+
prompt_version: parameters[:prompt_version]
|
|
42
|
+
)
|
|
43
|
+
prompt_variables = parameters[:prompt_variables] || {}
|
|
44
|
+
|
|
45
|
+
if prompt.present?
|
|
46
|
+
# Prompts for tts should only be text
|
|
47
|
+
if prompt.type != "text"
|
|
48
|
+
raise ObservedTextToSpeechPromptError,
|
|
49
|
+
"Prompt type for text-to-speech must be text: #{prompt.name}"
|
|
50
|
+
end
|
|
51
|
+
interpolated_prompt = prompt.compile(**prompt_variables)
|
|
52
|
+
|
|
53
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
54
|
+
parameters[:instructions] = interpolated_prompt
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
prompt
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_text_to_speech_upload(tts_file, trace_id)
|
|
61
|
+
upload_handler = NitroIntelligence::Observability::UploadHandler.new(
|
|
62
|
+
auth_token: @observer.project_client.project.auth_token
|
|
63
|
+
)
|
|
64
|
+
upload_handler.upload(
|
|
65
|
+
trace_id,
|
|
66
|
+
upload_queue: Queue.new([Audio.new(tts_file)])
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
uploaded_media = upload_handler.uploaded_media.first
|
|
70
|
+
"@@@langfuseMedia:type=#{uploaded_media.mime_type}|id=#{uploaded_media.reference_id}|source=bytes@@@"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def workflow(message:, parameters:, trace_id:)
|
|
74
|
+
tts = @base_handler.perform_request(message:, parameters:)
|
|
75
|
+
output = ""
|
|
76
|
+
|
|
77
|
+
Tempfile.create(["tts", ".#{parameters[:response_format]}"]) do |tempfile|
|
|
78
|
+
tempfile.binmode
|
|
79
|
+
tempfile.write(tts.string)
|
|
80
|
+
tempfile.rewind
|
|
81
|
+
|
|
82
|
+
output = handle_text_to_speech_upload(tempfile, trace_id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# We only get StringIO object as a response
|
|
86
|
+
# We dont have usage on tokens and the actual model that was used
|
|
87
|
+
# We will log the requested model instead
|
|
88
|
+
trace_attributes = {
|
|
89
|
+
model: parameters[:model],
|
|
90
|
+
input: message,
|
|
91
|
+
output:,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[tts, trace_attributes]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "openai"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Client
|
|
5
|
+
module Handlers
|
|
6
|
+
class TextToSpeechHandler
|
|
7
|
+
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::SpeechCreateParams.fields.keys.uniq.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(client:)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(message: "", parameters: {})
|
|
14
|
+
validate_and_resolve!(parameters)
|
|
15
|
+
perform_request(message:, parameters:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def perform_request(message: "", parameters: {})
|
|
19
|
+
@client.audio.speech.create(
|
|
20
|
+
input: message,
|
|
21
|
+
**parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_and_resolve!(parameters)
|
|
26
|
+
model_name = parameters[:model] || NitroIntelligence.model_catalog.default_text_to_speech_model&.name
|
|
27
|
+
model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
|
|
28
|
+
|
|
29
|
+
# Check model supported
|
|
30
|
+
raise ArgumentError, "Unsupported model: '#{model_name}'" unless model
|
|
31
|
+
|
|
32
|
+
default_parameters = {
|
|
33
|
+
metadata: {},
|
|
34
|
+
model: model.name,
|
|
35
|
+
voice: model.default_voice,
|
|
36
|
+
response_format: model.default_response_format,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
40
|
+
|
|
41
|
+
# Check voice supported
|
|
42
|
+
unless model.voices.include?(parameters[:voice])
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"Unsupported voice: '#{parameters[:voice]}'. " \
|
|
45
|
+
"Supported voices for #{model.name} are: #{model.voices}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check format supported
|
|
49
|
+
return if model.response_formats.include?(parameters[:response_format])
|
|
50
|
+
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"Unsupported response_format: '#{parameters[:response_format]}'. " \
|
|
53
|
+
"Supported response_formats for #{model.name} are: #{model.response_formats}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "nitro_intelligence/client/handlers/observed/audio_transcription_handler"
|
|
2
2
|
require "nitro_intelligence/client/handlers/observed/chat_handler"
|
|
3
3
|
require "nitro_intelligence/client/handlers/observed/image_handler"
|
|
4
|
+
require "nitro_intelligence/client/handlers/observed/text_to_speech_handler"
|
|
4
5
|
require "nitro_intelligence/observability/prompt_store"
|
|
5
6
|
require "nitro_intelligence/observability/upload_handler"
|
|
6
7
|
require "nitro_intelligence/trace"
|
|
@@ -33,6 +34,12 @@ module NitroIntelligence
|
|
|
33
34
|
base_handler: Handlers::ImageHandler.new(client: @client), observer: @observer
|
|
34
35
|
)
|
|
35
36
|
end
|
|
37
|
+
|
|
38
|
+
def text_to_speech_handler
|
|
39
|
+
@text_to_speech_handler ||= Handlers::Observed::TextToSpeechHandler.new(
|
|
40
|
+
base_handler: Handlers::TextToSpeechHandler.new(client: @client), observer: @observer
|
|
41
|
+
)
|
|
42
|
+
end
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
end
|
|
@@ -8,8 +8,6 @@ module NitroIntelligence
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
class TextModel < Model; end
|
|
12
|
-
|
|
13
11
|
class ImageModel < Model
|
|
14
12
|
attr_reader :aspect_ratios, :resolutions
|
|
15
13
|
|
|
@@ -19,4 +17,28 @@ module NitroIntelligence
|
|
|
19
17
|
@resolutions = resolutions
|
|
20
18
|
end
|
|
21
19
|
end
|
|
20
|
+
|
|
21
|
+
class TextModel < Model; end
|
|
22
|
+
|
|
23
|
+
class TextToSpeechModel < Model
|
|
24
|
+
attr_reader :default_voice,
|
|
25
|
+
:default_response_format,
|
|
26
|
+
:voices,
|
|
27
|
+
:response_formats
|
|
28
|
+
|
|
29
|
+
def initialize(
|
|
30
|
+
name:,
|
|
31
|
+
default_voice:,
|
|
32
|
+
default_response_format:,
|
|
33
|
+
voices: [],
|
|
34
|
+
response_formats: [],
|
|
35
|
+
**
|
|
36
|
+
)
|
|
37
|
+
super
|
|
38
|
+
@default_voice = default_voice
|
|
39
|
+
@default_response_format = default_response_format
|
|
40
|
+
@voices = voices
|
|
41
|
+
@response_formats = response_formats
|
|
42
|
+
end
|
|
43
|
+
end
|
|
22
44
|
end
|
|
@@ -2,13 +2,19 @@ require "nitro_intelligence/models/model_factory"
|
|
|
2
2
|
|
|
3
3
|
module NitroIntelligence
|
|
4
4
|
class ModelCatalog
|
|
5
|
-
attr_reader :models,
|
|
5
|
+
attr_reader :models,
|
|
6
|
+
:default_audio_transcription_model,
|
|
7
|
+
:default_image_model,
|
|
8
|
+
:default_text_model,
|
|
9
|
+
:default_text_to_speech_model
|
|
6
10
|
|
|
7
11
|
def initialize(model_config)
|
|
12
|
+
model_config = model_config.symbolize_keys
|
|
8
13
|
@models = (model_config[:models] || []).map { |model_metadata| ModelFactory.build(model_metadata) }
|
|
9
14
|
@default_audio_transcription_model = lookup_by_name(model_config[:default_audio_transcription_model])
|
|
10
15
|
@default_image_model = lookup_by_name(model_config[:default_image_model])
|
|
11
16
|
@default_text_model = lookup_by_name(model_config[:default_text_model])
|
|
17
|
+
@default_text_to_speech_model = lookup_by_name(model_config[:default_text_to_speech_model])
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def lookup_by_name(name)
|
|
@@ -2,18 +2,18 @@ require "nitro_intelligence/models/model"
|
|
|
2
2
|
|
|
3
3
|
module NitroIntelligence
|
|
4
4
|
class ModelFactory
|
|
5
|
+
TYPES = {
|
|
6
|
+
"text" => TextModel,
|
|
7
|
+
"audio_transcription" => TextModel,
|
|
8
|
+
"image" => ImageModel,
|
|
9
|
+
"text_to_speech" => TextToSpeechModel,
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
5
12
|
def self.build(model_metadata)
|
|
6
13
|
model_metadata = model_metadata.symbolize_keys
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
else
|
|
11
|
-
TextModel.new(**model_metadata)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.image_model?(model_metadata)
|
|
16
|
-
model_metadata.key?(:aspect_ratios) || model_metadata.key?(:resolutions)
|
|
14
|
+
type = model_metadata[:type]
|
|
15
|
+
model_class = TYPES.fetch(type) { raise ArgumentError, "Unknown model type: #{type.inspect}" }
|
|
16
|
+
model_class.new(**model_metadata)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "cgi"
|
|
3
|
-
require "
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
require "nitro_intelligence/observability/prompt"
|
|
6
7
|
|
|
@@ -75,15 +76,17 @@ module NitroIntelligence
|
|
|
75
76
|
|
|
76
77
|
def get_prompt_request(safe_prompt_name:, prompt_url_params:)
|
|
77
78
|
auth_token = Base64.strict_encode64("#{@observability_public_key}:#{@observability_secret_key}")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
uri = URI("#{@observability_host}/api/public/v2/prompts/#{safe_prompt_name}?#{prompt_url_params}")
|
|
80
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
81
|
+
http.use_ssl = uri.scheme == "https"
|
|
82
|
+
|
|
83
|
+
request = Net::HTTP::Get.new(uri)
|
|
84
|
+
request["Authorization"] = "Basic #{auth_token}"
|
|
85
|
+
|
|
86
|
+
response = http.request(request)
|
|
84
87
|
|
|
85
|
-
if response.code != 200
|
|
86
|
-
raise ObservabilityPromptNotFoundError, "Prompt: #{safe_prompt_name} Not Found" if response.code == 404
|
|
88
|
+
if response.code.to_i != 200
|
|
89
|
+
raise ObservabilityPromptNotFoundError, "Prompt: #{safe_prompt_name} Not Found" if response.code.to_i == 404
|
|
87
90
|
|
|
88
91
|
raise ObservabilityPromptError, response.body
|
|
89
92
|
end
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "digest"
|
|
3
|
-
require "
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
4
5
|
require "time"
|
|
6
|
+
require "uri"
|
|
5
7
|
|
|
6
8
|
module NitroIntelligence
|
|
7
9
|
module Observability
|
|
8
10
|
class UploadHandler
|
|
11
|
+
attr_reader :uploaded_media
|
|
12
|
+
|
|
9
13
|
def initialize(auth_token:)
|
|
10
14
|
@host = NitroIntelligence.config.observability_base_url
|
|
11
15
|
@auth_token = auth_token
|
|
@@ -92,43 +96,50 @@ module NitroIntelligence
|
|
|
92
96
|
private
|
|
93
97
|
|
|
94
98
|
def get_upload_url(request_body)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
uri = URI("#{@host}/api/public/media")
|
|
100
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
101
|
+
http.use_ssl = uri.scheme == "https"
|
|
102
|
+
|
|
103
|
+
request = Net::HTTP::Post.new(uri)
|
|
104
|
+
request["Content-Type"] = "application/json"
|
|
105
|
+
request["Authorization"] = "Basic #{@auth_token}"
|
|
106
|
+
request.body = request_body.to_json
|
|
107
|
+
|
|
108
|
+
response = http.request(request)
|
|
109
|
+
JSON.parse(response.body)
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def associate_media(media_id, upload_response)
|
|
106
113
|
request_body = {
|
|
107
114
|
uploadedAt: Time.now.utc.iso8601(6),
|
|
108
|
-
uploadHttpStatus: upload_response.code,
|
|
109
|
-
uploadHttpError: upload_response.code == 200 ? nil : upload_response.body,
|
|
115
|
+
uploadHttpStatus: upload_response.code.to_i,
|
|
116
|
+
uploadHttpError: upload_response.code.to_i == 200 ? nil : upload_response.body,
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
uri = URI("#{@host}/api/public/media/#{media_id}")
|
|
120
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
121
|
+
http.use_ssl = uri.scheme == "https"
|
|
122
|
+
|
|
123
|
+
request = Net::HTTP::Patch.new(uri)
|
|
124
|
+
request["Content-Type"] = "application/json"
|
|
125
|
+
request["Authorization"] = "Basic #{@auth_token}"
|
|
126
|
+
request.body = request_body.to_json
|
|
127
|
+
|
|
128
|
+
http.request(request)
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
def upload_media(media_id, upload_url, content_type, content_sha256, content_bytes)
|
|
123
132
|
if media_id.present? && upload_url.present?
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
uri = URI(upload_url)
|
|
134
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
135
|
+
http.use_ssl = uri.scheme == "https"
|
|
136
|
+
|
|
137
|
+
request = Net::HTTP::Put.new(uri)
|
|
138
|
+
request["Content-Type"] = content_type
|
|
139
|
+
request["x-amz-checksum-sha256"] = content_sha256
|
|
140
|
+
request.body = content_bytes
|
|
141
|
+
|
|
142
|
+
return http.request(request)
|
|
132
143
|
end
|
|
133
144
|
|
|
134
145
|
nil
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "base64"
|
|
2
|
-
require "
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
3
4
|
|
|
4
5
|
module NitroIntelligence
|
|
5
6
|
class Reporter
|
|
@@ -10,12 +11,16 @@ module NitroIntelligence
|
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def create_dataset_item(attributes)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
uri = URI("#{@host}/api/public/dataset-items")
|
|
15
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
16
|
+
http.use_ssl = uri.scheme == "https"
|
|
17
|
+
|
|
18
|
+
request = Net::HTTP::Post.new(uri)
|
|
19
|
+
request["Content-Type"] = "application/json"
|
|
20
|
+
request["Authorization"] = "Basic #{@project_client.project.auth_token}"
|
|
21
|
+
request.body = attributes.to_json
|
|
22
|
+
|
|
23
|
+
http.request(request)
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def score(trace_id:, name:, value:, id: "#{trace_id}-#{name}")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nitro_intelligence
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Artemenko
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.1'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: httparty
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.16'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.16'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: langfuse-rb
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,152 +65,6 @@ dependencies:
|
|
|
79
65
|
- - "~>"
|
|
80
66
|
- !ruby/object:Gem::Version
|
|
81
67
|
version: '0.58'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: railties
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '7.1'
|
|
89
|
-
type: :runtime
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '7.1'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: license_finder
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - '='
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: 7.2.1
|
|
103
|
-
type: :development
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - '='
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: 7.2.1
|
|
110
|
-
- !ruby/object:Gem::Dependency
|
|
111
|
-
name: parser
|
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - ">="
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '2.5'
|
|
117
|
-
- - "!="
|
|
118
|
-
- !ruby/object:Gem::Version
|
|
119
|
-
version: 2.5.1.1
|
|
120
|
-
type: :development
|
|
121
|
-
prerelease: false
|
|
122
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
123
|
-
requirements:
|
|
124
|
-
- - ">="
|
|
125
|
-
- !ruby/object:Gem::Version
|
|
126
|
-
version: '2.5'
|
|
127
|
-
- - "!="
|
|
128
|
-
- !ruby/object:Gem::Version
|
|
129
|
-
version: 2.5.1.1
|
|
130
|
-
- !ruby/object:Gem::Dependency
|
|
131
|
-
name: pry
|
|
132
|
-
requirement: !ruby/object:Gem::Requirement
|
|
133
|
-
requirements:
|
|
134
|
-
- - '='
|
|
135
|
-
- !ruby/object:Gem::Version
|
|
136
|
-
version: 0.14.2
|
|
137
|
-
type: :development
|
|
138
|
-
prerelease: false
|
|
139
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
-
requirements:
|
|
141
|
-
- - '='
|
|
142
|
-
- !ruby/object:Gem::Version
|
|
143
|
-
version: 0.14.2
|
|
144
|
-
- !ruby/object:Gem::Dependency
|
|
145
|
-
name: pry-byebug
|
|
146
|
-
requirement: !ruby/object:Gem::Requirement
|
|
147
|
-
requirements:
|
|
148
|
-
- - '='
|
|
149
|
-
- !ruby/object:Gem::Version
|
|
150
|
-
version: 3.10.1
|
|
151
|
-
type: :development
|
|
152
|
-
prerelease: false
|
|
153
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
-
requirements:
|
|
155
|
-
- - '='
|
|
156
|
-
- !ruby/object:Gem::Version
|
|
157
|
-
version: 3.10.1
|
|
158
|
-
- !ruby/object:Gem::Dependency
|
|
159
|
-
name: rainbow
|
|
160
|
-
requirement: !ruby/object:Gem::Requirement
|
|
161
|
-
requirements:
|
|
162
|
-
- - '='
|
|
163
|
-
- !ruby/object:Gem::Version
|
|
164
|
-
version: 2.2.2
|
|
165
|
-
type: :development
|
|
166
|
-
prerelease: false
|
|
167
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
168
|
-
requirements:
|
|
169
|
-
- - '='
|
|
170
|
-
- !ruby/object:Gem::Version
|
|
171
|
-
version: 2.2.2
|
|
172
|
-
- !ruby/object:Gem::Dependency
|
|
173
|
-
name: rspec
|
|
174
|
-
requirement: !ruby/object:Gem::Requirement
|
|
175
|
-
requirements:
|
|
176
|
-
- - '='
|
|
177
|
-
- !ruby/object:Gem::Version
|
|
178
|
-
version: 3.13.0
|
|
179
|
-
type: :development
|
|
180
|
-
prerelease: false
|
|
181
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
182
|
-
requirements:
|
|
183
|
-
- - '='
|
|
184
|
-
- !ruby/object:Gem::Version
|
|
185
|
-
version: 3.13.0
|
|
186
|
-
- !ruby/object:Gem::Dependency
|
|
187
|
-
name: rubocop-powerhome
|
|
188
|
-
requirement: !ruby/object:Gem::Requirement
|
|
189
|
-
requirements:
|
|
190
|
-
- - '='
|
|
191
|
-
- !ruby/object:Gem::Version
|
|
192
|
-
version: 0.6.1
|
|
193
|
-
type: :development
|
|
194
|
-
prerelease: false
|
|
195
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
196
|
-
requirements:
|
|
197
|
-
- - '='
|
|
198
|
-
- !ruby/object:Gem::Version
|
|
199
|
-
version: 0.6.1
|
|
200
|
-
- !ruby/object:Gem::Dependency
|
|
201
|
-
name: webmock
|
|
202
|
-
requirement: !ruby/object:Gem::Requirement
|
|
203
|
-
requirements:
|
|
204
|
-
- - '='
|
|
205
|
-
- !ruby/object:Gem::Version
|
|
206
|
-
version: 3.26.1
|
|
207
|
-
type: :development
|
|
208
|
-
prerelease: false
|
|
209
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
210
|
-
requirements:
|
|
211
|
-
- - '='
|
|
212
|
-
- !ruby/object:Gem::Version
|
|
213
|
-
version: 3.26.1
|
|
214
|
-
- !ruby/object:Gem::Dependency
|
|
215
|
-
name: yard
|
|
216
|
-
requirement: !ruby/object:Gem::Requirement
|
|
217
|
-
requirements:
|
|
218
|
-
- - '='
|
|
219
|
-
- !ruby/object:Gem::Version
|
|
220
|
-
version: 0.9.37
|
|
221
|
-
type: :development
|
|
222
|
-
prerelease: false
|
|
223
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
224
|
-
requirements:
|
|
225
|
-
- - '='
|
|
226
|
-
- !ruby/object:Gem::Version
|
|
227
|
-
version: 0.9.37
|
|
228
68
|
description: The Ruby client for Nitro Intelligence
|
|
229
69
|
email:
|
|
230
70
|
- igor.artemenko@powerhrg.com
|
|
@@ -245,6 +85,8 @@ files:
|
|
|
245
85
|
- lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
|
|
246
86
|
- lib/nitro_intelligence/client/handlers/observed/chat_handler.rb
|
|
247
87
|
- lib/nitro_intelligence/client/handlers/observed/image_handler.rb
|
|
88
|
+
- lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb
|
|
89
|
+
- lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb
|
|
248
90
|
- lib/nitro_intelligence/client/observed.rb
|
|
249
91
|
- lib/nitro_intelligence/client/observers/langfuse_observer.rb
|
|
250
92
|
- lib/nitro_intelligence/configuration.rb
|