ai-agents 0.9.1 → 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 +18 -0
- data/README.md +20 -2
- data/docs/guides/provider-params.md +90 -0
- data/docs/guides/rails-integration.md +13 -0
- data/docs/guides.md +1 -0
- data/docs/index.md +20 -0
- data/lib/agents/agent.rb +18 -6
- data/lib/agents/agent_runner.rb +5 -1
- data/lib/agents/agent_tool.rb +1 -1
- data/lib/agents/handoff.rb +1 -1
- data/lib/agents/helpers/hash_normalizer.rb +28 -0
- data/lib/agents/helpers/message_extractor.rb +24 -1
- data/lib/agents/helpers/name_normalizer.rb +13 -0
- data/lib/agents/helpers.rb +2 -1
- data/lib/agents/instrumentation/tracing_callbacks.rb +11 -21
- data/lib/agents/runner.rb +97 -88
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +7 -3
- metadata +6 -4
- data/lib/agents/helpers/headers.rb +0 -33
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,24 @@ 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)
|
|
17
|
+
|
|
18
|
+
## [0.10.0] - 2026-04-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Support for provider-specific params via `with_params` (#44)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Bump `ruby_llm` dependency**: now `~> 1.14` (was `~> 1.9.1`). Trusts upstream semantic versioning by dropping the PATCH-level pin so minor-version fixes are picked up automatically (#61)
|
|
25
|
+
- Various internal refactors to `TracingCallbacks`, `Runner`, and helper modules
|
|
26
|
+
|
|
27
|
+
|
|
10
28
|
## [0.9.1] - 2026-02-24
|
|
11
29
|
|
|
12
30
|
### Fixed
|
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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Provider-Specific Parameters
|
|
4
|
+
parent: Guides
|
|
5
|
+
nav_order: 7
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Provider-Specific Parameters
|
|
9
|
+
|
|
10
|
+
Provider-specific parameters let you pass additional options directly into the LLM request payload via RubyLLM's `with_params` method. This is useful for features like OpenAI's `service_tier`, Anthropic's `reasoning_effort`, or any other provider-specific option that isn't exposed as a first-class SDK attribute.
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
### Agent-Level Params
|
|
15
|
+
|
|
16
|
+
Set default parameters when creating an agent that will be applied to all requests:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
agent = Agents::Agent.new(
|
|
20
|
+
name: "Assistant",
|
|
21
|
+
instructions: "You are a helpful assistant",
|
|
22
|
+
params: {
|
|
23
|
+
service_tier: "flex",
|
|
24
|
+
max_completion_tokens: 2048
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
runner = Agents::Runner.with_agents(agent)
|
|
29
|
+
result = runner.run("Hello!")
|
|
30
|
+
# All requests will include the provider-specific params
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Runtime Params
|
|
34
|
+
|
|
35
|
+
Override or add parameters for specific requests:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
agent = Agents::Agent.new(
|
|
39
|
+
name: "Assistant",
|
|
40
|
+
instructions: "You are a helpful assistant"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
runner = Agents::Runner.with_agents(agent)
|
|
44
|
+
|
|
45
|
+
# Pass params at runtime
|
|
46
|
+
result = runner.run(
|
|
47
|
+
"Explain quantum computing",
|
|
48
|
+
params: {
|
|
49
|
+
service_tier: "default",
|
|
50
|
+
max_completion_tokens: 4096
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Parameter Precedence
|
|
56
|
+
|
|
57
|
+
When both agent-level and runtime params are provided, **runtime params take precedence**:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
agent = Agents::Agent.new(
|
|
61
|
+
name: "Assistant",
|
|
62
|
+
instructions: "You are a helpful assistant",
|
|
63
|
+
params: {
|
|
64
|
+
service_tier: "flex",
|
|
65
|
+
top_p: 0.9
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
runner = Agents::Runner.with_agents(agent)
|
|
70
|
+
|
|
71
|
+
result = runner.run(
|
|
72
|
+
"Hello!",
|
|
73
|
+
params: {
|
|
74
|
+
service_tier: "default", # Overrides agent's flex value
|
|
75
|
+
max_completion_tokens: 1000 # Additional param
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Final params sent to LLM API:
|
|
80
|
+
# {
|
|
81
|
+
# service_tier: "default", # Runtime value wins
|
|
82
|
+
# top_p: 0.9, # From agent
|
|
83
|
+
# max_completion_tokens: 1000 # From runtime
|
|
84
|
+
# }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## See Also
|
|
88
|
+
|
|
89
|
+
- [Custom Request Headers](request-headers.html) - Adding custom HTTP headers using the same two-level precedence pattern
|
|
90
|
+
- [Multi-Agent Systems](multi-agent-systems.html) - Using params across agent handoffs
|
|
@@ -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/guides.md
CHANGED
|
@@ -18,4 +18,5 @@ Practical guides for building real-world applications with the AI Agents library
|
|
|
18
18
|
- **[State Persistence](guides/state-persistence.html)** - Managing conversation state and context across sessions and processes
|
|
19
19
|
- **[Structured Output](guides/structured-output.html)** - Enforcing JSON schema validation for reliable agent responses
|
|
20
20
|
- **[Custom Request Headers](guides/request-headers.html)** - Adding custom HTTP headers for authentication, tracking, and provider-specific features
|
|
21
|
+
- **[Provider-Specific Parameters](guides/provider-params.html)** - Passing provider-specific parameters like service_tier to the underlying LLM request
|
|
21
22
|
- **[OpenTelemetry Instrumentation](guides/instrumentation.html)** - Trace agent execution with Langfuse and other OTel backends
|
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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Agents are immutable, thread-safe objects that can be cloned with modifications.
|
|
5
5
|
# They encapsulate the configuration needed to interact with an LLM including
|
|
6
6
|
# instructions, tools, and potential handoff targets.
|
|
7
|
-
require_relative "helpers/
|
|
7
|
+
require_relative "helpers/hash_normalizer"
|
|
8
8
|
# @example Creating a basic agent
|
|
9
9
|
# agent = Agents::Agent.new(
|
|
10
10
|
# name: "Assistant",
|
|
@@ -50,28 +50,35 @@ require_relative "helpers/headers"
|
|
|
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
|
-
|
|
66
|
-
|
|
68
|
+
# @param params [Hash, nil] Default provider-specific parameters applied to LLM requests (e.g., service_tier)
|
|
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)
|
|
67
71
|
@name = name
|
|
68
72
|
@instructions = instructions
|
|
69
73
|
@model = model
|
|
74
|
+
@provider = provider&.to_sym
|
|
75
|
+
@assume_model_exists = assume_model_exists
|
|
70
76
|
@tools = tools.dup
|
|
71
77
|
@handoff_agents = []
|
|
72
78
|
@temperature = temperature
|
|
73
79
|
@response_schema = response_schema
|
|
74
|
-
@headers = Helpers::
|
|
80
|
+
@headers = Helpers::HashNormalizer.normalize(headers, label: "headers", freeze_result: true)
|
|
81
|
+
@params = Helpers::HashNormalizer.normalize(params, label: "params", freeze_result: true)
|
|
75
82
|
|
|
76
83
|
# Mutex for thread-safe handoff registration
|
|
77
84
|
# While agents are typically configured at startup, we want to ensure
|
|
@@ -153,6 +160,8 @@ module Agents
|
|
|
153
160
|
# @option changes [String] :name New agent name
|
|
154
161
|
# @option changes [String, Proc] :instructions New instructions
|
|
155
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
|
|
156
165
|
# @option changes [Array<Agents::Tool>] :tools New tools array (replaces all tools)
|
|
157
166
|
# @option changes [Array<Agents::Agent>] :handoff_agents New handoff agents
|
|
158
167
|
# @option changes [Float] :temperature Temperature for LLM responses (0.0-1.0)
|
|
@@ -163,11 +172,14 @@ module Agents
|
|
|
163
172
|
name: changes.fetch(:name, @name),
|
|
164
173
|
instructions: changes.fetch(:instructions, @instructions),
|
|
165
174
|
model: changes.fetch(:model, @model),
|
|
175
|
+
provider: changes.fetch(:provider, @provider),
|
|
176
|
+
assume_model_exists: changes.fetch(:assume_model_exists, @assume_model_exists),
|
|
166
177
|
tools: changes.fetch(:tools, @tools.dup),
|
|
167
178
|
handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
|
|
168
179
|
temperature: changes.fetch(:temperature, @temperature),
|
|
169
180
|
response_schema: changes.fetch(:response_schema, @response_schema),
|
|
170
|
-
headers: changes.fetch(:headers, @headers)
|
|
181
|
+
headers: changes.fetch(:headers, @headers),
|
|
182
|
+
params: changes.fetch(:params, @params)
|
|
171
183
|
)
|
|
172
184
|
end
|
|
173
185
|
|
data/lib/agents/agent_runner.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Agents
|
|
|
29
29
|
# can safely register callbacks concurrently without data races.
|
|
30
30
|
#
|
|
31
31
|
class AgentRunner
|
|
32
|
+
attr_reader :agents
|
|
33
|
+
|
|
32
34
|
# Initialize with a list of agents. The first agent becomes the default entry point.
|
|
33
35
|
#
|
|
34
36
|
# @param agents [Array<Agents::Agent>] List of agents, first one is the default entry point
|
|
@@ -64,8 +66,9 @@ module Agents
|
|
|
64
66
|
# @param context [Hash] Conversation context (will be restored if continuing conversation)
|
|
65
67
|
# @param max_turns [Integer] Maximum turns before stopping (default: 10)
|
|
66
68
|
# @param headers [Hash, nil] Custom HTTP headers to pass through to the underlying LLM provider
|
|
69
|
+
# @param params [Hash, nil] Provider-specific parameters to pass through to the underlying LLM (e.g., service_tier)
|
|
67
70
|
# @return [RunResult] Execution result with output, messages, and updated context
|
|
68
|
-
def run(input, context: {}, max_turns: Runner::DEFAULT_MAX_TURNS, headers: nil)
|
|
71
|
+
def run(input, context: {}, max_turns: Runner::DEFAULT_MAX_TURNS, headers: nil, params: nil)
|
|
69
72
|
# Determine which agent should handle this conversation
|
|
70
73
|
# Uses conversation history to maintain continuity across handoffs
|
|
71
74
|
current_agent = determine_conversation_agent(context)
|
|
@@ -78,6 +81,7 @@ module Agents
|
|
|
78
81
|
registry: @registry,
|
|
79
82
|
max_turns: max_turns,
|
|
80
83
|
headers: headers,
|
|
84
|
+
params: params,
|
|
81
85
|
callbacks: @callbacks
|
|
82
86
|
)
|
|
83
87
|
end
|
data/lib/agents/agent_tool.rb
CHANGED
|
@@ -97,7 +97,7 @@ module Agents
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def transform_agent_name(name)
|
|
100
|
-
|
|
100
|
+
Helpers::NameNormalizer.to_tool_name(name)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Create isolated context that only shares state, not conversation artifacts
|
data/lib/agents/handoff.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Agents
|
|
|
53
53
|
@target_agent = target_agent
|
|
54
54
|
|
|
55
55
|
# Set up the tool with a standardized name and description
|
|
56
|
-
@tool_name = "handoff_to_#{target_agent.name
|
|
56
|
+
@tool_name = "handoff_to_#{Helpers::NameNormalizer.to_tool_name(target_agent.name)}"
|
|
57
57
|
@tool_description = "Transfer conversation to #{target_agent.name}"
|
|
58
58
|
|
|
59
59
|
super()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agents
|
|
4
|
+
module Helpers
|
|
5
|
+
module HashNormalizer
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# NOTE: freeze_result performs a shallow freeze on the top-level hash only.
|
|
9
|
+
# Nested values remain mutable — e.g. hash[:nested][:key] = "x" would succeed.
|
|
10
|
+
def normalize(input, label:, freeze_result: false)
|
|
11
|
+
return freeze_result ? {}.freeze : {} if input.nil? || (input.respond_to?(:empty?) && input.empty?)
|
|
12
|
+
|
|
13
|
+
hash = input.respond_to?(:to_h) ? input.to_h : input
|
|
14
|
+
raise ArgumentError, "#{label} must be a Hash or respond to #to_h" unless hash.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
result = hash.transform_keys { |key| key.is_a?(Symbol) ? key : key.to_sym }
|
|
17
|
+
freeze_result ? result.freeze : result
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def merge(base, override)
|
|
21
|
+
return override if base.empty?
|
|
22
|
+
return base if override.empty?
|
|
23
|
+
|
|
24
|
+
base.merge(override)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -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/helpers.rb
CHANGED
|
@@ -151,9 +151,9 @@ module Agents
|
|
|
151
151
|
llm_span = @tracer.start_span(@llm_span_name, with_parent: parent_context(tracing), attributes: attrs)
|
|
152
152
|
|
|
153
153
|
llm_span.set_attribute(ATTR_GEN_AI_REQUEST_MODEL, model) if model
|
|
154
|
-
set_llm_response_attributes(llm_span, message)
|
|
155
154
|
|
|
156
155
|
output = llm_output_text(message)
|
|
156
|
+
set_llm_response_attributes(llm_span, message, output)
|
|
157
157
|
tracing[:last_agent_output] = output unless output.empty?
|
|
158
158
|
|
|
159
159
|
llm_span.finish
|
|
@@ -187,29 +187,25 @@ module Agents
|
|
|
187
187
|
root_span.status = OpenTelemetry::Trace::Status.error(error.message)
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
def set_llm_response_attributes(span, response)
|
|
190
|
+
def set_llm_response_attributes(span, response, output)
|
|
191
191
|
if response.respond_to?(:input_tokens) && response.input_tokens
|
|
192
192
|
span.set_attribute(ATTR_GEN_AI_USAGE_INPUT, response.input_tokens)
|
|
193
193
|
end
|
|
194
194
|
if response.respond_to?(:output_tokens) && response.output_tokens
|
|
195
195
|
span.set_attribute(ATTR_GEN_AI_USAGE_OUTPUT, response.output_tokens)
|
|
196
196
|
end
|
|
197
|
-
output = llm_output_text(response)
|
|
198
197
|
span.set_attribute(ATTR_LANGFUSE_OBS_OUTPUT, output) unless output.empty?
|
|
199
198
|
end
|
|
200
199
|
|
|
201
|
-
#
|
|
202
|
-
#
|
|
200
|
+
# Returns serialized text content if present, otherwise falls back to tool call formatting.
|
|
201
|
+
# Uses .to_json for Hash/Array (structured output) to avoid Ruby's .to_s format.
|
|
203
202
|
def llm_output_text(response)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
text = serialize_content(content)
|
|
210
|
-
return format_tool_calls(response) if text.empty?
|
|
203
|
+
if response.respond_to?(:content) && response.content
|
|
204
|
+
text = serialize_output(response.content)
|
|
205
|
+
return text unless text.empty?
|
|
206
|
+
end
|
|
211
207
|
|
|
212
|
-
|
|
208
|
+
format_tool_calls(response)
|
|
213
209
|
end
|
|
214
210
|
|
|
215
211
|
# Excludes the last message (current response) — returns what was sent to the LLM.
|
|
@@ -223,21 +219,15 @@ module Agents
|
|
|
223
219
|
end
|
|
224
220
|
|
|
225
221
|
def format_single_message(msg)
|
|
226
|
-
text =
|
|
222
|
+
text = serialize_output(msg.content)
|
|
227
223
|
text = append_tool_calls(msg, text)
|
|
228
224
|
{ role: msg.role.to_s, content: text }
|
|
229
225
|
end
|
|
230
226
|
|
|
231
|
-
def serialize_content(content)
|
|
232
|
-
return serialize_multimodal_content(content) if multimodal_content?(content)
|
|
233
|
-
|
|
234
|
-
content.is_a?(Hash) || content.is_a?(Array) ? content.to_json : content.to_s
|
|
235
|
-
end
|
|
236
|
-
|
|
237
227
|
def append_tool_calls(msg, text)
|
|
238
228
|
return text unless msg.role == :assistant && msg.respond_to?(:tool_calls) && msg.tool_calls&.any?
|
|
239
229
|
|
|
240
|
-
calls = msg.tool_calls.values.map { |tc| "#{tc.name}(#{tc.arguments
|
|
230
|
+
calls = msg.tool_calls.values.map { |tc| "#{tc.name}(#{serialize_output(tc.arguments)})" }.join(", ")
|
|
241
231
|
text.empty? ? "Tool calls: #{calls}" : "#{text}\nTool calls: #{calls}"
|
|
242
232
|
end
|
|
243
233
|
|
data/lib/agents/runner.rb
CHANGED
|
@@ -81,9 +81,11 @@ module Agents
|
|
|
81
81
|
# @param registry [Hash] Registry of agents for handoff resolution
|
|
82
82
|
# @param max_turns [Integer] Maximum conversation turns before stopping
|
|
83
83
|
# @param headers [Hash, nil] Custom HTTP headers passed to the underlying LLM provider
|
|
84
|
+
# @param params [Hash, nil] Provider-specific parameters passed to the underlying LLM (e.g., service_tier)
|
|
84
85
|
# @param callbacks [Hash] Optional callbacks for real-time event notifications
|
|
85
86
|
# @return [RunResult] The result containing output, messages, and usage
|
|
86
|
-
def run(starting_agent, input, context: {}, registry: {}, max_turns: DEFAULT_MAX_TURNS, headers: nil,
|
|
87
|
+
def run(starting_agent, input, context: {}, registry: {}, max_turns: DEFAULT_MAX_TURNS, headers: nil, params: nil,
|
|
88
|
+
callbacks: {})
|
|
87
89
|
# The starting_agent is already determined by AgentRunner based on conversation history
|
|
88
90
|
current_agent = starting_agent
|
|
89
91
|
|
|
@@ -95,13 +97,21 @@ module Agents
|
|
|
95
97
|
# Emit run start event
|
|
96
98
|
context_wrapper.callback_manager.emit_run_start(current_agent.name, input, context_wrapper)
|
|
97
99
|
|
|
98
|
-
runtime_headers = Helpers::
|
|
99
|
-
agent_headers = Helpers::
|
|
100
|
+
runtime_headers = Helpers::HashNormalizer.normalize(headers, label: "headers")
|
|
101
|
+
agent_headers = Helpers::HashNormalizer.normalize(current_agent.headers, label: "headers")
|
|
102
|
+
runtime_params = Helpers::HashNormalizer.normalize(params, label: "params")
|
|
103
|
+
agent_params = Helpers::HashNormalizer.normalize(current_agent.params, label: "params")
|
|
100
104
|
|
|
101
105
|
# Create chat and restore conversation history
|
|
102
|
-
chat = RubyLLM::Chat.new(
|
|
103
|
-
|
|
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
|
+
)
|
|
111
|
+
current_headers = Helpers::HashNormalizer.merge(agent_headers, runtime_headers)
|
|
112
|
+
current_params = Helpers::HashNormalizer.merge(agent_params, runtime_params)
|
|
104
113
|
apply_headers(chat, current_headers)
|
|
114
|
+
apply_params(chat, current_params)
|
|
105
115
|
configure_chat_for_agent(chat, current_agent, context_wrapper, replace: false)
|
|
106
116
|
restore_conversation_history(chat, context_wrapper)
|
|
107
117
|
input_already_in_history = last_message_matches?(chat, input)
|
|
@@ -112,19 +122,20 @@ module Agents
|
|
|
112
122
|
raise MaxTurnsExceeded, "Exceeded maximum turns: #{max_turns}" if current_turn > max_turns
|
|
113
123
|
|
|
114
124
|
# Get response from LLM (RubyLLM handles tool execution with halting based handoff detection)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
message_count_before_response = chat_message_count(chat)
|
|
126
|
+
response = if current_turn == 1
|
|
127
|
+
# Emit agent thinking event for initial message
|
|
128
|
+
context_wrapper.callback_manager.emit_agent_thinking(current_agent.name, input, context_wrapper)
|
|
129
|
+
# If conversation history already ends with this user message (e.g. passed
|
|
130
|
+
# in via context from an external system), use complete to avoid duplicating it.
|
|
131
|
+
input_already_in_history ? chat.complete : chat.ask(input)
|
|
132
|
+
else
|
|
133
|
+
# Emit agent thinking event for continuation
|
|
134
|
+
context_wrapper.callback_manager.emit_agent_thinking(current_agent.name, "(continuing conversation)",
|
|
135
|
+
context_wrapper)
|
|
136
|
+
chat.complete
|
|
137
|
+
end
|
|
138
|
+
assign_agent_name_to_new_assistant_messages(chat, current_agent, message_count_before_response)
|
|
128
139
|
track_usage(response, context_wrapper)
|
|
129
140
|
|
|
130
141
|
# Emit LLM call complete event with model and response for instrumentation
|
|
@@ -140,22 +151,8 @@ module Agents
|
|
|
140
151
|
# Validate that the target agent is in our registry
|
|
141
152
|
# This prevents handoffs to agents that weren't explicitly provided
|
|
142
153
|
unless registry[next_agent.name]
|
|
143
|
-
save_conversation_state(chat, context_wrapper, current_agent)
|
|
144
154
|
error = AgentNotFoundError.new("Handoff failed: Agent '#{next_agent.name}' not found in registry")
|
|
145
|
-
|
|
146
|
-
result = RunResult.new(
|
|
147
|
-
output: nil,
|
|
148
|
-
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
149
|
-
usage: context_wrapper.usage,
|
|
150
|
-
context: context_wrapper.context,
|
|
151
|
-
error: error
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Emit agent complete and run complete events with error
|
|
155
|
-
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, error, context_wrapper)
|
|
156
|
-
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
157
|
-
|
|
158
|
-
return result
|
|
155
|
+
return finalize_run(chat, context_wrapper, current_agent, output: nil, error: error)
|
|
159
156
|
end
|
|
160
157
|
|
|
161
158
|
# Save current conversation state before switching
|
|
@@ -174,9 +171,12 @@ module Agents
|
|
|
174
171
|
|
|
175
172
|
# Reconfigure existing chat for new agent - preserves conversation history automatically
|
|
176
173
|
configure_chat_for_agent(chat, current_agent, context_wrapper, replace: true)
|
|
177
|
-
agent_headers = Helpers::
|
|
178
|
-
current_headers = Helpers::
|
|
174
|
+
agent_headers = Helpers::HashNormalizer.normalize(current_agent.headers, label: "headers")
|
|
175
|
+
current_headers = Helpers::HashNormalizer.merge(agent_headers, runtime_headers)
|
|
179
176
|
apply_headers(chat, current_headers)
|
|
177
|
+
agent_params = Helpers::HashNormalizer.normalize(current_agent.params, label: "params")
|
|
178
|
+
current_params = Helpers::HashNormalizer.merge(agent_params, runtime_params)
|
|
179
|
+
apply_params(chat, current_params)
|
|
180
180
|
context_wrapper.callback_manager.emit_chat_created(
|
|
181
181
|
chat, current_agent.name, current_agent.model, context_wrapper
|
|
182
182
|
)
|
|
@@ -189,81 +189,50 @@ module Agents
|
|
|
189
189
|
|
|
190
190
|
# Handle non-handoff halts - return the halt content as final response
|
|
191
191
|
if response.is_a?(RubyLLM::Tool::Halt)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
result = RunResult.new(
|
|
195
|
-
output: response.content,
|
|
196
|
-
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
197
|
-
usage: context_wrapper.usage,
|
|
198
|
-
context: context_wrapper.context
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# Emit agent complete and run complete events
|
|
202
|
-
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
|
203
|
-
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
204
|
-
|
|
205
|
-
return result
|
|
192
|
+
return finalize_run(chat, context_wrapper, current_agent, output: response.content)
|
|
206
193
|
end
|
|
207
194
|
|
|
208
195
|
# If tools were called, continue the loop to let them execute
|
|
209
196
|
next if response.tool_call?
|
|
210
197
|
|
|
211
198
|
# If no tools were called, we have our final response
|
|
212
|
-
|
|
213
|
-
# Save final state before returning
|
|
214
|
-
save_conversation_state(chat, context_wrapper, current_agent)
|
|
215
|
-
|
|
216
|
-
result = RunResult.new(
|
|
217
|
-
output: response.content,
|
|
218
|
-
messages: Helpers::MessageExtractor.extract_messages(chat, current_agent),
|
|
219
|
-
usage: context_wrapper.usage,
|
|
220
|
-
context: context_wrapper.context
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
# Emit agent complete and run complete events
|
|
224
|
-
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, nil, context_wrapper)
|
|
225
|
-
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
226
|
-
|
|
227
|
-
return result
|
|
199
|
+
return finalize_run(chat, context_wrapper, current_agent, output: response.content)
|
|
228
200
|
end
|
|
229
201
|
rescue MaxTurnsExceeded => e
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
|
236
|
-
usage: context_wrapper.usage,
|
|
237
|
-
error: e,
|
|
238
|
-
context: context_wrapper.context
|
|
239
|
-
)
|
|
202
|
+
finalize_run(chat, context_wrapper, current_agent,
|
|
203
|
+
output: "Conversation ended: #{e.message}", error: e)
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
finalize_run(chat, context_wrapper, current_agent, output: nil, error: e)
|
|
206
|
+
end
|
|
240
207
|
|
|
241
|
-
|
|
242
|
-
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
|
243
|
-
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
208
|
+
private
|
|
244
209
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
210
|
+
# Saves conversation state, builds a RunResult, emits completion callbacks, and returns it.
|
|
211
|
+
# Centralises the finalize-and-return pattern used by the normal path, halt path, and error rescues.
|
|
212
|
+
#
|
|
213
|
+
# @param chat [RubyLLM::Chat, nil] The chat instance (nil in early-failure rescues)
|
|
214
|
+
# @param context_wrapper [RunContext] Context wrapper for state and callbacks
|
|
215
|
+
# @param current_agent [Agents::Agent] The currently active agent
|
|
216
|
+
# @param output [String, nil] The output text for the result
|
|
217
|
+
# @param error [StandardError, nil] Optional error to attach to the result
|
|
218
|
+
# @return [RunResult]
|
|
219
|
+
def finalize_run(chat, context_wrapper, current_agent, output:, error: nil)
|
|
248
220
|
save_conversation_state(chat, context_wrapper, current_agent) if chat
|
|
249
221
|
|
|
250
222
|
result = RunResult.new(
|
|
251
|
-
output:
|
|
223
|
+
output: output,
|
|
252
224
|
messages: chat ? Helpers::MessageExtractor.extract_messages(chat, current_agent) : [],
|
|
253
225
|
usage: context_wrapper.usage,
|
|
254
|
-
error:
|
|
226
|
+
error: error,
|
|
255
227
|
context: context_wrapper.context
|
|
256
228
|
)
|
|
257
229
|
|
|
258
|
-
|
|
259
|
-
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, e, context_wrapper)
|
|
230
|
+
context_wrapper.callback_manager.emit_agent_complete(current_agent.name, result, error, context_wrapper)
|
|
260
231
|
context_wrapper.callback_manager.emit_run_complete(current_agent.name, result, context_wrapper)
|
|
261
232
|
|
|
262
233
|
result
|
|
263
234
|
end
|
|
264
235
|
|
|
265
|
-
private
|
|
266
|
-
|
|
267
236
|
# Creates a deep copy of context data for thread safety.
|
|
268
237
|
# Preserves conversation history array structure while avoiding agent mutation.
|
|
269
238
|
#
|
|
@@ -303,6 +272,7 @@ module Agents
|
|
|
303
272
|
next unless message_params # Skip invalid messages
|
|
304
273
|
|
|
305
274
|
message = RubyLLM::Message.new(**message_params)
|
|
275
|
+
assign_restored_agent_name(message, msg)
|
|
306
276
|
chat.add_message(message)
|
|
307
277
|
|
|
308
278
|
if message.role == :assistant && message_params[:tool_calls]
|
|
@@ -414,6 +384,33 @@ module Agents
|
|
|
414
384
|
context_wrapper.context.delete(:pending_handoff)
|
|
415
385
|
end
|
|
416
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
|
+
|
|
417
414
|
# Configures a RubyLLM chat instance with agent-specific settings.
|
|
418
415
|
# Uses RubyLLM's replace option to swap agent context while preserving conversation history during handoffs.
|
|
419
416
|
#
|
|
@@ -430,7 +427,13 @@ module Agents
|
|
|
430
427
|
all_tools = build_agent_tools(agent, context_wrapper)
|
|
431
428
|
|
|
432
429
|
# Switch model if different (important for handoffs between agents using different models)
|
|
433
|
-
|
|
430
|
+
if replace
|
|
431
|
+
chat.with_model(
|
|
432
|
+
agent.model,
|
|
433
|
+
provider: agent.provider,
|
|
434
|
+
assume_exists: agent.assume_model_exists
|
|
435
|
+
)
|
|
436
|
+
end
|
|
434
437
|
|
|
435
438
|
# Configure chat with instructions, temperature, tools, and schema
|
|
436
439
|
chat.with_instructions(system_prompt, replace: replace) if system_prompt
|
|
@@ -462,6 +465,12 @@ module Agents
|
|
|
462
465
|
chat.with_headers(**headers)
|
|
463
466
|
end
|
|
464
467
|
|
|
468
|
+
def apply_params(chat, params)
|
|
469
|
+
return if params.empty?
|
|
470
|
+
|
|
471
|
+
chat.with_params(**params)
|
|
472
|
+
end
|
|
473
|
+
|
|
465
474
|
def track_usage(response, context_wrapper)
|
|
466
475
|
return unless context_wrapper&.usage
|
|
467
476
|
|
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
|
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.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shivam Mishra
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.
|
|
18
|
+
version: '1.14'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.
|
|
25
|
+
version: '1.14'
|
|
26
26
|
description: Ruby AI Agents SDK enables creating complex AI workflows with multi-agent
|
|
27
27
|
orchestration, tool execution, safety guardrails, and provider-agnostic LLM integration.
|
|
28
28
|
email:
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- docs/guides/agent-as-tool-pattern.md
|
|
62
62
|
- docs/guides/instrumentation.md
|
|
63
63
|
- docs/guides/multi-agent-systems.md
|
|
64
|
+
- docs/guides/provider-params.md
|
|
64
65
|
- docs/guides/rails-integration.md
|
|
65
66
|
- docs/guides/request-headers.md
|
|
66
67
|
- docs/guides/state-persistence.md
|
|
@@ -106,8 +107,9 @@ files:
|
|
|
106
107
|
- lib/agents/callback_manager.rb
|
|
107
108
|
- lib/agents/handoff.rb
|
|
108
109
|
- lib/agents/helpers.rb
|
|
109
|
-
- lib/agents/helpers/
|
|
110
|
+
- lib/agents/helpers/hash_normalizer.rb
|
|
110
111
|
- lib/agents/helpers/message_extractor.rb
|
|
112
|
+
- lib/agents/helpers/name_normalizer.rb
|
|
111
113
|
- lib/agents/instrumentation.rb
|
|
112
114
|
- lib/agents/instrumentation/constants.rb
|
|
113
115
|
- lib/agents/instrumentation/tracing_callbacks.rb
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Agents
|
|
4
|
-
module Helpers
|
|
5
|
-
module Headers
|
|
6
|
-
module_function
|
|
7
|
-
|
|
8
|
-
def normalize(headers, freeze_result: false)
|
|
9
|
-
return freeze_result ? {}.freeze : {} if headers.nil? || (headers.respond_to?(:empty?) && headers.empty?)
|
|
10
|
-
|
|
11
|
-
hash = headers.respond_to?(:to_h) ? headers.to_h : headers
|
|
12
|
-
raise ArgumentError, "headers must be a Hash or respond to #to_h" unless hash.is_a?(Hash)
|
|
13
|
-
|
|
14
|
-
result = symbolize_keys(hash)
|
|
15
|
-
freeze_result ? result.freeze : result
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def merge(agent_headers, runtime_headers)
|
|
19
|
-
return runtime_headers if agent_headers.empty?
|
|
20
|
-
return agent_headers if runtime_headers.empty?
|
|
21
|
-
|
|
22
|
-
agent_headers.merge(runtime_headers) { |_key, _agent_value, runtime_value| runtime_value }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def symbolize_keys(hash)
|
|
26
|
-
hash.transform_keys do |key|
|
|
27
|
-
key.is_a?(Symbol) ? key : key.to_sym
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
private_class_method :symbolize_keys
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|