nitro_intelligence 1.0.1 → 2.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/docs/README.md +68 -0
- data/lib/nitro_intelligence/agent_server.rb +109 -24
- data/lib/nitro_intelligence/client/base.rb +10 -0
- data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +3 -5
- data/lib/nitro_intelligence/client/handlers/base_handler.rb +22 -0
- data/lib/nitro_intelligence/client/handlers/chat_handler.rb +3 -6
- data/lib/nitro_intelligence/client/handlers/image_handler.rb +3 -6
- 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 +56 -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 +4 -161
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d1c0e6c9ddffc032a989ebffb001d58aefabf86e50f92966a6d384a5218628f
|
|
4
|
+
data.tar.gz: 60b9ae5ef4fd8c7a79bc45f13a1b965c9d9a0792f38bc285e361822b7d91a8ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a13a9b1a0a58f58c142ab87ec89f491f409341717b3101b9bb1579c88b8ed9335413ea73d621c489749b7d151d62672e9277e3d39da80c4721008cad28a301b0
|
|
7
|
+
data.tar.gz: 1c498a0a4ec99af32f039dd43272ad61650da433c4f4d3b4297aadd473fba4c93af8d9ed08db0d2faf5012ed3a721e6d47587bf5429eb16368161d224f773cf7
|
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
|
|
@@ -7,6 +10,9 @@ module NitroIntelligence
|
|
|
7
10
|
class RunError < StandardError; end
|
|
8
11
|
class ThreadResumptionError < StandardError; end
|
|
9
12
|
|
|
13
|
+
# Aegra answers with a conflict when `ifExists: "raise"` is sent for a thread that already exists.
|
|
14
|
+
THREAD_CONFLICT_CODE = 409
|
|
15
|
+
|
|
10
16
|
attr_reader :base_url, :user_id
|
|
11
17
|
|
|
12
18
|
def initialize(base_url:, api_key:, user_id: "default-user")
|
|
@@ -18,6 +24,7 @@ module NitroIntelligence
|
|
|
18
24
|
@api_key = api_key
|
|
19
25
|
@user_id = user_id
|
|
20
26
|
@tool_call_review_validator = ToolCallReviewValidator.new
|
|
27
|
+
@graph_ids = {}
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
def await_run(thread_id:, assistant_id:, messages:, context: {})
|
|
@@ -26,7 +33,7 @@ module NitroIntelligence
|
|
|
26
33
|
initial_state = messages[0..-2]
|
|
27
34
|
last_message = messages.last
|
|
28
35
|
|
|
29
|
-
initialize_thread_if_needed(thread_id:, initial_state:)
|
|
36
|
+
initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
|
|
30
37
|
trigger_run(thread_id:, assistant_id:, context:, last_message:)
|
|
31
38
|
end
|
|
32
39
|
|
|
@@ -74,36 +81,94 @@ module NitroIntelligence
|
|
|
74
81
|
|
|
75
82
|
private
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
# Aegra accepts `initial_state` on thread creation but never applies it, so a brand new thread is
|
|
85
|
+
# seeded through the thread state endpoint instead. A thread that already exists is left untouched:
|
|
86
|
+
# its state was seeded when it was created and has been built up by every run since.
|
|
87
|
+
def initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
|
|
88
|
+
thread_response = create_thread(thread_id:, assistant_id:)
|
|
89
|
+
|
|
90
|
+
return if thread_already_exists?(thread_response)
|
|
91
|
+
raise ThreadInitializationError, thread_response.body if thread_response.code.to_i != 200
|
|
92
|
+
return if initial_state.blank?
|
|
93
|
+
|
|
94
|
+
seed_new_thread_state(thread_id:, initial_state:)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Creating the thread and seeding its state are separate requests, so a failure between them leaves
|
|
98
|
+
# an empty thread behind. A retry would find that thread, take it for one already under way, skip
|
|
99
|
+
# seeding and run without the history -- losing the very thing seeding exists for, without an error.
|
|
100
|
+
# Discard the thread instead, so a retry starts over from a clean slate.
|
|
101
|
+
def seed_new_thread_state(thread_id:, initial_state:)
|
|
102
|
+
seed_thread_state(thread_id:, initial_state:)
|
|
103
|
+
rescue
|
|
104
|
+
discard_thread(thread_id:)
|
|
105
|
+
raise
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def discard_thread(thread_id:)
|
|
109
|
+
delete(path: "/threads/#{thread_id}")
|
|
110
|
+
rescue
|
|
111
|
+
# Best effort. The seeding failure is the one worth surfacing, and it is raised either way.
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_thread(thread_id:, assistant_id:)
|
|
115
|
+
post(
|
|
79
116
|
path: "/threads",
|
|
80
117
|
body: {
|
|
81
118
|
threadId: thread_id.to_s,
|
|
82
|
-
ifExists: "
|
|
83
|
-
|
|
119
|
+
ifExists: "raise",
|
|
120
|
+
# Without a graph_id, the thread state cannot be updated before the thread's first run.
|
|
121
|
+
metadata: { graph_id: graph_id_for(assistant_id) },
|
|
84
122
|
user_id:,
|
|
85
123
|
}
|
|
86
124
|
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def graph_id_for(assistant_id)
|
|
128
|
+
@graph_ids[assistant_id] ||= fetch_graph_id(assistant_id)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def fetch_graph_id(assistant_id)
|
|
132
|
+
assistant_response = get(path: "/assistants/#{assistant_id}")
|
|
133
|
+
|
|
134
|
+
raise ThreadInitializationError, assistant_response.body if assistant_response.code.to_i != 200
|
|
135
|
+
|
|
136
|
+
graph_id = JSON.parse(assistant_response.body)["graph_id"]
|
|
137
|
+
|
|
138
|
+
raise ThreadInitializationError, "Assistant #{assistant_id} has no graph_id" if graph_id.blank?
|
|
139
|
+
|
|
140
|
+
graph_id
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def seed_thread_state(thread_id:, initial_state:)
|
|
144
|
+
state_response = post(
|
|
145
|
+
path: "/threads/#{thread_id}/state",
|
|
146
|
+
body: { values: { messages: initial_state } }
|
|
147
|
+
)
|
|
87
148
|
|
|
88
|
-
raise ThreadInitializationError,
|
|
149
|
+
raise ThreadInitializationError, state_response.body if state_response.code.to_i != 200
|
|
89
150
|
|
|
90
|
-
|
|
151
|
+
JSON.parse(state_response.body)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def thread_already_exists?(response)
|
|
155
|
+
response.code.to_i == THREAD_CONFLICT_CODE
|
|
91
156
|
end
|
|
92
157
|
|
|
93
158
|
def get_thread_state(thread_id:)
|
|
94
159
|
state_response = get(path: "/threads/#{thread_id}/state")
|
|
95
160
|
|
|
96
|
-
raise ThreadResumptionError, state_response.body if state_response.code != 200
|
|
161
|
+
raise ThreadResumptionError, state_response.body if state_response.code.to_i != 200
|
|
97
162
|
|
|
98
|
-
state_response
|
|
163
|
+
JSON.parse(state_response.body)
|
|
99
164
|
end
|
|
100
165
|
|
|
101
166
|
def get_thread(thread_id:)
|
|
102
167
|
thread_response = get(path: "/threads/#{thread_id}")
|
|
103
168
|
|
|
104
|
-
raise ThreadResumptionError, thread_response.body if thread_response.code != 200
|
|
169
|
+
raise ThreadResumptionError, thread_response.body if thread_response.code.to_i != 200
|
|
105
170
|
|
|
106
|
-
thread_response
|
|
171
|
+
JSON.parse(thread_response.body)
|
|
107
172
|
end
|
|
108
173
|
|
|
109
174
|
def trigger_run(thread_id:, assistant_id:, last_message:, context: {})
|
|
@@ -118,9 +183,10 @@ module NitroIntelligence
|
|
|
118
183
|
}
|
|
119
184
|
)
|
|
120
185
|
|
|
121
|
-
raise RunError, run_response.body if run_response.code != 200
|
|
186
|
+
raise RunError, run_response.body if run_response.code.to_i != 200
|
|
122
187
|
|
|
123
|
-
|
|
188
|
+
run = JSON.parse(run_response.body)
|
|
189
|
+
Array(run["messages"]).last&.dig("content")
|
|
124
190
|
end
|
|
125
191
|
|
|
126
192
|
def resume_run(thread_id:, assistant_id:, resume:, context:)
|
|
@@ -135,9 +201,9 @@ module NitroIntelligence
|
|
|
135
201
|
}
|
|
136
202
|
)
|
|
137
203
|
|
|
138
|
-
raise ThreadResumptionError, run_response.body if run_response.code != 200
|
|
204
|
+
raise ThreadResumptionError, run_response.body if run_response.code.to_i != 200
|
|
139
205
|
|
|
140
|
-
run_response
|
|
206
|
+
JSON.parse(run_response.body)
|
|
141
207
|
end
|
|
142
208
|
|
|
143
209
|
def interrupted?(thread)
|
|
@@ -163,18 +229,37 @@ module NitroIntelligence
|
|
|
163
229
|
end
|
|
164
230
|
|
|
165
231
|
def get(path:)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
232
|
+
uri = URI("#{base_url}#{path}")
|
|
233
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
234
|
+
http.use_ssl = uri.scheme == "https"
|
|
235
|
+
|
|
236
|
+
request = Net::HTTP::Get.new(uri)
|
|
237
|
+
request_headers.each { |k, v| request[k] = v }
|
|
238
|
+
|
|
239
|
+
http.request(request)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def delete(path:)
|
|
243
|
+
uri = URI("#{base_url}#{path}")
|
|
244
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
245
|
+
http.use_ssl = uri.scheme == "https"
|
|
246
|
+
|
|
247
|
+
request = Net::HTTP::Delete.new(uri)
|
|
248
|
+
request_headers.each { |k, v| request[k] = v }
|
|
249
|
+
|
|
250
|
+
http.request(request)
|
|
170
251
|
end
|
|
171
252
|
|
|
172
253
|
def post(path:, body:)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
254
|
+
uri = URI("#{base_url}#{path}")
|
|
255
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
256
|
+
http.use_ssl = uri.scheme == "https"
|
|
257
|
+
|
|
258
|
+
request = Net::HTTP::Post.new(uri)
|
|
259
|
+
request_headers.each { |k, v| request[k] = v }
|
|
260
|
+
request.body = body.to_json
|
|
261
|
+
|
|
262
|
+
http.request(request)
|
|
178
263
|
end
|
|
179
264
|
|
|
180
265
|
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
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
require "openai"
|
|
2
|
+
require "nitro_intelligence/client/handlers/base_handler"
|
|
2
3
|
|
|
3
4
|
module NitroIntelligence
|
|
4
5
|
module Client
|
|
5
6
|
module Handlers
|
|
6
|
-
class AudioTranscriptionHandler
|
|
7
|
+
class AudioTranscriptionHandler < BaseHandler
|
|
7
8
|
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::TranscriptionCreateParams.fields.keys.uniq.freeze
|
|
8
9
|
|
|
9
|
-
def initialize(client:)
|
|
10
|
-
@client = client
|
|
11
|
-
end
|
|
12
|
-
|
|
13
10
|
def create(audio_file:, message: "", parameters: {})
|
|
14
11
|
validate_and_resolve!(parameters)
|
|
15
12
|
perform_request(audio_file:, message:, parameters:)
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def perform_request(audio_file:, message: "", parameters: {})
|
|
16
|
+
add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
|
|
19
17
|
@client.audio.transcriptions.create(
|
|
20
18
|
prompt: message,
|
|
21
19
|
file: audio_file,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module NitroIntelligence
|
|
2
|
+
module Client
|
|
3
|
+
module Handlers
|
|
4
|
+
class BaseHandler
|
|
5
|
+
MODALITY_HEADER = "nip-modality".freeze
|
|
6
|
+
REQUESTED_MODEL_HEADER = "nip-requested-model".freeze
|
|
7
|
+
|
|
8
|
+
def initialize(client:)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def add_request_headers(parameters, headers)
|
|
15
|
+
request_options = (parameters[:request_options] ||= {})
|
|
16
|
+
(request_options[:extra_headers] ||= {}).merge!(headers.compact)
|
|
17
|
+
parameters
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
require "openai"
|
|
2
|
+
require "nitro_intelligence/client/handlers/base_handler"
|
|
2
3
|
|
|
3
4
|
module NitroIntelligence
|
|
4
5
|
module Client
|
|
5
6
|
module Handlers
|
|
6
|
-
class ChatHandler
|
|
7
|
+
class ChatHandler < BaseHandler
|
|
7
8
|
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Chat::CompletionCreateParams.fields.keys.uniq.freeze
|
|
8
9
|
|
|
9
|
-
def initialize(client:)
|
|
10
|
-
@client = client
|
|
11
|
-
end
|
|
12
|
-
|
|
13
10
|
def create(message: "", parameters: {})
|
|
14
11
|
validate_and_resolve!(parameters, message)
|
|
15
12
|
perform_request(parameters:)
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def perform_request(parameters: {})
|
|
16
|
+
add_request_headers(parameters, REQUESTED_MODEL_HEADER => parameters[:model])
|
|
19
17
|
@client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
|
|
20
18
|
end
|
|
21
19
|
|
|
@@ -29,7 +27,6 @@ module NitroIntelligence
|
|
|
29
27
|
metadata: {},
|
|
30
28
|
messages: [],
|
|
31
29
|
model: NitroIntelligence.model_catalog.default_text_model&.name,
|
|
32
|
-
extra_headers: { "Prefer" => "wait" },
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
parameters.replace(default_parameters.merge(parameters))
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
require "openai"
|
|
2
|
+
require "nitro_intelligence/client/handlers/base_handler"
|
|
2
3
|
require "nitro_intelligence/media/image_generation"
|
|
3
4
|
|
|
4
5
|
module NitroIntelligence
|
|
5
6
|
module Client
|
|
6
7
|
module Handlers
|
|
7
|
-
class ImageHandler
|
|
8
|
+
class ImageHandler < BaseHandler
|
|
8
9
|
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Chat::CompletionCreateParams.fields.keys.uniq.freeze
|
|
9
10
|
|
|
10
|
-
def initialize(client:)
|
|
11
|
-
@client = client
|
|
12
|
-
end
|
|
13
|
-
|
|
14
11
|
def create(message: "", target_image: nil, reference_images: [], parameters: {})
|
|
15
12
|
image_generation = build_image_generation(message:, target_image:, reference_images:, parameters:)
|
|
16
13
|
|
|
@@ -23,6 +20,7 @@ module NitroIntelligence
|
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
def perform_request(parameters: {})
|
|
23
|
+
add_request_headers(parameters, MODALITY_HEADER => "image", REQUESTED_MODEL_HEADER => parameters[:model])
|
|
26
24
|
@client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
|
|
27
25
|
end
|
|
28
26
|
|
|
@@ -32,7 +30,6 @@ module NitroIntelligence
|
|
|
32
30
|
metadata: {},
|
|
33
31
|
messages: image_generation.messages,
|
|
34
32
|
model: image_generation.config.model,
|
|
35
|
-
extra_headers: { "Prefer" => "wait" },
|
|
36
33
|
request_options: {
|
|
37
34
|
extra_body: {
|
|
38
35
|
image_config: {
|
|
@@ -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,56 @@
|
|
|
1
|
+
require "openai"
|
|
2
|
+
require "nitro_intelligence/client/handlers/base_handler"
|
|
3
|
+
|
|
4
|
+
module NitroIntelligence
|
|
5
|
+
module Client
|
|
6
|
+
module Handlers
|
|
7
|
+
class TextToSpeechHandler < BaseHandler
|
|
8
|
+
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::SpeechCreateParams.fields.keys.uniq.freeze
|
|
9
|
+
|
|
10
|
+
def create(message: "", parameters: {})
|
|
11
|
+
validate_and_resolve!(parameters)
|
|
12
|
+
perform_request(message:, parameters:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform_request(message: "", parameters: {})
|
|
16
|
+
add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
|
|
17
|
+
@client.audio.speech.create(
|
|
18
|
+
input: message,
|
|
19
|
+
**parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_and_resolve!(parameters)
|
|
24
|
+
model_name = parameters[:model] || NitroIntelligence.model_catalog.default_text_to_speech_model&.name
|
|
25
|
+
model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
|
|
26
|
+
raise ArgumentError, "Unsupported model: '#{model_name}'" unless model
|
|
27
|
+
|
|
28
|
+
default_parameters = {
|
|
29
|
+
metadata: {},
|
|
30
|
+
model: model.name,
|
|
31
|
+
voice: model.default_voice,
|
|
32
|
+
response_format: model.default_response_format,
|
|
33
|
+
}
|
|
34
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
35
|
+
validate_voice_and_format!(model, parameters)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_voice_and_format!(model, parameters)
|
|
41
|
+
unless model.voices.include?(parameters[:voice])
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"Unsupported voice: '#{parameters[:voice]}'. " \
|
|
44
|
+
"Supported voices for #{model.name} are: #{model.voices}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return if model.response_formats.include?(parameters[:response_format])
|
|
48
|
+
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Unsupported response_format: '#{parameters[:response_format]}'. " \
|
|
51
|
+
"Supported response_formats for #{model.name} are: #{model.response_formats}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
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: 1.0
|
|
4
|
+
version: 2.1.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
|
|
@@ -240,11 +80,14 @@ files:
|
|
|
240
80
|
- lib/nitro_intelligence/client/client.rb
|
|
241
81
|
- lib/nitro_intelligence/client/factory.rb
|
|
242
82
|
- lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb
|
|
83
|
+
- lib/nitro_intelligence/client/handlers/base_handler.rb
|
|
243
84
|
- lib/nitro_intelligence/client/handlers/chat_handler.rb
|
|
244
85
|
- lib/nitro_intelligence/client/handlers/image_handler.rb
|
|
245
86
|
- lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
|
|
246
87
|
- lib/nitro_intelligence/client/handlers/observed/chat_handler.rb
|
|
247
88
|
- lib/nitro_intelligence/client/handlers/observed/image_handler.rb
|
|
89
|
+
- lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb
|
|
90
|
+
- lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb
|
|
248
91
|
- lib/nitro_intelligence/client/observed.rb
|
|
249
92
|
- lib/nitro_intelligence/client/observers/langfuse_observer.rb
|
|
250
93
|
- lib/nitro_intelligence/configuration.rb
|