nitro_intelligence 3.1.0 → 4.1.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/Rakefile +21 -1
  4. data/docs/ASSISTANTS.md +594 -0
  5. data/docs/CHANGELOG.md +243 -0
  6. data/docs/README.md +190 -12
  7. data/lib/nitro_intelligence/assistant.rb +82 -3
  8. data/lib/nitro_intelligence/assistant_registry.rb +18 -0
  9. data/lib/nitro_intelligence/assistants.rb +340 -102
  10. data/lib/nitro_intelligence/client/base.rb +88 -8
  11. data/lib/nitro_intelligence/client/client.rb +22 -2
  12. data/lib/nitro_intelligence/client/factory.rb +12 -3
  13. data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +3 -1
  14. data/lib/nitro_intelligence/client/handlers/base_handler.rb +17 -2
  15. data/lib/nitro_intelligence/client/handlers/chat_handler.rb +3 -1
  16. data/lib/nitro_intelligence/client/handlers/decision_handler.rb +75 -0
  17. data/lib/nitro_intelligence/client/handlers/image_handler.rb +6 -2
  18. data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +30 -12
  19. data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +16 -3
  20. data/lib/nitro_intelligence/client/handlers/observed/decision_handler.rb +72 -0
  21. data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +20 -5
  22. data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +27 -9
  23. data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +3 -1
  24. data/lib/nitro_intelligence/client/observed.rb +8 -0
  25. data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +35 -7
  26. data/lib/nitro_intelligence/configuration.rb +48 -0
  27. data/lib/nitro_intelligence/decision.rb +105 -0
  28. data/lib/nitro_intelligence/deprecator.rb +10 -0
  29. data/lib/nitro_intelligence/langfuse_extension.rb +1 -0
  30. data/lib/nitro_intelligence/langfuse_tracer_provider.rb +7 -6
  31. data/lib/nitro_intelligence/media/audio.rb +6 -0
  32. data/lib/nitro_intelligence/media/image.rb +14 -1
  33. data/lib/nitro_intelligence/media/image_generation.rb +14 -2
  34. data/lib/nitro_intelligence/media/media.rb +4 -0
  35. data/lib/nitro_intelligence/models/model.rb +7 -0
  36. data/lib/nitro_intelligence/models/model_catalog.rb +3 -0
  37. data/lib/nitro_intelligence/models/model_factory.rb +2 -0
  38. data/lib/nitro_intelligence/null_cache.rb +2 -0
  39. data/lib/nitro_intelligence/observability/project.rb +41 -2
  40. data/lib/nitro_intelligence/observability/project_client.rb +10 -1
  41. data/lib/nitro_intelligence/observability/project_client_registry.rb +18 -0
  42. data/lib/nitro_intelligence/observability/prompt.rb +4 -3
  43. data/lib/nitro_intelligence/observability/prompt_resolver.rb +2 -0
  44. data/lib/nitro_intelligence/observability/prompt_store.rb +9 -0
  45. data/lib/nitro_intelligence/observability/upload_handler.rb +3 -1
  46. data/lib/nitro_intelligence/pending_review.rb +257 -0
  47. data/lib/nitro_intelligence/railtie.rb +10 -0
  48. data/lib/nitro_intelligence/reporter.rb +29 -0
  49. data/lib/nitro_intelligence/review_decision.rb +36 -0
  50. data/lib/nitro_intelligence/run_result.rb +160 -0
  51. data/lib/nitro_intelligence/tool_call_review_interrupt.rb +9 -13
  52. data/lib/nitro_intelligence/tool_call_review_validator.rb +6 -8
  53. data/lib/nitro_intelligence/trace.rb +8 -0
  54. data/lib/nitro_intelligence/version.rb +2 -1
  55. data/lib/nitro_intelligence.rb +40 -0
  56. metadata +12 -1
