nitro_intelligence 0.0.1 → 1.0.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 +83 -11
- data/lib/nitro_intelligence/agent_server.rb +119 -8
- data/lib/nitro_intelligence/client/base.rb +52 -0
- data/lib/nitro_intelligence/client/client.rb +13 -0
- data/lib/nitro_intelligence/client/factory.rb +53 -0
- data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +38 -0
- data/lib/nitro_intelligence/client/handlers/chat_handler.rb +41 -0
- data/lib/nitro_intelligence/client/handlers/image_handler.rb +61 -0
- data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +90 -0
- data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +74 -0
- data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +109 -0
- data/lib/nitro_intelligence/client/observed.rb +38 -0
- data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +75 -0
- data/lib/nitro_intelligence/configuration.rb +2 -0
- data/lib/nitro_intelligence/langfuse_extension.rb +10 -53
- data/lib/nitro_intelligence/media/audio.rb +50 -0
- data/lib/nitro_intelligence/media/image_generation.rb +4 -2
- data/lib/nitro_intelligence/models/model_catalog.rb +7 -2
- data/lib/nitro_intelligence/observability/project.rb +33 -0
- data/lib/nitro_intelligence/observability/project_client.rb +18 -0
- data/lib/nitro_intelligence/observability/project_client_registry.rb +44 -0
- data/lib/nitro_intelligence/observability/prompt.rb +58 -0
- data/lib/nitro_intelligence/observability/prompt_store.rb +112 -0
- data/lib/nitro_intelligence/observability/upload_handler.rb +138 -0
- data/lib/nitro_intelligence/reporter.rb +43 -0
- data/lib/nitro_intelligence/tool_call_review_validator.rb +69 -0
- data/lib/nitro_intelligence/trace.rb +2 -2
- data/lib/nitro_intelligence/version.rb +1 -1
- data/lib/nitro_intelligence.rb +10 -44
- metadata +26 -10
- data/lib/nitro_intelligence/client.rb +0 -337
- data/lib/nitro_intelligence/media/upload_handler.rb +0 -135
- data/lib/nitro_intelligence/prompt/prompt.rb +0 -56
- data/lib/nitro_intelligence/prompt/prompt_store.rb +0 -110
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
require "nitro_intelligence/trace"
|
|
2
|
-
|
|
3
|
-
module NitroIntelligence
|
|
4
|
-
class Client
|
|
5
|
-
attr_accessor :client
|
|
6
|
-
|
|
7
|
-
def initialize(observability_project_slug: nil)
|
|
8
|
-
@inference_api_key = NitroIntelligence.config.inference_api_key
|
|
9
|
-
@inference_host = NitroIntelligence.config.inference_base_url
|
|
10
|
-
@observability_host = NitroIntelligence.config.observability_base_url
|
|
11
|
-
@observability_project_slug = observability_project_slug
|
|
12
|
-
@client = OpenAI::Client.new(
|
|
13
|
-
api_key: @inference_api_key,
|
|
14
|
-
base_url: @inference_host
|
|
15
|
-
)
|
|
16
|
-
@langfuse_client = NitroIntelligence.langfuse_clients[observability_project_slug]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def chat(message: "", parameters: {})
|
|
20
|
-
default_params = CUSTOM_PARAMS.index_with { |_param| nil }
|
|
21
|
-
.merge({
|
|
22
|
-
metadata: {},
|
|
23
|
-
messages: [],
|
|
24
|
-
model: NitroIntelligence.model_catalog.default_text_model.name,
|
|
25
|
-
observability_project_slug:,
|
|
26
|
-
})
|
|
27
|
-
parameters = default_params.merge(parameters)
|
|
28
|
-
|
|
29
|
-
parameters[:messages] = [{ role: "user", content: message }] if parameters[:messages].blank? && message.present?
|
|
30
|
-
|
|
31
|
-
return chat_with_tracing(parameters:) if observability_available?
|
|
32
|
-
|
|
33
|
-
client_chat(parameters:)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# We abstract the image generation for now because of usage
|
|
37
|
-
# across various apis: chat/completions, image/edits, image/generations
|
|
38
|
-
# Input images should be byte strings
|
|
39
|
-
# Returns NitroIntelligence::ImageGeneration
|
|
40
|
-
def generate_image(
|
|
41
|
-
message: "",
|
|
42
|
-
target_image: nil,
|
|
43
|
-
reference_images: [],
|
|
44
|
-
parameters: {}
|
|
45
|
-
)
|
|
46
|
-
image_generation = NitroIntelligence::ImageGeneration.new(message:, target_image:, reference_images:) do |config|
|
|
47
|
-
config.aspect_ratio = parameters[:aspect_ratio] if parameters.key?(:aspect_ratio)
|
|
48
|
-
config.model = parameters[:model] if parameters.key?(:model)
|
|
49
|
-
config.resolution = parameters[:resolution] if parameters.key?(:resolution)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
default_params = CUSTOM_PARAMS.index_with { |_param| nil }
|
|
53
|
-
.merge({
|
|
54
|
-
image_generation:,
|
|
55
|
-
metadata: {},
|
|
56
|
-
messages: image_generation.messages,
|
|
57
|
-
model: image_generation.config.model,
|
|
58
|
-
observability_project_slug:,
|
|
59
|
-
extra_headers: {
|
|
60
|
-
"Prefer" => "wait",
|
|
61
|
-
},
|
|
62
|
-
request_options: {
|
|
63
|
-
extra_body: {
|
|
64
|
-
image_config: {
|
|
65
|
-
aspect_ratio: image_generation.config.aspect_ratio,
|
|
66
|
-
image_size: image_generation.config.resolution,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
parameters = default_params.merge(parameters)
|
|
72
|
-
|
|
73
|
-
if observability_available?
|
|
74
|
-
chat_with_tracing(parameters:)
|
|
75
|
-
else
|
|
76
|
-
chat_completion = client_chat(parameters:)
|
|
77
|
-
image_generation.parse_file(chat_completion)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
image_generation
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def score(trace_id:, name:, value:, id: "#{trace_id}-#{name}")
|
|
84
|
-
raise ObservabilityUnavailableError, "Observability project slug not configured" unless observability_available?
|
|
85
|
-
|
|
86
|
-
if @langfuse_client.nil?
|
|
87
|
-
raise LangfuseClientNotFoundError,
|
|
88
|
-
"No Langfuse client found for slug: #{observability_project_slug}"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
@langfuse_client.create_score(
|
|
92
|
-
id:,
|
|
93
|
-
trace_id:,
|
|
94
|
-
name:,
|
|
95
|
-
value:,
|
|
96
|
-
environment: NitroIntelligence.environment
|
|
97
|
-
)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def create_dataset_item(attributes)
|
|
101
|
-
HTTParty.post("#{@observability_host}/api/public/dataset-items",
|
|
102
|
-
body: attributes.to_json,
|
|
103
|
-
headers: {
|
|
104
|
-
"Content-Type" => "application/json",
|
|
105
|
-
"Authorization" => "Basic #{observability_auth_token}",
|
|
106
|
-
})
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
attr_reader :observability_project_slug
|
|
112
|
-
|
|
113
|
-
def current_revision
|
|
114
|
-
return @current_revision if defined?(@current_revision)
|
|
115
|
-
|
|
116
|
-
path = Rails.root.join("REVISION")
|
|
117
|
-
@current_revision = File.exist?(path) ? File.read(path).strip.presence : nil
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def observability_available?
|
|
121
|
-
observability_project_slug.present?
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def method_missing(method_name, *, &)
|
|
125
|
-
@client.send(method_name, *, &)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
129
|
-
@client.respond_to?(method_name, include_private) || super
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def project_config
|
|
133
|
-
return @project_config if @project_config.present?
|
|
134
|
-
|
|
135
|
-
projects = NitroIntelligence.config.observability_projects
|
|
136
|
-
@project_config = projects.find { |project| project["slug"] == observability_project_slug }
|
|
137
|
-
|
|
138
|
-
if @project_config.nil?
|
|
139
|
-
raise ObservabilityProjectConfigNotFoundError,
|
|
140
|
-
"No observability project config found for slug: #{observability_project_slug}"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
@project_config
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def client_chat(parameters:)
|
|
147
|
-
# When requesting an OpenAI model, OpenAI API will return a 400 because it does not ignore custom params
|
|
148
|
-
@client.chat.completions.create(**parameters.except(*NitroIntelligence.omit_params))
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def chat_with_tracing(parameters:)
|
|
152
|
-
project = get_project(
|
|
153
|
-
project_id: project_config["id"],
|
|
154
|
-
observability_public_key: project_config["public_key"]
|
|
155
|
-
)
|
|
156
|
-
prompt = handle_prompt(parameters:, project_config:)
|
|
157
|
-
|
|
158
|
-
instrument_tracing(prompt:, project:, parameters:)
|
|
159
|
-
rescue ObservabilityProjectNotFoundError, LangfuseClientNotFoundError => e
|
|
160
|
-
# We should still send the request if we have problems with observability
|
|
161
|
-
NitroIntelligence.logger.warn(
|
|
162
|
-
"#{self.class} - Observability configuration provided, but could not be processed. #{e}. " \
|
|
163
|
-
"Sending request regardless."
|
|
164
|
-
)
|
|
165
|
-
client_chat(parameters:)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def handle_prompt(parameters:, project_config:)
|
|
169
|
-
return if parameters[:prompt_name].blank?
|
|
170
|
-
|
|
171
|
-
prompt_store = NitroIntelligence::PromptStore.new(
|
|
172
|
-
observability_project_slug:,
|
|
173
|
-
observability_public_key: project_config["public_key"],
|
|
174
|
-
observability_secret_key: project_config["secret_key"]
|
|
175
|
-
)
|
|
176
|
-
prompt = prompt_store.get_prompt(
|
|
177
|
-
prompt_name: parameters[:prompt_name],
|
|
178
|
-
prompt_label: parameters[:prompt_label],
|
|
179
|
-
prompt_version: parameters[:prompt_version]
|
|
180
|
-
)
|
|
181
|
-
prompt_variables = parameters[:prompt_variables] || {}
|
|
182
|
-
|
|
183
|
-
if prompt.present?
|
|
184
|
-
parameters[:messages] = prompt.interpolate(
|
|
185
|
-
messages: parameters[:messages],
|
|
186
|
-
variables: prompt_variables
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
prompt
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def instrument_tracing(prompt:, project:, parameters:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
196
|
-
if @langfuse_client.nil?
|
|
197
|
-
raise LangfuseClientNotFoundError,
|
|
198
|
-
"No Langfuse client found for slug: #{observability_project_slug}"
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
default_trace_name = project["name"]
|
|
202
|
-
input = parameters[:messages]
|
|
203
|
-
image_generation = parameters[:image_generation]
|
|
204
|
-
metadata = parameters[:metadata]
|
|
205
|
-
|
|
206
|
-
if prompt
|
|
207
|
-
metadata[:prompt_name] = prompt.name
|
|
208
|
-
metadata[:prompt_version] = prompt.version
|
|
209
|
-
default_trace_name = prompt.name
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
seed = parameters[:trace_seed]
|
|
213
|
-
trace_id = NitroIntelligence::Trace.create_id(seed:) if seed.present?
|
|
214
|
-
|
|
215
|
-
chat_completion = nil
|
|
216
|
-
|
|
217
|
-
Langfuse.propagate_attributes(
|
|
218
|
-
user_id: parameters[:user_id] || "default-user",
|
|
219
|
-
metadata: metadata.transform_values(&:to_s)
|
|
220
|
-
) do
|
|
221
|
-
@langfuse_client.observe(
|
|
222
|
-
"llm-response",
|
|
223
|
-
as_type: :generation,
|
|
224
|
-
trace_id:,
|
|
225
|
-
environment: NitroIntelligence.environment.to_s,
|
|
226
|
-
input:,
|
|
227
|
-
model: parameters[:model],
|
|
228
|
-
metadata: metadata.transform_values(&:to_s)
|
|
229
|
-
) do |generation|
|
|
230
|
-
generation.update_trace(
|
|
231
|
-
name: parameters[:trace_name] || default_trace_name,
|
|
232
|
-
release: current_revision
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
if prompt
|
|
236
|
-
generation.update({
|
|
237
|
-
prompt: {
|
|
238
|
-
name: prompt.name,
|
|
239
|
-
version: prompt.version,
|
|
240
|
-
},
|
|
241
|
-
})
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
chat_completion = client_chat(parameters:)
|
|
245
|
-
output = chat_completion.choices.first.message.to_h
|
|
246
|
-
|
|
247
|
-
# Handle image generation media
|
|
248
|
-
if image_generation
|
|
249
|
-
image_generation.trace_id = generation.trace_id
|
|
250
|
-
image_generation.parse_file(chat_completion)
|
|
251
|
-
handle_image_generation_uploads(input, output, image_generation)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Handle truncating any unnecessary data before storing into trace
|
|
255
|
-
handle_truncation(input, output, chat_completion.model)
|
|
256
|
-
|
|
257
|
-
generation.model = chat_completion.model
|
|
258
|
-
generation.usage_details = {
|
|
259
|
-
prompt_tokens: chat_completion.usage.prompt_tokens,
|
|
260
|
-
completion_tokens: chat_completion.usage.completion_tokens,
|
|
261
|
-
total_tokens: chat_completion.usage.total_tokens,
|
|
262
|
-
}
|
|
263
|
-
generation.input = input
|
|
264
|
-
generation.output = output
|
|
265
|
-
|
|
266
|
-
generation.update_trace(input:, output:)
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
chat_completion
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Returns name and metadata
|
|
274
|
-
def get_project(project_id:, observability_public_key:)
|
|
275
|
-
cache_key = OBSERVABILITY_PROJECTS_CACHE_KEY_PREFIX + project_id
|
|
276
|
-
cached_project = NitroIntelligence.cache.read(cache_key)
|
|
277
|
-
return cached_project if cached_project.present?
|
|
278
|
-
|
|
279
|
-
target_project = get_projects(observability_public_key:).find do |project|
|
|
280
|
-
project["id"] == project_id
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
raise ObservabilityProjectNotFoundError, "Project with ID: #{project_id} not found" if target_project.nil?
|
|
284
|
-
|
|
285
|
-
NitroIntelligence.cache.write(cache_key, target_project, expires_in: 12.hours)
|
|
286
|
-
target_project
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def get_projects(observability_public_key:)
|
|
290
|
-
response = HTTParty.get("#{@observability_host}/api/public/projects",
|
|
291
|
-
headers: {
|
|
292
|
-
"Authorization" => "Basic #{observability_auth_token}",
|
|
293
|
-
})
|
|
294
|
-
data = JSON.parse(response.body)["data"]
|
|
295
|
-
if data.nil?
|
|
296
|
-
raise(
|
|
297
|
-
ObservabilityProjectNotFoundError,
|
|
298
|
-
"No projects were found. Public key: #{observability_public_key || 'missing'}"
|
|
299
|
-
)
|
|
300
|
-
end
|
|
301
|
-
data
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def handle_image_generation_uploads(input, output, image_generation)
|
|
305
|
-
# If we are doing image generation we should upload the media to observability manually
|
|
306
|
-
upload_handler = NitroIntelligence::UploadHandler.new(@observability_host, observability_auth_token)
|
|
307
|
-
upload_handler.upload(
|
|
308
|
-
image_generation.trace_id,
|
|
309
|
-
upload_queue: Queue.new(image_generation.files)
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
# Replace base64 strings with media references
|
|
313
|
-
upload_handler.replace_base64_with_media_references(input)
|
|
314
|
-
upload_handler.replace_base64_with_media_references(output)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def handle_truncation(_input, output, model_name)
|
|
318
|
-
model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
|
|
319
|
-
|
|
320
|
-
return unless model&.omit_output_fields
|
|
321
|
-
|
|
322
|
-
model.omit_output_fields.each do |omit_output_field|
|
|
323
|
-
last_key = omit_output_field.last
|
|
324
|
-
parent_keys = omit_output_field[0...-1]
|
|
325
|
-
parent = parent_keys.empty? ? output : output.dig(*parent_keys)
|
|
326
|
-
|
|
327
|
-
parent[last_key] = "[Truncated...]" if parent.is_a?(Hash) && parent.key?(last_key)
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def observability_auth_token
|
|
332
|
-
public_key = project_config["public_key"]
|
|
333
|
-
secret_key = project_config["secret_key"]
|
|
334
|
-
@observability_auth_token ||= Base64.strict_encode64("#{public_key}:#{secret_key}")
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
end
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
require "base64"
|
|
2
|
-
require "digest"
|
|
3
|
-
require "httparty"
|
|
4
|
-
require "time"
|
|
5
|
-
|
|
6
|
-
module NitroIntelligence
|
|
7
|
-
class UploadHandler
|
|
8
|
-
def initialize(observability_host, observability_auth_token)
|
|
9
|
-
@observability_host = observability_host
|
|
10
|
-
@observability_auth_token = observability_auth_token
|
|
11
|
-
@uploaded_media = []
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def replace_base64_with_media_references(payload)
|
|
15
|
-
# Make it easier to lookup message style image_urls
|
|
16
|
-
media_lookup = @uploaded_media.index_by { |image| "data:#{image.mime_type};base64,#{image.base64}" }
|
|
17
|
-
|
|
18
|
-
replace_base64_image_url = ->(node) do
|
|
19
|
-
case node
|
|
20
|
-
when Hash
|
|
21
|
-
url_key = if node.key?(:url)
|
|
22
|
-
:url
|
|
23
|
-
else
|
|
24
|
-
(node.key?("url") ? "url" : nil)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
if url_key
|
|
28
|
-
url_value = node[url_key]
|
|
29
|
-
|
|
30
|
-
# Replace base64 strings if they match with our uploaded media
|
|
31
|
-
if (media = media_lookup[url_value])
|
|
32
|
-
# Overwrite base64 string with Langfuse media ref
|
|
33
|
-
# This *should* be rendering inline in the gui
|
|
34
|
-
# https://github.com/langfuse/langfuse/issues/4555
|
|
35
|
-
# https://github.com/langfuse/langfuse/issues/5030
|
|
36
|
-
node[url_key] = "@@@langfuseMedia:type=#{media.mime_type}|id=#{media.reference_id}|source=bytes@@@"
|
|
37
|
-
# Sometimes models can generate unwanted images that will not have a reference
|
|
38
|
-
# these untracked base64 strings can easily push 4.5mb Langfuse limit
|
|
39
|
-
elsif url_value.is_a?(String) && url_value.start_with?("data:")
|
|
40
|
-
node[url_key] = "[Discarded media]"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
node.each_value { |val| replace_base64_image_url.call(val) }
|
|
45
|
-
|
|
46
|
-
when Array
|
|
47
|
-
node.each { |val| replace_base64_image_url.call(val) }
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
replace_base64_image_url.call(payload)
|
|
52
|
-
|
|
53
|
-
payload
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def upload(trace_id, upload_queue: Queue.new)
|
|
57
|
-
until upload_queue.empty?
|
|
58
|
-
media = upload_queue.pop
|
|
59
|
-
|
|
60
|
-
content_length = media.byte_string.bytesize
|
|
61
|
-
content_sha256 = Base64.strict_encode64(Digest::SHA256.digest(media.byte_string))
|
|
62
|
-
|
|
63
|
-
# returns {"mediaId" -> "", "uploadUrl" => ""}
|
|
64
|
-
upload_url_response = get_upload_url({
|
|
65
|
-
traceId: trace_id,
|
|
66
|
-
contentType: media.mime_type,
|
|
67
|
-
contentLength: content_length,
|
|
68
|
-
sha256Hash: content_sha256,
|
|
69
|
-
field: media.direction,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
# NOTE: uploadUrl is None if the file is stored in Langfuse already as then there is no need to upload it again.
|
|
73
|
-
upload_response = upload_media(
|
|
74
|
-
upload_url_response["mediaId"],
|
|
75
|
-
upload_url_response["uploadUrl"],
|
|
76
|
-
media.mime_type,
|
|
77
|
-
content_sha256,
|
|
78
|
-
media.byte_string
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
associate_media(upload_url_response["mediaId"], upload_response) if upload_response.present?
|
|
82
|
-
|
|
83
|
-
media.reference_id = upload_url_response["mediaId"]
|
|
84
|
-
@uploaded_media.append(media)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
@uploaded_media
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
def get_upload_url(request_body)
|
|
93
|
-
HTTParty.post(
|
|
94
|
-
"#{@observability_host}/api/public/media",
|
|
95
|
-
body: request_body.to_json,
|
|
96
|
-
headers: {
|
|
97
|
-
"Content-Type" => "application/json",
|
|
98
|
-
"Authorization" => "Basic #{@observability_auth_token}",
|
|
99
|
-
}
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def associate_media(media_id, upload_response)
|
|
104
|
-
request_body = {
|
|
105
|
-
uploadedAt: Time.now.utc.iso8601(6),
|
|
106
|
-
uploadHttpStatus: upload_response.code,
|
|
107
|
-
uploadHttpError: upload_response.code == 200 ? nil : upload_response.body,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
HTTParty.patch(
|
|
111
|
-
"#{@observability_host}/api/public/media/#{media_id}",
|
|
112
|
-
body: request_body.to_json,
|
|
113
|
-
headers: {
|
|
114
|
-
"Content-Type" => "application/json",
|
|
115
|
-
"Authorization" => "Basic #{@observability_auth_token}",
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def upload_media(media_id, upload_url, content_type, content_sha256, content_bytes)
|
|
121
|
-
if media_id.present? && upload_url.present?
|
|
122
|
-
return HTTParty.put(
|
|
123
|
-
upload_url,
|
|
124
|
-
headers: {
|
|
125
|
-
"Content-Type" => content_type,
|
|
126
|
-
"x-amz-checksum-sha256" => content_sha256,
|
|
127
|
-
},
|
|
128
|
-
body: content_bytes
|
|
129
|
-
)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
nil
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
module NitroIntelligence
|
|
2
|
-
class Prompt
|
|
3
|
-
attr_reader :name, :type, :prompt, :version, :config, :labels, :tags
|
|
4
|
-
|
|
5
|
-
VARIABLE_REGEX = /\{\{([a-zA-Z0-9_]+)\}\}/
|
|
6
|
-
|
|
7
|
-
def initialize(name:, type:, prompt:, version:, **extra_args)
|
|
8
|
-
@name = name
|
|
9
|
-
@type = type
|
|
10
|
-
@prompt = prompt
|
|
11
|
-
@version = version
|
|
12
|
-
@config = extra_args[:config] || {}
|
|
13
|
-
@labels = extra_args[:labels] || []
|
|
14
|
-
@tags = extra_args[:tags] || []
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Returns prompt "content" from API with prompt variables replaced
|
|
18
|
-
# Prompt "content" will either be a string or an array of hashes
|
|
19
|
-
# based on prompt "type" ("text" or "chat")
|
|
20
|
-
def compile(**replacements)
|
|
21
|
-
return replace_variables(@prompt, **replacements) if @type == "text"
|
|
22
|
-
|
|
23
|
-
@prompt.map do |message|
|
|
24
|
-
message[:content] = replace_variables(message[:content], **replacements)
|
|
25
|
-
message
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Takes provided chat messages and inserts the compiled prompt
|
|
30
|
-
# into the correct position based on prompt "type" ("text" or "chat")
|
|
31
|
-
def interpolate(messages:, variables:)
|
|
32
|
-
if @type == "text"
|
|
33
|
-
messages.prepend({ role: "system", content: compile(**variables) })
|
|
34
|
-
elsif @type == "chat"
|
|
35
|
-
compile(**variables) + messages
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def variables
|
|
40
|
-
messages = @type == "text" ? [@prompt] : @prompt.pluck(:content)
|
|
41
|
-
|
|
42
|
-
messages.map do |message|
|
|
43
|
-
message.scan(VARIABLE_REGEX).flatten.map(&:to_sym)
|
|
44
|
-
end.flatten
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def replace_variables(input, **replacements)
|
|
50
|
-
input.gsub(VARIABLE_REGEX) do |match|
|
|
51
|
-
key = ::Regexp.last_match(1).to_sym
|
|
52
|
-
replacements.fetch(key, match)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
require "base64"
|
|
2
|
-
require "cgi"
|
|
3
|
-
require "httparty"
|
|
4
|
-
|
|
5
|
-
require "nitro_intelligence/prompt/prompt"
|
|
6
|
-
|
|
7
|
-
module NitroIntelligence
|
|
8
|
-
class PromptStore
|
|
9
|
-
OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX = "nitro_intelligence_observability_prompts_".freeze
|
|
10
|
-
|
|
11
|
-
class ObservabilityPromptError < StandardError; end
|
|
12
|
-
class ObservabilityPromptNotFoundError < StandardError; end
|
|
13
|
-
|
|
14
|
-
def initialize(observability_project_slug:, observability_public_key:, observability_secret_key:)
|
|
15
|
-
@observability_project_slug = observability_project_slug
|
|
16
|
-
@observability_public_key = observability_public_key
|
|
17
|
-
@observability_secret_key = observability_secret_key
|
|
18
|
-
@observability_host = NitroIntelligence.config.observability_base_url
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def get_prompt(prompt_name:, prompt_label: nil, prompt_version: nil)
|
|
22
|
-
safe_prompt_name = CGI.escapeURIComponent(prompt_name)
|
|
23
|
-
prompt = nil
|
|
24
|
-
|
|
25
|
-
if prompt_version.present?
|
|
26
|
-
prompt = get_prompt_by_version(safe_prompt_name:, prompt_version:)
|
|
27
|
-
else
|
|
28
|
-
prompt_label = "production" if prompt_label.nil?
|
|
29
|
-
prompt = get_prompt_by_label(safe_prompt_name:, prompt_label:)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
prompt = NitroIntelligence::Prompt.new(**prompt) if prompt.present?
|
|
33
|
-
|
|
34
|
-
prompt
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def get_prompt_by_label(safe_prompt_name:, prompt_label:)
|
|
40
|
-
cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
|
|
41
|
-
"#{safe_prompt_name}_#{prompt_label}"
|
|
42
|
-
if (cached_prompt = NitroIntelligence.cache.read(cache_key)).present?
|
|
43
|
-
return cached_prompt
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
NitroIntelligence.logger.info(
|
|
47
|
-
"#{self.class} - Prompt label cache miss. Fetching prompt: #{safe_prompt_name} - #{prompt_label}"
|
|
48
|
-
)
|
|
49
|
-
get_prompt_request(safe_prompt_name:, prompt_url_params: "label=#{prompt_label}")
|
|
50
|
-
rescue => e
|
|
51
|
-
if (rolling_cached_prompt = NitroIntelligence.cache.read("#{cache_key}_rolling")).present?
|
|
52
|
-
NitroIntelligence.logger.warn(
|
|
53
|
-
"#{self.class} #{e} - Using rolling cached prompt: #{safe_prompt_name} - #{prompt_label}"
|
|
54
|
-
)
|
|
55
|
-
return rolling_cached_prompt
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
raise e
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def get_prompt_by_version(safe_prompt_name:, prompt_version:)
|
|
62
|
-
cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
|
|
63
|
-
"#{safe_prompt_name}_#{prompt_version}"
|
|
64
|
-
|
|
65
|
-
if (cached_prompt = NitroIntelligence.cache.read(cache_key)).present?
|
|
66
|
-
return cached_prompt
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
NitroIntelligence.logger.info(
|
|
70
|
-
"#{self.class} - Prompt version cache miss. Fetching prompt: #{safe_prompt_name} - #{prompt_version}"
|
|
71
|
-
)
|
|
72
|
-
get_prompt_request(safe_prompt_name:, prompt_url_params: "version=#{prompt_version}")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def get_prompt_request(safe_prompt_name:, prompt_url_params:)
|
|
76
|
-
auth_token = Base64.strict_encode64("#{@observability_public_key}:#{@observability_secret_key}")
|
|
77
|
-
response = HTTParty.get(
|
|
78
|
-
"#{@observability_host}/api/public/v2/prompts/#{safe_prompt_name}?#{prompt_url_params}",
|
|
79
|
-
headers: {
|
|
80
|
-
"Authorization" => "Basic #{auth_token}",
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
if response.code != 200
|
|
85
|
-
raise ObservabilityPromptNotFoundError, "Prompt: #{safe_prompt_name} Not Found" if response.code == 404
|
|
86
|
-
|
|
87
|
-
raise ObservabilityPromptError, response.body
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
prompt = JSON.parse(response.body, symbolize_names: true)
|
|
91
|
-
write_prompt_caches(safe_prompt_name:, prompt:)
|
|
92
|
-
prompt
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def write_prompt_caches(safe_prompt_name:, prompt:)
|
|
96
|
-
# Write versioned cache key
|
|
97
|
-
version_cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
|
|
98
|
-
"#{safe_prompt_name}_#{prompt[:version]}"
|
|
99
|
-
NitroIntelligence.cache.write(version_cache_key, prompt, expires_in: nil)
|
|
100
|
-
|
|
101
|
-
# Store all versions in an array cache per label
|
|
102
|
-
prompt[:labels].each do |label|
|
|
103
|
-
label_cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
|
|
104
|
-
"#{safe_prompt_name}_#{label}"
|
|
105
|
-
NitroIntelligence.cache.write(label_cache_key, prompt, expires_in: 5.minutes)
|
|
106
|
-
NitroIntelligence.cache.write("#{label_cache_key}_rolling", prompt, expires_in: nil)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|