ai-agents 0.10.0 → 0.11.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: 6d4dc10b4aceae77705002488794ecdd15531f604641adaa03445d72374220f0
4
- data.tar.gz: b056fcf690121a790808e6aa1229dc1a16b6e19b609fbaa09a5e32613f5bc58f
3
+ metadata.gz: '071869beb00d53446a27f489c3817c58b0f8d163f334f0030c95b789a7c4895b'
4
+ data.tar.gz: 874227bd4dd05dd2947269bec7da2d593777a8600540ef9ec5a8fb50c346a884
5
5
  SHA512:
6
- metadata.gz: b80b45ccaa92140f6155dceeb2227235150f913c90385197878074af9df0c22b913e611c46424f9efa9b3f8f128b8921795c3136685a889a758c4b8dd81f4dbd
7
- data.tar.gz: 63662db9f1dcbed10439dfa64746a868a0ee2cc64266c48e55f613296e90dca0d1661466af01825bbec3015c928209f20f000bc9aeb882f5ff510be7ad7110f7
6
+ metadata.gz: 7e3814e0e76359be595534eb92ce11d655e19e3997f78aea006be89721aae771c6a1f97ad277ed80581957cdea5a0919506518dfd8e8de1911a03a3641668a8b
7
+ data.tar.gz: 351b99abda0942df5253e1735faca522b5eee988fc248ec16564dea700b2bcfcf31a8ec4ba8f9db73f3f57103e5f8b37a0997c6e71f4b1a1872ca796046cad2a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] - 2026-05-27
11
+
12
+ ### Added
13
+ - Support RubyLLM provider overrides and custom model/deployment IDs on agents, including Azure configuration passthrough (#66)
14
+
15
+ ### Fixed
16
+ - Preserve per-message assistant agent attribution when restoring and snapshotting multi-agent conversation history, preventing earlier assistant messages from being re-labeled as the final active agent after handoffs (#68)
10
17
 
11
18
  ## [0.10.0] - 2026-04-20
12
19
 
data/README.md CHANGED
@@ -193,22 +193,40 @@ ruby examples/isp-support/interactive.rb
193
193
  Agents.configure do |config|
194
194
  # Provider API keys
195
195
  config.openai_api_key = ENV['OPENAI_API_KEY']
196
+ config.azure_api_base = ENV['AZURE_API_BASE']
197
+ config.azure_api_key = ENV['AZURE_API_KEY']
196
198
  config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
197
199
  config.gemini_api_key = ENV['GEMINI_API_KEY']
198
200
 
199
201
  # Defaults
200
- config.default_provider = :openai
201
202
  config.default_model = 'gpt-4o'
202
203
 
203
204
  # Performance
204
205
  config.request_timeout = 120
205
- config.max_turns = 10
206
206
 
207
207
  # Debugging
208
208
  config.debug = true
209
209
  end
210
210
  ```
211
211
 
212
+ ### Azure and Custom Deployments
213
+
214
+ ```ruby
215
+ Agents.configure do |config|
216
+ config.azure_api_base = ENV["AZURE_API_BASE"]
217
+ config.azure_api_key = ENV["AZURE_API_KEY"]
218
+ end
219
+
220
+ agent = Agents::Agent.new(
221
+ name: "Support",
222
+ model: "my-azure-deployment",
223
+ provider: :azure,
224
+ assume_model_exists: true
225
+ )
226
+ ```
227
+
228
+ `provider` is optional for known, unambiguous registry models. Set it for custom deployment names and for model IDs that can exist under multiple providers, such as Azure and OpenAI deployments.
229
+
212
230
  ## 🔍 Observability
213
231
 
214
232
  Optional OpenTelemetry instrumentation for tracing agent execution, compatible with
@@ -24,12 +24,25 @@ Configure your LLM providers in an initializer:
24
24
  # config/initializers/ai_agents.rb
25
25
  Agents.configure do |config|
26
26
  config.openai_api_key = Rails.application.credentials.openai_api_key
27
+ config.azure_api_base = Rails.application.credentials.azure_api_base
28
+ config.azure_api_key = Rails.application.credentials.azure_api_key
27
29
  config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
28
30
  config.default_model = 'gpt-4o-mini'
29
31
  config.debug = Rails.env.development?
30
32
  end
31
33
  ```
32
34
 
35
+ For Azure custom deployment names, pass the provider at the agent level:
36
+
37
+ ```ruby
38
+ support = Agents::Agent.new(
39
+ name: "Support",
40
+ model: "my-azure-deployment",
41
+ provider: :azure,
42
+ assume_model_exists: true
43
+ )
44
+ ```
45
+
33
46
  ## ActiveRecord Integration
34
47
 
35
48
  ### Conversation Persistence
data/docs/index.md CHANGED
@@ -61,6 +61,8 @@ require 'agents'
61
61
  # Configure your API keys
62
62
  Agents.configure do |config|
63
63
  config.openai_api_key = ENV['OPENAI_API_KEY']
64
+ # config.azure_api_base = ENV['AZURE_API_BASE']
65
+ # config.azure_api_key = ENV['AZURE_API_KEY']
64
66
  # config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
65
67
  # config.gemini_api_key = ENV['GEMINI_API_KEY']
66
68
  config.default_model = 'gpt-4o-mini'
@@ -87,6 +89,24 @@ result = runner.run("I need help with a technical issue")
87
89
  puts result.output
88
90
  ```
89
91
 
92
+ ### Azure and Custom Deployments
93
+
94
+ ```ruby
95
+ Agents.configure do |config|
96
+ config.azure_api_base = ENV["AZURE_API_BASE"]
97
+ config.azure_api_key = ENV["AZURE_API_KEY"]
98
+ end
99
+
100
+ agent = Agents::Agent.new(
101
+ name: "Support",
102
+ model: "my-azure-deployment",
103
+ provider: :azure,
104
+ assume_model_exists: true
105
+ )
106
+ ```
107
+
108
+ `provider` is optional for known, unambiguous registry models. Set it for custom deployment names and duplicate model IDs, such as Azure and OpenAI deployments.
109
+
90
110
  ## Next Steps
91
111
 
92
112
  - [Learn about Agents](concepts/agents.html)
data/lib/agents/agent.rb CHANGED
@@ -50,24 +50,29 @@ require_relative "helpers/hash_normalizer"
50
50
  # )
51
51
  module Agents
52
52
  class Agent
53
- attr_reader :name, :instructions, :model, :tools, :handoff_agents, :temperature, :response_schema, :headers, :params
53
+ attr_reader :name, :instructions, :model, :provider, :assume_model_exists, :tools, :handoff_agents, :temperature,
54
+ :response_schema, :headers, :params
54
55
 
55
56
  # Initialize a new Agent instance
56
57
  #
57
58
  # @param name [String] The name of the agent
58
59
  # @param instructions [String, Proc, nil] Static string or dynamic Proc that returns instructions
59
60
  # @param model [String] The LLM model to use (default: "gpt-4.1-mini")
61
+ # @param provider [Symbol, String, nil] Optional RubyLLM provider override
62
+ # @param assume_model_exists [Boolean] Whether RubyLLM should skip registry validation for custom model IDs
60
63
  # @param tools [Array<Agents::Tool>] Array of tool instances the agent can use
61
64
  # @param handoff_agents [Array<Agents::Agent>] Array of agents this agent can hand off to
62
65
  # @param temperature [Float] Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
63
66
  # @param response_schema [Hash, nil] JSON schema for structured output responses
64
67
  # @param headers [Hash, nil] Default HTTP headers applied to LLM requests
65
68
  # @param params [Hash, nil] Default provider-specific parameters applied to LLM requests (e.g., service_tier)
66
- def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7,
67
- response_schema: nil, headers: nil, params: nil)
69
+ def initialize(name:, instructions: nil, model: "gpt-4.1-mini", provider: nil, assume_model_exists: false,
70
+ tools: [], handoff_agents: [], temperature: 0.7, response_schema: nil, headers: nil, params: nil)
68
71
  @name = name
69
72
  @instructions = instructions
70
73
  @model = model
74
+ @provider = provider&.to_sym
75
+ @assume_model_exists = assume_model_exists
71
76
  @tools = tools.dup
72
77
  @handoff_agents = []
73
78
  @temperature = temperature
@@ -155,6 +160,8 @@ module Agents
155
160
  # @option changes [String] :name New agent name
156
161
  # @option changes [String, Proc] :instructions New instructions
157
162
  # @option changes [String] :model New model identifier
163
+ # @option changes [Symbol, String, nil] :provider New provider override
164
+ # @option changes [Boolean] :assume_model_exists Whether to skip model registry validation
158
165
  # @option changes [Array<Agents::Tool>] :tools New tools array (replaces all tools)
159
166
  # @option changes [Array<Agents::Agent>] :handoff_agents New handoff agents
160
167
  # @option changes [Float] :temperature Temperature for LLM responses (0.0-1.0)
@@ -165,6 +172,8 @@ module Agents
165
172
  name: changes.fetch(:name, @name),
166
173
  instructions: changes.fetch(:instructions, @instructions),
167
174
  model: changes.fetch(:model, @model),
175
+ provider: changes.fetch(:provider, @provider),
176
+ assume_model_exists: changes.fetch(:assume_model_exists, @assume_model_exists),
168
177
  tools: changes.fetch(:tools, @tools.dup),
169
178
  handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
170
179
  temperature: changes.fetch(:temperature, @temperature),
@@ -18,8 +18,30 @@
18
18
  module Agents
19
19
  module Helpers
20
20
  module MessageExtractor
21
+ # RubyLLM::Message has no metadata/extension API, so agent ownership is stored
22
+ # as an SDK-namespaced ivar on the message object until it is extracted.
23
+ #
24
+ # Caveat: RubyLLM::Message#instance_variables is overridden to hide :@raw but does
25
+ # not hide this ivar, so it will appear in instance_variables listings. This is
26
+ # harmless for our own code path (we read it via instance_variable_get and
27
+ # serialize through extract_messages, not Marshal), but external introspection
28
+ # of a message's ivars will see :@agents_authoring_agent.
29
+ AUTHORING_AGENT_IVAR = :@agents_authoring_agent
30
+
21
31
  module_function
22
32
 
33
+ def assign_agent_name(message, agent_name)
34
+ return unless message && agent_name
35
+
36
+ message.instance_variable_set(AUTHORING_AGENT_IVAR, agent_name)
37
+ end
38
+
39
+ def attributed_agent_name_for(message)
40
+ return unless message&.instance_variable_defined?(AUTHORING_AGENT_IVAR)
41
+
42
+ message.instance_variable_get(AUTHORING_AGENT_IVAR)
43
+ end
44
+
23
45
  # Check if content is considered empty (handles both String and Hash content)
24
46
  #
25
47
  # @param content [String, Hash, nil] The content to check
@@ -65,7 +87,8 @@ module Agents
65
87
 
66
88
  return message unless msg.role == :assistant
67
89
 
68
- message[:agent_name] = current_agent.name if current_agent
90
+ attributed_agent_name = attributed_agent_name_for(msg) || current_agent&.name
91
+ message[:agent_name] = attributed_agent_name if attributed_agent_name
69
92
 
70
93
  if tool_calls_present
71
94
  # RubyLLM stores tool_calls as Hash with call_id => ToolCall object
data/lib/agents/runner.rb CHANGED
@@ -103,7 +103,11 @@ module Agents
103
103
  agent_params = Helpers::HashNormalizer.normalize(current_agent.params, label: "params")
104
104
 
105
105
  # Create chat and restore conversation history
106
- chat = RubyLLM::Chat.new(model: current_agent.model)
106
+ chat = RubyLLM::Chat.new(
107
+ model: current_agent.model,
108
+ provider: current_agent.provider,
109
+ assume_model_exists: current_agent.assume_model_exists
110
+ )
107
111
  current_headers = Helpers::HashNormalizer.merge(agent_headers, runtime_headers)
108
112
  current_params = Helpers::HashNormalizer.merge(agent_params, runtime_params)
109
113
  apply_headers(chat, current_headers)
@@ -118,6 +122,7 @@ module Agents
118
122
  raise MaxTurnsExceeded, "Exceeded maximum turns: #{max_turns}" if current_turn > max_turns
119
123
 
120
124
  # Get response from LLM (RubyLLM handles tool execution with halting based handoff detection)
125
+ message_count_before_response = chat_message_count(chat)
121
126
  response = if current_turn == 1
122
127
  # Emit agent thinking event for initial message
123
128
  context_wrapper.callback_manager.emit_agent_thinking(current_agent.name, input, context_wrapper)
@@ -130,6 +135,7 @@ module Agents
130
135
  context_wrapper)
131
136
  chat.complete
132
137
  end
138
+ assign_agent_name_to_new_assistant_messages(chat, current_agent, message_count_before_response)
133
139
  track_usage(response, context_wrapper)
134
140
 
135
141
  # Emit LLM call complete event with model and response for instrumentation
@@ -266,6 +272,7 @@ module Agents
266
272
  next unless message_params # Skip invalid messages
267
273
 
268
274
  message = RubyLLM::Message.new(**message_params)
275
+ assign_restored_agent_name(message, msg)
269
276
  chat.add_message(message)
270
277
 
271
278
  if message.role == :assistant && message_params[:tool_calls]
@@ -377,6 +384,33 @@ module Agents
377
384
  context_wrapper.context.delete(:pending_handoff)
378
385
  end
379
386
 
387
+ def assign_agent_name_to_new_assistant_messages(chat, current_agent, start_index)
388
+ # Runtime chats are RubyLLM::Chat instances and expose messages. Keep this
389
+ # no-op guard for chat-like doubles/adapters that do not expose history.
390
+ return unless chat.respond_to?(:messages)
391
+
392
+ chat.messages[start_index..]&.each do |message|
393
+ next unless message.role == :assistant
394
+
395
+ Helpers::MessageExtractor.assign_agent_name(message, current_agent.name)
396
+ end
397
+ end
398
+
399
+ def chat_message_count(chat)
400
+ # Runtime chats are RubyLLM::Chat instances and expose messages. Keep this
401
+ # fallback for chat-like doubles/adapters where attribution is irrelevant.
402
+ return 0 unless chat.respond_to?(:messages)
403
+
404
+ chat.messages.length
405
+ end
406
+
407
+ def assign_restored_agent_name(message, msg)
408
+ return unless message.role == :assistant
409
+
410
+ restored_agent_name = msg[:agent_name] || msg["agent_name"]
411
+ Helpers::MessageExtractor.assign_agent_name(message, restored_agent_name)
412
+ end
413
+
380
414
  # Configures a RubyLLM chat instance with agent-specific settings.
381
415
  # Uses RubyLLM's replace option to swap agent context while preserving conversation history during handoffs.
382
416
  #
@@ -393,7 +427,13 @@ module Agents
393
427
  all_tools = build_agent_tools(agent, context_wrapper)
394
428
 
395
429
  # Switch model if different (important for handoffs between agents using different models)
396
- chat.with_model(agent.model) if replace
430
+ if replace
431
+ chat.with_model(
432
+ agent.model,
433
+ provider: agent.provider,
434
+ assume_exists: agent.assume_model_exists
435
+ )
436
+ end
397
437
 
398
438
  # Configure chat with instructions, temperature, tools, and schema
399
439
  chat.with_instructions(system_prompt, replace: replace) if system_prompt
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Agents
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/agents.rb CHANGED
@@ -56,6 +56,9 @@ module Agents
56
56
 
57
57
  # Other providers
58
58
  apply_if_present(config, :anthropic_api_key)
59
+ apply_if_present(config, :azure_api_base)
60
+ apply_if_present(config, :azure_api_key)
61
+ apply_if_present(config, :azure_ai_auth_token)
59
62
  apply_if_present(config, :gemini_api_key)
60
63
  apply_if_present(config, :deepseek_api_key)
61
64
  apply_if_present(config, :openrouter_api_key)
@@ -83,8 +86,9 @@ module Agents
83
86
  class Configuration
84
87
  # Provider API keys and configuration
85
88
  attr_accessor :openai_api_key, :openai_api_base, :openai_organization_id, :openai_project_id
86
- attr_accessor :anthropic_api_key, :gemini_api_key, :deepseek_api_key, :openrouter_api_key, :ollama_api_base,
87
- :bedrock_api_key, :bedrock_secret_key, :bedrock_region, :bedrock_session_token
89
+ attr_accessor :anthropic_api_key, :azure_api_base, :azure_api_key, :azure_ai_auth_token, :gemini_api_key,
90
+ :deepseek_api_key, :openrouter_api_key, :ollama_api_base, :bedrock_api_key, :bedrock_secret_key,
91
+ :bedrock_region, :bedrock_session_token
88
92
 
89
93
  # General configuration
90
94
  attr_accessor :request_timeout, :default_model, :debug
@@ -100,7 +104,7 @@ module Agents
100
104
  def configured?
101
105
  @openai_api_key || @anthropic_api_key || @gemini_api_key ||
102
106
  @deepseek_api_key || @openrouter_api_key || @ollama_api_base ||
103
- @bedrock_api_key
107
+ @bedrock_api_key || (@azure_api_base && (@azure_api_key || @azure_ai_auth_token))
104
108
  end
105
109
  end
106
110
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shivam Mishra