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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +20 -2
- data/docs/guides/rails-integration.md +13 -0
- data/docs/index.md +20 -0
- data/lib/agents/agent.rb +12 -3
- data/lib/agents/helpers/message_extractor.rb +24 -1
- data/lib/agents/runner.rb +42 -2
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +7 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '071869beb00d53446a27f489c3817c58b0f8d163f334f0030c95b789a7c4895b'
|
|
4
|
+
data.tar.gz: 874227bd4dd05dd2947269bec7da2d593777a8600540ef9ec5a8fb50c346a884
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, :
|
|
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",
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
data/lib/agents/version.rb
CHANGED
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, :
|
|
87
|
-
:
|
|
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
|