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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c03a8a4c436205c01d926c024c1bcd12542553f36e3998b10462653cec4cdc1
4
- data.tar.gz: 58f340818e47d7b6e36dd84f44b4072bf8636a83d4fd6931e65e62ec1ed29ab5
3
+ metadata.gz: 7d1c0e6c9ddffc032a989ebffb001d58aefabf86e50f92966a6d384a5218628f
4
+ data.tar.gz: 60b9ae5ef4fd8c7a79bc45f13a1b965c9d9a0792f38bc285e361822b7d91a8ad
5
5
  SHA512:
6
- metadata.gz: 70eab830245ab3c7bd4a6835c080770ee324dca9c440ba200f0ac29fc528e9b4be5579091259a9abed7c35ce51372da82d4452d1df71ee26c51a61bdb342acaa
7
- data.tar.gz: 549cd0d2477d1382cff28d866266797983b0ddd860396c10ff5edd8087f5538a4834b1194d1ae6d5d16d497825bf660308bcacfcb3da12846e2f69ef611264a4
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
- def initialize_thread_if_needed(thread_id:, initial_state:)
81
- thread_response = post(
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: "do_nothing",
86
- initial_state: { messages: initial_state },
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
- raise ThreadInitializationError, thread_response.body if thread_response.code.to_i != 200
127
+ def graph_id_for(assistant_id)
128
+ @graph_ids[assistant_id] ||= fetch_graph_id(assistant_id)
129
+ end
92
130
 
93
- JSON.parse(thread_response.body)
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
- # Check voice supported
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,
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "2.0.0".freeze
2
+ VERSION = "2.1.0".freeze
3
3
  end
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.0.0
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