@@ -0,0 +1,160 @@
1
+ require "nitro_intelligence/pending_review"
2
+
3
+ module NitroIntelligence
4
+ # The outcome of one `/runs/wait`: either the run ended, possibly with a reply, or it paused on a
5
+ # review.
6
+ #
7
+ # A run has at most one reply: its final message, an AI message with text and no tool calls. Text
8
+ # the model writes alongside tool calls is working text, never the reply. A paused run's last AI
9
+ # message is a draft awaiting review, so `#reply` is nil whenever the run paused; a reply is final
10
+ # only once a run ends without `__interrupt__`.
11
+ class RunResult
12
+ # How the platform delivered the reply through the assistant's delivery tool, read from the
13
+ # reply's `response_metadata.delivery` stamp. `status` is "delivered" (with `result`, what the
14
+ # tool returned, and `delivered_at`), "failed" (with the tool's `error` and `failed_at`), or
15
+ # "pending": the platform stamps a settled reply "pending" just before it delivers it, so a
16
+ # finished run still showing it was cut off before delivery recorded an outcome. Treat a
17
+ # pending reply as not sent; the next customer message removes it and the model answers both. A
18
+ # field the stamp does not carry, or carries as the wrong type, is nil.
19
+ #
20
+ # @!attribute [r] status
21
+ # @return [String, nil] `"delivered"`, `"failed"` or `"pending"`
22
+ # @!attribute [r] tool
23
+ # @return [String, nil] the delivery tool's name
24
+ # @!attribute [r] attempts
25
+ # @return [Integer, nil] how many times the platform called the tool
26
+ # @!attribute [r] error
27
+ # @return [String, nil] the tool's error, when delivery failed
28
+ # @!attribute [r] result
29
+ # @return [Object, nil] what the tool returned, when it succeeded
30
+ # @!attribute [r] delivered_at
31
+ # @return [String, nil] when the reply was delivered, as an ISO 8601 timestamp
32
+ # @!attribute [r] failed_at
33
+ # @return [String, nil] when delivery gave up, as an ISO 8601 timestamp
34
+ Delivery = Struct.new(:status, :tool, :attempts, :error, :result, :delivered_at, :failed_at,
35
+ keyword_init: true) do
36
+ # @return [Boolean] whether the platform delivered the reply
37
+ def delivered?
38
+ status == "delivered"
39
+ end
40
+
41
+ # @return [Boolean] whether delivery failed
42
+ def failed?
43
+ status == "failed"
44
+ end
45
+
46
+ # @return [Boolean] whether delivery was cut off before it recorded an outcome
47
+ def pending?
48
+ status == "pending"
49
+ end
50
+ end
51
+
52
+ # @api private
53
+ # The `response_metadata.review` type the platform stamps on a draft when it pauses on it.
54
+ PENDING_REVIEW = "pending".freeze
55
+
56
+ # The response exactly as Assistants returned it.
57
+ #
58
+ # @return [Hash]
59
+ attr_reader :raw
60
+
61
+ # @api private
62
+ def initialize(run)
63
+ @raw = run.to_h
64
+ end
65
+
66
+ # @return [Boolean] whether the run paused for review
67
+ def interrupted?
68
+ Array(raw["__interrupt__"]).any?
69
+ end
70
+
71
+ # What the paused run is waiting on, or nil when it did not pause.
72
+ #
73
+ # @return [PendingReview, nil]
74
+ def review
75
+ return @review if defined?(@review)
76
+
77
+ @review = PendingReview.from_run(raw)
78
+ end
79
+
80
+ # The run's messages as Assistants reports them, oldest first.
81
+ #
82
+ # @return [Array<Hash>]
83
+ def messages
84
+ Array(raw["messages"])
85
+ end
86
+
87
+ # The text of the run's reply, always a String: content given as blocks is reduced to its text.
88
+ # Nil while the run is paused, and when it ended without one: a rejected draft is removed,
89
+ # leaving a tool result or the human's message last, and an earlier turn's text is never the
90
+ # reply. Blank text is no reply.
91
+ #
92
+ # @return [String, nil]
93
+ def reply
94
+ reply_message && text(reply_message["content"]).presence
95
+ end
96
+
97
+ # The reply's delivery, or nil when the reply carries no delivery stamp: no reply, or no
98
+ # delivery tool configured for the assistant.
99
+ #
100
+ # @return [Delivery, nil]
101
+ def delivery
102
+ return @delivery if defined?(@delivery)
103
+
104
+ stamp = reply_message&.dig("response_metadata", "delivery")
105
+ @delivery = stamp.is_a?(Hash) ? build_delivery(stamp) : nil
106
+ end
107
+
108
+ private
109
+
110
+ # The run's last message, when the run ended and that message is the assistant's reply: an AI
111
+ # message with no tool calls. A draft still stamped as awaiting review is never the reply, even
112
+ # in a response that reports no pause.
113
+ def reply_message
114
+ return nil if interrupted?
115
+
116
+ message = messages.last
117
+ return nil unless message.is_a?(Hash)
118
+ return nil if message["type"].present? && message["type"] != "ai"
119
+ return nil if Array(message["tool_calls"]).any?
120
+ return nil if awaiting_review?(message)
121
+
122
+ message
123
+ end
124
+
125
+ def awaiting_review?(message)
126
+ review = message.dig("response_metadata", "review")
127
+ review.is_a?(Hash) && review["type"] == PENDING_REVIEW
128
+ end
129
+
130
+ # A message's content is a string, or a list of content blocks. Its text is the string, or the
131
+ # bare strings and `text` blocks joined, as the platform reads it (LangChain's `message.text`);
132
+ # other blocks -- tool use, reasoning, images -- are not part of the reply.
133
+ def text(content)
134
+ return content if content.is_a?(String)
135
+ return nil unless content.is_a?(Array)
136
+
137
+ content.filter_map do |block|
138
+ next block if block.is_a?(String)
139
+
140
+ block["text"] if block.is_a?(Hash) && block["type"] == "text" && block["text"].is_a?(String)
141
+ end.join
142
+ end
143
+
144
+ def build_delivery(stamp)
145
+ Delivery.new(
146
+ status: string_or_nil(stamp["status"]),
147
+ tool: string_or_nil(stamp["tool"]),
148
+ attempts: stamp["attempts"].is_a?(Integer) ? stamp["attempts"] : nil,
149
+ error: string_or_nil(stamp["error"]),
150
+ result: stamp["result"],
151
+ delivered_at: string_or_nil(stamp["delivered_at"]),
152
+ failed_at: string_or_nil(stamp["failed_at"])
153
+ )
154
+ end
155
+
156
+ def string_or_nil(value)
157
+ value.is_a?(String) ? value : nil
158
+ end
159
+ end
160
+ end
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/hash/indifferent_access"
2
+ require "nitro_intelligence/review_decision"
2
3
 
