nitro_intelligence 2.0.0 → 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/lib/nitro_intelligence/agent_server.rb +80 -7
- 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/text_to_speech_handler.rb +8 -10
- data/lib/nitro_intelligence/version.rb +1 -1
- metadata +2 -1
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
|
|
@@ -10,6 +10,9 @@ module NitroIntelligence
|
|
|
10
10
|
class RunError < StandardError; end
|
|
11
11
|
class ThreadResumptionError < StandardError; end
|
|
12
12
|
|
|
13
|
+
# Aegra answers with a conflict when `ifExists: "raise"` is sent for a thread that already exists.
|
|
14
|
+
THREAD_CONFLICT_CODE = 409
|
|
15
|
+
|
|
13
16
|
attr_reader :base_url, :user_id
|
|
14
17
|
|
|
15
18
|
def initialize(base_url:, api_key:, user_id: "default-user")
|
|
@@ -21,6 +24,7 @@ module NitroIntelligence
|
|
|
21
24
|
@api_key = api_key
|
|
22
25
|
@user_id = user_id
|
|
23
26
|
@tool_call_review_validator = ToolCallReviewValidator.new
|
|
27
|
+
@graph_ids = {}
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def await_run(thread_id:, assistant_id:, messages:, context: {})
|
|
@@ -29,7 +33,7 @@ module NitroIntelligence
|
|
|
29
33
|
initial_state = messages[0..-2]
|
|
30
34
|
last_message = messages.last
|
|
31
35
|
|
|
32
|
-
initialize_thread_if_needed(thread_id:, initial_state:)
|
|
36
|
+
initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
|
|
33
37
|
trigger_run(thread_id:, assistant_id:, context:, last_message:)
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -77,20 +81,78 @@ module NitroIntelligence
|
|
|
77
81
|
|
|
78
82
|
private
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
82
116
|
path: "/threads",
|
|
83
117
|
body: {
|
|
84
118
|
threadId: thread_id.to_s,
|
|
85
|
-
ifExists: "
|
|
86
|
-
|
|
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) },
|
|
87
122
|
user_id:,
|
|
88
123
|
}
|
|
89
124
|
)
|
|
125
|
+
end
|
|
90
126
|
|
|
91
|
-
|
|
127
|
+
def graph_id_for(assistant_id)
|
|
128
|
+
@graph_ids[assistant_id] ||= fetch_graph_id(assistant_id)
|
|
129
|
+
end
|
|
92
130
|
|
|
93
|
-
|
|
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
|
+
)
|
|
148
|
+
|
|
149
|
+
raise ThreadInitializationError, state_response.body if state_response.code.to_i != 200
|
|
150
|
+
|
|
151
|
+
JSON.parse(state_response.body)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def thread_already_exists?(response)
|
|
155
|
+
response.code.to_i == THREAD_CONFLICT_CODE
|
|
94
156
|
end
|
|
95
157
|
|
|
96
158
|
def get_thread_state(thread_id:)
|
|
@@ -177,6 +239,17 @@ module NitroIntelligence
|
|
|
177
239
|
http.request(request)
|
|
178
240
|
end
|
|
179
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)
|
|
251
|
+
end
|
|
252
|
+
|
|
180
253
|
def post(path:, body:)
|
|
181
254
|
uri = URI("#{base_url}#{path}")
|
|
182
255
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
@@ -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: {
|
|
@@ -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 TextToSpeechHandler
|
|
7
|
+
class TextToSpeechHandler < BaseHandler
|
|
7
8
|
ALLOWED_EXTRA_PARAMETERS = OpenAI::Models::Audio::SpeechCreateParams.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)
|
|
15
12
|
perform_request(message:, parameters:)
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def perform_request(message: "", parameters: {})
|
|
16
|
+
add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
|
|
19
17
|
@client.audio.speech.create(
|
|
20
18
|
input: message,
|
|
21
19
|
**parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
|
|
@@ -25,8 +23,6 @@ module NitroIntelligence
|
|
|
25
23
|
def validate_and_resolve!(parameters)
|
|
26
24
|
model_name = parameters[:model] || NitroIntelligence.model_catalog.default_text_to_speech_model&.name
|
|
27
25
|
model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
|
|
28
|
-
|
|
29
|
-
# Check model supported
|
|
30
26
|
raise ArgumentError, "Unsupported model: '#{model_name}'" unless model
|
|
31
27
|
|
|
32
28
|
default_parameters = {
|
|
@@ -35,17 +31,19 @@ module NitroIntelligence
|
|
|
35
31
|
voice: model.default_voice,
|
|
36
32
|
response_format: model.default_response_format,
|
|
37
33
|
}
|
|
38
|
-
|
|
39
34
|
parameters.replace(default_parameters.merge(parameters))
|
|
35
|
+
validate_voice_and_format!(model, parameters)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
def validate_voice_and_format!(model, parameters)
|
|
42
41
|
unless model.voices.include?(parameters[:voice])
|
|
43
42
|
raise ArgumentError,
|
|
44
43
|
"Unsupported voice: '#{parameters[:voice]}'. " \
|
|
45
44
|
"Supported voices for #{model.name} are: #{model.voices}"
|
|
46
45
|
end
|
|
47
46
|
|
|
48
|
-
# Check format supported
|
|
49
47
|
return if model.response_formats.include?(parameters[:response_format])
|
|
50
48
|
|
|
51
49
|
raise ArgumentError,
|
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: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Artemenko
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- lib/nitro_intelligence/client/client.rb
|
|
81
81
|
- lib/nitro_intelligence/client/factory.rb
|
|
82
82
|
- lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb
|
|
83
|
+
- lib/nitro_intelligence/client/handlers/base_handler.rb
|
|
83
84
|
- lib/nitro_intelligence/client/handlers/chat_handler.rb
|
|
84
85
|
- lib/nitro_intelligence/client/handlers/image_handler.rb
|
|
85
86
|
- lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
|