nitro_intelligence 2.5.0 → 2.7.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: 0bc1642f0475d8292a1e9611003d79e31983fb9afe2fd5e35d5ddf821a191e53
4
- data.tar.gz: f61c77b0a3cd8231ba52bd7df317ca715423dc08eb72639e028febf508d52472
3
+ metadata.gz: d87085d656ea4f0e2ee7ee00f1d1e360de33aa91257847a7c127ba65082b5e7c
4
+ data.tar.gz: f7587153b62be7ab17a60302ce8438ab483d35e9b499aee634c69cd1388cf2a9
5
5
  SHA512:
6
- metadata.gz: 7603cb32f91bda923f1728c5318db1a1b0e7fb59c1a46bca3a4ac3c01567de9d850b578626dd76f05d17b3348e1cdb5495fa6e98f28a6e9c871323147f02db24
7
- data.tar.gz: 251976a344210f499e8b7b0c7c05bec38461fd4d941cd852fe1d5b52bc4d69cc052f992b22fe83a0241845b8d8212773f77e2e5a0ea5addcc7c459d4d65bde2f
6
+ metadata.gz: ea8307af597a82b4e7511589eb5258ab1006805a63e3cc358dc7db6b2ca55882b0e522701667d7b98f5d851e89b214b9290d3c9330ff27a8957290c3c00584c8
7
+ data.tar.gz: 9a891b5281ec8f6f34b4611ab8a0204027b5c14a9b63ea02999f273386a06ab9b4d2a5410b18be0041b6acb4ad7ace1f8146c2115148dc81d70c12b2372a37a6
data/docs/README.md CHANGED
@@ -18,10 +18,10 @@ NitroIntelligence.configure do |config|
18
18
 
19
19
  # Inference (LLM) settings
20
20
  config.inference_api_key = "..." # API key for the inference service
21
- config.inference_base_url = "https://..." # Base URL for the inference service
21
+ config.inference_base_url = "https://..." # Base URL for the inference service (optional; defaults to the inference gateway)
22
22
 
23
23
  # Observability (Langfuse) settings