3
4
  module NitroIntelligence
4
5
  # The tool calls one interrupt is holding, and the resume payload that answers them.
@@ -10,6 +11,8 @@ module NitroIntelligence
10
11
  # Action requests carry no tool call id, and ids are what a review interface works in, so each is
11
12
  # matched back onto the tool calls of the thread's last AI message -- the ones the middleware built
12
13
  # them from -- to recover the id. The order of `#tool_calls` is the order `decisions` must be in.
14
+ #
15
+ # @api private
13
16
  class ToolCallReviewInterrupt
14
17
  def initialize(thread_state)
15
18
  @thread_state = thread_state
@@ -68,14 +71,10 @@ module NitroIntelligence
68
71
  review = (review || {}).with_indifferent_access
69
72
 
70
73
  case review[:action].to_s
71
- when "approve"
72
- { "type" => "approve" }
73
- when "edit"
74
- { "type" => "edit", "edited_action" => edited_action(tool_call, review) }
75
- when "reject"
76
- review[:message].nil? ? { "type" => "reject" } : { "type" => "reject", "message" => review[:message] }
77
- when "respond"
78
- { "type" => "respond", "message" => review[:message] }
74
+ when "approve" then ReviewDecision.approve
75
+ when "edit" then ReviewDecision.edit(tool_call["name"], edited_args(tool_call, review))
76
+ when "reject" then ReviewDecision.reject(review[:message])
77
+ when "respond" then ReviewDecision.respond(review[:message])
79
78
  else
80
79
  raise Assistants::ThreadResumptionError,
81
80
  "Tool call #{tool_call['id']} has no review naming `approve`, `edit`, `reject` or `respond`"
@@ -84,11 +83,8 @@ module NitroIntelligence
84
83
 
85
84
  # Edited arguments are merged over the call the model made, so a reviewer changing one of them
86
85
  # does not have to restate the rest -- and cannot drop one by omitting it.
87
- def edited_action(tool_call, review)
88
- {
89
- "name" => tool_call["name"],
90
- "args" => tool_call["args"].merge(review[:args] || {}),
91
- }
86
+ def edited_args(tool_call, review)
87
+ tool_call["args"].merge(review[:args] || {})
92
88
  end
93
89
 
94
90
  def interrupt_value
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/hash/indifferent_access"
2
+ require "nitro_intelligence/review_decision"
2
3
 
3
4
  module NitroIntelligence
