ai-agents 0.2.2 → 0.4.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 +25 -0
- data/README.md +1 -0
- data/docs/_config.yml +8 -6
- data/docs/assets/images/ai-agents.png +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/concepts/agents.md +3 -1
- data/docs/concepts/context.md +2 -2
- data/docs/concepts/runner.md +1 -1
- data/docs/concepts/tools.md +19 -33
- data/docs/guides/multi-agent-systems.md +1 -1
- data/docs/guides/rails-integration.md +3 -3
- data/docs/guides/state-persistence.md +4 -6
- data/docs/guides/structured-output.md +76 -0
- data/docs/guides.md +2 -1
- data/examples/collaborative-copilot/agents/analysis_agent.rb +117 -8
- data/examples/collaborative-copilot/interactive.rb +48 -1
- data/examples/isp-support/agents_factory.rb +41 -3
- data/examples/isp-support/interactive.rb +18 -1
- data/lib/agents/agent.rb +29 -3
- data/lib/agents/chat.rb +8 -1
- data/lib/agents/runner.rb +3 -1
- data/lib/agents/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a2d71857e9614f2dad748a9510a5f29925cf070dd7a7dad59ac8a243eadbc28
|
4
|
+
data.tar.gz: e0f836ad647303fdd3482ca1f1d144bda34573d74ff8741487a4a0b550860f01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2728cc0f9e4b377c438ecfd73db1ff0e72e3399f8c5602c330b78fcfb53f081bbd80e44bb3036549f0b906b8df09da5ecff371af3b895f7b0e98645e74809967
|
7
|
+
data.tar.gz: c22baa1ce3c5aef5e1a2d17e208eb040f49edb46f4d275946e93fab506dcb3cc40eb1dfc9638f62beadec19a61421fe27d9fe079268b0b867b7c787a2c31cb19
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.4.0] - 2025-08-01
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **Structured Output Support**: Agents can now enforce JSON schema validation for responses
|
12
|
+
- Added `response_schema` parameter to `Agent#initialize` supporting both JSON Schema objects and `RubyLLM::Schema` classes
|
13
|
+
- Automatic response validation ensures agents return data in predictable formats
|
14
|
+
- Full support for complex nested schemas with objects, arrays, and validation constraints
|
15
|
+
- Multi-agent systems can use different response schemas per agent
|
16
|
+
- Comprehensive documentation with examples for data extraction and API integrations
|
17
|
+
- Updated to RubyLLM 1.5.0 with enhanced structured output capabilities
|
18
|
+
- New structured output documentation guide with practical examples
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- Enhanced `Chat` class to pass through `response_schema` configuration to underlying RubyLLM provider
|
22
|
+
- Improved agent cloning to preserve and override response schemas
|
23
|
+
|
24
|
+
## [0.3.0] - 2025-07-22
|
25
|
+
|
26
|
+
### Added
|
27
|
+
- Temperature control for agent responses
|
28
|
+
- Added `temperature` parameter to `Agent#initialize` with default value of 0.7
|
29
|
+
- Temperature controls randomness in LLM responses (0.0 = deterministic, 1.0 = very random)
|
30
|
+
- Temperature is passed through to underlying Chat instance for model configuration
|
31
|
+
- Agent cloning supports temperature overrides via `clone(temperature: value)`
|
32
|
+
|
8
33
|
## [0.2.2] - 2025-07-14
|
9
34
|
|
10
35
|
### Added
|
data/README.md
CHANGED
@@ -18,6 +18,7 @@ A delightful provider agnostic Ruby SDK for building multi-agent AI workflows wi
|
|
18
18
|
- **🤖 Multi-Agent Orchestration**: Create specialized AI agents that work together
|
19
19
|
- **🔄 Seamless Handoffs**: Transparent agent-to-agent transfers (users never know!)
|
20
20
|
- **🛠️ Tool Integration**: Agents can use custom tools and functions
|
21
|
+
- **📊 Structured Output**: JSON schema-validated responses for reliable data extraction
|
21
22
|
- **💾 Shared Context**: State management across agent interactions
|
22
23
|
- **🔌 Provider Agnostic**: Works with OpenAI, Anthropic, and other LLM providers
|
23
24
|
|
data/docs/_config.yml
CHANGED
@@ -2,6 +2,8 @@ title: AI Agents
|
|
2
2
|
description: A Ruby SDK for building multi-agent AI workflows
|
3
3
|
baseurl: ""
|
4
4
|
url: ""
|
5
|
+
logo: "/assets/images/ai-agents.png"
|
6
|
+
favicon_ico: "/assets/images/favicon.png"
|
5
7
|
|
6
8
|
# Theme
|
7
9
|
theme: just-the-docs
|
@@ -12,19 +14,19 @@ color_scheme: ruby
|
|
12
14
|
# Search
|
13
15
|
search_enabled: true
|
14
16
|
search:
|
15
|
-
heading_level:
|
16
|
-
previews: 3
|
17
|
-
preview_words_before: 5
|
18
|
-
preview_words_after: 10
|
19
|
-
tokenizer_separator: /[\s/]+/
|
17
|
+
heading_level: 3
|
20
18
|
rel_url: true
|
21
|
-
|
19
|
+
focus_shortcut_key: "k"
|
22
20
|
|
23
21
|
# Navigation
|
24
22
|
nav_sort: case_insensitive
|
25
23
|
nav_external_links:
|
26
24
|
- title: GitHub
|
27
25
|
url: https://github.com/chatwoot/ai-agents
|
26
|
+
- title: RubyLLM
|
27
|
+
url: https://rubyllm.com
|
28
|
+
- title: Chatwoot
|
29
|
+
url: https://chatwoot.com/
|
28
30
|
|
29
31
|
# Footer
|
30
32
|
footer_content: "Copyright © 2025 Chatwoot Inc."
|
Binary file
|
Binary file
|
data/docs/concepts/agents.md
CHANGED
@@ -21,6 +21,7 @@ This project is in early development. While thread safety is a core design goal
|
|
21
21
|
* **`model`**: The language model the agent will use (e.g., `"gpt-4.1-mini"`).
|
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
|
+
* **`temperature`**: Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
|
24
25
|
|
25
26
|
### Example
|
26
27
|
|
@@ -30,7 +31,8 @@ assistant_agent = Agents::Agent.new(
|
|
30
31
|
name: "Assistant",
|
31
32
|
instructions: "You are a helpful assistant.",
|
32
33
|
model: "gpt-4.1-mini",
|
33
|
-
tools: [CalculatorTool.new]
|
34
|
+
tools: [CalculatorTool.new],
|
35
|
+
temperature: 0.3
|
34
36
|
)
|
35
37
|
|
36
38
|
# Create a specialized agent by cloning the base agent
|
data/docs/concepts/context.md
CHANGED
@@ -19,7 +19,7 @@ The main context hash that persists across sessions:
|
|
19
19
|
context = {
|
20
20
|
user_id: 123,
|
21
21
|
conversation_history: [...],
|
22
|
-
|
22
|
+
current_agent: "Billing",
|
23
23
|
state: { customer_tier: "premium" } # Tools use this nested hash for persistent data
|
24
24
|
}
|
25
25
|
```
|
@@ -69,7 +69,7 @@ runner = Agents::Runner.with_agents(triage_agent, billing_agent)
|
|
69
69
|
# First interaction
|
70
70
|
result1 = runner.run("I need billing help")
|
71
71
|
# Triage agent hands off to billing agent
|
72
|
-
# Context includes:
|
72
|
+
# Context includes: current_agent: "Billing"
|
73
73
|
|
74
74
|
# Continue conversation
|
75
75
|
result2 = runner.run("What payment methods do you accept?", context: result1.context)
|
data/docs/concepts/runner.md
CHANGED
@@ -84,4 +84,4 @@ result1 = runner.run("I need help with my bill")
|
|
84
84
|
result2 = runner.run("What payment methods do you accept?", context: result1.context)
|
85
85
|
```
|
86
86
|
|
87
|
-
The `run` method returns a `RunResult` with output,
|
87
|
+
The `run` method returns a `RunResult` with output, messages, usage metrics, error status, and updated context.
|
data/docs/concepts/tools.md
CHANGED
@@ -27,36 +27,22 @@ By passing all the necessary data through the `ToolContext`, we ensure that tool
|
|
27
27
|
|
28
28
|
## Creating a Tool
|
29
29
|
|
30
|
-
You
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
2. **Using the Functional Tool Definition:** For simpler tools, you can use the `Agents::Tool.tool` helper to define a tool with a block.
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
calculator_tool = Agents::Tool.tool(
|
54
|
-
"calculate",
|
55
|
-
description: "Perform a mathematical calculation."
|
56
|
-
) do |tool_context, expression:|
|
57
|
-
# Perform the calculation and return the result
|
58
|
-
eval(expression).to_s
|
59
|
-
end
|
60
|
-
```
|
61
|
-
|
62
|
-
In both cases, the `perform` method receives the `tool_context` and the tool's parameters as arguments. This design ensures that your tools are always thread-safe and easy to test.
|
30
|
+
You create tools by creating a class that inherits from `Agents::Tool` and implements the `perform` method:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class WeatherTool < Agents::Tool
|
34
|
+
name "get_weather"
|
35
|
+
description "Get the current weather for a location."
|
36
|
+
param :location, type: "string", desc: "The city and state, e.g., San Francisco, CA"
|
37
|
+
|
38
|
+
def perform(tool_context, location:)
|
39
|
+
# Access the API key from the shared context
|
40
|
+
api_key = tool_context.context[:weather_api_key]
|
41
|
+
|
42
|
+
# Call the weather API and return the result
|
43
|
+
WeatherApi.get(location, api_key)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
The `perform` method receives the `tool_context` and the tool's parameters as arguments. This design ensures that your tools are always thread-safe and easy to test.
|
@@ -210,7 +210,7 @@ RSpec.describe "Customer Support Workflow" do
|
|
210
210
|
result = runner.run("I have a billing question")
|
211
211
|
|
212
212
|
# Verify handoff occurred
|
213
|
-
expect(result.context
|
213
|
+
expect(result.context[:current_agent]).to eq("Billing")
|
214
214
|
|
215
215
|
# Test continued conversation
|
216
216
|
followup = runner.run("What are your payment terms?", context: result.context)
|
@@ -85,7 +85,7 @@ class Conversation < ApplicationRecord
|
|
85
85
|
create!(
|
86
86
|
user: user,
|
87
87
|
context: result.context.to_h,
|
88
|
-
current_agent: result.context
|
88
|
+
current_agent: result.context[:current_agent]
|
89
89
|
)
|
90
90
|
end
|
91
91
|
end
|
@@ -209,7 +209,7 @@ class AgentConversationsController < ApplicationController
|
|
209
209
|
|
210
210
|
render json: {
|
211
211
|
response: result.output,
|
212
|
-
agent: result.context
|
212
|
+
agent: result.context[:current_agent],
|
213
213
|
conversation_id: result.context[:conversation_id]
|
214
214
|
}
|
215
215
|
rescue => e
|
@@ -313,7 +313,7 @@ class AgentConversationJob < ApplicationJob
|
|
313
313
|
"agent_conversation_#{user_id}",
|
314
314
|
{
|
315
315
|
response: result.output,
|
316
|
-
agent: result.context
|
316
|
+
agent: result.context[:current_agent],
|
317
317
|
conversation_id: conversation_id
|
318
318
|
}
|
319
319
|
)
|
@@ -183,7 +183,7 @@ class ContextCleaner
|
|
183
183
|
# Limit total context size
|
184
184
|
if cleaned.keys.size > MAX_CONTEXT_KEYS
|
185
185
|
# Keep essential keys, remove extras
|
186
|
-
essential_keys = [:user_id, :
|
186
|
+
essential_keys = [:user_id, :current_agent, :conversation_history]
|
187
187
|
extra_keys = cleaned.keys - essential_keys
|
188
188
|
extra_keys.first(cleaned.keys.size - MAX_CONTEXT_KEYS).each do |key|
|
189
189
|
cleaned.delete(key)
|
@@ -292,7 +292,7 @@ class VersionedContext
|
|
292
292
|
{
|
293
293
|
version: index,
|
294
294
|
timestamp: context[:updated_at],
|
295
|
-
agent: context[:
|
295
|
+
agent: context[:current_agent]
|
296
296
|
}
|
297
297
|
end
|
298
298
|
end
|
@@ -409,12 +409,10 @@ class ContextMigrator
|
|
409
409
|
private
|
410
410
|
|
411
411
|
def self.migrate_v1_to_v2(context)
|
412
|
-
# V1 -> V2: Rename 'current_agent' to '
|
412
|
+
# V1 -> V2: Rename 'current_agent' to 'current_agent' (no change needed)
|
413
413
|
migrated = context.deep_dup
|
414
414
|
|
415
|
-
|
416
|
-
migrated[:current_agent_name] = migrated.delete(:current_agent)
|
417
|
-
end
|
415
|
+
# No migration needed - current_agent is already the correct field name
|
418
416
|
|
419
417
|
migrated[:_version] = 2
|
420
418
|
migrated
|
@@ -0,0 +1,76 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Structured Output
|
4
|
+
parent: Guides
|
5
|
+
nav_order: 5
|
6
|
+
---
|
7
|
+
|
8
|
+
# Structured Output
|
9
|
+
|
10
|
+
Structured output ensures AI agents return responses in a predictable JSON format that conforms to a specified schema. This is useful for building reliable integrations, data extraction workflows, and applications that need consistent response formats.
|
11
|
+
|
12
|
+
## Basic Usage
|
13
|
+
|
14
|
+
Add a `response_schema` when creating an agent to enforce structured responses:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# JSON Schema approach
|
18
|
+
extraction_agent = Agents::Agent.new(
|
19
|
+
name: "DataExtractor",
|
20
|
+
instructions: "Extract key information from user messages",
|
21
|
+
response_schema: {
|
22
|
+
type: 'object',
|
23
|
+
properties: {
|
24
|
+
entities: { type: 'array', items: { type: 'string' } },
|
25
|
+
sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
|
26
|
+
summary: { type: 'string' }
|
27
|
+
},
|
28
|
+
required: ['entities', 'sentiment'],
|
29
|
+
additionalProperties: false
|
30
|
+
}
|
31
|
+
)
|
32
|
+
|
33
|
+
runner = Agents::AgentRunner.with_agents(extraction_agent)
|
34
|
+
result = runner.run("I love the new product features, especially the API and dashboard!")
|
35
|
+
|
36
|
+
# Response will be valid JSON matching the schema:
|
37
|
+
# {
|
38
|
+
# "entities": ["API", "dashboard"],
|
39
|
+
# "sentiment": "positive",
|
40
|
+
# "summary": "User expresses enthusiasm for new product features"
|
41
|
+
# }
|
42
|
+
```
|
43
|
+
|
44
|
+
## RubyLLM::Schema (Recommended)
|
45
|
+
|
46
|
+
For more complex schemas, use `RubyLLM::Schema` which provides a cleaner Ruby DSL:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class ContactSchema < RubyLLM::Schema
|
50
|
+
string :name, description: "Full name of the person"
|
51
|
+
string :email, description: "Email address"
|
52
|
+
string :phone, description: "Phone number", required: false
|
53
|
+
string :company, description: "Company name", required: false
|
54
|
+
array :interests, description: "List of interests or topics mentioned" do
|
55
|
+
string description: "Individual interest or topic"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
contact_agent = Agents::Agent.new(
|
60
|
+
name: "ContactExtractor",
|
61
|
+
instructions: "Extract contact information from business communications",
|
62
|
+
response_schema: ContactSchema
|
63
|
+
)
|
64
|
+
|
65
|
+
runner = Agents::AgentRunner.with_agents(contact_agent)
|
66
|
+
result = runner.run("Hi, I'm Sarah Johnson from TechCorp. You can reach me at sarah@techcorp.com or 555-0123. I'm interested in AI and automation solutions.")
|
67
|
+
|
68
|
+
# Returns structured contact data:
|
69
|
+
# {
|
70
|
+
# "name": "Sarah Johnson",
|
71
|
+
# "email": "sarah@techcorp.com",
|
72
|
+
# "phone": "555-0123",
|
73
|
+
# "company": "TechCorp",
|
74
|
+
# "interests": ["AI", "automation solutions"]
|
75
|
+
# }
|
76
|
+
```
|
data/docs/guides.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
layout: default
|
3
3
|
title: Guides
|
4
4
|
nav_order: 3
|
5
|
-
published:
|
5
|
+
published: true
|
6
6
|
has_children: true
|
7
7
|
---
|
8
8
|
|
@@ -16,3 +16,4 @@ Practical guides for building real-world applications with the AI Agents library
|
|
16
16
|
- **[Agent-as-Tool Pattern](guides/agent-as-tool-pattern.html)** - Enable agent collaboration behind the scenes without conversation handoffs
|
17
17
|
- **[Rails Integration](guides/rails-integration.html)** - Integrating agents with Ruby on Rails applications and ActiveRecord persistence
|
18
18
|
- **[State Persistence](guides/state-persistence.html)** - Managing conversation state and context across sessions and processes
|
19
|
+
- **[Structured Output](guides/structured-output.html)** - Enforcing JSON schema validation for reliable agent responses
|
@@ -11,7 +11,8 @@ module Copilot
|
|
11
11
|
model: "gpt-4o-mini",
|
12
12
|
tools: [
|
13
13
|
GetConversationTool.new
|
14
|
-
]
|
14
|
+
],
|
15
|
+
response_schema: analysis_response_schema
|
15
16
|
)
|
16
17
|
end
|
17
18
|
|
@@ -34,15 +35,123 @@ module Copilot
|
|
34
35
|
3. Assess how well the conversation is progressing
|
35
36
|
4. Identify any escalation risks or satisfaction issues
|
36
37
|
|
37
|
-
|
38
|
-
- **Conversation Health**: Overall assessment of how the conversation is going
|
39
|
-
- **Customer Sentiment**: Current emotional state and any changes over time
|
40
|
-
- **Communication Quality**: How well the agent is handling the situation
|
41
|
-
- **Risk Assessment**: Any signs of escalation or dissatisfaction
|
42
|
-
- **Tone Recommendations**: Suggested communication approach and tone
|
43
|
-
|
38
|
+
Your response MUST be in the required JSON format with all the specified fields.
|
44
39
|
Focus on practical communication advice that will improve the interaction.
|
45
40
|
INSTRUCTIONS
|
46
41
|
end
|
42
|
+
|
43
|
+
def self.analysis_response_schema
|
44
|
+
{
|
45
|
+
type: "object",
|
46
|
+
properties: {
|
47
|
+
conversation_health: {
|
48
|
+
type: "object",
|
49
|
+
properties: {
|
50
|
+
status: {
|
51
|
+
type: "string",
|
52
|
+
enum: %w[excellent good fair concerning critical],
|
53
|
+
description: "Overall health status of the conversation"
|
54
|
+
},
|
55
|
+
summary: {
|
56
|
+
type: "string",
|
57
|
+
description: "Brief assessment of how the conversation is progressing"
|
58
|
+
}
|
59
|
+
},
|
60
|
+
required: %w[status summary]
|
61
|
+
},
|
62
|
+
customer_sentiment: {
|
63
|
+
type: "object",
|
64
|
+
properties: {
|
65
|
+
current: {
|
66
|
+
type: "string",
|
67
|
+
enum: %w[very_positive positive neutral negative very_negative],
|
68
|
+
description: "Current emotional state"
|
69
|
+
},
|
70
|
+
trajectory: {
|
71
|
+
type: "string",
|
72
|
+
enum: %w[improving stable declining],
|
73
|
+
description: "How sentiment is changing over time"
|
74
|
+
},
|
75
|
+
key_emotions: {
|
76
|
+
type: "array",
|
77
|
+
items: { type: "string" },
|
78
|
+
description: "Dominant emotions detected (e.g., frustrated, satisfied, confused)"
|
79
|
+
}
|
80
|
+
},
|
81
|
+
required: %w[current trajectory key_emotions]
|
82
|
+
},
|
83
|
+
communication_quality: {
|
84
|
+
type: "object",
|
85
|
+
properties: {
|
86
|
+
score: {
|
87
|
+
type: "integer",
|
88
|
+
minimum: 1,
|
89
|
+
maximum: 10,
|
90
|
+
description: "Overall communication quality score"
|
91
|
+
},
|
92
|
+
strengths: {
|
93
|
+
type: "array",
|
94
|
+
items: { type: "string" },
|
95
|
+
description: "What the agent is doing well"
|
96
|
+
},
|
97
|
+
areas_for_improvement: {
|
98
|
+
type: "array",
|
99
|
+
items: { type: "string" },
|
100
|
+
description: "Areas that could be improved"
|
101
|
+
}
|
102
|
+
},
|
103
|
+
required: %w[score strengths areas_for_improvement]
|
104
|
+
},
|
105
|
+
risk_assessment: {
|
106
|
+
type: "object",
|
107
|
+
properties: {
|
108
|
+
escalation_risk: {
|
109
|
+
type: "string",
|
110
|
+
enum: %w[none low medium high],
|
111
|
+
description: "Risk of conversation escalating"
|
112
|
+
},
|
113
|
+
churn_risk: {
|
114
|
+
type: "string",
|
115
|
+
enum: %w[none low medium high],
|
116
|
+
description: "Risk of customer churning"
|
117
|
+
},
|
118
|
+
warning_signs: {
|
119
|
+
type: "array",
|
120
|
+
items: { type: "string" },
|
121
|
+
description: "Specific warning signs detected"
|
122
|
+
}
|
123
|
+
},
|
124
|
+
required: %w[escalation_risk churn_risk warning_signs]
|
125
|
+
},
|
126
|
+
tone_recommendations: {
|
127
|
+
type: "object",
|
128
|
+
properties: {
|
129
|
+
recommended_tone: {
|
130
|
+
type: "string",
|
131
|
+
description: "Suggested overall tone (e.g., empathetic, professional, friendly)"
|
132
|
+
},
|
133
|
+
key_phrases: {
|
134
|
+
type: "array",
|
135
|
+
items: { type: "string" },
|
136
|
+
description: "Suggested phrases to use"
|
137
|
+
},
|
138
|
+
avoid_phrases: {
|
139
|
+
type: "array",
|
140
|
+
items: { type: "string" },
|
141
|
+
description: "Phrases or approaches to avoid"
|
142
|
+
},
|
143
|
+
next_steps: {
|
144
|
+
type: "string",
|
145
|
+
description: "Recommended immediate next action"
|
146
|
+
}
|
147
|
+
},
|
148
|
+
required: %w[recommended_tone key_phrases avoid_phrases next_steps]
|
149
|
+
}
|
150
|
+
},
|
151
|
+
required: %w[conversation_health customer_sentiment communication_quality risk_assessment
|
152
|
+
tone_recommendations],
|
153
|
+
additionalProperties: false
|
154
|
+
}
|
155
|
+
end
|
47
156
|
end
|
48
157
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "json"
|
4
5
|
require_relative "../../lib/agents"
|
5
6
|
require_relative "agents/copilot_orchestrator"
|
6
7
|
|
@@ -80,7 +81,53 @@ loop do
|
|
80
81
|
# Update context with the returned context from Runner
|
81
82
|
context = result.context if result.respond_to?(:context) && result.context
|
82
83
|
|
83
|
-
|
84
|
+
output = result.output
|
85
|
+
|
86
|
+
# Check if the output might be structured JSON (e.g., from Analysis Agent)
|
87
|
+
if output&.strip&.start_with?("{") && context[:current_agent] == "Analysis Agent"
|
88
|
+
begin
|
89
|
+
analysis = JSON.parse(output)
|
90
|
+
|
91
|
+
# Display structured analysis in a readable format
|
92
|
+
puts "\n📊 Conversation Analysis:"
|
93
|
+
puts "━" * 60
|
94
|
+
|
95
|
+
# Conversation Health
|
96
|
+
health = analysis["conversation_health"]
|
97
|
+
puts "🏥 Health: #{health["status"].upcase} - #{health["summary"]}"
|
98
|
+
|
99
|
+
# Customer Sentiment
|
100
|
+
sentiment = analysis["customer_sentiment"]
|
101
|
+
puts "😊 Sentiment: #{sentiment["current"]} (#{sentiment["trajectory"]})"
|
102
|
+
puts " Emotions: #{sentiment["key_emotions"].join(", ")}"
|
103
|
+
|
104
|
+
# Communication Quality
|
105
|
+
quality = analysis["communication_quality"]
|
106
|
+
puts "💬 Communication: #{quality["score"]}/10"
|
107
|
+
puts " ✅ Strengths: #{quality["strengths"].join(", ")}" if quality["strengths"].any?
|
108
|
+
puts " ⚠️ Improve: #{quality["areas_for_improvement"].join(", ")}" if quality["areas_for_improvement"].any?
|
109
|
+
|
110
|
+
# Risk Assessment
|
111
|
+
risk = analysis["risk_assessment"]
|
112
|
+
puts "⚠️ Risks: Escalation=#{risk["escalation_risk"]}, Churn=#{risk["churn_risk"]}"
|
113
|
+
puts " Warning signs: #{risk["warning_signs"].join(", ")}" if risk["warning_signs"].any?
|
114
|
+
|
115
|
+
# Tone Recommendations
|
116
|
+
tone = analysis["tone_recommendations"]
|
117
|
+
puts "\n💡 Recommendations:"
|
118
|
+
puts " Tone: #{tone["recommended_tone"]}"
|
119
|
+
puts " Next: #{tone["next_steps"]}"
|
120
|
+
puts " Use: #{tone["key_phrases"].join("; ")}" if tone["key_phrases"].any?
|
121
|
+
puts " Avoid: #{tone["avoid_phrases"].join("; ")}" if tone["avoid_phrases"].any?
|
122
|
+
|
123
|
+
puts "━" * 60
|
124
|
+
rescue JSON::ParserError
|
125
|
+
# Fall back to regular output if not valid JSON
|
126
|
+
puts output
|
127
|
+
end
|
128
|
+
else
|
129
|
+
puts output
|
130
|
+
end
|
84
131
|
rescue StandardError => e
|
85
132
|
puts "Error: #{e.message}"
|
86
133
|
end
|
@@ -44,7 +44,9 @@ module ISPSupport
|
|
44
44
|
name: "Triage Agent",
|
45
45
|
instructions: triage_instructions,
|
46
46
|
model: "gpt-4.1-mini",
|
47
|
-
tools: []
|
47
|
+
tools: [],
|
48
|
+
temperature: 0.3, # Lower temperature for consistent routing decisions
|
49
|
+
response_schema: triage_response_schema
|
48
50
|
)
|
49
51
|
end
|
50
52
|
|
@@ -53,7 +55,8 @@ module ISPSupport
|
|
53
55
|
name: "Sales Agent",
|
54
56
|
instructions: sales_instructions_with_state,
|
55
57
|
model: "gpt-4.1-mini",
|
56
|
-
tools: [ISPSupport::CreateLeadTool.new, ISPSupport::CreateCheckoutTool.new]
|
58
|
+
tools: [ISPSupport::CreateLeadTool.new, ISPSupport::CreateCheckoutTool.new],
|
59
|
+
temperature: 0.8 # Higher temperature for more persuasive, varied sales language
|
57
60
|
)
|
58
61
|
end
|
59
62
|
|
@@ -66,7 +69,8 @@ module ISPSupport
|
|
66
69
|
ISPSupport::CrmLookupTool.new,
|
67
70
|
ISPSupport::SearchDocsTool.new,
|
68
71
|
ISPSupport::EscalateToHumanTool.new
|
69
|
-
]
|
72
|
+
],
|
73
|
+
temperature: 0.5 # Balanced temperature for helpful but consistent technical support
|
70
74
|
)
|
71
75
|
end
|
72
76
|
|
@@ -85,9 +89,43 @@ module ISPSupport
|
|
85
89
|
- If unclear, ask one clarifying question before routing
|
86
90
|
|
87
91
|
Keep responses brief and professional. Use handoff tools to transfer to specialists.
|
92
|
+
|
93
|
+
Your response MUST be in the required JSON format with greeting, intent_category, needs_clarification, clarifying_question, and recommended_agent fields.
|
88
94
|
INSTRUCTIONS
|
89
95
|
end
|
90
96
|
|
97
|
+
def triage_response_schema
|
98
|
+
{
|
99
|
+
type: "object",
|
100
|
+
properties: {
|
101
|
+
greeting: {
|
102
|
+
type: "string",
|
103
|
+
description: "A brief, friendly greeting acknowledging the customer's inquiry"
|
104
|
+
},
|
105
|
+
intent_category: {
|
106
|
+
type: "string",
|
107
|
+
enum: %w[sales support unclear],
|
108
|
+
description: "The detected category of the customer's intent"
|
109
|
+
},
|
110
|
+
needs_clarification: {
|
111
|
+
type: "boolean",
|
112
|
+
description: "Whether the intent is unclear and needs clarification"
|
113
|
+
},
|
114
|
+
clarifying_question: {
|
115
|
+
type: ["string", "null"],
|
116
|
+
description: "A question to ask if the intent is unclear (null if clear)"
|
117
|
+
},
|
118
|
+
recommended_agent: {
|
119
|
+
type: ["string", "null"],
|
120
|
+
enum: ["Sales Agent", "Support Agent", null],
|
121
|
+
description: "The recommended specialist agent to route to (null if unclear)"
|
122
|
+
}
|
123
|
+
},
|
124
|
+
required: %w[greeting intent_category needs_clarification],
|
125
|
+
additionalProperties: false
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
91
129
|
def sales_instructions
|
92
130
|
<<~INSTRUCTIONS
|
93
131
|
You are the Sales Agent for an ISP. You handle new customer acquisition, service upgrades,
|
@@ -55,7 +55,24 @@ class ISPSupportDemo
|
|
55
55
|
|
56
56
|
# Clear status and show response
|
57
57
|
clear_status_line
|
58
|
-
|
58
|
+
|
59
|
+
# Handle structured output from triage agent
|
60
|
+
output = result.output || "[No output]"
|
61
|
+
if @context[:current_agent] == "Triage Agent" && output.start_with?("{")
|
62
|
+
begin
|
63
|
+
structured = JSON.parse(output)
|
64
|
+
# Display the greeting from structured response
|
65
|
+
puts "🤖 #{structured["greeting"]}"
|
66
|
+
if structured["intent_category"]
|
67
|
+
puts " [Intent: #{structured["intent_category"]}, Routing to: #{structured["recommended_agent"] || "TBD"}]"
|
68
|
+
end
|
69
|
+
rescue JSON::ParserError
|
70
|
+
# Fall back to regular output if not valid JSON
|
71
|
+
puts "🤖 #{output}"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
puts "🤖 #{output}"
|
75
|
+
end
|
59
76
|
|
60
77
|
puts
|
61
78
|
end
|
data/lib/agents/agent.rb
CHANGED
@@ -13,6 +13,23 @@
|
|
13
13
|
# tools: [calculator_tool, weather_tool]
|
14
14
|
# )
|
15
15
|
#
|
16
|
+
# @example Creating an agent with structured output
|
17
|
+
# agent = Agents::Agent.new(
|
18
|
+
# name: "DataExtractor",
|
19
|
+
# instructions: "Extract structured information from user input",
|
20
|
+
# model: "gpt-4o",
|
21
|
+
# response_schema: {
|
22
|
+
# type: 'object',
|
23
|
+
# properties: {
|
24
|
+
# entities: { type: 'array', items: { type: 'string' } },
|
25
|
+
# sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
|
26
|
+
# summary: { type: 'string' }
|
27
|
+
# },
|
28
|
+
# required: ['entities', 'sentiment'],
|
29
|
+
# additionalProperties: false
|
30
|
+
# }
|
31
|
+
# )
|
32
|
+
#
|
16
33
|
# @example Creating an agent with dynamic state-aware instructions
|
17
34
|
# agent = Agents::Agent.new(
|
18
35
|
# name: "Support Agent",
|
@@ -33,7 +50,7 @@
|
|
33
50
|
# )
|
34
51
|
module Agents
|
35
52
|
class Agent
|
36
|
-
attr_reader :name, :instructions, :model, :tools, :handoff_agents
|
53
|
+
attr_reader :name, :instructions, :model, :tools, :handoff_agents, :temperature, :response_schema
|
37
54
|
|
38
55
|
# Initialize a new Agent instance
|
39
56
|
#
|
@@ -42,12 +59,17 @@ module Agents
|
|
42
59
|
# @param model [String] The LLM model to use (default: "gpt-4.1-mini")
|
43
60
|
# @param tools [Array<Agents::Tool>] Array of tool instances the agent can use
|
44
61
|
# @param handoff_agents [Array<Agents::Agent>] Array of agents this agent can hand off to
|
45
|
-
|
62
|
+
# @param temperature [Float] Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)
|
63
|
+
# @param response_schema [Hash, nil] JSON schema for structured output responses
|
64
|
+
def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7,
|
65
|
+
response_schema: nil)
|
46
66
|
@name = name
|
47
67
|
@instructions = instructions
|
48
68
|
@model = model
|
49
69
|
@tools = tools.dup
|
50
70
|
@handoff_agents = []
|
71
|
+
@temperature = temperature
|
72
|
+
@response_schema = response_schema
|
51
73
|
|
52
74
|
# Mutex for thread-safe handoff registration
|
53
75
|
# While agents are typically configured at startup, we want to ensure
|
@@ -131,6 +153,8 @@ module Agents
|
|
131
153
|
# @option changes [String] :model New model identifier
|
132
154
|
# @option changes [Array<Agents::Tool>] :tools New tools array (replaces all tools)
|
133
155
|
# @option changes [Array<Agents::Agent>] :handoff_agents New handoff agents
|
156
|
+
# @option changes [Float] :temperature Temperature for LLM responses (0.0-1.0)
|
157
|
+
# @option changes [Hash, nil] :response_schema JSON schema for structured output
|
134
158
|
# @return [Agents::Agent] A new frozen agent instance with the specified changes
|
135
159
|
def clone(**changes)
|
136
160
|
self.class.new(
|
@@ -138,7 +162,9 @@ module Agents
|
|
138
162
|
instructions: changes.fetch(:instructions, @instructions),
|
139
163
|
model: changes.fetch(:model, @model),
|
140
164
|
tools: changes.fetch(:tools, @tools.dup),
|
141
|
-
handoff_agents: changes.fetch(:handoff_agents, @handoff_agents)
|
165
|
+
handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
|
166
|
+
temperature: changes.fetch(:temperature, @temperature),
|
167
|
+
response_schema: changes.fetch(:response_schema, @response_schema)
|
142
168
|
)
|
143
169
|
end
|
144
170
|
|
data/lib/agents/chat.rb
CHANGED
@@ -26,11 +26,18 @@ module Agents
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
def initialize(model: nil, handoff_tools: [], context_wrapper: nil,
|
29
|
+
def initialize(model: nil, handoff_tools: [], context_wrapper: nil, temperature: nil, response_schema: nil,
|
30
|
+
**options)
|
30
31
|
super(model: model, **options)
|
31
32
|
@handoff_tools = handoff_tools
|
32
33
|
@context_wrapper = context_wrapper
|
33
34
|
|
35
|
+
# Set temperature if provided (RubyLLM::Chat sets this via accessor)
|
36
|
+
@temperature = temperature if temperature
|
37
|
+
|
38
|
+
# Set response schema if provided
|
39
|
+
with_schema(response_schema) if response_schema
|
40
|
+
|
34
41
|
# Register handoff tools with RubyLLM for schema generation
|
35
42
|
@handoff_tools.each { |tool| with_tool(tool) }
|
36
43
|
end
|
data/lib/agents/runner.rb
CHANGED
@@ -246,8 +246,10 @@ module Agents
|
|
246
246
|
# Create extended chat with handoff awareness and context
|
247
247
|
chat = Agents::Chat.new(
|
248
248
|
model: agent.model,
|
249
|
+
temperature: agent.temperature,
|
249
250
|
handoff_tools: handoff_tools, # Direct tools, no wrapper
|
250
|
-
context_wrapper: context_wrapper
|
251
|
+
context_wrapper: context_wrapper, # Pass context directly
|
252
|
+
response_schema: agent.response_schema # Pass structured output schema
|
251
253
|
)
|
252
254
|
|
253
255
|
chat.with_instructions(system_prompt) if system_prompt
|
data/lib/agents/version.rb
CHANGED
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shivam Mishra
|
@@ -46,6 +46,8 @@ files:
|
|
46
46
|
- docs/_sass/custom/custom.scss
|
47
47
|
- docs/architecture.md
|
48
48
|
- docs/assets/fonts/InterVariable.woff2
|
49
|
+
- docs/assets/images/ai-agents.png
|
50
|
+
- docs/assets/images/favicon.png
|
49
51
|
- docs/concepts.md
|
50
52
|
- docs/concepts/agent-tool.md
|
51
53
|
- docs/concepts/agents.md
|
@@ -59,6 +61,7 @@ files:
|
|
59
61
|
- docs/guides/multi-agent-systems.md
|
60
62
|
- docs/guides/rails-integration.md
|
61
63
|
- docs/guides/state-persistence.md
|
64
|
+
- docs/guides/structured-output.md
|
62
65
|
- docs/index.md
|
63
66
|
- examples/README.md
|
64
67
|
- examples/collaborative-copilot/README.md
|