nitro_intelligence 2.3.0 → 2.4.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: 2c196d3b804d5a9137a2af97b45df17a137382114b2d592d835d3477af0541e6
4
- data.tar.gz: f0fb2d024492cd2f04b52d12f41930fc0ac9ed323aca3681a59016eccc3db7fb
3
+ metadata.gz: b9041d12c07ae072e5a7a5a2c5fccbec275d260dfd2c99cba7a5bc92337eab4d
4
+ data.tar.gz: 496aed9a03587308b2af556fa846686edfee3e3a1aaf4a7a3a686cf4f08b322b
5
5
  SHA512:
6
- metadata.gz: c41e6aa4592a56a75a7a45715ffc2a313f0a1ecf39c227a780334d60b204292790bb715da83783763931da5af95d1e845dffb3addc208329c9dbd01ed2f75d0b
7
- data.tar.gz: 7eb926cd024f0b73c2b372e75d65a43cf51a82f7afd487cd589b61be0f4130549d3fe08c3f9a01965668e092f092a5d431423af925533997f863c88f70c7534c
6
+ metadata.gz: ffa988435a0128d981e79a94f28530d1525b82c231f44e6a3cd87f15d459bcdd27ad2ef19b57499153a0a583256bb7bec2c711f51bd3cf519d9c9827cb080937
7
+ data.tar.gz: 18e594810e1c4815f759375f4692cb726dbc5415c00495aa03b3943d16c77d0079aa115f5aeb9ed368d715429aaafff6967f986c4b1496984160c7cb95c8aeea
data/docs/README.md CHANGED
@@ -31,8 +31,8 @@ NitroIntelligence.configure do |config|
31
31
  },
32
32
  ]
33
33
 
34
- # Agent server settings (optional)
35
- config.agent_server_config = {} # Hash of AgentServer keyword arguments
34
+ # Nitro Intelligence Assistants settings (optional)
35
+ config.assistants_config = {} # Hash of Assistants keyword arguments
36
36
 
37
37
  # Model configuration
