nitro_intelligence 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d1c0e6c9ddffc032a989ebffb001d58aefabf86e50f92966a6d384a5218628f
4
- data.tar.gz: 60b9ae5ef4fd8c7a79bc45f13a1b965c9d9a0792f38bc285e361822b7d91a8ad
3
+ metadata.gz: 1181b17cc855ffa2f039b1401fd5dd2040c262d2878e6d265e65f9a581f6106e
4
+ data.tar.gz: 28b3a022b8d6d7e8e8472930fcf089fa5a3ead8a6c0d917e7587a73b69b8fba5
5
5
  SHA512:
6
- metadata.gz: a13a9b1a0a58f58c142ab87ec89f491f409341717b3101b9bb1579c88b8ed9335413ea73d621c489749b7d151d62672e9277e3d39da80c4721008cad28a301b0
7
- data.tar.gz: 1c498a0a4ec99af32f039dd43272ad61650da433c4f4d3b4297aadd473fba4c93af8d9ed08db0d2faf5012ed3a721e6d47587bf5429eb16368161d224f773cf7
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:
@@ -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
- return nil if parameters[:prompt_name].blank?
38
-
39
- prompt = @observer.project_client.project.prompt_store.get_prompt(
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
- prompt_variables = parameters[:prompt_variables] || {}
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
- message.prepend("#{interpolated_prompt} ").strip!
55
- parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
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
- return nil if parameters[:prompt_name].blank?
32
-
33
- prompt = @observer.project_client.project.prompt_store.get_prompt(
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
- prompt_variables = parameters[:prompt_variables] || {}
37
+ return nil if prompt.blank?
39
38
 
40
- if prompt.present?
41
- parameters[:messages] = prompt.interpolate(
42
- messages: parameters[:messages],
43
- variables: prompt_variables
44
- )
39
+ parameters[:messages] = prompt.interpolate(
40
+ messages: parameters[:messages],
41
+ variables: parameters[:prompt_variables] || {}
42
+ )
45
43
 
46
- parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
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
- return nil if parameters[:prompt_name].blank?
62
-
63
- prompt = @observer.project_client.project.prompt_store.get_prompt(
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
- prompt_variables = parameters[:prompt_variables] || {}
66
+ return nil if prompt.blank?
69
67
 
70
- if prompt.present?
71
- parameters[:messages] = prompt.interpolate(
72
- messages: parameters[:messages],
73
- variables: prompt_variables
74
- )
68
+ parameters[:messages] = prompt.interpolate(
69
+ messages: parameters[:messages],
70
+ variables: parameters[:prompt_variables] || {}
71
+ )
75
72
 
76
- parameters.merge!(prompt.config) unless parameters[:prompt_config_disabled]
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
- return nil if parameters[:prompt_name].blank?
37
-
38
- prompt = @observer.project_client.project.prompt_store.get_prompt(
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
- prompt_variables = parameters[:prompt_variables] || {}
44
-
45
- if prompt.present?
46
- # Prompts for tts should only be text
47
- if prompt.type != "text"
48
- raise ObservedTextToSpeechPromptError,
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "2.1.0".freeze
2
+ VERSION = "2.2.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nitro_intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -105,6 +105,7 @@ files:
105
105
  - lib/nitro_intelligence/observability/project_client.rb
106
106
  - lib/nitro_intelligence/observability/project_client_registry.rb
107
107
  - lib/nitro_intelligence/observability/prompt.rb
108
+ - lib/nitro_intelligence/observability/prompt_resolver.rb
108
109
  - lib/nitro_intelligence/observability/prompt_store.rb
109
110
  - lib/nitro_intelligence/observability/upload_handler.rb
110
111
  - lib/nitro_intelligence/reporter.rb