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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/docs/README.md +83 -11
  3. data/lib/nitro_intelligence/agent_server.rb +119 -8
  4. data/lib/nitro_intelligence/client/base.rb +52 -0
  5. data/lib/nitro_intelligence/client/client.rb +13 -0
  6. data/lib/nitro_intelligence/client/factory.rb +53 -0
  7. data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +38 -0
  8. data/lib/nitro_intelligence/client/handlers/chat_handler.rb +41 -0
  9. data/lib/nitro_intelligence/client/handlers/image_handler.rb +61 -0
  10. data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +90 -0
  11. data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +74 -0
  12. data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +109 -0
  13. data/lib/nitro_intelligence/client/observed.rb +38 -0
  14. data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +75 -0
  15. data/lib/nitro_intelligence/configuration.rb +2 -0
  16. data/lib/nitro_intelligence/langfuse_extension.rb +10 -53
  17. data/lib/nitro_intelligence/media/audio.rb +50 -0
  18. data/lib/nitro_intelligence/media/image_generation.rb +4 -2
  19. data/lib/nitro_intelligence/models/model_catalog.rb +7 -2
  20. data/lib/nitro_intelligence/observability/project.rb +33 -0
  21. data/lib/nitro_intelligence/observability/project_client.rb +18 -0
  22. data/lib/nitro_intelligence/observability/project_client_registry.rb +44 -0
  23. data/lib/nitro_intelligence/observability/prompt.rb +58 -0
  24. data/lib/nitro_intelligence/observability/prompt_store.rb +112 -0
  25. data/lib/nitro_intelligence/observability/upload_handler.rb +138 -0
  26. data/lib/nitro_intelligence/reporter.rb +43 -0
  27. data/lib/nitro_intelligence/tool_call_review_validator.rb +69 -0
  28. data/lib/nitro_intelligence/trace.rb +2 -2
  29. data/lib/nitro_intelligence/version.rb +1 -1
  30. data/lib/nitro_intelligence.rb +10 -44
  31. metadata +26 -10
  32. data/lib/nitro_intelligence/client.rb +0 -337
  33. data/lib/nitro_intelligence/media/upload_handler.rb +0 -135
  34. data/lib/nitro_intelligence/prompt/prompt.rb +0 -56
  35. 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