24
- config.observability_base_url = "https://..." # Base URL for the observability service
24
+ config.observability_base_url = "https://..." # Base URL for the observability service (optional; defaults to Cerebro)
25
25
  config.observability_projects = [ # Array of project credential hashes
26
26
  {
27
27
  "slug" => "my-feature-project",
@@ -32,7 +32,13 @@ NitroIntelligence.configure do |config|
32
32
  ]
33
33
 
34
34
  # Nitro Intelligence Assistants settings (optional)
35
- config.assistants_config = {} # Hash of Assistants keyword arguments
35
+ config.assistants_config = { # See "Assistants" below
36
+ "base_url" => "https://nip-assistants.example.com",
37
+ "user_id" => "my-app",
38
+ "definitions" => {
39
+ "candidate-concierge" => {},
40
+ }
41
+ }
36
42
 
37
43
  # Model configuration
38
44
  config.model_config = {
@@ -77,10 +83,10 @@ end
77
83
  | `environment` | `String` | `"test"` | Runtime environment name |
78
84
  | `cache_provider` | cache store | `NullCache` | ActiveSupport-compatible cache store |
79
85
  | `inference_api_key` | `String` | `""` | API key for the LLM inference service |
80
- | `inference_base_url` | `String` | `""` | Base URL for the LLM inference service |
81
- | `observability_base_url` | `String` | `""` | Base URL for the Langfuse observability service |
86
+ | `inference_base_url` | `String` | `"https://inference.powerhome.ai"` | Base URL for the LLM inference service. Defaults to the shared inference gateway, so only a host talking to a different one needs to set it |
87
+ | `observability_base_url` | `String` | `"https://cerebro.powerhome.ai"` | Base URL for the Langfuse observability service. Defaults to Cerebro, so only a host talking to a different one needs to set it |
82
88
  | `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key) |
83
- | `assistants_config` | `Hash` | `{}` | Credentials for `Assistants.new`. Expected keys: `base_url` (String) HTTP base URL of Nitro Intelligence Assistants; `api_key` (String) bearer token; `user_id` (String, default: `"default-user"`) — caller identity |
89
+ | `assistants_config` | `Hash` | `{}` | Assistants to make addressable by key. `base_url` (String, default: `"https://assistants.powerhome.ai"`) and `user_id` (String, default: `"default-user"`) are shared by every entry; `definitions` (Hash) holds one entry per assistant, keyed by what it is looked up with, each able to override a shared value. Without `definitions` the hash is read as credentials for a single `Assistants.new`, which takes the same two defaults see [Assistants](#assistants) |
84
90
  | `model_config` | `Hash` | `{}` | Model defaults and per-model settings. Top-level keys: `default_text_model`, `default_audio_transcription_model`, `default_image_model`, `default_text_to_speech_model`, and `models` (array of per-model hashes keyed by `name` and `type`, with type-specific options like `aspect_ratios`/`resolutions` for images or `voices`/`response_formats` for TTS) |
85
91
 
86
92
  ## Basic Usage
@@ -494,3 +500,77 @@ client.chat(
494
500
  `NitroIntelligence::Assistants` is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect agent tool calls pending human approval, and resume interrupted threads after human reviews.
495
501
 
496
502
  For the full guide, see [ASSISTANTS.md](ASSISTANTS.md). For the service this SDK talks to, see the [Nitro Intelligence Assistants documentation](https://portal.powerapp.cloud/docs/default/system/nip-assistants).
503
+
504
+ ## Assistants
505
+
506
+ An assistant is addressed by the key it is configured under:
507
+
508
+ ```ruby
509
+ assistant = NitroIntelligence.assistants["candidate-concierge"]
510
+
511
+ assistant.await_run(thread_id: thread.id, messages: messages)
512
+ assistant.thread_state(thread_id: thread.id)
513
+ ```
514
+
515
+ `await_run` and `review_tool_calls` supply the assistant's own id, so a caller never passes
516
+ it -- passing `assistant_id:` to either raises `ArgumentError` rather than quietly sending the
517
+ call to a different assistant, which is what a migrated caller carrying its old argument would
518
+ otherwise do. Everything else is thread-scoped and delegates to the client untouched. Reach
519
+ the client directly with `assistant.client` if you need it.
520
+
521
+ ### Configuration
522
+
523
+ Connection settings shared by every assistant sit at the top level; `definitions` holds one
524
+ entry per assistant, keyed by what you look it up with:
525
+
526
+ ```ruby
527
+ config.assistants_config = {
528
+ # Optional. Defaults to the shared Assistants deployment.
529
+ "base_url" => "https://nip-assistants.example.com",
530
+ "user_id" => "my-app",
531
+ "definitions" => {
532
+ "candidate-concierge" => {},
533
+ "home-studio" => {
534
+ # Overrides the shared value for this assistant only.
535
+ "base_url" => "https://pr306.nip-assistants.example.com"
536
+ }
537
+ }
538
+ }
539
+ ```
540
+
541
+ An entry may carry keys this gem has no use for — a graph id, or the Cerebro project an
542
+ assistant reports to — and they are ignored, so one structure can serve both a deployment and
543
+ the application reading it. That includes a `name` of its own: an entry's `name` is a
544
+ human-readable label for the assistant's record, distinct from the key it is filed under.
545
+ `Assistant#key` returns the latter.
546
+
547
+ ### Credentials
548
+
549
+ Each entry supplies its own `api_key` and `assistant_id`:
550
+
551
+ ```ruby
552
+ "definitions" => {
553
+ "candidate-concierge" => {
554
+ "api_key" => "...",
555
+ "assistant_id" => "..."
556
+ }
557
+ }
558
+ ```
559
+
560
+ Where a host gets them is its own business — an environment variable its deployment mounts, a
561
+ secrets store, a literal for local work. This gem reads no environment and assumes no naming
562
+ convention, so nothing here is tied to one deployment's wiring.
563
+
564
+ A key that resolves without them raises `Assistant::ConfigurationError`, naming every field it
565
+ is missing at once so a host resolving them from elsewhere can see which lookup failed.
566
+ `base_url` is not among them: an entry that says nothing about where to reach its assistant
567
+ gets `https://assistants.powerhome.ai`, so only a host talking to a different deployment — a
568
+ review environment, a local server — sets it.
569
+
570
+ ### Without `definitions`
571
+
572
+ `assistants_config` lacking `definitions` is read as keyword arguments for a single
573
+ `Assistants` client, and `NitroIntelligence.assistants` returns that client rather than a
574
+ registry. This is the shape that predates lookup by key; a host still on it is left alone.
575
+ `base_url` is optional here too, and a configuration without one reaches the same shared
576
+ deployment.
@@ -0,0 +1,80 @@
1
+ require "nitro_intelligence/assistants"
2
+
3
+ module NitroIntelligence
4
+ # One assistant resolved by name: the client for its deployment, plus the id every run has to
5
+ # carry.
6
+ #
7
+ # Credentials are supplied by the host. Where they come from is the host's business -- an
8
+ # environment variable its deployment mounts, a secrets store, a literal in configuration --
9
+ # so nothing here encodes one deployment's wiring.
10
+ class Assistant
11
+ class ConfigurationError < StandardError; end
12
+
13
+ DEFAULT_USER_ID = Assistants::DEFAULT_USER_ID
14
+
15
+ # `base_url` is not among them: the client defaults it, so it cannot be missing.
16
+ REQUIRED = %w[api_key assistant_id].freeze
17
+
18
+ # The key an assistant is filed and looked up under. Not its name: an entry usually carries
19
+ # a `name` of its own, a human-readable label for the assistant's record, and the two are
20
+ # different things.
21
+ attr_reader :key
22
+
23
+ attr_reader :assistant_id, :base_url, :user_id
24
+
25
+ # The key is positional so that splatting an entry cannot overwrite it, whatever the entry
26
+ # happens to carry.
27
+ #
28
+ # Extra keys are accepted and ignored for the same reason: an entry carries fields the
29
+ # application has no use for, such as the graph or the observability project it reports to.
30
+ def initialize(key, base_url: nil, api_key: nil, assistant_id: nil, user_id: nil, **_kwargs)
31
+ @key = key.to_s
32
+ @base_url = base_url.presence || Assistants::DEFAULT_BASE_URL
33
+ @api_key = api_key.presence
34
+ @assistant_id = assistant_id.presence
35
+ @user_id = user_id.presence || DEFAULT_USER_ID
36
+
37
+ validate!
38
+ end
39
+
40
+ def client
41
+ @client ||= Assistants.new(base_url: @base_url, api_key: @api_key, user_id: @user_id)
42
+ end
43
+
44
+ # The two calls that identify an assistant take it from here rather than from the caller.
45
+ # Everything else is thread-scoped and delegates untouched.
46
+ def await_run(thread_id:, messages:, **kwargs)
47
+ reject_assistant_id!(kwargs)
48
+ client.await_run(thread_id:, assistant_id:, messages:, **kwargs)
49
+ end
50
+
51
+ def review_tool_calls(thread_id:, reviewer_id:, tool_calls:, **kwargs)
52
+ reject_assistant_id!(kwargs)
53
+ client.review_tool_calls(thread_id:, assistant_id:, reviewer_id:, tool_calls:, **kwargs)
54
+ end
55
+
56
+ delegate :thread_state, :thread_messages, :tool_calls_pending_review, to: :client
57
+
58
+ private
59
+
60
+ # An assistant supplies its own id, so a caller has no business passing one. It would land
61
+ # in the trailing splat, which Ruby applies last, and quietly replace the configured id --
62
+ # routing the call to a different assistant with nothing to show for it. Refuse instead.
63
+ def reject_assistant_id!(kwargs)
64
+ return unless kwargs.key?(:assistant_id)
65
+
66
+ raise ArgumentError,
67
+ "assistant #{@key.inspect} supplies its own assistant_id; remove it from the call"
68
+ end
69
+
70
+ # Reported together and named, since a host resolving these from somewhere else needs to
71
+ # know which one it failed to supply.
72
+ def validate!
73
+ values = { "api_key" => @api_key, "assistant_id" => @assistant_id }
74
+ missing = REQUIRED.select { |field| values[field].blank? }
75
+ return if missing.empty?
76
+
77
+ raise ConfigurationError, "assistant #{@key.inspect} is missing #{missing.join(', ')}"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ require "nitro_intelligence/assistant"
2
+
3
+ module NitroIntelligence
4
+ # Assistants addressable by name.
5
+ #
6
+ # NitroIntelligence.assistants["candidate-concierge"].await_run(...)
7
+ #
8
+ # Built from `assistants_config`: connection settings shared by every assistant sit at the
9
+ # top level, and each entry under `definitions` overrides them where it needs to. The key an
10
+ # entry is filed under is what it is looked up by, distinct from any `name` it carries.
11
+ #
12
+ # {
13
+ # "base_url" => "https://nip-assistants.example.com",
14
+ # "user_id" => "nitro-web",
15
+ # "definitions" => {
16
+ # "candidate-concierge" => { "graph_id" => "react-agent" },
17
+ # },
18
+ # }
19
+ class AssistantRegistry
20
+ class UnknownAssistantError < StandardError; end
21
+
22
+ DEFINITIONS_KEY = "definitions".freeze
23
+ SHARED_KEYS = %w[base_url user_id].freeze
24
+
25
+ def initialize(config = {})
26
+ @config = config.to_h.deep_stringify_keys
27
+ @assistants = {}
28
+ end
29
+
30
+ def [](key)
31
+ fetch(key)
32
+ end
33
+
34
+ def fetch(key)
35
+ key = key.to_s
36
+ @assistants[key] ||= build(key)
37
+ end
38
+
39
+ def key?(key)
40
+ definitions.key?(key.to_s)
41
+ end
42
+
43
+ def keys
44
+ definitions.keys
45
+ end
46
+
47
+ private
48
+
49
+ def definitions
50
+ @config[DEFINITIONS_KEY] || {}
51
+ end
52
+
53
+ def build(key)
54
+ definition = definitions[key]
55
+
56
+ unless definition
57
+ raise UnknownAssistantError,
58
+ "No assistant configured for #{key.inspect}. " \
59
+ "Configured: #{keys.sort.join(', ').presence || '(none)'}"
60
+ end
61
+
62
+ # Blank entry values are dropped rather than merged, so a definition built from a source
63
+ # that writes an unset field as null falls back to the shared setting instead of shadowing
64
+ # it with nothing.
65
+ entry = definition.to_h.deep_stringify_keys.compact_blank
66
+ attributes = @config.slice(*SHARED_KEYS).merge(entry)
67
+ Assistant.new(key, **attributes.symbolize_keys)
68
+ end
69
+ end
70
+ end
@@ -14,14 +14,20 @@ module NitroIntelligence
14
14
  # Assistants answers with a conflict when `ifExists: "raise"` is sent for a thread that already exists.
15
15
  THREAD_CONFLICT_CODE = 409
16
16
 
17
+ # Every assistant this gem is built for is served by the same deployment, so a client told
18
+ # nothing about where to reach one gets that deployment. A host talking to a different one
19
+ # -- a review environment, a local server -- still says so.
20
+ DEFAULT_BASE_URL = "https://assistants.powerhome.ai".freeze
21
+
22
+ DEFAULT_USER_ID = "default-user".freeze
23
+
17
24
  attr_reader :base_url, :user_id
18
25
 
19
- def initialize(base_url:, api_key:, user_id: "default-user")
20
- raise ConfigurationError, "base_url is required" if base_url.blank?
26
+ def initialize(api_key:, base_url: nil, user_id: DEFAULT_USER_ID)
21
27
  raise ConfigurationError, "api_key is required" if api_key.blank?
22
28
  raise ConfigurationError, "user_id is required" if user_id.blank?
23
29
 
24
- @base_url = base_url
30
+ @base_url = base_url.presence || DEFAULT_BASE_URL
25
31
  @api_key = api_key
26
32
  @user_id = user_id
27
33
  @tool_call_review_validator = ToolCallReviewValidator.new
@@ -12,9 +12,9 @@ module NitroIntelligence
12
12
  config_accessor :environment, default: "test"
13
13
  config_accessor :assistants_config, default: {}
14
14
  config_accessor :inference_api_key, default: ""
15
- config_accessor :inference_base_url, default: ""
15
+ config_accessor :inference_base_url, default: "https://inference.powerhome.ai"
16
16
  config_accessor :model_config, default: {}
17
- config_accessor :observability_base_url, default: ""
17
+ config_accessor :observability_base_url, default: "https://cerebro.powerhome.ai"
18
18
  config_accessor :observability_projects, default: []
19
19
  config_accessor :observability_user_id, default: ""
20
20
 
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "2.5.0".freeze
2
+ VERSION = "2.7.0".freeze
3
3
  end
@@ -7,6 +7,7 @@ require "openai"
7
7
 
8
8
  require "nitro_intelligence/version"
9
9
  require "nitro_intelligence/agent_server"
10
+ require "nitro_intelligence/assistant_registry"
10
11
  require "nitro_intelligence/assistants"
11
12
  require "nitro_intelligence/client/base"
12
13
  require "nitro_intelligence/client/client"
@@ -23,8 +24,15 @@ module NitroIntelligence
23
24
  class << self
24
25
  delegate :configure, :config, :logger, :environment, to: :configuration
25
26
 
27
+ # A registry addressable by name when `assistants_config` carries `definitions`, and the
28
+ # single pre-registry client otherwise. The two shapes are mutually exclusive, so the
29
+ # configuration decides which one a host gets: one that has not reshaped its config keeps
30
+ # the client it already had.
26
31
  def assistants
27
- Assistants.new(**assistants_config.symbolize_keys)
32
+ current = assistants_config.to_h.deep_stringify_keys
33
+ return AssistantRegistry.new(current) if current.key?(AssistantRegistry::DEFINITIONS_KEY)
34
+
35
+ Assistants.new(**current.symbolize_keys)
28
36
  end
29
37
 
30
38
  # Deprecated: use `NitroIntelligence.assistants`.
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.5.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -76,6 +76,8 @@ files:
76
76
  - docs/README.md
77
77
  - lib/nitro_intelligence.rb
78
78
  - lib/nitro_intelligence/agent_server.rb
79
+ - lib/nitro_intelligence/assistant.rb
80
+ - lib/nitro_intelligence/assistant_registry.rb
79
81
  - lib/nitro_intelligence/assistants.rb
80
82
  - lib/nitro_intelligence/client/base.rb
81
83
  - lib/nitro_intelligence/client/client.rb