4
5
  # The reviews a caller submitted, checked against the tool calls the interrupt is holding, before
@@ -7,6 +8,8 @@ module NitroIntelligence
7
8
  # Internal to `Assistants#review_tool_calls`, which builds the only instance there is. What
8
9
  # `#validate!` takes follows what that method needs and has changed with it before, so it carries
9
10
  # no promise to anything calling it directly.
11
+ #
12
+ # @api private
10
13
  class ToolCallReviewValidator
11
14
  def validate!(tool_calls:, tool_calls_under_review:)
12
15
  tool_calls = normalize_tool_calls(tool_calls)
@@ -72,15 +75,10 @@ module NitroIntelligence
72
75
  end
73
76
 
74
77
  def validate_message!(tool_call_id:, review:, required:)
75
- message = review[:message]
76
-
77
- if message.nil?
78
- raise_error!("Review for tool call #{tool_call_id} must include a message") if required
79
- return
78
+ case ReviewDecision.message_problem(review[:message], required:)
79
+ when :missing then raise_error!("Review for tool call #{tool_call_id} must include a message")
80
+ when :not_string then raise_error!("Message for tool call #{tool_call_id} must be a string")
80
81
  end
81
-
82
- raise_error!("Message for tool call #{tool_call_id} must be a string") unless message.is_a?(String)
83
- raise_error!("Review for tool call #{tool_call_id} must include a message") if required && message.blank?
84
82
  end
85
83
 
86
84
  def validate_completeness!(submitted_tool_call_ids:, tool_calls_under_review:)
@@ -1,5 +1,13 @@
1
1
  module NitroIntelligence
2
+ # Trace IDs, for correlating an observed request with what else a host records about it.
2
3
  module Trace
4
+ # The ID of the trace a request passing `trace_seed: seed` is recorded under. The same seed
5
+ # always gives the same ID.
6
+ #
7
+ # @param seed [String] the request's `trace_seed`, such as a document's ID
8
+ # @return [String] the trace ID
9
+ # @example
10
+ # NitroIntelligence::Trace.create_id(seed: "document-123")
3
11
  def self.create_id(seed:)
4
12
  Langfuse::TraceId.create(seed:)
5
13
  end
@@ -1,3 +1,4 @@
1
1
  module NitroIntelligence
2
- VERSION = "3.1.0".freeze
2
+ # The gem's version.
3
+ VERSION = "4.1.0".freeze
3
4
  end
@@ -11,21 +11,54 @@ require "nitro_intelligence/assistants"
11
11
  require "nitro_intelligence/client/base"
12
12
  require "nitro_intelligence/client/client"
13
13
  require "nitro_intelligence/configuration"
14
+ require "nitro_intelligence/deprecator"
14
15
  require "nitro_intelligence/media/image_generation"
15
16
  require "nitro_intelligence/models/model_catalog"
16
17
  require "nitro_intelligence/observability/project_client_registry"
17
18
  require "nitro_intelligence/reporter"
18
19
 
20
+ # The Ruby client for the Nitro Intelligence Platform.
21
+ #
22
+ # The public API is everything in this reference not marked `@api private`, and follows Semantic
23
+ # Versioning. `docs/README.md` and `docs/ASSISTANTS.md` are guides to using it, not a list of
24
+ # everything public. What the reference leaves out may change in any release.
19
25
  module NitroIntelligence
26
+ # @!attribute [rw] configuration
27
+ # @api private
28
+ # @return [Class<Configuration>]
20
29
  mattr_accessor :configuration, default: Configuration
21
30
 
22
31
  class << self
32
+ # @!method configure
33
+ # Sets the configuration, normally once, from an initializer.
34
+ # @yieldparam config [ActiveSupport::Configurable::Configuration] the settings that
35
+ # {Configuration} lists
36
+ # @return [void]
37
+ # @example
38
+ # NitroIntelligence.configure do |config|
39
+ # config.inference_api_key = ENV.fetch("NITRO_INTELLIGENCE_API_KEY")
40
+ # end
41
+ # @!method config
42
+ # The current configuration.
43
+ # @return [ActiveSupport::Configurable::Configuration] the settings that {Configuration} lists
44
+ # @!method logger
45
+ # @api private
46
+ # @!method environment
47
+ # @api private
23
48
  delegate :configure, :config, :logger, :environment, to: :configuration
24
49
 
