ai-agents 0.12.0 → 0.13.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 +4 -0
- data/CLAUDE.md +1 -1
- data/README.md +3 -3
- data/docs/architecture.md +1 -1
- data/docs/concepts/agents.md +16 -0
- data/docs/concepts/tools.md +1 -1
- data/docs/guides/instrumentation.md +54 -5
- data/docs/guides/rails-integration.md +2 -2
- data/docs/guides/state-persistence.md +2 -2
- data/docs/guides/structured-output.md +3 -3
- data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +5 -5
- data/examples/collaborative-copilot/tools/get_article_tool.rb +1 -1
- data/examples/collaborative-copilot/tools/get_contact_tool.rb +1 -1
- data/examples/collaborative-copilot/tools/get_conversation_tool.rb +1 -1
- data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +1 -1
- data/examples/collaborative-copilot/tools/search_contacts_tool.rb +3 -2
- data/examples/collaborative-copilot/tools/search_conversations_tool.rb +2 -2
- data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +4 -3
- data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +5 -4
- data/examples/isp-support/agents_factory.rb +4 -4
- data/examples/isp-support/tools/create_checkout_tool.rb +1 -1
- data/examples/isp-support/tools/create_lead_tool.rb +3 -3
- data/examples/isp-support/tools/crm_lookup_tool.rb +1 -1
- data/examples/isp-support/tools/search_docs_tool.rb +1 -1
- data/lib/agents/agent.rb +19 -6
- data/lib/agents/agent_tool.rb +1 -1
- data/lib/agents/handoff.rb +2 -5
- data/lib/agents/helpers/message_extractor.rb +24 -8
- data/lib/agents/instrumentation/constants.rb +2 -0
- data/lib/agents/instrumentation/tracing_callbacks.rb +53 -24
- data/lib/agents/run_context.rb +5 -4
- data/lib/agents/runner.rb +76 -57
- data/lib/agents/tool.rb +8 -1
- data/lib/agents/tool_wrapper.rb +16 -13
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +4 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66cc6b306e84c6d62c5bf30b34104de9f683a3ec106eb11de12625000b07795d
|
|
4
|
+
data.tar.gz: dfe243cb0723ef5ef6c79fbb14342cb78a8b5362ccbb4d42356c2f2386d832a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9db32ad036739a09f198995cf98426c477e5afd4a309dae9f33af11a0baf785808268d5d23539b71fe01c0c3f44ff2e7f1a34fcb3d783d44c171bb7175bed00f
|
|
7
|
+
data.tar.gz: a263f704efb3122b60bcd4b245e7d84a04c65ed640eae72befbc7e51c6948e668c01bb4115a2d02351e311bf4933a88a081883947970c522484bed4731cdf0e6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Changed
|
|
11
|
+
- Require RubyLLM 2.0 starting with ai-agents 0.13.0. Applications using RubyLLM 1.x can keep ai-agents 0.12.0.
|
|
12
|
+
- Keep the previous `param :name, desc: "..."` tool declaration syntax working with RubyLLM 2.0.
|
|
13
|
+
|
|
10
14
|
## [0.12.0] - 2026-06-29
|
|
11
15
|
|
|
12
16
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -256,7 +256,7 @@ The SDK includes optional OpenTelemetry instrumentation (`lib/agents/instrumenta
|
|
|
256
256
|
|
|
257
257
|
6. **Don't use `.delete` for shared tracing state**: If a value in `context[:__otel_tracing]` needs to be read by multiple callbacks, use `tracing[:key]` not `tracing.delete(:key)`. Delete is a destructive side-effect that breaks subsequent reads.
|
|
258
258
|
|
|
259
|
-
7. **Per-call LLM spans via `
|
|
259
|
+
7. **Per-call LLM spans via `after_message`**: Individual GENERATION spans are created by hooking into RubyLLM's `chat.after_message` (registered in `on_chat_created`). Each span is created and immediately finished. There is no `current_llm_span` in tracing state — only `current_tool_span` needs single-slot tracking.
|
|
260
260
|
|
|
261
261
|
8. **Conversation history deduplication**: `Runner#last_message_matches?` checks if the last restored message already matches the current input. If so, uses `chat.complete` instead of `chat.ask(input)` to avoid sending the user message twice.
|
|
262
262
|
|
data/README.md
CHANGED
|
@@ -128,9 +128,9 @@ agent.register_handoffs(technical_support, billing)
|
|
|
128
128
|
```ruby
|
|
129
129
|
class EmailTool < Agents::Tool
|
|
130
130
|
description "Send emails to customers"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
parameter :to, type: "string", description: "Email address"
|
|
132
|
+
parameter :subject, type: "string", description: "Email subject"
|
|
133
|
+
parameter :body, type: "string", description: "Email body"
|
|
134
134
|
|
|
135
135
|
def perform(tool_context, to:, subject:, body:)
|
|
136
136
|
# Send email logic here
|
data/docs/architecture.md
CHANGED
|
@@ -301,7 +301,7 @@ The tool system is fully extensible:
|
|
|
301
301
|
class CustomTool < Agents::Tool
|
|
302
302
|
name "custom_action"
|
|
303
303
|
description "Perform custom business logic"
|
|
304
|
-
|
|
304
|
+
parameter :input, type: "string"
|
|
305
305
|
|
|
306
306
|
def perform(tool_context, input:)
|
|
307
307
|
# Access context for state
|
data/docs/concepts/agents.md
CHANGED
|
@@ -22,6 +22,8 @@ This project is in early development. While thread safety is a core design goal
|
|
|
22
22
|
* **`tools`**: An array of `Agents::Tool` instances that the agent can use to perform actions.
|
|
23
23
|
* **`handoff_agents`**: An array of other agents that this agent can hand off conversations to.
|
|
24
24
|
* **`temperature`**: Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
|
|
25
|
+
* **`protocol`**: Optional RubyLLM provider protocol, such as `:responses` or `:chat_completions`.
|
|
26
|
+
* **`thinking`**: Optional RubyLLM thinking options, such as `effort:` and `display:`.
|
|
25
27
|
|
|
26
28
|
### Example
|
|
27
29
|
|
|
@@ -43,3 +45,17 @@ specialized_agent = assistant_agent.clone(
|
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
In this example, we create a base `assistant_agent` and then create a `specialized_agent` by cloning it and adding a new tool. This approach allows for easy composition and reuse of agent configurations.
|
|
48
|
+
|
|
49
|
+
For an OpenAI reasoning model, select Responses for that agent and leave temperature unset:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
reasoning_agent = Agents::Agent.new(
|
|
53
|
+
name: "Reasoning",
|
|
54
|
+
model: "your-reasoning-model",
|
|
55
|
+
protocol: :responses,
|
|
56
|
+
temperature: nil,
|
|
57
|
+
thinking: { effort: :medium, display: :summarized }
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Other agents can use `protocol: :chat_completions`. The runner applies each agent's protocol and thinking settings when it starts or hands off a conversation.
|
data/docs/concepts/tools.md
CHANGED
|
@@ -33,7 +33,7 @@ You create tools by creating a class that inherits from `Agents::Tool` and imple
|
|
|
33
33
|
class WeatherTool < Agents::Tool
|
|
34
34
|
name "get_weather"
|
|
35
35
|
description "Get the current weather for a location."
|
|
36
|
-
|
|
36
|
+
parameter :location, type: "string", description: "The city and state, e.g., San Francisco, CA"
|
|
37
37
|
|
|
38
38
|
def perform(tool_context, location:)
|
|
39
39
|
# Access the API key from the shared context
|
|
@@ -18,8 +18,41 @@ The `Agents::Instrumentation` module produces OTel spans that give you full visi
|
|
|
18
18
|
- **Agent container spans** grouping related LLM and tool calls
|
|
19
19
|
- **Handoff events** recording agent-to-agent transfers
|
|
20
20
|
|
|
21
|
+
Generation inputs include MIME types for RubyLLM message attachments. Their URLs, local paths, and image data are omitted. Structured image inputs also omit the image URL.
|
|
22
|
+
|
|
21
23
|
Spans follow the [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) and include Langfuse-specific attributes for rich rendering in the Langfuse dashboard.
|
|
22
24
|
|
|
25
|
+
### RubyLLM 2.0 events and Langfuse
|
|
26
|
+
|
|
27
|
+
RubyLLM 2.0 emits `chat.ruby_llm`, `tool_call.ruby_llm`, and `usage.ruby_llm` events. In Rails, these use `ActiveSupport::Notifications`. Outside Rails, configure a RubyLLM instrumenter to receive them. The events are useful for logging and per-attempt usage, but RubyLLM does not turn them into this runner's Langfuse trace. Keep `Agents::Instrumentation` when you need the following mapping:
|
|
28
|
+
|
|
29
|
+
| Runner span or event | Data in Langfuse | RubyLLM event alone |
|
|
30
|
+
|----------------------|------------------|---------------------|
|
|
31
|
+
| Root span | Overall input and output, session, user, tags, and Chatwoot metadata | No runner-level input, output, or Chatwoot context |
|
|
32
|
+
| Agent span | Agent name and the generations and tools it owns | No agent handoff boundary |
|
|
33
|
+
| Generation span | Request messages, response, model, temperature, and response tokens | `chat.ruby_llm` has the model and response, but needs an OTLP and Langfuse attribute adapter |
|
|
34
|
+
| Tool span | Tool arguments, result, and error status | `tool_call.ruby_llm` has the call, but needs the same adapter |
|
|
35
|
+
| Handoff event | Source agent, target agent, and reason | No RubyLLM handoff event for this runner |
|
|
36
|
+
|
|
37
|
+
RubyLLM's `usage.ruby_llm` event reports each provider attempt, including failed and cancelled attempts. The generation spans here use tokens from completed messages. Use a separate `usage.ruby_llm` subscriber for attempt-level accounting; do not add its tokens to the same generation spans or costs may be counted twice. See the [RubyLLM instrumentation guide](https://rubyllm.com/instrumentation/) for event payloads.
|
|
38
|
+
|
|
39
|
+
For an app outside Rails, add `activesupport` and configure RubyLLM before creating a chat:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "active_support"
|
|
43
|
+
require "active_support/notifications"
|
|
44
|
+
|
|
45
|
+
RubyLLM.configure do |config|
|
|
46
|
+
config.instrumenter = ActiveSupport::Notifications
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
ActiveSupport::Notifications.subscribe("usage.ruby_llm") do |event|
|
|
50
|
+
puts event.payload.inspect # Replace with your per-attempt usage collector.
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This enables RubyLLM's native events. The runner spans still require `Agents::Instrumentation.install` and an OTel exporter, as shown below.
|
|
55
|
+
|
|
23
56
|
## Setup
|
|
24
57
|
|
|
25
58
|
### 1. Install dependencies
|
|
@@ -144,7 +177,7 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
144
177
|
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
145
178
|
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
146
179
|
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
147
|
-
headers: { "Authorization" => "Basic #{auth_token}" }
|
|
180
|
+
headers: { "Authorization" => "Basic #{auth_token}", "x-langfuse-ingestion-version" => "4" }
|
|
148
181
|
)
|
|
149
182
|
)
|
|
150
183
|
)
|
|
@@ -157,8 +190,8 @@ The instrumentation sets Langfuse-specific attributes that map to the Langfuse U
|
|
|
157
190
|
|
|
158
191
|
| Attribute | Set On | Langfuse Display |
|
|
159
192
|
|-----------|--------|-----------------|
|
|
160
|
-
| `langfuse.trace.input` | Root span |
|
|
161
|
-
| `langfuse.trace.output` | Root span |
|
|
193
|
+
| `langfuse.trace.input` | Root span | Legacy trace input for existing evaluators |
|
|
194
|
+
| `langfuse.trace.output` | Root span | Legacy trace output for existing evaluators |
|
|
162
195
|
| `langfuse.observation.input` | All spans | Observation input (sidebar click) |
|
|
163
196
|
| `langfuse.observation.output` | All spans | Observation output (sidebar click) |
|
|
164
197
|
| `langfuse.observation.type` | Tool spans | `"tool"` type indicator |
|
|
@@ -168,6 +201,12 @@ The instrumentation sets Langfuse-specific attributes that map to the Langfuse U
|
|
|
168
201
|
| `gen_ai.request.model` | Generation spans only | Model name + cost calculation |
|
|
169
202
|
| `gen_ai.usage.input_tokens` | Generation spans | Token usage |
|
|
170
203
|
| `gen_ai.usage.output_tokens` | Generation spans | Token usage |
|
|
204
|
+
| `gen_ai.usage.reasoning.output_tokens` | Generation spans, when reported | Reasoning tokens within output tokens |
|
|
205
|
+
| `langfuse.observation.metadata.reasoning_summary` | Responses generation spans with `thinking: { display: :summarized }` | Returned summary, when provided |
|
|
206
|
+
|
|
207
|
+
Langfuse v4 reads overall input and output from `langfuse.observation.input` and `langfuse.observation.output` on the root span. This integration also keeps the deprecated trace input and output attributes for existing consumers. The Langfuse v4 ingestion path needs the `x-langfuse-ingestion-version: 4` exporter header. See the [Langfuse migration guide](https://langfuse.com/integrations/native/opentelemetry/migration-to-v4).
|
|
208
|
+
|
|
209
|
+
Reasoning tokens are already part of output tokens, so do not add the two counts. The summary field records only text returned for a requested Responses summary. Encrypted reasoning signatures stay in conversation history for stateless replay and are not added to Langfuse spans. Treat persisted conversation history as sensitive data.
|
|
171
210
|
|
|
172
211
|
### EU vs US Cloud
|
|
173
212
|
|
|
@@ -202,7 +241,7 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
202
241
|
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
203
242
|
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
204
243
|
endpoint: "#{langfuse_host}/api/public/otel/v1/traces",
|
|
205
|
-
headers: { "Authorization" => "Basic #{auth_token}" }
|
|
244
|
+
headers: { "Authorization" => "Basic #{auth_token}", "x-langfuse-ingestion-version" => "4" }
|
|
206
245
|
)
|
|
207
246
|
)
|
|
208
247
|
)
|
|
@@ -251,7 +290,7 @@ Langfuse renders empty string attributes as "undefined". The instrumentation gua
|
|
|
251
290
|
|
|
252
291
|
### Double-counted costs
|
|
253
292
|
|
|
254
|
-
If token costs appear inflated, verify that `gen_ai.request.model` is only set on GENERATION spans, not on container or root spans.
|
|
293
|
+
If token costs appear inflated, verify that `gen_ai.request.model` is only set on GENERATION spans, not on container or root spans. `Agents::Instrumentation` handles this by default. If you set custom `span_attributes` that include `gen_ai.request.model`, costs will be double-counted.
|
|
255
294
|
|
|
256
295
|
### Empty spans / missing data
|
|
257
296
|
|
|
@@ -266,3 +305,13 @@ If token costs appear inflated, verify that `gen_ai.request.model` is only set o
|
|
|
266
305
|
- Check that the Authorization header uses `Basic` (not `Bearer`) with base64-encoded `pk:sk`
|
|
267
306
|
- Use `BatchSpanProcessor` for production; `SimpleSpanProcessor` can be useful for debugging
|
|
268
307
|
- **SSL CRL errors on Ruby 3.4+**: The OTLP exporter silently fails when SSL certificate revocation list (CRL) checks fail. The exporter reports SUCCESS but no data arrives. Fix by passing `ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE` to the exporter in development, or ensure your system CA certificates are up to date
|
|
308
|
+
|
|
309
|
+
## End-to-end check
|
|
310
|
+
|
|
311
|
+
The optional Langfuse check stubs the model response, exports real OTel spans to your Langfuse project, and reads them back through the observations API. It covers a handoff, token usage, session and user attributes, and a failed tool. Set `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, and `LANGFUSE_SECRET_KEY`, then run:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
RUN_LANGFUSE_E2E=1 bundle exec rspec spec/integration/langfuse_e2e_spec.rb
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The model call is stubbed, so this check does not require a provider API key. Use the separate live LLM smoke suite when you also need to check provider access.
|
|
@@ -264,7 +264,7 @@ Create Rails-specific tools for database operations:
|
|
|
264
264
|
class CustomerLookupTool < Agents::Tool
|
|
265
265
|
name "lookup_customer"
|
|
266
266
|
description "Look up customer information by email or ID"
|
|
267
|
-
|
|
267
|
+
parameter :identifier, type: "string", description: "Email address or customer ID"
|
|
268
268
|
|
|
269
269
|
def perform(tool_context, identifier:)
|
|
270
270
|
# Access Rails models safely
|
|
@@ -286,7 +286,7 @@ end
|
|
|
286
286
|
class BillingTool < Agents::Tool
|
|
287
287
|
name "get_billing_info"
|
|
288
288
|
description "Retrieve billing information for a customer"
|
|
289
|
-
|
|
289
|
+
parameter :user_id, type: "integer", description: "Customer user ID"
|
|
290
290
|
|
|
291
291
|
def perform(tool_context, user_id:)
|
|
292
292
|
user = User.find(user_id)
|
|
@@ -210,7 +210,7 @@ Tools should be stateless and rely on context for all data:
|
|
|
210
210
|
class DatabaseTool < Agents::Tool
|
|
211
211
|
name "query_database"
|
|
212
212
|
description "Query the application database"
|
|
213
|
-
|
|
213
|
+
parameter :query, type: "string", description: "SQL query to execute"
|
|
214
214
|
|
|
215
215
|
def perform(tool_context, query:)
|
|
216
216
|
# Get database connection from context, not instance variables
|
|
@@ -238,7 +238,7 @@ Store tool-specific data in context:
|
|
|
238
238
|
class FileProcessorTool < Agents::Tool
|
|
239
239
|
name "process_file"
|
|
240
240
|
description "Process uploaded files"
|
|
241
|
-
|
|
241
|
+
parameter :file_path, type: "string", description: "Path to file"
|
|
242
242
|
|
|
243
243
|
def perform(tool_context, file_path:)
|
|
244
244
|
# Initialize tool state in context if needed
|
|
@@ -41,12 +41,12 @@ result = runner.run("I love the new product features, especially the API and das
|
|
|
41
41
|
# }
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Schematist::Schema (Recommended)
|
|
45
45
|
|
|
46
|
-
For more complex schemas, use `
|
|
46
|
+
For more complex schemas, use `Schematist::Schema` which provides a cleaner Ruby DSL:
|
|
47
47
|
|
|
48
48
|
```ruby
|
|
49
|
-
class ContactSchema <
|
|
49
|
+
class ContactSchema < Schematist::Schema
|
|
50
50
|
string :name, description: "Full name of the person"
|
|
51
51
|
string :email, description: "Email address"
|
|
52
52
|
string :phone, description: "Phone number", required: false
|
|
@@ -6,11 +6,11 @@ module Copilot
|
|
|
6
6
|
# Tool for creating Linear tickets for engineering issues
|
|
7
7
|
class CreateLinearTicketTool < Agents::Tool
|
|
8
8
|
description "Create a Linear ticket for engineering issues or feature requests"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
parameter :title, type: "string", description: "Title of the issue"
|
|
10
|
+
parameter :description, type: "string", description: "Detailed description of the issue"
|
|
11
|
+
parameter :priority, type: "string", description: "Priority level (low, medium, high)"
|
|
12
|
+
parameter :assignee, type: "string", description: "Optional: email of person to assign to", required: false
|
|
13
|
+
parameter :labels, type: "string", description: "Comma-separated labels (e.g., bug,api,production)"
|
|
14
14
|
|
|
15
15
|
def perform(tool_context, title:, description:, priority: "medium", assignee: nil, labels: "")
|
|
16
16
|
# Generate a ticket ID
|
|
@@ -6,7 +6,7 @@ module Copilot
|
|
|
6
6
|
# Tool for retrieving specific knowledge base articles by ID
|
|
7
7
|
class GetArticleTool < Agents::Tool
|
|
8
8
|
description "Get the full content of a specific knowledge base article"
|
|
9
|
-
|
|
9
|
+
parameter :article_id, type: "string", description: "ID of the article to retrieve (e.g., ART-001)"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, article_id:)
|
|
12
12
|
data_file = File.join(__dir__, "../data/knowledge_base.json")
|
|
@@ -6,7 +6,7 @@ module Copilot
|
|
|
6
6
|
# Tool for retrieving contact/customer information
|
|
7
7
|
class GetContactTool < Agents::Tool
|
|
8
8
|
description "Get customer profile and contact information"
|
|
9
|
-
|
|
9
|
+
parameter :contact_id, type: "string", description: "ID of the contact to retrieve"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, contact_id:)
|
|
12
12
|
data_file = File.join(__dir__, "../data/contacts.json")
|
|
@@ -6,7 +6,7 @@ module Copilot
|
|
|
6
6
|
# Tool for retrieving conversation details and context
|
|
7
7
|
class GetConversationTool < Agents::Tool
|
|
8
8
|
description "Get conversation details, messages, and context for analysis"
|
|
9
|
-
|
|
9
|
+
parameter :conversation_id, type: "string", description: "ID of the conversation to retrieve"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, conversation_id:)
|
|
12
12
|
data_file = File.join(__dir__, "../data/conversations.json")
|
|
@@ -6,7 +6,7 @@ module Copilot
|
|
|
6
6
|
# Tool for retrieving Stripe billing information
|
|
7
7
|
class GetStripeBillingTool < Agents::Tool
|
|
8
8
|
description "Get customer billing information and payment history from Stripe"
|
|
9
|
-
|
|
9
|
+
parameter :customer_email, type: "string", description: "Customer email to look up billing info"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, customer_email:)
|
|
12
12
|
data_file = File.join(__dir__, "../data/stripe_billing.json")
|
|
@@ -6,8 +6,9 @@ module Copilot
|
|
|
6
6
|
# Tool for searching contacts to find patterns and related customers
|
|
7
7
|
class SearchContactsTool < Agents::Tool
|
|
8
8
|
description "Search contacts to find patterns, related customers, or specific profiles"
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
parameter :query, type: "string", description: "Search terms (name, email, company, or tags)"
|
|
10
|
+
parameter :plan, type: "string", description: "Filter by plan: Basic, Pro, or Enterprise",
|
|
11
|
+
required: false
|
|
11
12
|
|
|
12
13
|
def perform(_tool_context, query:, plan: nil)
|
|
13
14
|
data_file = File.join(__dir__, "../data/contacts.json")
|
|
@@ -6,8 +6,8 @@ module Copilot
|
|
|
6
6
|
# Tool for searching through conversation history to find similar cases
|
|
7
7
|
class SearchConversationsTool < Agents::Tool
|
|
8
8
|
description "Search past conversations for similar cases and resolutions"
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
parameter :query, type: "string", description: "Search terms (keywords, tags, or issue description)"
|
|
10
|
+
parameter :contact_id, type: "string", description: "Optional: limit search to specific contact", required: false
|
|
11
11
|
|
|
12
12
|
def perform(_tool_context, query:, contact_id: nil)
|
|
13
13
|
data_file = File.join(__dir__, "../data/conversations.json")
|
|
@@ -6,9 +6,10 @@ module Copilot
|
|
|
6
6
|
# Tool for searching knowledge base articles and documentation
|
|
7
7
|
class SearchKnowledgeBaseTool < Agents::Tool
|
|
8
8
|
description "Search help documentation and knowledge base for solutions"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
parameter :query, type: "string", description: "Search terms or keywords to find relevant articles"
|
|
10
|
+
parameter :category, type: "string",
|
|
11
|
+
description: "Filter by category: troubleshooting, account, development, or billing",
|
|
12
|
+
required: false
|
|
12
13
|
|
|
13
14
|
def perform(_tool_context, query:, category: nil)
|
|
14
15
|
data_file = File.join(__dir__, "../data/knowledge_base.json")
|
|
@@ -6,10 +6,11 @@ module Copilot
|
|
|
6
6
|
# Tool for searching Linear issues for development context and bug reports
|
|
7
7
|
class SearchLinearIssuesTool < Agents::Tool
|
|
8
8
|
description "Search Linear issues for bug reports, feature requests, and development context"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
parameter :query, type: "string", description: "Search issue keywords, errors, or feature descriptions"
|
|
10
|
+
parameter :status, type: "string", description: "Filter by status: backlog, in progress, completed, or resolved",
|
|
11
|
+
required: false
|
|
12
|
+
parameter :priority, type: "string", description: "Filter by priority: low, medium, or high",
|
|
13
|
+
required: false
|
|
13
14
|
|
|
14
15
|
def perform(_tool_context, query:, status: nil, priority: nil)
|
|
15
16
|
data_file = File.join(__dir__, "../data/linear_issues.json")
|
|
@@ -6,7 +6,7 @@ require_relative "tools/create_lead_tool"
|
|
|
6
6
|
require_relative "tools/create_checkout_tool"
|
|
7
7
|
require_relative "tools/search_docs_tool"
|
|
8
8
|
require_relative "tools/escalate_to_human_tool"
|
|
9
|
-
require "
|
|
9
|
+
require "schematist"
|
|
10
10
|
|
|
11
11
|
module ISPSupport
|
|
12
12
|
# Factory for creating all ISP support agents with proper handoff relationships.
|
|
@@ -98,7 +98,7 @@ module ISPSupport
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def triage_response_schema
|
|
101
|
-
|
|
101
|
+
Schematist::Schema.create do
|
|
102
102
|
string :response, description: "Your response to the customer"
|
|
103
103
|
string :intent, enum: %w[sales support unclear], description: "The detected intent category"
|
|
104
104
|
array :sentiment, description: "Customer sentiment indicators" do
|
|
@@ -108,7 +108,7 @@ module ISPSupport
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def support_response_schema
|
|
111
|
-
|
|
111
|
+
Schematist::Schema.create do
|
|
112
112
|
string :response, description: "Your response to the customer"
|
|
113
113
|
string :intent, enum: %w[support], description: "The intent category (always support)"
|
|
114
114
|
array :sentiment, description: "Customer sentiment indicators" do
|
|
@@ -118,7 +118,7 @@ module ISPSupport
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
def sales_response_schema
|
|
121
|
-
|
|
121
|
+
Schematist::Schema.create do
|
|
122
122
|
string :response, description: "Your response to the customer"
|
|
123
123
|
string :intent, enum: %w[sales], description: "The intent category (always sales)"
|
|
124
124
|
array :sentiment, description: "Customer sentiment indicators" do
|
|
@@ -6,7 +6,7 @@ module ISPSupport
|
|
|
6
6
|
# Tool for creating checkout links for new service subscriptions.
|
|
7
7
|
class CreateCheckoutTool < Agents::Tool
|
|
8
8
|
description "Create a secure checkout link for a service plan"
|
|
9
|
-
|
|
9
|
+
parameter :plan_name, type: "string", description: "Name of the plan to purchase"
|
|
10
10
|
|
|
11
11
|
def perform(_tool_context, plan_name:)
|
|
12
12
|
session_id = SecureRandom.hex(8)
|
|
@@ -4,9 +4,9 @@ module ISPSupport
|
|
|
4
4
|
# Tool for creating sales leads in the CRM system.
|
|
5
5
|
class CreateLeadTool < Agents::Tool
|
|
6
6
|
description "Create a new sales lead with customer information"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
parameter :name, type: "string", description: "Customer's full name"
|
|
8
|
+
parameter :email, type: "string", description: "Customer's email address"
|
|
9
|
+
parameter :desired_plan, type: "string", description: "Plan the customer is interested in"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, name:, email:, desired_plan:)
|
|
12
12
|
# Store lead information in state for follow-up
|
|
@@ -6,7 +6,7 @@ module ISPSupport
|
|
|
6
6
|
# Tool for looking up customer information from the CRM system.
|
|
7
7
|
class CrmLookupTool < Agents::Tool
|
|
8
8
|
description "Look up customer account information by account ID"
|
|
9
|
-
|
|
9
|
+
parameter :account_id, type: "string", description: "Customer account ID (e.g., CUST001)"
|
|
10
10
|
|
|
11
11
|
def perform(tool_context, account_id:)
|
|
12
12
|
data_file = File.join(__dir__, "../data/customers.json")
|
|
@@ -4,7 +4,7 @@ module ISPSupport
|
|
|
4
4
|
# Tool for searching the knowledge base documentation.
|
|
5
5
|
class SearchDocsTool < Agents::Tool
|
|
6
6
|
description "Search knowledge base for troubleshooting steps and solutions"
|
|
7
|
-
|
|
7
|
+
parameter :query, type: "string", description: "Search terms or description of the issue"
|
|
8
8
|
|
|
9
9
|
def perform(_tool_context, query:)
|
|
10
10
|
case query.downcase
|
data/lib/agents/agent.rb
CHANGED
|
@@ -50,8 +50,8 @@ require_relative "helpers/hash_normalizer"
|
|
|
50
50
|
# )
|
|
51
51
|
module Agents
|
|
52
52
|
class Agent
|
|
53
|
-
attr_reader :name, :instructions, :model, :provider, :assume_model_exists, :tools, :handoff_agents,
|
|
54
|
-
:response_schema, :headers, :params
|
|
53
|
+
attr_reader :name, :instructions, :model, :provider, :protocol, :assume_model_exists, :tools, :handoff_agents,
|
|
54
|
+
:temperature, :thinking, :response_schema, :headers, :params
|
|
55
55
|
|
|
56
56
|
# Initialize a new Agent instance
|
|
57
57
|
#
|
|
@@ -59,23 +59,32 @@ module Agents
|
|
|
59
59
|
# @param instructions [String, Proc, nil] Static string or dynamic Proc that returns instructions
|
|
60
60
|
# @param model [String] The LLM model to use (default: "gpt-4.1-mini")
|
|
61
61
|
# @param provider [Symbol, String, nil] Optional RubyLLM provider override
|
|
62
|
+
# @param protocol [Symbol, String, nil] Optional provider protocol override (e.g., :responses)
|
|
62
63
|
# @param assume_model_exists [Boolean] Whether RubyLLM should skip registry validation for custom model IDs
|
|
63
64
|
# @param tools [Array<Agents::Tool>] Array of tool instances the agent can use
|
|
64
65
|
# @param handoff_agents [Array<Agents::Agent>] Array of agents this agent can hand off to
|
|
65
|
-
# @param temperature [Float]
|
|
66
|
+
# @param temperature [Float, nil] Sampling temperature; nil uses the model default
|
|
67
|
+
# @param thinking [Hash, nil] RubyLLM thinking options (e.g., effort: :medium, display: :summarized)
|
|
66
68
|
# @param response_schema [Hash, nil] JSON schema for structured output responses
|
|
67
69
|
# @param headers [Hash, nil] Default HTTP headers applied to LLM requests
|
|
68
70
|
# @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,
|
|
70
|
-
tools: [], handoff_agents: [], temperature: 0.7,
|
|
71
|
+
def initialize(name:, instructions: nil, model: "gpt-4.1-mini", provider: nil, protocol: nil,
|
|
72
|
+
assume_model_exists: false, tools: [], handoff_agents: [], temperature: 0.7, thinking: nil,
|
|
73
|
+
response_schema: nil, headers: nil, params: nil)
|
|
71
74
|
@name = name
|
|
72
75
|
@instructions = instructions
|
|
73
76
|
@model = model
|
|
74
77
|
@provider = provider&.to_sym
|
|
78
|
+
@protocol = protocol&.to_sym
|
|
75
79
|
@assume_model_exists = assume_model_exists
|
|
76
80
|
@tools = tools.dup
|
|
77
81
|
@handoff_agents = []
|
|
78
82
|
@temperature = temperature
|
|
83
|
+
@thinking = if thinking.nil?
|
|
84
|
+
nil
|
|
85
|
+
else
|
|
86
|
+
Helpers::HashNormalizer.normalize(thinking, label: "thinking", freeze_result: true)
|
|
87
|
+
end
|
|
79
88
|
@response_schema = response_schema
|
|
80
89
|
@headers = Helpers::HashNormalizer.normalize(headers, label: "headers", freeze_result: true)
|
|
81
90
|
@params = Helpers::HashNormalizer.normalize(params, label: "params", freeze_result: true)
|
|
@@ -131,7 +140,7 @@ module Agents
|
|
|
131
140
|
|
|
132
141
|
# Creates a new agent instance with modified attributes while preserving immutability.
|
|
133
142
|
# The clone method is used when you need to create variations of agents without mutating the original.
|
|
134
|
-
# This can be used for runtime agent modifications,
|
|
143
|
+
# This can be used for runtime agent modifications, such as multi-tenant settings:
|
|
135
144
|
#
|
|
136
145
|
# @example Multi-tenant agent customization
|
|
137
146
|
# def agent_for_tenant(tenant)
|
|
@@ -161,10 +170,12 @@ module Agents
|
|
|
161
170
|
# @option changes [String, Proc] :instructions New instructions
|
|
162
171
|
# @option changes [String] :model New model identifier
|
|
163
172
|
# @option changes [Symbol, String, nil] :provider New provider override
|
|
173
|
+
# @option changes [Symbol, String, nil] :protocol New provider protocol override
|
|
164
174
|
# @option changes [Boolean] :assume_model_exists Whether to skip model registry validation
|
|
165
175
|
# @option changes [Array<Agents::Tool>] :tools New tools array (replaces all tools)
|
|
166
176
|
# @option changes [Array<Agents::Agent>] :handoff_agents New handoff agents
|
|
167
177
|
# @option changes [Float] :temperature Temperature for LLM responses (0.0-1.0)
|
|
178
|
+
# @option changes [Hash, nil] :thinking RubyLLM thinking options
|
|
168
179
|
# @option changes [Hash, nil] :response_schema JSON schema for structured output
|
|
169
180
|
# @return [Agents::Agent] A new frozen agent instance with the specified changes
|
|
170
181
|
def clone(**changes)
|
|
@@ -173,10 +184,12 @@ module Agents
|
|
|
173
184
|
instructions: changes.fetch(:instructions, @instructions),
|
|
174
185
|
model: changes.fetch(:model, @model),
|
|
175
186
|
provider: changes.fetch(:provider, @provider),
|
|
187
|
+
protocol: changes.fetch(:protocol, @protocol),
|
|
176
188
|
assume_model_exists: changes.fetch(:assume_model_exists, @assume_model_exists),
|
|
177
189
|
tools: changes.fetch(:tools, @tools.dup),
|
|
178
190
|
handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
|
|
179
191
|
temperature: changes.fetch(:temperature, @temperature),
|
|
192
|
+
thinking: changes.fetch(:thinking, @thinking),
|
|
180
193
|
response_schema: changes.fetch(:response_schema, @response_schema),
|
|
181
194
|
headers: changes.fetch(:headers, @headers),
|
|
182
195
|
params: changes.fetch(:params, @params)
|
data/lib/agents/agent_tool.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Agents
|
|
|
40
40
|
attr_reader :wrapped_agent, :tool_name, :tool_description, :output_extractor
|
|
41
41
|
|
|
42
42
|
# Default parameter for agent tools
|
|
43
|
-
|
|
43
|
+
parameter :input, type: "string", description: "Input message for the agent"
|
|
44
44
|
|
|
45
45
|
# Initialize an AgentTool that wraps an agent as a callable tool
|
|
46
46
|
#
|
data/lib/agents/handoff.rb
CHANGED
|
@@ -69,19 +69,16 @@ module Agents
|
|
|
69
69
|
@tool_description
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
#
|
|
72
|
+
# The runner switches agents after this tool records the handoff.
|
|
73
73
|
# Store handoff info in context for Runner to detect and process
|
|
74
74
|
def perform(tool_context)
|
|
75
75
|
# Store handoff information in context for Runner to detect
|
|
76
|
-
# TODO: The following is a race condition that needs to be addressed in future versions
|
|
77
|
-
# If multiple handoff tools execute concurrently, they overwrite each other's pending_handoff data.
|
|
78
76
|
tool_context.run_context.context[:pending_handoff] = {
|
|
79
77
|
target_agent: @target_agent,
|
|
80
78
|
timestamp: Time.now
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
halt("I'll transfer you to #{@target_agent.name} who can better assist you with this.")
|
|
81
|
+
"I'll transfer you to #{@target_agent.name} who can better assist you with this."
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
# NOTE: RubyLLM will handle schema generation internally when needed
|