38
38
  config.model_config = {
@@ -80,7 +80,7 @@ end
80
80
  | `inference_base_url` | `String` | `""` | Base URL for the LLM inference service |
81
81
  | `observability_base_url` | `String` | `""` | Base URL for the Langfuse observability service |
82
82
  | `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key) |
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 |
83
+ | `assistants_config` | `Hash` | `{}` | Credentials for `Assistants.new`. Expected keys: `base_url` (String) — HTTP base URL of Nitro Intelligence Assistants; `api_key` (String) — bearer token; `user_id` (String, default: `"default-user"`) — caller identity |
84
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) |
85
85
 
86
86
  ## Basic Usage
@@ -489,8 +489,8 @@ client.chat(
489
489
  )
490
490
  ```
491
491
 
492
- ## Agent Server
492
+ ## Nitro Intelligence Assistants
493
493
 
494
- The Agent Server is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect agent tool calls pending human approval, and resume interrupted threads after human reviews.
494
+ `NitroIntelligence::Assistants` is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect agent tool calls pending human approval, and resume interrupted threads after human reviews.
495
495
 
496
- For the full Agent Server guide, see [AGENT_SERVER.md](AGENT_SERVER.md).
496
+ For the full guide, see [ASSISTANTS.md](ASSISTANTS.md). For the service this SDK talks to, see the [Nitro Intelligence Assistants documentation](https://portal.powerapp.cloud/docs/default/system/nip-assistants).
@@ -1,272 +1,12 @@
1
- require "json"
2
- require "net/http"
3
- require "uri"
4
- require "nitro_intelligence/tool_call_review_validator"
1
+ require "active_support/deprecation/constant_accessor"
2
+ require "nitro_intelligence/assistants"
3
+ require "nitro_intelligence/deprecation"
5
4
 
6
5
  module NitroIntelligence
7
- class AgentServer
8
- class ConfigurationError < StandardError; end
9
- class ThreadInitializationError < StandardError; end
10
- class RunError < StandardError; end
11
- class ThreadResumptionError < StandardError; end
6
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
12
7
 
13
- # Aegra answers with a conflict when `ifExists: "raise"` is sent for a thread that already exists.
14
- THREAD_CONFLICT_CODE = 409
15
-
16
- attr_reader :base_url, :user_id
17
-
18
- def initialize(base_url:, api_key:, user_id: "default-user")
19
- raise ConfigurationError, "base_url is required" if base_url.blank?
20
- raise ConfigurationError, "api_key is required" if api_key.blank?
21
- raise ConfigurationError, "user_id is required" if user_id.blank?
22
-
23
- @base_url = base_url
24
- @api_key = api_key
25
- @user_id = user_id
26
- @tool_call_review_validator = ToolCallReviewValidator.new
27
- @graph_ids = {}
28
- end
29
-
30
- def await_run(thread_id:, assistant_id:, messages:, context: {})
31
- raise RunError, "messages cannot be empty" if messages.blank?
32
-
33
- initial_state = messages[0..-2]
34
- last_message = messages.last
35
-
36
- initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
37
- trigger_run(thread_id:, assistant_id:, context:, last_message:)
38
- end
39
-
40
- def tool_calls_pending_review(thread_id:)
41
- thread_state = get_thread_state(thread_id:)
42
- messages = thread_messages(thread_state)
43
- reviewed_tool_call_ids = tool_messages(messages).map { |message| message["tool_call_id"] }
44
-
45
- messages.each_with_index.flat_map do |message, index|
46
- next [] unless message["type"] == "ai"
47
-
48
- pending_tool_calls(message, reviewed_tool_call_ids).map do |tool_call|
49
- {
50
- "previous_message_id" => index.zero? ? nil : messages[index - 1]&.dig("id"),
51
- "id" => tool_call["id"],
52
- "name" => tool_call["name"],
53
- "args" => tool_call["args"] || {},
54
- }
55
- end
56
- end
57
- end
58
-
59
- def review_tool_calls(thread_id:, assistant_id:, reviewer_id:, tool_calls:, reviewed_at: DateTime.current.iso8601)
60
- resume = { reviewer_id:, reviewed_at:, tool_calls: }.with_indifferent_access
61
- thread = get_thread(thread_id:)
62
- raise ThreadResumptionError, "Thread #{thread_id} is not in the interrupted state" unless interrupted?(thread)
63
-
64
- thread_state = get_thread_state(thread_id:)
65
-
66
- @tool_call_review_validator.validate!(
67
- thread_state:,
68
- tool_calls: resume[:tool_calls],
69
- pending_tool_calls: tool_calls_pending_review(thread_id:)
70
- )
71
-
72
- resume_run(
73
- thread_id:,
74
- assistant_id:,
75
- resume:,
76
- context: interrupt_context(thread_state)
77
- )
78
-
79
- nil
80
- end
81
-
82
- private
83
-
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(
116
- path: "/threads",
117
- body: {
118
- threadId: thread_id.to_s,
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) },
122
- user_id:,
123
- }
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
- )
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
156
- end
157
-
158
- def get_thread_state(thread_id:)
159
- state_response = get(path: "/threads/#{thread_id}/state")
160
-
161
- raise ThreadResumptionError, state_response.body if state_response.code.to_i != 200
162
-
163
- JSON.parse(state_response.body)
164
- end
165
-
166
- def get_thread(thread_id:)
167
- thread_response = get(path: "/threads/#{thread_id}")
168
-
169
- raise ThreadResumptionError, thread_response.body if thread_response.code.to_i != 200
170
-
171
- JSON.parse(thread_response.body)
172
- end
173
-
174
- def trigger_run(thread_id:, assistant_id:, last_message:, context: {})
175
- run_response = post(
176
- path: "/threads/#{thread_id}/runs/wait",
177
- body: {
178
- assistant_id:,
179
- context:,
180
- input: {
181
- messages: [last_message],
182
- },
183
- }
184
- )
185
-
186
- raise RunError, run_response.body if run_response.code.to_i != 200
187
-
188
- run = JSON.parse(run_response.body)
189
- Array(run["messages"]).last&.dig("content")
190
- end
191
-
192
- def resume_run(thread_id:, assistant_id:, resume:, context:)
193
- run_response = post(
194
- path: "/threads/#{thread_id}/runs/wait",
195
- body: {
196
- assistant_id:,
197
- command: {
198
- resume:,
199
- },
200
- context:,
201
- }
202
- )
203
-
204
- raise ThreadResumptionError, run_response.body if run_response.code.to_i != 200
205
-
206
- JSON.parse(run_response.body)
207
- end
208
-
209
- def interrupted?(thread)
210
- thread["status"] == "interrupted"
211
- end
212
-
213
- def interrupt_context(thread_state)
214
- thread_state.dig("interrupts", 0, "value", "context") || {}
215
- end
216
-
217
- def thread_messages(thread_state)
218
- Array(thread_state.dig("values", "messages"))
219
- end
220
-
221
- def tool_messages(messages)
222
- messages.select { |message| message["type"] == "tool" }
223
- end
224
-
225
- def pending_tool_calls(message, reviewed_tool_call_ids)
226
- Array(message["tool_calls"]).reject do |tool_call|
227
- reviewed_tool_call_ids.include?(tool_call["id"])
228
- end
229
- end
230
-
231
- def get(path:)
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)
251
- end
252
-
253
- def post(path:, body:)
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)
263
- end
264
-
265
- def request_headers
266
- {
267
- "Content-Type" => "application/json",
268
- "Authorization" => "Bearer #{@api_key}",
269
- }
270
- end
271
- end
8
+ # Deprecated: the agent server is now Nitro Intelligence Assistants. Resolved through
9
+ # `const_missing` so that the old name returns the real class rather than a stand-in for it,
10
+ # keeping `is_a?`, `===`, `rescue` and the nested error constants working on upgrade.
11
+ deprecate_constant :AgentServer, "NitroIntelligence::Assistants", deprecator:
272
12
  end