25
50
  # A registry addressable by name when `assistants_config` carries `definitions`, and the
26
51
  # single pre-registry client otherwise. The two shapes are mutually exclusive, so the
27
52
  # configuration decides which one a host gets: one that has not reshaped its config keeps
28
53
  # the client it already had.
54
+ #
55
+ # @return [AssistantRegistry, Assistants]
56
+ # @raise [ArgumentError] without `definitions`, when `assistants_config` has no `api_key`, as
57
+ # it does by default
58
+ # @raise [Assistants::ConfigurationError] without `definitions`, when `assistants_config`
59
+ # has a blank `api_key` or `user_id`
60
+ # @example
61
+ # NitroIntelligence.assistants["candidate-concierge"].run(thread_id:, messages:)
29
62
  def assistants
30
63
  current = configuration.assistants_config.to_h.deep_stringify_keys
31
64
  return AssistantRegistry.new(current) if current.key?(AssistantRegistry::DEFINITIONS_KEY)
@@ -33,14 +66,19 @@ module NitroIntelligence
33
66
  Assistants.new(**current.symbolize_keys)
34
67
  end
35
68
 
69
+ # @api private
36
70
  def cache
37
71
  configuration.cache_provider
38
72
  end
39
73
 
74
+ # @api private
40
75
  def model_catalog
41
76
  @model_catalog ||= ModelCatalog.new(configuration.model_config)
42
77
  end
43
78
 
79
+ # Each Cerebro project's client.
80
+ #
81
+ # @return [Observability::ProjectClientRegistry]
44
82
  def project_client_registry
45
83
  @project_client_registry ||= Observability::ProjectClientRegistry.new(
46
84
  base_url: configuration.observability_base_url
@@ -48,3 +86,5 @@ module NitroIntelligence
48
86
  end
49
87
  end
50
88
  end
89
+
90
+ require "nitro_intelligence/railtie" if defined?(Rails::Railtie)
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: 3.1.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -72,7 +72,10 @@ executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - ".yardopts"
75
76
  - Rakefile
77
+ - docs/ASSISTANTS.md
78
+ - docs/CHANGELOG.md
76
79
  - docs/README.md
77
80
  - lib/nitro_intelligence.rb
78
81
  - lib/nitro_intelligence/assistant.rb
@@ -84,15 +87,19 @@ files:
84
87
  - lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb
85
88
  - lib/nitro_intelligence/client/handlers/base_handler.rb
86
89
  - lib/nitro_intelligence/client/handlers/chat_handler.rb
90
+ - lib/nitro_intelligence/client/handlers/decision_handler.rb
87
91
  - lib/nitro_intelligence/client/handlers/image_handler.rb
88
92
  - lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
89
93
  - lib/nitro_intelligence/client/handlers/observed/chat_handler.rb
94
+ - lib/nitro_intelligence/client/handlers/observed/decision_handler.rb
90
95
  - lib/nitro_intelligence/client/handlers/observed/image_handler.rb
91
96
  - lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb
92
97
  - lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb
93
98
  - lib/nitro_intelligence/client/observed.rb
94
99
  - lib/nitro_intelligence/client/observers/langfuse_observer.rb
95
100
  - lib/nitro_intelligence/configuration.rb
101
+ - lib/nitro_intelligence/decision.rb
102
+ - lib/nitro_intelligence/deprecator.rb
96
103
  - lib/nitro_intelligence/langfuse_extension.rb
97
104
  - lib/nitro_intelligence/langfuse_tracer_provider.rb
98
105
  - lib/nitro_intelligence/media/audio.rb
@@ -110,7 +117,11 @@ files:
110
117
  - lib/nitro_intelligence/observability/prompt_resolver.rb
111
118
  - lib/nitro_intelligence/observability/prompt_store.rb
112
119
  - lib/nitro_intelligence/observability/upload_handler.rb
120
+ - lib/nitro_intelligence/pending_review.rb
121
+ - lib/nitro_intelligence/railtie.rb
113
122
  - lib/nitro_intelligence/reporter.rb
123
+ - lib/nitro_intelligence/review_decision.rb
124
+ - lib/nitro_intelligence/run_result.rb
114
125
  - lib/nitro_intelligence/tool_call_review_interrupt.rb
115
126
  - lib/nitro_intelligence/tool_call_review_validator.rb
116
127
  - lib/nitro_intelligence/trace.rb