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
@@ -0,0 +1,112 @@
1
+ require "base64"
2
+ require "cgi"
3
+ require "httparty"
4
+
5
+ require "nitro_intelligence/observability/prompt"
6
+
7
+ module NitroIntelligence
8
+ module Observability
9
+ class PromptStore
10
+ OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX = "nitro_intelligence_observability_prompts_".freeze
11
+
12
+ class ObservabilityPromptError < StandardError; end
13
+ class ObservabilityPromptNotFoundError < StandardError; end
14
+
15
+ def initialize(observability_project_slug:, observability_public_key:, observability_secret_key:)
16
+ @observability_project_slug = observability_project_slug
17
+ @observability_public_key = observability_public_key
18
+ @observability_secret_key = observability_secret_key
19
+ @observability_host = NitroIntelligence.config.observability_base_url
20
+ end
21
+
22
+ def get_prompt(prompt_name:, prompt_label: nil, prompt_version: nil)
23
+ safe_prompt_name = CGI.escapeURIComponent(prompt_name)
24
+ prompt = nil
25
+
26
+ if prompt_version.present?
27
+ prompt = get_prompt_by_version(safe_prompt_name:, prompt_version:)
28
+ else
29
+ prompt_label = "production" if prompt_label.nil?
30
+ prompt = get_prompt_by_label(safe_prompt_name:, prompt_label:)
31
+ end
32
+
33
+ prompt = Prompt.new(**prompt) if prompt.present?
34
+
35
+ prompt
36
+ end
37
+
38
+ private
39
+
40
+ def get_prompt_by_label(safe_prompt_name:, prompt_label:)
41
+ cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
42
+ "#{safe_prompt_name}_#{prompt_label}"
43
+ if (cached_prompt = NitroIntelligence.cache.read(cache_key)).present?
44
+ return cached_prompt
45
+ end
46
+
47
+ NitroIntelligence.logger.info(
48
+ "#{self.class} - Prompt label cache miss. Fetching prompt: #{safe_prompt_name} - #{prompt_label}"
49
+ )
50
+ get_prompt_request(safe_prompt_name:, prompt_url_params: "label=#{prompt_label}")
51
+ rescue => e
52
+ if (rolling_cached_prompt = NitroIntelligence.cache.read("#{cache_key}_rolling")).present?
53
+ NitroIntelligence.logger.warn(
54
+ "#{self.class} #{e} - Using rolling cached prompt: #{safe_prompt_name} - #{prompt_label}"
55
+ )
56
+ return rolling_cached_prompt
57
+ end
58
+
59
+ raise e
60
+ end
61
+
62
+ def get_prompt_by_version(safe_prompt_name:, prompt_version:)
63
+ cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
64
+ "#{safe_prompt_name}_#{prompt_version}"
65
+
66
+ if (cached_prompt = NitroIntelligence.cache.read(cache_key)).present?
67
+ return cached_prompt
68
+ end
69
+
70
+ NitroIntelligence.logger.info(
71
+ "#{self.class} - Prompt version cache miss. Fetching prompt: #{safe_prompt_name} - #{prompt_version}"
72
+ )
73
+ get_prompt_request(safe_prompt_name:, prompt_url_params: "version=#{prompt_version}")
74
+ end
75
+
76
+ def get_prompt_request(safe_prompt_name:, prompt_url_params:)
77
+ auth_token = Base64.strict_encode64("#{@observability_public_key}:#{@observability_secret_key}")
78
+ response = HTTParty.get(
79
+ "#{@observability_host}/api/public/v2/prompts/#{safe_prompt_name}?#{prompt_url_params}",
80
+ headers: {
81
+ "Authorization" => "Basic #{auth_token}",
82
+ }
83
+ )
84
+
85
+ if response.code != 200
86
+ raise ObservabilityPromptNotFoundError, "Prompt: #{safe_prompt_name} Not Found" if response.code == 404
87
+
88
+ raise ObservabilityPromptError, response.body
89
+ end
90
+
91
+ prompt = JSON.parse(response.body, symbolize_names: true)
92
+ write_prompt_caches(safe_prompt_name:, prompt:)
93
+ prompt
94
+ end
95
+
96
+ def write_prompt_caches(safe_prompt_name:, prompt:)
97
+ # Write versioned cache key
98
+ version_cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
99
+ "#{safe_prompt_name}_#{prompt[:version]}"
100
+ NitroIntelligence.cache.write(version_cache_key, prompt, expires_in: nil)
101
+
102
+ # Store all versions in an array cache per label
103
+ prompt[:labels].each do |label|
104
+ label_cache_key = "#{OBSERVABILITY_PROMPTS_CACHE_KEY_PREFIX}#{@observability_project_slug}_" \
105
+ "#{safe_prompt_name}_#{label}"
106
+ NitroIntelligence.cache.write(label_cache_key, prompt, expires_in: 5.minutes)
107
+ NitroIntelligence.cache.write("#{label_cache_key}_rolling", prompt, expires_in: nil)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,138 @@
1
+ require "base64"
2
+ require "digest"
3
+ require "httparty"
4
+ require "time"
5
+
6
+ module NitroIntelligence
7
+ module Observability
8
+ class UploadHandler
9
+ def initialize(auth_token:)
10
+ @host = NitroIntelligence.config.observability_base_url
11
+ @auth_token = auth_token
12
+ @uploaded_media = []
13
+ end
14
+
15
+ def replace_base64_with_media_references(payload)
16
+ # Make it easier to lookup message style image_urls
17
+ media_lookup = @uploaded_media.index_by { |image| "data:#{image.mime_type};base64,#{image.base64}" }
18
+
19
+ replace_base64_image_url = ->(node) do
20
+ case node
21
+ when Hash
22
+ url_key = if node.key?(:url)
23
+ :url
24
+ else
25
+ (node.key?("url") ? "url" : nil)
26
+ end
27
+
28
+ if url_key
29
+ url_value = node[url_key]
30
+
31
+ # Replace base64 strings if they match with our uploaded media
32
+ if (media = media_lookup[url_value])
33
+ # Overwrite base64 string with Langfuse media ref
34
+ # This *should* be rendering inline in the gui
35
+ # https://github.com/langfuse/langfuse/issues/4555
36
+ # https://github.com/langfuse/langfuse/issues/5030
37
+ node[url_key] = "@@@langfuseMedia:type=#{media.mime_type}|id=#{media.reference_id}|source=bytes@@@"
38
+ # Sometimes models can generate unwanted images that will not have a reference
39
+ # these untracked base64 strings can easily push 4.5mb Langfuse limit
40
+ elsif url_value.is_a?(String) && url_value.start_with?("data:")
41
+ node[url_key] = "[Discarded media]"
42
+ end
43
+ end
44
+
45
+ node.each_value { |val| replace_base64_image_url.call(val) }
46
+
47
+ when Array
48
+ node.each { |val| replace_base64_image_url.call(val) }
49
+ end
50
+ end
51
+
52
+ replace_base64_image_url.call(payload)
53
+
54
+ payload
55
+ end
56
+
57
+ def upload(trace_id, upload_queue: Queue.new)
58
+ until upload_queue.empty?
59
+ media = upload_queue.pop
60
+
61
+ content_length = media.byte_string.bytesize
62
+ content_sha256 = Base64.strict_encode64(Digest::SHA256.digest(media.byte_string))
63
+
64
+ # returns {"mediaId" -> "", "uploadUrl" => ""}
65
+ upload_url_response = get_upload_url({
66
+ traceId: trace_id,
67
+ contentType: media.mime_type,
68
+ contentLength: content_length,
69
+ sha256Hash: content_sha256,
70
+ field: media.direction,
71
+ })
72
+
73
+ # NOTE: uploadUrl is None if the file is stored in Langfuse already
74
+ # there is no need to upload it again.
75
+ upload_response = upload_media(
76
+ upload_url_response["mediaId"],
77
+ upload_url_response["uploadUrl"],
78
+ media.mime_type,
79
+ content_sha256,
80
+ media.byte_string
81
+ )
82
+
83
+ associate_media(upload_url_response["mediaId"], upload_response) if upload_response.present?
84
+
85
+ media.reference_id = upload_url_response["mediaId"]
86
+ @uploaded_media.append(media)
87
+ end
88
+
89
+ @uploaded_media
90
+ end
91
+
92
+ private
93
+
94
+ def get_upload_url(request_body)
95
+ HTTParty.post(
96
+ "#{@host}/api/public/media",
97
+ body: request_body.to_json,
98
+ headers: {
99
+ "Content-Type" => "application/json",
100
+ "Authorization" => "Basic #{@auth_token}",
101
+ }
102
+ )
103
+ end
104
+
105
+ def associate_media(media_id, upload_response)
106
+ request_body = {
107
+ uploadedAt: Time.now.utc.iso8601(6),
108
+ uploadHttpStatus: upload_response.code,
109
+ uploadHttpError: upload_response.code == 200 ? nil : upload_response.body,
110
+ }
111
+
112
+ HTTParty.patch(
113
+ "#{@host}/api/public/media/#{media_id}",
114
+ body: request_body.to_json,
115
+ headers: {
116
+ "Content-Type" => "application/json",
117
+ "Authorization" => "Basic #{@auth_token}",
118
+ }
119
+ )
120
+ end
121
+
122
+ def upload_media(media_id, upload_url, content_type, content_sha256, content_bytes)
123
+ if media_id.present? && upload_url.present?
124
+ return HTTParty.put(
125
+ upload_url,
126
+ headers: {
127
+ "Content-Type" => content_type,
128
+ "x-amz-checksum-sha256" => content_sha256,
129
+ },
130
+ body: content_bytes
131
+ )
132
+ end
133
+
134
+ nil
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,43 @@
1
+ require "base64"
2
+ require "httparty"
3
+
4
+ module NitroIntelligence
5
+ class Reporter
6
+ def initialize(observability_project_slug:)
7
+ @observability_project_slug = observability_project_slug
8
+ @project_client = fetch_project_client
9
+ @host = NitroIntelligence.config.observability_base_url
10
+ end
11
+
12
+ def create_dataset_item(attributes)
13
+ HTTParty.post("#{@host}/api/public/dataset-items",
14
+ body: attributes.to_json,
15
+ headers: {
16
+ "Content-Type" => "application/json",
17
+ "Authorization" => "Basic #{@project_client.project.auth_token}",
18
+ })
19
+ end
20
+
21
+ def score(trace_id:, name:, value:, id: "#{trace_id}-#{name}")
22
+ @project_client.observability_client.create_score(
23
+ id:,
24
+ trace_id:,
25
+ name:,
26
+ value:,
27
+ environment: NitroIntelligence.environment
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def fetch_project_client
34
+ project_client = NitroIntelligence.project_client_registry.fetch(@observability_project_slug)
35
+ if project_client.nil?
36
+ raise NitroIntelligence::Observability::ProjectClient::NotFoundError,
37
+ "No project session found for slug: #{@observability_project_slug}"
38
+ end
39
+
40
+ project_client
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,69 @@
1
+ require "active_support/core_ext/hash/indifferent_access"
2
+
3
+ module NitroIntelligence
4
+ class ToolCallReviewValidator
5
+ def validate!(thread_state:, tool_calls:, pending_tool_calls:)
6
+ tool_calls = normalize_tool_calls(tool_calls)
7
+ pending_tool_calls_by_id = Array(pending_tool_calls).index_by { |tool_call| tool_call["id"] }
8
+ review_actions = Array(thread_state.dig("interrupts", 0, "value", "review_actions"))
9
+
10
+ tool_calls.each do |tool_call_id, review|
11
+ pending_tool_call = pending_tool_calls_by_id[tool_call_id]&.with_indifferent_access
12
+ raise_error!("Unknown tool call ids: #{tool_call_id}") unless pending_tool_call
13
+
14
+ review = normalize_review(tool_call_id, review)
15
+ review_action = review[:action].to_s
16
+
17
+ unless review_actions.include?(review_action)
18
+ raise_error!("Invalid review action `#{review_action}` for tool call #{tool_call_id}")
19
+ end
20
+
21
+ validate_edited_args!(tool_call_id:, review:, pending_tool_call:) if review_action == "edit"
22
+ end
23
+
24
+ validate_completeness!(
25
+ submitted_tool_call_ids: tool_calls.keys,
26
+ pending_tool_calls:
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_tool_calls(tool_calls)
33
+ raise_error!("tool_calls must be a hash") unless tool_calls.is_a?(Hash)
34
+
35
+ tool_calls.with_indifferent_access
36
+ end
37
+
38
+ def normalize_review(tool_call_id, review)
39
+ raise_error!("Review for tool call #{tool_call_id} must be a hash") unless review.is_a?(Hash)
40
+
41
+ review.with_indifferent_access
42
+ end
43
+
44
+ def validate_edited_args!(tool_call_id:, review:, pending_tool_call:)
45
+ provided_args = review[:args]
46
+ raise_error!("Edited args for tool call #{tool_call_id} must be a hash") unless provided_args.is_a?(Hash)
47
+
48
+ valid_arg_names = pending_tool_call.fetch(:args, {}).keys.map(&:to_s)
49
+ invalid_arg_names = provided_args.keys.map(&:to_s) - valid_arg_names
50
+ return if invalid_arg_names.empty?
51
+
52
+ raise_error!("Invalid edited args for tool call #{tool_call_id}: #{invalid_arg_names.join(', ')}")
53
+ end
54
+
55
+ def validate_completeness!(submitted_tool_call_ids:, pending_tool_calls:)
56
+ missing_tool_call_ids = Array(pending_tool_calls).filter_map do |tool_call|
57
+ tool_call_id = tool_call["id"].to_s
58
+ tool_call_id unless submitted_tool_call_ids.include?(tool_call_id)
59
+ end
60
+ return if missing_tool_call_ids.empty?
61
+
62
+ raise_error!("Missing reviews for tool calls: #{missing_tool_call_ids.join(', ')}")
63
+ end
64
+
65
+ def raise_error!(message)
66
+ raise NitroIntelligence::AgentServer::ThreadResumptionError, message
67
+ end
68
+ end
69
+ end
@@ -1,7 +1,7 @@
1
1
  module NitroIntelligence
2
2
  module Trace
3
- def self.create_id(seed: SecureRandom.uuid, length: 32)
4
- Digest::SHA256.hexdigest(seed)[0, length]
3
+ def self.create_id(seed:)
4
+ Langfuse::TraceId.create(seed:)
5
5
  end
6
6
  end
7
7
  end
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -1,31 +1,21 @@
1
1
  require "active_support"
2
2
  require "active_support/core_ext"
3
3
  require "base64"
4
- require "httparty"
4
+
5
5
  require "langfuse"
6
6
  require "openai"
7
7
 
8
8
  require "nitro_intelligence/version"
9
9
  require "nitro_intelligence/agent_server"
10
- require "nitro_intelligence/client"
10
+ require "nitro_intelligence/client/base"
11
+ require "nitro_intelligence/client/client"
11
12
  require "nitro_intelligence/configuration"
12
- require "nitro_intelligence/langfuse_extension"
13
- require "nitro_intelligence/langfuse_tracer_provider"
14
13
  require "nitro_intelligence/media/image_generation"
15
- require "nitro_intelligence/media/upload_handler"
16
14
  require "nitro_intelligence/models/model_catalog"
17
- require "nitro_intelligence/prompt/prompt_store"
15
+ require "nitro_intelligence/observability/project_client_registry"
16
+ require "nitro_intelligence/reporter"
18
17
 
19
18
  module NitroIntelligence
20
- OBSERVABILITY_PROJECTS_CACHE_KEY_PREFIX = "nitro_intelligence_observability_projects_".freeze
21
- CUSTOM_PARAMS = %i[observability_project_slug prompt_config_disabled prompt_label
22
- prompt_name prompt_variables prompt_version trace_name user_id trace_seed].freeze
23
-
24
- class ObservabilityUnavailableError < StandardError; end
25
- class ObservabilityProjectNotFoundError < StandardError; end
26
- class ObservabilityProjectConfigNotFoundError < StandardError; end
27
- class LangfuseClientNotFoundError < StandardError; end
28
-
29
19
  mattr_accessor :configuration, default: Configuration
30
20
 
31
21
  class << self
@@ -40,37 +30,13 @@ module NitroIntelligence
40
30
  end
41
31
 
42
32
  def model_catalog
43
- @model_catalog ||= NitroIntelligence::ModelCatalog.new(configuration.model_config)
44
- end
45
-
46
- def omit_params
47
- (CUSTOM_PARAMS + ImageGeneration::Config::CUSTOM_PARAMS).uniq
33
+ @model_catalog ||= ModelCatalog.new(configuration.model_config)
48
34
  end
49
35
 
50
- def langfuse_clients
51
- if @langfuse_clients.nil?
52
- NitroIntelligence.logger.warn("Langfuse clients were not initialized")
53
- return {}
54
- end
55
-
56
- @langfuse_clients
57
- end
58
-
59
- def initialize_langfuse_clients
60
- @langfuse_clients = configuration.observability_projects.to_h do |project|
61
- key = project["slug"]
62
-
63
- value = LangfuseExtension.new do |config|
64
- config.public_key = project["public_key"]
65
- config.secret_key = project["secret_key"]
66
- config.base_url = NitroIntelligence.config.observability_base_url
67
- # Default flush of 60 seconds can be too quick when
68
- # dealing with longer responses like image gen
69
- config.flush_interval = 120
70
- end
71
-
72
- [key, value]
73
- end
36
+ def project_client_registry
37
+ @project_client_registry ||= Observability::ProjectClientRegistry.new(
38
+ base_url: configuration.observability_base_url
39
+ )
74
40
  end
75
41
  end
76
42
  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: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 0.6.0
46
+ version: 0.7.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 0.6.0
53
+ version: 0.7.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: mini_magick
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 0.23.0
74
+ version: '0.58'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: 0.23.0
81
+ version: '0.58'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: railties
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -236,20 +236,36 @@ files:
236
236
  - docs/README.md
237
237
  - lib/nitro_intelligence.rb
238
238
  - lib/nitro_intelligence/agent_server.rb
239
- - lib/nitro_intelligence/client.rb
239
+ - lib/nitro_intelligence/client/base.rb
240
+ - lib/nitro_intelligence/client/client.rb
241
+ - lib/nitro_intelligence/client/factory.rb
242
+ - lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb
243
+ - lib/nitro_intelligence/client/handlers/chat_handler.rb
244
+ - lib/nitro_intelligence/client/handlers/image_handler.rb
245
+ - lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
246
+ - lib/nitro_intelligence/client/handlers/observed/chat_handler.rb
247
+ - lib/nitro_intelligence/client/handlers/observed/image_handler.rb
248
+ - lib/nitro_intelligence/client/observed.rb
249
+ - lib/nitro_intelligence/client/observers/langfuse_observer.rb
240
250
  - lib/nitro_intelligence/configuration.rb
241
251
  - lib/nitro_intelligence/langfuse_extension.rb
242
252
  - lib/nitro_intelligence/langfuse_tracer_provider.rb
253
+ - lib/nitro_intelligence/media/audio.rb
243
254
  - lib/nitro_intelligence/media/image.rb
244
255
  - lib/nitro_intelligence/media/image_generation.rb
245
256
  - lib/nitro_intelligence/media/media.rb
246
- - lib/nitro_intelligence/media/upload_handler.rb
247
257
  - lib/nitro_intelligence/models/model.rb
248
258
  - lib/nitro_intelligence/models/model_catalog.rb
249
259
  - lib/nitro_intelligence/models/model_factory.rb
250
260
  - lib/nitro_intelligence/null_cache.rb
251
- - lib/nitro_intelligence/prompt/prompt.rb
252
- - lib/nitro_intelligence/prompt/prompt_store.rb
261
+ - lib/nitro_intelligence/observability/project.rb
262
+ - lib/nitro_intelligence/observability/project_client.rb
263
+ - lib/nitro_intelligence/observability/project_client_registry.rb
264
+ - lib/nitro_intelligence/observability/prompt.rb
265
+ - lib/nitro_intelligence/observability/prompt_store.rb
266
+ - lib/nitro_intelligence/observability/upload_handler.rb
267
+ - lib/nitro_intelligence/reporter.rb
268
+ - lib/nitro_intelligence/tool_call_review_validator.rb
253
269
  - lib/nitro_intelligence/trace.rb
254
270
  - lib/nitro_intelligence/version.rb
255
271
  homepage: https://github.com/powerhome/nitro-intelligence.rb
@@ -264,7 +280,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
264
280
  requirements:
265
281
  - - ">="
266
282
  - !ruby/object:Gem::Version
267
- version: '3.2'
283
+ version: '3.3'
268
284
  required_rubygems_version: !ruby/object:Gem::Requirement
269
285
  requirements:
270
286
  - - ">="