riffer 0.7.0 → 0.8.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/.agents/architecture.md +113 -0
- data/.agents/code-style.md +42 -0
- data/.agents/providers.md +46 -0
- data/.agents/rdoc.md +51 -0
- data/.agents/testing.md +56 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +21 -308
- data/CHANGELOG.md +10 -0
- data/README.md +21 -112
- data/Rakefile +1 -1
- data/docs/01_OVERVIEW.md +106 -0
- data/docs/02_GETTING_STARTED.md +128 -0
- data/docs/03_AGENTS.md +226 -0
- data/docs/04_TOOLS.md +251 -0
- data/docs/05_MESSAGES.md +173 -0
- data/docs/06_STREAM_EVENTS.md +191 -0
- data/docs/07_CONFIGURATION.md +195 -0
- data/docs_providers/01_PROVIDERS.md +168 -0
- data/docs_providers/02_AMAZON_BEDROCK.md +196 -0
- data/docs_providers/03_ANTHROPIC.md +211 -0
- data/docs_providers/04_OPENAI.md +157 -0
- data/docs_providers/05_TEST_PROVIDER.md +163 -0
- data/docs_providers/06_CUSTOM_PROVIDERS.md +304 -0
- data/lib/riffer/agent.rb +97 -43
- data/lib/riffer/config.rb +20 -12
- data/lib/riffer/core.rb +7 -7
- data/lib/riffer/helpers/class_name_converter.rb +6 -3
- data/lib/riffer/helpers/dependencies.rb +18 -0
- data/lib/riffer/helpers/validations.rb +9 -0
- data/lib/riffer/messages/assistant.rb +23 -1
- data/lib/riffer/messages/base.rb +15 -0
- data/lib/riffer/messages/converter.rb +15 -5
- data/lib/riffer/messages/system.rb +8 -1
- data/lib/riffer/messages/tool.rb +45 -2
- data/lib/riffer/messages/user.rb +8 -1
- data/lib/riffer/messages.rb +7 -0
- data/lib/riffer/providers/amazon_bedrock.rb +8 -4
- data/lib/riffer/providers/anthropic.rb +209 -0
- data/lib/riffer/providers/base.rb +17 -12
- data/lib/riffer/providers/open_ai.rb +7 -1
- data/lib/riffer/providers/repository.rb +9 -4
- data/lib/riffer/providers/test.rb +25 -7
- data/lib/riffer/providers.rb +6 -0
- data/lib/riffer/stream_events/base.rb +13 -1
- data/lib/riffer/stream_events/reasoning_delta.rb +15 -1
- data/lib/riffer/stream_events/reasoning_done.rb +15 -1
- data/lib/riffer/stream_events/text_delta.rb +14 -1
- data/lib/riffer/stream_events/text_done.rb +14 -1
- data/lib/riffer/stream_events/tool_call_delta.rb +18 -11
- data/lib/riffer/stream_events/tool_call_done.rb +22 -12
- data/lib/riffer/stream_events.rb +9 -0
- data/lib/riffer/tool.rb +57 -25
- data/lib/riffer/tools/param.rb +19 -16
- data/lib/riffer/tools/params.rb +28 -22
- data/lib/riffer/tools.rb +5 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +21 -21
- metadata +34 -1
data/docs/04_TOOLS.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Tools are callable functions that agents can invoke to interact with external systems, fetch data, or perform actions.
|
|
4
|
+
|
|
5
|
+
## Defining a Tool
|
|
6
|
+
|
|
7
|
+
Create a tool by subclassing `Riffer::Tool`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class WeatherTool < Riffer::Tool
|
|
11
|
+
description "Gets the current weather for a city"
|
|
12
|
+
|
|
13
|
+
params do
|
|
14
|
+
required :city, String, description: "The city name"
|
|
15
|
+
optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(context:, city:, units: nil)
|
|
19
|
+
weather = WeatherAPI.fetch(city, units: units || "celsius")
|
|
20
|
+
"The weather in #{city} is #{weather.temperature} #{units}."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration Methods
|
|
26
|
+
|
|
27
|
+
### description
|
|
28
|
+
|
|
29
|
+
Sets a description that helps the LLM understand when to use the tool:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
class SearchTool < Riffer::Tool
|
|
33
|
+
description "Searches the knowledge base for relevant information"
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### identifier / name
|
|
38
|
+
|
|
39
|
+
Sets a custom identifier (defaults to snake_case class name):
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class SearchTool < Riffer::Tool
|
|
43
|
+
identifier 'kb_search'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
SearchTool.identifier # => "kb_search"
|
|
47
|
+
SearchTool.name # => "kb_search" (alias)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### params
|
|
51
|
+
|
|
52
|
+
Defines the tool's parameters using a DSL:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class CreateOrderTool < Riffer::Tool
|
|
56
|
+
params do
|
|
57
|
+
required :product_id, Integer, description: "The product ID"
|
|
58
|
+
required :quantity, Integer, description: "Number of items"
|
|
59
|
+
optional :notes, String, description: "Order notes"
|
|
60
|
+
optional :priority, String, default: "normal", enum: ["low", "normal", "high"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Parameter DSL
|
|
66
|
+
|
|
67
|
+
### required
|
|
68
|
+
|
|
69
|
+
Defines a required parameter:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
params do
|
|
73
|
+
required :name, String, description: "The user's name"
|
|
74
|
+
required :age, Integer, description: "The user's age"
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
|
|
80
|
+
- `description` - Human-readable description for the LLM
|
|
81
|
+
- `enum` - Array of allowed values
|
|
82
|
+
|
|
83
|
+
### optional
|
|
84
|
+
|
|
85
|
+
Defines an optional parameter:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
params do
|
|
89
|
+
optional :limit, Integer, default: 10, description: "Max results"
|
|
90
|
+
optional :format, String, enum: ["json", "xml"], description: "Output format"
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
|
|
96
|
+
- `description` - Human-readable description
|
|
97
|
+
- `default` - Default value when not provided
|
|
98
|
+
- `enum` - Array of allowed values
|
|
99
|
+
|
|
100
|
+
### Supported Types
|
|
101
|
+
|
|
102
|
+
| Ruby Type | JSON Schema Type |
|
|
103
|
+
| -------------------------- | ---------------- |
|
|
104
|
+
| `String` | `string` |
|
|
105
|
+
| `Integer` | `integer` |
|
|
106
|
+
| `Float` | `number` |
|
|
107
|
+
| `TrueClass` / `FalseClass` | `boolean` |
|
|
108
|
+
| `Array` | `array` |
|
|
109
|
+
| `Hash` | `object` |
|
|
110
|
+
|
|
111
|
+
## The call Method
|
|
112
|
+
|
|
113
|
+
Every tool must implement the `call` method:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
def call(context:, **kwargs)
|
|
117
|
+
# context - The tool_context passed to agent.generate()
|
|
118
|
+
# kwargs - Validated parameters
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Accessing Context
|
|
123
|
+
|
|
124
|
+
The `context` argument receives whatever was passed to `tool_context`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class UserOrdersTool < Riffer::Tool
|
|
128
|
+
description "Gets the current user's orders"
|
|
129
|
+
|
|
130
|
+
def call(context:)
|
|
131
|
+
user_id = context&.dig(:user_id)
|
|
132
|
+
return "No user ID provided" unless user_id
|
|
133
|
+
|
|
134
|
+
orders = Order.where(user_id: user_id)
|
|
135
|
+
orders.map(&:to_s).join("\n")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Usage
|
|
140
|
+
agent.generate("Show my orders", tool_context: {user_id: 123})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Return Values
|
|
144
|
+
|
|
145
|
+
Return a string that will be sent back to the LLM:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
def call(context:, query:)
|
|
149
|
+
results = Database.search(query)
|
|
150
|
+
|
|
151
|
+
if results.empty?
|
|
152
|
+
"No results found for '#{query}'"
|
|
153
|
+
else
|
|
154
|
+
results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Timeout Configuration
|
|
160
|
+
|
|
161
|
+
Configure timeouts to prevent tools from running indefinitely. The default timeout is 10 seconds.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class SlowExternalApiTool < Riffer::Tool
|
|
165
|
+
description "Calls a slow external API"
|
|
166
|
+
timeout 30 # 30 seconds
|
|
167
|
+
|
|
168
|
+
def call(context:, query:)
|
|
169
|
+
ExternalAPI.search(query)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When a tool times out, the error is reported to the LLM with error type `:timeout_error`, allowing it to respond appropriately (e.g., suggest retrying or using a different approach).
|
|
175
|
+
|
|
176
|
+
## Validation
|
|
177
|
+
|
|
178
|
+
Arguments are automatically validated before `call` is invoked:
|
|
179
|
+
|
|
180
|
+
- Required parameters must be present
|
|
181
|
+
- Types must match the schema
|
|
182
|
+
- Enum values must be in the allowed list
|
|
183
|
+
|
|
184
|
+
Validation errors are captured and sent back to the LLM as tool results.
|
|
185
|
+
|
|
186
|
+
## JSON Schema Generation
|
|
187
|
+
|
|
188
|
+
Riffer automatically generates JSON Schema for each tool:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
WeatherTool.parameters_schema
|
|
192
|
+
# => {
|
|
193
|
+
# type: "object",
|
|
194
|
+
# properties: {
|
|
195
|
+
# "city" => {type: "string", description: "The city name"},
|
|
196
|
+
# "units" => {type: "string", enum: ["celsius", "fahrenheit"]}
|
|
197
|
+
# },
|
|
198
|
+
# required: ["city"],
|
|
199
|
+
# additionalProperties: false
|
|
200
|
+
# }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Registering Tools with Agents
|
|
204
|
+
|
|
205
|
+
### Static Registration
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
class MyAgent < Riffer::Agent
|
|
209
|
+
model 'openai/gpt-4o'
|
|
210
|
+
uses_tools [WeatherTool, SearchTool]
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Dynamic Registration
|
|
215
|
+
|
|
216
|
+
Use a lambda for context-aware tool resolution:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
class MyAgent < Riffer::Agent
|
|
220
|
+
model 'openai/gpt-4o'
|
|
221
|
+
|
|
222
|
+
uses_tools ->(context) {
|
|
223
|
+
tools = [PublicSearchTool]
|
|
224
|
+
|
|
225
|
+
if context&.dig(:user)&.premium?
|
|
226
|
+
tools << PremiumAnalyticsTool
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if context&.dig(:user)&.admin?
|
|
230
|
+
tools << AdminTool
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
tools
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Error Handling
|
|
239
|
+
|
|
240
|
+
Errors in tools are captured and reported back to the LLM:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
def call(context:, query:)
|
|
244
|
+
raise "API rate limit exceeded"
|
|
245
|
+
rescue => e
|
|
246
|
+
# Error is caught by Riffer and sent as tool result:
|
|
247
|
+
# "Error executing tool: API rate limit exceeded"
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The LLM can then decide how to respond (retry, apologize, ask for different input, etc.).
|
data/docs/05_MESSAGES.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Messages
|
|
2
|
+
|
|
3
|
+
Messages represent the conversation between users and the assistant. Riffer uses strongly-typed message objects to ensure consistency and type safety.
|
|
4
|
+
|
|
5
|
+
## Message Types
|
|
6
|
+
|
|
7
|
+
### System
|
|
8
|
+
|
|
9
|
+
System messages provide instructions to the LLM:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
msg = Riffer::Messages::System.new("You are a helpful assistant.")
|
|
13
|
+
msg.role # => :system
|
|
14
|
+
msg.content # => "You are a helpful assistant."
|
|
15
|
+
msg.to_h # => {role: :system, content: "You are a helpful assistant."}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
System messages are typically set via agent `instructions` and automatically prepended to conversations.
|
|
19
|
+
|
|
20
|
+
### User
|
|
21
|
+
|
|
22
|
+
User messages represent input from the user:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
msg = Riffer::Messages::User.new("Hello, how are you?")
|
|
26
|
+
msg.role # => :user
|
|
27
|
+
msg.content # => "Hello, how are you?"
|
|
28
|
+
msg.to_h # => {role: :user, content: "Hello, how are you?"}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Assistant
|
|
32
|
+
|
|
33
|
+
Assistant messages represent LLM responses, potentially including tool calls:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# Text-only response
|
|
37
|
+
msg = Riffer::Messages::Assistant.new("I'm doing well, thank you!")
|
|
38
|
+
msg.role # => :assistant
|
|
39
|
+
msg.content # => "I'm doing well, thank you!"
|
|
40
|
+
msg.tool_calls # => []
|
|
41
|
+
|
|
42
|
+
# Response with tool calls
|
|
43
|
+
msg = Riffer::Messages::Assistant.new("", tool_calls: [
|
|
44
|
+
{id: "call_123", call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}'}
|
|
45
|
+
])
|
|
46
|
+
msg.tool_calls # => [{id: "call_123", ...}]
|
|
47
|
+
msg.to_h # => {role: "assistant", content: "", tool_calls: [...]}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Tool
|
|
51
|
+
|
|
52
|
+
Tool messages contain the results of tool executions:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
msg = Riffer::Messages::Tool.new(
|
|
56
|
+
"The weather in Tokyo is 22C and sunny.",
|
|
57
|
+
tool_call_id: "call_123",
|
|
58
|
+
name: "weather_tool"
|
|
59
|
+
)
|
|
60
|
+
msg.role # => :tool
|
|
61
|
+
msg.content # => "The weather in Tokyo is 22C and sunny."
|
|
62
|
+
msg.tool_call_id # => "call_123"
|
|
63
|
+
msg.name # => "weather_tool"
|
|
64
|
+
msg.error? # => false
|
|
65
|
+
|
|
66
|
+
# Error result
|
|
67
|
+
msg = Riffer::Messages::Tool.new(
|
|
68
|
+
"API rate limit exceeded",
|
|
69
|
+
tool_call_id: "call_123",
|
|
70
|
+
name: "weather_tool",
|
|
71
|
+
error: "API rate limit exceeded",
|
|
72
|
+
error_type: :execution_error
|
|
73
|
+
)
|
|
74
|
+
msg.error? # => true
|
|
75
|
+
msg.error # => "API rate limit exceeded"
|
|
76
|
+
msg.error_type # => :execution_error
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Using Messages with Agents
|
|
80
|
+
|
|
81
|
+
### String Prompts
|
|
82
|
+
|
|
83
|
+
The simplest way to interact with an agent:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
agent = MyAgent.new
|
|
87
|
+
response = agent.generate("Hello!")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This creates a `User` message internally.
|
|
91
|
+
|
|
92
|
+
### Message Arrays
|
|
93
|
+
|
|
94
|
+
For multi-turn conversations, pass an array of messages:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
messages = [
|
|
98
|
+
{role: :user, content: "What's the weather?"},
|
|
99
|
+
{role: :assistant, content: "I'll check that for you."},
|
|
100
|
+
{role: :user, content: "Thanks, I meant in Tokyo specifically."}
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
response = agent.generate(messages)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Messages can be hashes or `Riffer::Messages::Base` objects:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
messages = [
|
|
110
|
+
Riffer::Messages::User.new("Hello"),
|
|
111
|
+
Riffer::Messages::Assistant.new("Hi there!"),
|
|
112
|
+
Riffer::Messages::User.new("How are you?")
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
response = agent.generate(messages)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Accessing Message History
|
|
119
|
+
|
|
120
|
+
After calling `generate` or `stream`, access the full conversation:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
agent = MyAgent.new
|
|
124
|
+
agent.generate("Hello!")
|
|
125
|
+
|
|
126
|
+
agent.messages.each do |msg|
|
|
127
|
+
puts "[#{msg.role}] #{msg.content}"
|
|
128
|
+
end
|
|
129
|
+
# [system] You are a helpful assistant.
|
|
130
|
+
# [user] Hello!
|
|
131
|
+
# [assistant] Hi there! How can I help you today?
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Tool Call Structure
|
|
135
|
+
|
|
136
|
+
Tool calls in assistant messages have this structure:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
{
|
|
140
|
+
id: "item_123", # Item identifier
|
|
141
|
+
call_id: "call_456", # Call identifier for response matching
|
|
142
|
+
name: "weather_tool", # Tool name
|
|
143
|
+
arguments: '{"city":"Tokyo"}' # JSON string of arguments
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When creating tool result messages, use the `id` as `tool_call_id`.
|
|
148
|
+
|
|
149
|
+
## Message Emission
|
|
150
|
+
|
|
151
|
+
Agents can emit messages as they're added during generation via the `on_message` callback. This is useful for persistence or real-time logging. Only agent-generated messages (Assistant, Tool) are emitted—not inputs (System, User).
|
|
152
|
+
|
|
153
|
+
See [Agents - on_message](03_AGENTS.md#on_message) for details.
|
|
154
|
+
|
|
155
|
+
## Base Class
|
|
156
|
+
|
|
157
|
+
All messages inherit from `Riffer::Messages::Base`:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
class Riffer::Messages::Base
|
|
161
|
+
attr_reader :content
|
|
162
|
+
|
|
163
|
+
def role
|
|
164
|
+
raise NotImplementedError
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def to_h
|
|
168
|
+
{role: role, content: content}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Subclasses implement `role` and optionally extend `to_h` with additional fields.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Stream Events
|
|
2
|
+
|
|
3
|
+
When streaming responses, Riffer emits typed events that represent incremental updates from the LLM.
|
|
4
|
+
|
|
5
|
+
## Using Streaming
|
|
6
|
+
|
|
7
|
+
Use `stream` instead of `generate` to receive events as they arrive:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
agent = MyAgent.new
|
|
11
|
+
|
|
12
|
+
agent.stream("Tell me a story").each do |event|
|
|
13
|
+
case event
|
|
14
|
+
when Riffer::StreamEvents::TextDelta
|
|
15
|
+
print event.content
|
|
16
|
+
when Riffer::StreamEvents::TextDone
|
|
17
|
+
puts "\n[Complete]"
|
|
18
|
+
when Riffer::StreamEvents::ToolCallDelta
|
|
19
|
+
# Tool call being built
|
|
20
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
21
|
+
puts "[Tool: #{event.name}]"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Event Types
|
|
27
|
+
|
|
28
|
+
### TextDelta
|
|
29
|
+
|
|
30
|
+
Emitted when incremental text content is received:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
event = Riffer::StreamEvents::TextDelta.new("Hello ")
|
|
34
|
+
event.role # => "assistant"
|
|
35
|
+
event.content # => "Hello "
|
|
36
|
+
event.to_h # => {role: "assistant", content: "Hello "}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use this to display text in real-time as it streams.
|
|
40
|
+
|
|
41
|
+
### TextDone
|
|
42
|
+
|
|
43
|
+
Emitted when text generation is complete:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
event = Riffer::StreamEvents::TextDone.new("Hello, how can I help you?")
|
|
47
|
+
event.role # => "assistant"
|
|
48
|
+
event.content # => "Hello, how can I help you?"
|
|
49
|
+
event.to_h # => {role: "assistant", content: "Hello, how can I help you?"}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Contains the complete final text.
|
|
53
|
+
|
|
54
|
+
### ToolCallDelta
|
|
55
|
+
|
|
56
|
+
Emitted when tool call arguments are being streamed:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
event = Riffer::StreamEvents::ToolCallDelta.new(
|
|
60
|
+
item_id: "item_123",
|
|
61
|
+
name: "weather_tool",
|
|
62
|
+
arguments_delta: '{"city":'
|
|
63
|
+
)
|
|
64
|
+
event.role # => "assistant"
|
|
65
|
+
event.item_id # => "item_123"
|
|
66
|
+
event.name # => "weather_tool"
|
|
67
|
+
event.arguments_delta # => '{"city":'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `name` may only be present in the first delta. Accumulate `arguments_delta` to build the complete arguments.
|
|
71
|
+
|
|
72
|
+
### ToolCallDone
|
|
73
|
+
|
|
74
|
+
Emitted when a tool call is complete:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
event = Riffer::StreamEvents::ToolCallDone.new(
|
|
78
|
+
item_id: "item_123",
|
|
79
|
+
call_id: "call_456",
|
|
80
|
+
name: "weather_tool",
|
|
81
|
+
arguments: '{"city":"Tokyo"}'
|
|
82
|
+
)
|
|
83
|
+
event.role # => "assistant"
|
|
84
|
+
event.item_id # => "item_123"
|
|
85
|
+
event.call_id # => "call_456"
|
|
86
|
+
event.name # => "weather_tool"
|
|
87
|
+
event.arguments # => '{"city":"Tokyo"}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Contains the complete tool call information.
|
|
91
|
+
|
|
92
|
+
### ReasoningDelta
|
|
93
|
+
|
|
94
|
+
Emitted when reasoning/thinking content is streamed (OpenAI with reasoning enabled):
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
event = Riffer::StreamEvents::ReasoningDelta.new("Let me think about ")
|
|
98
|
+
event.role # => "assistant"
|
|
99
|
+
event.content # => "Let me think about "
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### ReasoningDone
|
|
103
|
+
|
|
104
|
+
Emitted when reasoning is complete:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
event = Riffer::StreamEvents::ReasoningDone.new("Let me think about this step by step...")
|
|
108
|
+
event.role # => "assistant"
|
|
109
|
+
event.content # => "Let me think about this step by step..."
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Streaming with Tools
|
|
113
|
+
|
|
114
|
+
When an agent uses tools during streaming, the flow is:
|
|
115
|
+
|
|
116
|
+
1. Text events stream in (`TextDelta`, `TextDone`)
|
|
117
|
+
2. If tool calls are present: `ToolCallDelta` events, then `ToolCallDone`
|
|
118
|
+
3. Agent executes tools internally
|
|
119
|
+
4. Agent sends results back to LLM
|
|
120
|
+
5. More text events stream in
|
|
121
|
+
6. Repeat until no more tool calls
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
agent.stream("What's the weather in Tokyo?").each do |event|
|
|
125
|
+
case event
|
|
126
|
+
when Riffer::StreamEvents::TextDelta
|
|
127
|
+
print event.content
|
|
128
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
129
|
+
puts "\n[Calling #{event.name}...]"
|
|
130
|
+
when Riffer::StreamEvents::TextDone
|
|
131
|
+
puts "\n"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Complete Example
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class WeatherAgent < Riffer::Agent
|
|
140
|
+
model 'openai/gpt-4o'
|
|
141
|
+
instructions 'You are a weather assistant.'
|
|
142
|
+
uses_tools [WeatherTool]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
agent = WeatherAgent.new
|
|
146
|
+
text_buffer = ""
|
|
147
|
+
|
|
148
|
+
agent.stream("What's the weather in Tokyo and New York?").each do |event|
|
|
149
|
+
case event
|
|
150
|
+
when Riffer::StreamEvents::TextDelta
|
|
151
|
+
print event.content
|
|
152
|
+
text_buffer += event.content
|
|
153
|
+
|
|
154
|
+
when Riffer::StreamEvents::TextDone
|
|
155
|
+
# Final text available
|
|
156
|
+
puts "\n---"
|
|
157
|
+
puts "Complete response: #{event.content}"
|
|
158
|
+
|
|
159
|
+
when Riffer::StreamEvents::ToolCallDelta
|
|
160
|
+
# Could show "typing..." indicator
|
|
161
|
+
|
|
162
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
163
|
+
puts "\n[Tool: #{event.name}(#{event.arguments})]"
|
|
164
|
+
|
|
165
|
+
when Riffer::StreamEvents::ReasoningDelta
|
|
166
|
+
# Show thinking process if desired
|
|
167
|
+
print "[thinking] #{event.content}"
|
|
168
|
+
|
|
169
|
+
when Riffer::StreamEvents::ReasoningDone
|
|
170
|
+
puts "\n[reasoning complete]"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Base Class
|
|
176
|
+
|
|
177
|
+
All events inherit from `Riffer::StreamEvents::Base`:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
class Riffer::StreamEvents::Base
|
|
181
|
+
attr_reader :role
|
|
182
|
+
|
|
183
|
+
def initialize(role: "assistant")
|
|
184
|
+
@role = role
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def to_h
|
|
188
|
+
raise NotImplementedError
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|