nitro_intelligence 4.0.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 (55) 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 +140 -4
  7. data/lib/nitro_intelligence/assistant.rb +70 -1
  8. data/lib/nitro_intelligence/assistant_registry.rb +18 -0
  9. data/lib/nitro_intelligence/assistants.rb +146 -24
  10. data/lib/nitro_intelligence/client/base.rb +88 -8
  11. data/lib/nitro_intelligence/client/client.rb +20 -0
  12. data/lib/nitro_intelligence/client/factory.rb +1 -0
  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 +3 -1
  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 +46 -0
  27. data/lib/nitro_intelligence/decision.rb +105 -0
  28. data/lib/nitro_intelligence/langfuse_extension.rb +1 -0
  29. data/lib/nitro_intelligence/langfuse_tracer_provider.rb +7 -6
  30. data/lib/nitro_intelligence/media/audio.rb +6 -0
  31. data/lib/nitro_intelligence/media/image.rb +14 -1
  32. data/lib/nitro_intelligence/media/image_generation.rb +14 -2
  33. data/lib/nitro_intelligence/media/media.rb +4 -0
  34. data/lib/nitro_intelligence/models/model.rb +7 -0
  35. data/lib/nitro_intelligence/models/model_catalog.rb +3 -0
  36. data/lib/nitro_intelligence/models/model_factory.rb +2 -0
  37. data/lib/nitro_intelligence/null_cache.rb +2 -0
  38. data/lib/nitro_intelligence/observability/project.rb +41 -2
  39. data/lib/nitro_intelligence/observability/project_client.rb +10 -1
  40. data/lib/nitro_intelligence/observability/project_client_registry.rb +18 -0
  41. data/lib/nitro_intelligence/observability/prompt.rb +4 -3
  42. data/lib/nitro_intelligence/observability/prompt_resolver.rb +2 -0
  43. data/lib/nitro_intelligence/observability/prompt_store.rb +9 -0
  44. data/lib/nitro_intelligence/observability/upload_handler.rb +3 -1
  45. data/lib/nitro_intelligence/pending_review.rb +36 -1
  46. data/lib/nitro_intelligence/railtie.rb +1 -0
  47. data/lib/nitro_intelligence/reporter.rb +29 -0
  48. data/lib/nitro_intelligence/review_decision.rb +2 -0
  49. data/lib/nitro_intelligence/run_result.rb +31 -0
  50. data/lib/nitro_intelligence/tool_call_review_interrupt.rb +2 -0
  51. data/lib/nitro_intelligence/tool_call_review_validator.rb +2 -0
  52. data/lib/nitro_intelligence/trace.rb +8 -0
  53. data/lib/nitro_intelligence/version.rb +2 -1
  54. data/lib/nitro_intelligence.rb +37 -0
  55. metadata +7 -1
@@ -7,6 +7,7 @@ require "uri"
7
7
 
8
8
  module NitroIntelligence
9
9
  module Observability
10
+ # @api private
10
11
  class UploadHandler
11
12
  attr_reader :uploaded_media
12
13
 
@@ -58,7 +59,7 @@ module NitroIntelligence
58
59
  payload
59
60
  end
60
61
 
61
- def upload(trace_id, upload_queue: Queue.new)
62
+ def upload(trace_id, observation_id:, upload_queue: Queue.new)
62
63
  until upload_queue.empty?
63
64
  media = upload_queue.pop
64
65
 
@@ -68,6 +69,7 @@ module NitroIntelligence
68
69
  # returns {"mediaId" -> "", "uploadUrl" => ""}