@@ -0,0 +1,287 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+ require "nitro_intelligence/tool_call_review_validator"
5
+
6
+ module NitroIntelligence
7
+ class Assistants
8
+ class ConfigurationError < StandardError; end
9
+ class ThreadInitializationError < StandardError; end
10
+ class RunError < StandardError; end
11
+ class ThreadResumptionError < StandardError; end
12
+ class ThreadStateError < StandardError; end
13
+
14
+ # Assistants answers with a conflict when `ifExists: "raise"` is sent for a thread that already exists.
15
+ THREAD_CONFLICT_CODE = 409
16
+
17
+ attr_reader :base_url, :user_id
18
+
19
+ def initialize(base_url:, api_key:, user_id: "default-user")
20
+ raise ConfigurationError, "base_url is required" if base_url.blank?
21
+ raise ConfigurationError, "api_key is required" if api_key.blank?
22
+ raise ConfigurationError, "user_id is required" if user_id.blank?
23
+
24
+ @base_url = base_url
25
+ @api_key = api_key
26
+ @user_id = user_id
27
+ @tool_call_review_validator = ToolCallReviewValidator.new
28
+ @graph_ids = {}
29
+ end
30
+
31
+ def await_run(thread_id:, assistant_id:, messages:, context: {})
32
+ raise RunError, "messages cannot be empty" if messages.blank?
33
+
34
+ initial_state = messages[0..-2]
35
+ last_message = messages.last
36
+
37
+ initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
38
+ trigger_run(thread_id:, assistant_id:, context:, last_message:)
39
+ end
40
+
41
+ # The thread's state as Assistants reports it, unformatted. Callers that only want the
42
+ # conversation should reach for #thread_messages instead.
43
+ def thread_state(thread_id:)
44
+ get_thread_state(thread_id:, error: ThreadStateError)
45
+ end
46
+
47
+ # The thread's messages as Assistants reports them, unformatted, oldest first. Each message
48
+ # carries its own `type` ("human", "ai", "tool", ...), which callers map to their own roles.
49
+ def thread_messages(thread_id:)
50
+ messages_in(thread_state(thread_id:))
51
+ end
52
+
53
+ def tool_calls_pending_review(thread_id:)
54
+ thread_state = get_thread_state(thread_id:)
55
+ messages = messages_in(thread_state)
56
+ reviewed_tool_call_ids = tool_messages(messages).map { |message| message["tool_call_id"] }
57
+
58
+ messages.each_with_index.flat_map do |message, index|
59
+ next [] unless message["type"] == "ai"
60
+
61
+ pending_tool_calls(message, reviewed_tool_call_ids).map do |tool_call|
62
+ {
63
+ "previous_message_id" => index.zero? ? nil : messages[index - 1]&.dig("id"),
64
+ "id" => tool_call["id"],
65
+ "name" => tool_call["name"],
66
+ "args" => tool_call["args"] || {},
67
+ }
68
+ end
69
+ end
70
+ end
71
+
72
+ def review_tool_calls(thread_id:, assistant_id:, reviewer_id:, tool_calls:, reviewed_at: DateTime.current.iso8601)
73
+ resume = { reviewer_id:, reviewed_at:, tool_calls: }.with_indifferent_access
74
+ thread = get_thread(thread_id:)
75
+ raise ThreadResumptionError, "Thread #{thread_id} is not in the interrupted state" unless interrupted?(thread)
76
+
77
+ thread_state = get_thread_state(thread_id:)
78
+
79
+ @tool_call_review_validator.validate!(
80
+ thread_state:,
81
+ tool_calls: resume[:tool_calls],
82
+ pending_tool_calls: tool_calls_pending_review(thread_id:)
83
+ )
84
+
85
+ resume_run(
86
+ thread_id:,
87
+ assistant_id:,
88
+ resume:,
89
+ context: interrupt_context(thread_state)
90
+ )
91
+
92
+ nil
93
+ end
94
+
95
+ private
96
+
97
+ # Assistants accepts `initial_state` on thread creation but never applies it, so a brand new thread is
98
+ # seeded through the thread state endpoint instead. A thread that already exists is left untouched:
99
+ # its state was seeded when it was created and has been built up by every run since.
100
+ def initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
101
+ thread_response = create_thread(thread_id:, assistant_id:)
102
+
103
+ return if thread_already_exists?(thread_response)
104
+ raise ThreadInitializationError, thread_response.body if thread_response.code.to_i != 200
105
+ return if initial_state.blank?
106
+
107
+ seed_new_thread_state(thread_id:, initial_state:)
108
+ end
109
+
110
+ # Creating the thread and seeding its state are separate requests, so a failure between them leaves
111
+ # an empty thread behind. A retry would find that thread, take it for one already under way, skip
112
+ # seeding and run without the history -- losing the very thing seeding exists for, without an error.
113
+ # Discard the thread instead, so a retry starts over from a clean slate.
114
+ def seed_new_thread_state(thread_id:, initial_state:)
115
+ seed_thread_state(thread_id:, initial_state:)
116
+ rescue
117
+ discard_thread(thread_id:)
118
+ raise
119
+ end
120
+
121
+ def discard_thread(thread_id:)
122
+ delete(path: "/threads/#{thread_id}")
123
+ rescue
124
+ # Best effort. The seeding failure is the one worth surfacing, and it is raised either way.
125
+ end
126
+
127
+ def create_thread(thread_id:, assistant_id:)
128
+ post(
129
+ path: "/threads",
130
+ body: {
131
+ threadId: thread_id.to_s,
132
+ ifExists: "raise",
133
+ # Without a graph_id, the thread state cannot be updated before the thread's first run.
134
+ metadata: { graph_id: graph_id_for(assistant_id) },
135
+ user_id:,
136
+ }
137
+ )
138
+ end
139
+
140
+ def graph_id_for(assistant_id)
141
+ @graph_ids[assistant_id] ||= fetch_graph_id(assistant_id)
142
+ end
143
+
144
+ def fetch_graph_id(assistant_id)
145
+ assistant_response = get(path: "/assistants/#{assistant_id}")
146
+
147
+ raise ThreadInitializationError, assistant_response.body if assistant_response.code.to_i != 200
148
+
149
+ graph_id = JSON.parse(assistant_response.body)["graph_id"]
150
+
151
+ raise ThreadInitializationError, "Assistant #{assistant_id} has no graph_id" if graph_id.blank?
152
+
153
+ graph_id
154
+ end
155
+
156
+ def seed_thread_state(thread_id:, initial_state:)
157
+ state_response = post(
158
+ path: "/threads/#{thread_id}/state",
159
+ body: { values: { messages: initial_state } }
160
+ )
161
+
162
+ raise ThreadInitializationError, state_response.body if state_response.code.to_i != 200
163
+
164
+ JSON.parse(state_response.body)
165
+ end
166
+
167
+ def thread_already_exists?(response)
168
+ response.code.to_i == THREAD_CONFLICT_CODE
169
+ end
170
+
171
+ # The review flows have always raised ThreadResumptionError when a state read fails, and consumers
172
+ # rescue it as such. A plain read resumes nothing, so #thread_state asks for ThreadStateError.
173
+ def get_thread_state(thread_id:, error: ThreadResumptionError)
174
+ state_response = get(path: "/threads/#{thread_id}/state")
175
+
176
+ raise error, state_response.body if state_response.code.to_i != 200
177
+
178
+ JSON.parse(state_response.body)
179
+ end
180
+
181
+ def get_thread(thread_id:)
182
+ thread_response = get(path: "/threads/#{thread_id}")
183
+
184
+ raise ThreadResumptionError, thread_response.body if thread_response.code.to_i != 200
185
+
186
+ JSON.parse(thread_response.body)
187
+ end
188
+
189
+ def trigger_run(thread_id:, assistant_id:, last_message:, context: {})
190
+ run_response = post(
191
+ path: "/threads/#{thread_id}/runs/wait",
192
+ body: {
193
+ assistant_id:,
194
+ context:,
195
+ input: {
196
+ messages: [last_message],
197
+ },
198
+ }
199
+ )
200
+
201
+ raise RunError, run_response.body if run_response.code.to_i != 200
202
+
203
+ run = JSON.parse(run_response.body)
204
+ Array(run["messages"]).last&.dig("content")
205
+ end
206
+
207
+ def resume_run(thread_id:, assistant_id:, resume:, context:)
208
+ run_response = post(
209
+ path: "/threads/#{thread_id}/runs/wait",
210
+ body: {
211
+ assistant_id:,
212
+ command: {
213
+ resume:,
214
+ },
215
+ context:,
216
+ }
217
+ )
218
+
219
+ raise ThreadResumptionError, run_response.body if run_response.code.to_i != 200
220
+
221
+ JSON.parse(run_response.body)
222
+ end
223
+
224
+ def interrupted?(thread)
225
+ thread["status"] == "interrupted"
226
+ end
227
+
228
+ def interrupt_context(thread_state)
229
+ thread_state.dig("interrupts", 0, "value", "context") || {}
230
+ end
231
+
232
+ def messages_in(thread_state)
233
+ Array(thread_state.dig("values", "messages"))
234
+ end
235
+
236
+ def tool_messages(messages)
237
+ messages.select { |message| message["type"] == "tool" }
238
+ end
239
+
240
+ def pending_tool_calls(message, reviewed_tool_call_ids)
241
+ Array(message["tool_calls"]).reject do |tool_call|
242
+ reviewed_tool_call_ids.include?(tool_call["id"])
243
+ end
244
+ end
245
+
246
+ def get(path:)
247
+ uri = URI("#{base_url}#{path}")
248
+ http = Net::HTTP.new(uri.host, uri.port)
249
+ http.use_ssl = uri.scheme == "https"
250
+
251
+ request = Net::HTTP::Get.new(uri)
252
+ request_headers.each { |k, v| request[k] = v }
253
+
254
+ http.request(request)
255
+ end
256
+
257
+ def delete(path:)
258
+ uri = URI("#{base_url}#{path}")
259
+ http = Net::HTTP.new(uri.host, uri.port)
260
+ http.use_ssl = uri.scheme == "https"
261
+
262
+ request = Net::HTTP::Delete.new(uri)
263
+ request_headers.each { |k, v| request[k] = v }
264
+
265
+ http.request(request)
266
+ end
267
+
268
+ def post(path:, body:)
269
+ uri = URI("#{base_url}#{path}")
270
+ http = Net::HTTP.new(uri.host, uri.port)
271
+ http.use_ssl = uri.scheme == "https"
272
+
273
+ request = Net::HTTP::Post.new(uri)
274
+ request_headers.each { |k, v| request[k] = v }
275
+ request.body = body.to_json
276
+
277
+ http.request(request)
278
+ end
279
+
280
+ def request_headers
281
+ {
282
+ "Content-Type" => "application/json",
283
+ "Authorization" => "Bearer #{@api_key}",
284
+ }
285
+ end
286
+ end
287
+ end
@@ -10,7 +10,7 @@ module NitroIntelligence
10
10
  config_accessor :cache_provider, default: NitroIntelligence::NullCache.new
