nitro_intelligence 2.0.0 → 2.2.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 +43 -0
- 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/observed/audio_transcription_handler.rb +13 -17
- data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +11 -14
- data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +10 -14
- data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +14 -18
- data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +8 -10
- data/lib/nitro_intelligence/observability/prompt_resolver.rb +69 -0
- data/lib/nitro_intelligence/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1181b17cc855ffa2f039b1401fd5dd2040c262d2878e6d265e65f9a581f6106e
|
|
4
|
+
data.tar.gz: 28b3a022b8d6d7e8e8472930fcf089fa5a3ead8a6c0d917e7587a73b69b8fba5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5fd5911aa6740d71f9dc5564766fe80137d7c2da6e7195ee406649387a22a8387bc02db78671e643d90445c6b8578e2b7b47d7573d4abdf677b35edcc41c6a3
|
|
7
|
+
data.tar.gz: d11585c4f9731911910660e67eb8702634475fd7bbf523b7313c81910d2775852f918a789a669719840196d64c474c1452842f6948e7618563ee1d5b2a1cd5e1
|
data/docs/README.md
CHANGED
|
@@ -254,6 +254,49 @@ client.chat(
|
|
|
254
254
|
|
|
255
255
|
If no `prompt_label` or `prompt_version` is provided, the 'production' prompt is used by default.
|
|
256
256
|
|
|
257
|
+
### Prompt Fallbacks
|
|
258
|
+
|
|
259
|
+
Pass `prompt_fallback_name` to name a second prompt to use when the one you asked for isn't there. The prompt in `prompt_name` is tried first; if it doesn't exist, or its lookup fails, the fallback is used instead:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
client = NitroIntelligence::Client.new(observability_project_slug: "fake-feature-project")
|
|
263
|
+
client.chat(
|
|
264
|
+
message: "Why is the sky blue?",
|
|
265
|
+
parameters: {
|
|
266
|
+
prompt_name: "My Prompt - Scheduling", # used when it exists
|
|
267
|
+
prompt_fallback_name: "My Prompt", # used when it doesn't
|
|
268
|
+
# prompt_fallback_label: "debug",
|
|
269
|
+
# prompt_fallback_version: "v2",
|
|
270
|
+
prompt_variables: {
|
|
271
|
+
appointment_id: "1234",
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This is for shipping a variant of a prompt — per domain, per locale, per experiment — without having to create one for every caller. Build the specific name however your feature likes; the fallback is just the prompt you'd have asked for anyway.
|
|
278
|
+
|
|
279
|
+
In practice the caller composes the specific name from something it already knows. Summarising a call, for example: each queue holds a different kind of conversation, so a queue that needs its own wording can have its own prompt while the rest use the general one.
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
base = "call_summary"
|
|
283
|
+
queue = call.queue.slug # "sales", "support", "collections", ...
|
|
284
|
+
|
|
285
|
+
client.chat(
|
|
286
|
+
parameters: {
|
|
287
|
+
prompt_name: [base, queue].compact.join("."), # call_summary.collections
|
|
288
|
+
prompt_fallback_name: base,
|
|
289
|
+
prompt_variables: { transcript: call.transcript },
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The fallback is looked up at its own `prompt_fallback_label` and `prompt_fallback_version`, and inherits neither from the prompt you asked for first. They describe a different prompt: a version number is minted per prompt, so version 3 of one name is an unrelated revision of another, and a label the first prompt carries need not exist on the fallback at all.
|
|
295
|
+
|
|
296
|
+
That means an omitted `prompt_fallback_label` resolves the fallback the same way any unlabelled lookup does — at the 'production' label. **If you want both prompts to come from the same label, name it for both.**
|
|
297
|
+
|
|
298
|
+
Whichever prompt is used is the one linked to the trace and merged into your request. If the fallback is missing too, the error is raised rather than swallowed — a request that asked for a prompt shouldn't quietly run without one.
|
|
299
|
+
|
|
257
300
|
### Custom Trace Names
|
|
258
301
|
|
|
259
302
|
To provide custom trace names to the observability platform, you can pass 'trace_name' in parameters. Example:
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require "nitro_intelligence/media/audio"
|
|
3
|
+
require "nitro_intelligence/observability/prompt_resolver"
|
|
3
4
|
|
|
4
5
|
module NitroIntelligence
|
|
5
6
|
module Client
|
|
@@ -34,26 +35,21 @@ module NitroIntelligence
|
|
|
34
35
|
private
|
|
35
36
|
|
|
36
37
|
def handle_prompt(message:, parameters:)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
prompt_name: parameters[:prompt_name],
|
|
41
|
-
prompt_label: parameters[:prompt_label],
|
|
42
|
-
prompt_version: parameters[:prompt_version]
|
|
38
|
+
prompt = NitroIntelligence::Observability::PromptResolver.for(
|
|
39
|
+
store: @observer.project_client.project.prompt_store,
|
|
40
|
+
parameters:
|
|
43
41
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if prompt.present?
|
|
47
|
-
# Prompts for audio transcriptions should only be text
|
|
48
|
-
if prompt.type != "text"
|
|
49
|
-
raise ObservedAudioTranscriptionPromptError,
|
|
50
|
-
"Prompt type for audio transcription must be text: #{prompt.name}"
|
|
51
|
-
end
|
|
52
|
-
interpolated_prompt = prompt.compile(**prompt_variables)
|
|
42
|
+
return nil if prompt.blank?
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
# Prompts for audio transcriptions should only be text
|
|
45
|
+
if prompt.type != "text"
|
|
46
|
+
raise ObservedAudioTranscriptionPromptError,
|
|
47
|
+
"Prompt type for audio transcription must be text: #{prompt.name}"
|
|
56
48
|
end
|
|
49
|
+
interpolated_prompt = prompt.compile(**(parameters[:prompt_variables] || {}))
|
|
50
|
+
|
|
51
|
+
message.prepend("#{interpolated_prompt} ").strip!
|
|
52
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
57
53
|
|
|
58
54
|
prompt
|
|
59
55
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "nitro_intelligence/observability/prompt_resolver"
|
|
2
|
+
|
|
1
3
|
module NitroIntelligence
|
|
2
4
|
module Client
|
|
3
5
|
module Handlers
|
|
@@ -28,23 +30,18 @@ module NitroIntelligence
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def handle_prompt(parameters:)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
prompt_name: parameters[:prompt_name],
|
|
35
|
-
prompt_label: parameters[:prompt_label],
|
|
36
|
-
prompt_version: parameters[:prompt_version]
|
|
33
|
+
prompt = NitroIntelligence::Observability::PromptResolver.for(
|
|
34
|
+
store: @observer.project_client.project.prompt_store,
|
|
35
|
+
parameters:
|
|
37
36
|
)
|
|
38
|
-
|
|
37
|
+
return nil if prompt.blank?
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
parameters[:messages]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
39
|
+
parameters[:messages] = prompt.interpolate(
|
|
40
|
+
messages: parameters[:messages],
|
|
41
|
+
variables: parameters[:prompt_variables] || {}
|
|
42
|
+
)
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
end
|
|
44
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
48
45
|
|
|
49
46
|
prompt
|
|
50
47
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "nitro_intelligence/media/image_generation"
|
|
2
|
+
require "nitro_intelligence/observability/prompt_resolver"
|
|
2
3
|
|
|
3
4
|
module NitroIntelligence
|
|
4
5
|
module Client
|
|
@@ -58,23 +59,18 @@ module NitroIntelligence
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def handle_prompt(parameters:)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
prompt_name: parameters[:prompt_name],
|
|
65
|
-
prompt_label: parameters[:prompt_label],
|
|
66
|
-
prompt_version: parameters[:prompt_version]
|
|
62
|
+
prompt = NitroIntelligence::Observability::PromptResolver.for(
|
|
63
|
+
store: @observer.project_client.project.prompt_store,
|
|
64
|
+
parameters:
|
|
67
65
|
)
|
|
68
|
-
|
|
66
|
+
return nil if prompt.blank?
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
parameters[:messages]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
68
|
+
parameters[:messages] = prompt.interpolate(
|
|
69
|
+
messages: parameters[:messages],
|
|
70
|
+
variables: parameters[:prompt_variables] || {}
|
|
71
|
+
)
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
end
|
|
73
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
78
74
|
|
|
79
75
|
prompt
|
|
80
76
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "nitro_intelligence/media/audio"
|
|
2
|
+
require "nitro_intelligence/observability/prompt_resolver"
|
|
2
3
|
|
|
3
4
|
module NitroIntelligence
|
|
4
5
|
module Client
|
|
@@ -33,26 +34,21 @@ module NitroIntelligence
|
|
|
33
34
|
private
|
|
34
35
|
|
|
35
36
|
def handle_prompt(parameters:)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
prompt_name: parameters[:prompt_name],
|
|
40
|
-
prompt_label: parameters[:prompt_label],
|
|
41
|
-
prompt_version: parameters[:prompt_version]
|
|
37
|
+
prompt = NitroIntelligence::Observability::PromptResolver.for(
|
|
38
|
+
store: @observer.project_client.project.prompt_store,
|
|
39
|
+
parameters:
|
|
42
40
|
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
41
|
+
return nil if prompt.blank?
|
|
42
|
+
|
|
43
|
+
# Prompts for tts should only be text
|
|
44
|
+
if prompt.type != "text"
|
|
45
|
+
raise ObservedTextToSpeechPromptError,
|
|
46
|
+
"Prompt type for text-to-speech must be text: #{prompt.name}"
|
|
55
47
|
end
|
|
48
|
+
interpolated_prompt = prompt.compile(**(parameters[:prompt_variables] || {}))
|
|
49
|
+
|
|
50
|
+
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
51
|
+
parameters[:instructions] = interpolated_prompt
|
|
56
52
|
|
|
57
53
|
prompt
|
|
58
54
|
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 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,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "nitro_intelligence/observability/prompt_store"
|
|
2
|
+
|
|
3
|
+
module NitroIntelligence
|
|
4
|
+
module Observability
|
|
5
|
+
# Resolves a managed prompt from the lookups a request asks for, most specific first. A lookup that is
|
|
6
|
+
# missing -- or that fails -- yields to the next; the last one's failure propagates.
|
|
7
|
+
class PromptResolver
|
|
8
|
+
# One prompt lookup: the name asked for, at a label or a version.
|
|
9
|
+
Lookup = Data.define(:name, :label, :version)
|
|
10
|
+
|
|
11
|
+
def self.for(store:, parameters:)
|
|
12
|
+
new(store: store, lookups: lookups_for(parameters)).prompt
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The fallback names its own label and version and inherits neither: a version is minted per prompt,
|
|
16
|
+
# and a label the requested prompt carries need not exist on the fallback at all.
|
|
17
|
+
def self.lookups_for(parameters)
|
|
18
|
+
[
|
|
19
|
+
Lookup.new(
|
|
20
|
+
name: parameters[:prompt_name],
|
|
21
|
+
label: parameters[:prompt_label],
|
|
22
|
+
version: parameters[:prompt_version]
|
|
23
|
+
),
|
|
24
|
+
Lookup.new(
|
|
25
|
+
name: parameters[:prompt_fallback_name],
|
|
26
|
+
label: parameters[:prompt_fallback_label],
|
|
27
|
+
version: parameters[:prompt_fallback_version]
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(store:, lookups:)
|
|
33
|
+
@store = store
|
|
34
|
+
@lookups = lookups.select { |lookup| lookup.name.present? }.uniq
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def prompt
|
|
38
|
+
*optional, final = @lookups
|
|
39
|
+
return nil if final.nil?
|
|
40
|
+
|
|
41
|
+
optional.each do |lookup|
|
|
42
|
+
resolved = fetch_optional(lookup)
|
|
43
|
+
return resolved if resolved
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
fetch(final)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def fetch(lookup)
|
|
52
|
+
@store.get_prompt(
|
|
53
|
+
prompt_name: lookup.name,
|
|
54
|
+
prompt_label: lookup.label,
|
|
55
|
+
prompt_version: lookup.version
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fetch_optional(lookup)
|
|
60
|
+
fetch(lookup)
|
|
61
|
+
rescue => e
|
|
62
|
+
NitroIntelligence.logger.info(
|
|
63
|
+
"#{self.class} #{e} - Falling back to the next prompt name after a failed lookup: #{lookup.name}"
|
|
64
|
+
)
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
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.
|
|
4
|
+
version: 2.2.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
|
|
@@ -104,6 +105,7 @@ files:
|
|
|
104
105
|
- lib/nitro_intelligence/observability/project_client.rb
|
|
105
106
|
- lib/nitro_intelligence/observability/project_client_registry.rb
|
|
106
107
|
- lib/nitro_intelligence/observability/prompt.rb
|
|
108
|
+
- lib/nitro_intelligence/observability/prompt_resolver.rb
|
|
107
109
|
- lib/nitro_intelligence/observability/prompt_store.rb
|
|
108
110
|
- lib/nitro_intelligence/observability/upload_handler.rb
|
|
109
111
|
- lib/nitro_intelligence/reporter.rb
|