69
70
  upload_url_response = get_upload_url({
70
71
  traceId: trace_id,
72
+ observationId: observation_id,
71
73
  contentType: media.mime_type,
72
74
  contentLength: content_length,
73
75
  sha256Hash: content_sha256,
@@ -20,29 +20,46 @@ module NitroIntelligence
20
20
  # response of a paused `/runs/wait` (which reports `__interrupt__`); both carry the messages the
21
21
  # tool calls are matched back onto.
22
22
  class PendingReview
23
+ # @api private
23
24
  REPLY_ACTION = "__reply__".freeze
24
25
 
26
+ # @api private
25
27
  # What a reviewer may do with a reply. LangChain's `respond` supplies a tool's result, which a
26
28
  # reply does not have, so it is never offered for one, whatever the interrupt lists.
27
29
  REPLY_DECISIONS = %w[approve edit reject].freeze
28
30
 
31
+ # @api private
29
32
  KIND_NAMES = { tool_calls: "tool calls", reply: "a reply" }.freeze
30
33
 
31
34
  # The draft reply a reviewer is asked to approve, edit or reject.
35
+ #
36
+ # @!attribute [r] content
37
+ # @return [String] the draft
38
+ # @!attribute [r] allowed_decisions
39
+ # @return [Array<String>] what the reviewer may do, from `approve`, `edit` and `reject`
40
+ # @!attribute [r] description
41
+ # @return [String, nil] the guidance the pause gives the reviewer
42
+ # @!attribute [r] interrupt_id
43
+ # @return [String] the pause, as {PendingReview#interrupt_id}
32
44
  Reply = Struct.new(:content, :allowed_decisions, :description, :interrupt_id, keyword_init: true)
33
45
 
34
46
  # The pending review in a thread state, or nil when the thread is not paused.
47
+ #
48
+ # @api private
35
49
  def self.from_thread_state(thread_state)
36
50
  interrupt = Array(thread_state["interrupts"]).first
37
51
  interrupt && new(interrupt:, messages: Array(thread_state.dig("values", "messages")))
38
52
  end
39
53
 
40
54
  # The pending review in a `/runs/wait` response, or nil when the run did not pause.
55
+ #
56
+ # @api private
41
57
  def self.from_run(run)
42
58
  interrupt = Array(run["__interrupt__"]).first
43
59
  interrupt && new(interrupt:, messages: Array(run["messages"]))
44
60
  end
45
61
 
62
+ # @api private
46
63
  def initialize(interrupt:, messages:)
47
64
  @interrupt = interrupt.to_h
48
65
  @messages = messages
@@ -50,25 +67,35 @@ module NitroIntelligence
50
67
 
51
68
  # Identifies this pause. A thread that has moved on -- a new message superseded the review, or
52
69
  # another reviewer decided it first -- reports a different interrupt, or none.
70
+ #
71
+ # @return [String, nil] nil for a tool-call pause the service reported without an id, which
72
+ # {Assistants#review} then resumes without checking that it is still the same pause
53
73
  def interrupt_id
54
74
  @interrupt["id"] || @interrupt["interrupt_id"]
55
75
  end
56
76
 
57
- # `:tool_calls` or `:reply`.
77
+ # What the run is paused on.
78
+ #
79
+ # @return [Symbol] `:tool_calls` or `:reply`
58
80
  def kind
59
81
  action_name == REPLY_ACTION ? :reply : :tool_calls
60
82
  end
61
83
 
84
+ # @return [Boolean] whether the run is paused on its tool calls
62
85
  def tool_calls?
63
86
  kind == :tool_calls
64
87
  end
65
88
 
89
+ # @return [Boolean] whether the run is paused on its reply
66
90
  def reply?
67
91
  kind == :reply
68
92
  end
69
93
 
70
94
  # The tool calls the interrupt is holding, as `Assistants#tool_calls_under_review` reports them.
71
95
  # Empty when the pause is on a reply.
96
+ #
97
+ # @return [Array<Hash>] each with `previous_message_id`, `id`, `name`, `args` and
98
+ # `allowed_decisions`
72
99
  def tool_calls
73
100
  return [] unless tool_calls?
74
101
 
@@ -76,6 +103,8 @@ module NitroIntelligence
76
103
  end
77
104
 
78
105
  # The held draft, or nil when the pause is not on a reply.
106
+ #
107
+ # @return [Reply, nil]
79
108
  def reply
80
109
  return nil unless reply?
81
110
 
@@ -92,6 +121,8 @@ module NitroIntelligence
92
121
  # thread moved on between reading this pause and resuming it -- it decides nothing, and the
93
122
  # thread stays paused on the newer interrupt. An interrupt reported without an id can only be
94
123
  # resumed unkeyed.
124
+ #
125
+ # @api private
95
126
  def resume(decisions)
96
127
  interrupt_id.blank? ? { decisions: } : { interrupt_id => { decisions: } }
97
128
  end
@@ -110,6 +141,8 @@ module NitroIntelligence
110
141
  # state still on the reported pause (a new message may already have moved it on), or when the
111
142
  # state cannot be read, it is false and the result stands as returned. That a stale decision is
112
143
  # never applied is guaranteed by the keyed resume, not by this.
144
+ #
145
+ # @api private
113
146
  def unapplied_resume?(run:, run_id:)
114
147
  paused_on = self.class.from_run(run)&.interrupt_id
115
148
  return false if interrupt_id.blank? || run_id.blank? || paused_on.blank? || paused_on == interrupt_id
@@ -127,6 +160,8 @@ module NitroIntelligence
127
160
  # For a reply, `reviews` is a single review hash. For tool calls, it is either a hash of reviews
128
161
  # keyed by tool call id, as `Assistants#review_tool_calls` has always taken them, or a list of
129
162
  # reviews that each carry their tool call's `id`.
163
+ #
164
+ # @api private
130
165
  def decisions(reviews)
131
166
  return [reply_decision(reviews)] if reply?
132
167
 
@@ -1,6 +1,7 @@
1
1
  require "nitro_intelligence/deprecator"
2
2
 
3
3
  module NitroIntelligence
4
+ # @api private
4
5
  class Railtie < ::Rails::Railtie
5
6
  initializer "nitro_intelligence.deprecator" do |app|
6
7
  app.deprecators[:nitro_intelligence] = NitroIntelligence.deprecator
@@ -3,15 +3,33 @@ require "net/http"
3
3
  require "uri"
4
4
 
5
5
  module NitroIntelligence
6
+ # Records evaluations of traces in Cerebro.
7
+ #
8
+ # @example
9
+ # reporter = NitroIntelligence::Reporter.new(observability_project_slug: "my-feature-project")
10
+ # reporter.score(name: "precision", value: 0.5, trace_id:)
6
11
  class Reporter
12
+ # Raised by {#create_dataset_item} when Cerebro does not store the item. The message carries the
13
+ # response's status and body.
7
14
  class DatasetItemError < StandardError; end
8
15
 
16
+ # @param observability_project_slug [String] the Cerebro project the traces are recorded in
17
+ # @raise [Observability::Project::NotFoundError] when `observability_projects` does not configure
18
+ # the project
9
19
  def initialize(observability_project_slug:)
10
20
  @observability_project_slug = observability_project_slug
11
21
  @project_client = fetch_project_client
12
22
  @host = NitroIntelligence.config.observability_base_url
13
23
  end
14
24
 
25
+ # Adds an item to a dataset in the project, for evaluating prompts against later.
26
+ #
27
+ # @param attributes [Hash] the item, as Langfuse's `POST /api/public/dataset-items` takes it:
28
+ # `datasetName`, `input`, `expectedOutput`, `metadata`, `sourceTraceId` and the rest
29
+ # @return [Net::HTTPSuccess] the response
30
+ # @raise [DatasetItemError] when Cerebro does not store the item
31
+ # @example
32
+ # reporter.create_dataset_item({ datasetName: "sms-screening", input: { message: }, expectedOutput: "yes" })
15
33
  def create_dataset_item(attributes)
16
34
  uri = URI("#{@host}/api/public/dataset-items")
17
35
  http = Net::HTTP.new(uri.host, uri.port)
@@ -35,6 +53,17 @@ module NitroIntelligence
35
53
  response
36
54
  end
37
55
 
56
+ # Scores a trace. Scoring it again under the same `id` replaces the score.
57
+ #
58
+ # @param trace_id [String] the trace, such as {Trace.create_id} gives for a seed
59
+ # @param name [String] what the score measures
60
+ # @param value [Numeric, String, Boolean] the score: a number, `true` or `false` for `:boolean`,
61
+ # or the category's name for `:categorical`
62
+ # @param id [String] identifies the score, so that recording it again replaces it. Defaults to
63
+ # one per trace and name.
64
+ # @param data_type [Symbol] `:numeric`, `:boolean` or `:categorical`
65
+ # @return [void]
66
+ # @raise [ArgumentError] when `value` does not suit `data_type`
38
67
  def score(trace_id:, name:, value:, id: "#{trace_id}-#{name}", data_type: :numeric)
39
68
  @project_client.observability_client.create_score(
40
69
  id:,
@@ -2,6 +2,8 @@ module NitroIntelligence
2
2
  # The `command.resume` decisions a review sends, and the check on the `message` a decision
3
3
  # carries, shared by tool call reviews and reply reviews. Each caller keeps its own
4
4
  # error wording, so this reports what is wrong rather than raising.
5
+ #
6
+ # @api private
5
7
  module ReviewDecision
6
8
  module_function
7
9
 
@@ -16,36 +16,61 @@ module NitroIntelligence
16
16
  # finished run still showing it was cut off before delivery recorded an outcome. Treat a
17
17
  # pending reply as not sent; the next customer message removes it and the model answers both. A
18
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
19
34
  Delivery = Struct.new(:status, :tool, :attempts, :error, :result, :delivered_at, :failed_at,
20
35
  keyword_init: true) do
36
+ # @return [Boolean] whether the platform delivered the reply
21
37
  def delivered?
22
38
  status == "delivered"
23
39
  end
24
40
 
41
+ # @return [Boolean] whether delivery failed
25
42
  def failed?
26
43
  status == "failed"
27
44
  end
28
45
 
46
+ # @return [Boolean] whether delivery was cut off before it recorded an outcome
29
47
  def pending?
30
48
  status == "pending"
31
49
  end
32
50
  end
33
51
 
52
+ # @api private
34
53
  # The `response_metadata.review` type the platform stamps on a draft when it pauses on it.
35
54
  PENDING_REVIEW = "pending".freeze
36
55
 
37
56
  # The response exactly as Assistants returned it.
57
+ #
58
+ # @return [Hash]
38
59
  attr_reader :raw
39
60
 
61
+ # @api private
40
62
  def initialize(run)
41
63
  @raw = run.to_h
42
64
  end
43
65
 
66
+ # @return [Boolean] whether the run paused for review
44
67
  def interrupted?
45
68
  Array(raw["__interrupt__"]).any?
46
69
  end
47
70
 
48
71
  # What the paused run is waiting on, or nil when it did not pause.
72
+ #
73
+ # @return [PendingReview, nil]
49
74
  def review
50
75
  return @review if defined?(@review)
51
76
 
@@ -53,6 +78,8 @@ module NitroIntelligence
53
78
  end
54
79
 
55
80
  # The run's messages as Assistants reports them, oldest first.
81
+ #
82
+ # @return [Array<Hash>]
56
83
  def messages
57
84
  Array(raw["messages"])
58
85
  end
@@ -61,12 +88,16 @@ module NitroIntelligence
61
88
  # Nil while the run is paused, and when it ended without one: a rejected draft is removed,
62
89
  # leaving a tool result or the human's message last, and an earlier turn's text is never the
63
90
  # reply. Blank text is no reply.
91
+ #
92
+ # @return [String, nil]
64
93
  def reply
65
94
  reply_message && text(reply_message["content"]).presence
66
95
  end
67
96
 
68
97
  # The reply's delivery, or nil when the reply carries no delivery stamp: no reply, or no
69
98
  # delivery tool configured for the assistant.
99
+ #
100
+ # @return [Delivery, nil]
70
101
  def delivery
71
102
  return @delivery if defined?(@delivery)
72
103
 
@@ -11,6 +11,8 @@ module NitroIntelligence
11
11
  # Action requests carry no tool call id, and ids are what a review interface works in, so each is
12
12
  # matched back onto the tool calls of the thread's last AI message -- the ones the middleware built
13
13
  # them from -- to recover the id. The order of `#tool_calls` is the order `decisions` must be in.
14
+ #
15
+ # @api private
14
16
  class ToolCallReviewInterrupt
15
17
  def initialize(thread_state)
16
18
  @thread_state = thread_state
@@ -8,6 +8,8 @@ module NitroIntelligence
8
8
  # Internal to `Assistants#review_tool_calls`, which builds the only instance there is. What
9
9
  # `#validate!` takes follows what that method needs and has changed with it before, so it carries
10
10
  # no promise to anything calling it directly.
11
+ #
12
+ # @api private
11
13
  class ToolCallReviewValidator
12
14
  def validate!(tool_calls:, tool_calls_under_review:)
13
15
  tool_calls = normalize_tool_calls(tool_calls)
@@ -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 = "4.0.0".freeze
2
+ # The gem's version.
3
+ VERSION = "4.1.0".freeze
3
4
  end
@@ -17,16 +17,48 @@ require "nitro_intelligence/models/model_catalog"
17
17
  require "nitro_intelligence/observability/project_client_registry"
18
18
  require "nitro_intelligence/reporter"
19
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.
20
25
  module NitroIntelligence
26
+ # @!attribute [rw] configuration
27
+ # @api private
28
+ # @return [Class<Configuration>]
21
29
  mattr_accessor :configuration, default: Configuration
22
30
 
23
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
24
48
  delegate :configure, :config, :logger, :environment, to: :configuration
25
49
 
26
50
  # A registry addressable by name when `assistants_config` carries `definitions`, and the
27
51
  # single pre-registry client otherwise. The two shapes are mutually exclusive, so the
28
52
  # configuration decides which one a host gets: one that has not reshaped its config keeps
29
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:)
30
62
  def assistants
31
63
  current = configuration.assistants_config.to_h.deep_stringify_keys
32
64
  return AssistantRegistry.new(current) if current.key?(AssistantRegistry::DEFINITIONS_KEY)
@@ -34,14 +66,19 @@ module NitroIntelligence
34
66
  Assistants.new(**current.symbolize_keys)
35
67
  end
36
68
 
69
+ # @api private
37
70
  def cache
38
71
  configuration.cache_provider
39
72
  end
40
73
 
74
+ # @api private
41
75
  def model_catalog
42
76
  @model_catalog ||= ModelCatalog.new(configuration.model_config)
43
77
  end
44
78
 
79
+ # Each Cerebro project's client.
80
+ #
81
+ # @return [Observability::ProjectClientRegistry]
45
82
  def project_client_registry
46
83
  @project_client_registry ||= Observability::ProjectClientRegistry.new(
47
84
  base_url: configuration.observability_base_url
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: 4.0.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,18 @@ 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
96
102
  - lib/nitro_intelligence/deprecator.rb
97
103
  - lib/nitro_intelligence/langfuse_extension.rb
98
104
  - lib/nitro_intelligence/langfuse_tracer_provider.rb