11
11
  config_accessor :current_revision, default: ""
12
12
  config_accessor :environment, default: "test"
13
- config_accessor :agent_server_config, default: {}
13
+ config_accessor :assistants_config, default: {}
14
14
  config_accessor :inference_api_key, default: ""
15
15
  config_accessor :inference_base_url, default: ""
16
16
  config_accessor :model_config, default: {}
@@ -18,6 +18,10 @@ module NitroIntelligence
18
18
  config_accessor :observability_projects, default: []
19
19
  config_accessor :observability_user_id, default: ""
20
20
 
21
+ # Deprecated: configure `assistants_config` instead. Keeps its original `{}` default through the
22
+ # deprecation window, so a host building the hash up in place still has one to build on.
23
+ config_accessor :agent_server_config, default: {}
24
+
21
25
  class << self
22
26
  def configure
23
27
  yield config
@@ -0,0 +1,11 @@
1
+ require "active_support/deprecation"
2
+
3
+ module NitroIntelligence
4
+ class << self
5
+ # Deprecations introduced while the agent server was renamed to Nitro Intelligence Assistants.
6
+ # The names they cover are removed in 3.0.
7
+ def deprecator
8
+ @deprecator ||= ActiveSupport::Deprecation.new("3.0", "Nitro Intelligence")
9
+ end
10
+ end
11
+ end
@@ -63,7 +63,7 @@ module NitroIntelligence
63
63
  end
64
64
 
65
65
  def raise_error!(message)
66
- raise NitroIntelligence::AgentServer::ThreadResumptionError, message
66
+ raise NitroIntelligence::Assistants::ThreadResumptionError, message
67
67
  end
68
68
  end
69
69
  end
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "2.3.0".freeze
2
+ VERSION = "2.4.0".freeze
3
3
  end
@@ -7,9 +7,11 @@ require "openai"
7
7
 
8
8
  require "nitro_intelligence/version"
9
9
  require "nitro_intelligence/agent_server"
10
+ require "nitro_intelligence/assistants"
10
11
  require "nitro_intelligence/client/base"
11
12
  require "nitro_intelligence/client/client"
12
13
  require "nitro_intelligence/configuration"
14
+ require "nitro_intelligence/deprecation"
13
15
  require "nitro_intelligence/media/image_generation"
14
16
  require "nitro_intelligence/models/model_catalog"
15
17
  require "nitro_intelligence/observability/project_client_registry"
@@ -21,8 +23,14 @@ module NitroIntelligence
21
23
  class << self
22
24
  delegate :configure, :config, :logger, :environment, to: :configuration
23
25
 
26
+ def assistants
27
+ Assistants.new(**assistants_config.symbolize_keys)
28
+ end
29
+
30
+ # Deprecated: use `NitroIntelligence.assistants`.
24
31
  def agent_server
25
- AgentServer.new(**configuration.agent_server_config.symbolize_keys)
32
+ deprecator.warn("`NitroIntelligence.agent_server` is deprecated. Use `NitroIntelligence.assistants` instead.")
33
+ assistants
26
34
  end
27
35
 
28
36
  def cache
@@ -38,5 +46,20 @@ module NitroIntelligence
38
46
  base_url: configuration.observability_base_url
39
47
  )
40
48
  end
49
+
50
+ private
51
+
52
+ # A host that has migrated is left alone, so a stale `agent_server_config` cannot override the
53
+ # configuration it was replaced by.
54
+ def assistants_config
55
+ current_config = configuration.assistants_config
56
+ legacy_config = configuration.agent_server_config
57
+ return current_config if current_config.present? || legacy_config.blank?
58
+
59
+ deprecator.warn(
60
+ "`agent_server_config` is deprecated. Configure `assistants_config` instead."
61
+ )
62
+ legacy_config
63
+ end
41
64
  end
42
65
  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.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -76,6 +76,7 @@ files:
76
76
  - docs/README.md
77
77
  - lib/nitro_intelligence.rb
78
78
  - lib/nitro_intelligence/agent_server.rb
79
+ - lib/nitro_intelligence/assistants.rb
79
80
  - lib/nitro_intelligence/client/base.rb
80
81
  - lib/nitro_intelligence/client/client.rb
81
82
  - lib/nitro_intelligence/client/factory.rb
@@ -91,6 +92,7 @@ files:
91
92
  - lib/nitro_intelligence/client/observed.rb
92
93
  - lib/nitro_intelligence/client/observers/langfuse_observer.rb
93
94
  - lib/nitro_intelligence/configuration.rb
95
+ - lib/nitro_intelligence/deprecation.rb
94
96
  - lib/nitro_intelligence/langfuse_extension.rb
95
97
  - lib/nitro_intelligence/langfuse_tracer_provider.rb
96
98
  - lib/nitro_intelligence/media/audio.rb