riffer 0.7.0 → 0.9.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 +17 -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 +342 -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 +103 -63
- 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 +92 -25
- data/lib/riffer/tools/param.rb +19 -16
- data/lib/riffer/tools/params.rb +28 -22
- data/lib/riffer/tools/response.rb +90 -0
- data/lib/riffer/tools.rb +6 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +21 -21
- metadata +35 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Creating Custom Providers
|
|
2
|
+
|
|
3
|
+
You can create custom providers to connect Riffer to other LLM services.
|
|
4
|
+
|
|
5
|
+
## Basic Structure
|
|
6
|
+
|
|
7
|
+
Extend `Riffer::Providers::Base` and implement the required methods:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
11
|
+
def initialize(**options)
|
|
12
|
+
# Initialize your client
|
|
13
|
+
@api_key = options[:api_key] || ENV['MY_PROVIDER_API_KEY']
|
|
14
|
+
@client = MyProviderClient.new(api_key: @api_key)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def perform_generate_text(messages, model:, **options)
|
|
20
|
+
# Convert messages to provider format
|
|
21
|
+
formatted = convert_messages(messages)
|
|
22
|
+
|
|
23
|
+
# Call your provider's API
|
|
24
|
+
response = @client.generate(
|
|
25
|
+
model: model,
|
|
26
|
+
messages: formatted,
|
|
27
|
+
**options
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Return a Riffer::Messages::Assistant
|
|
31
|
+
Riffer::Messages::Assistant.new(
|
|
32
|
+
response.text,
|
|
33
|
+
tool_calls: extract_tool_calls(response)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def perform_stream_text(messages, model:, **options)
|
|
38
|
+
Enumerator.new do |yielder|
|
|
39
|
+
formatted = convert_messages(messages)
|
|
40
|
+
|
|
41
|
+
@client.stream(model: model, messages: formatted, **options) do |chunk|
|
|
42
|
+
# Yield appropriate stream events
|
|
43
|
+
case chunk.type
|
|
44
|
+
when :text
|
|
45
|
+
yielder << Riffer::StreamEvents::TextDelta.new(chunk.content)
|
|
46
|
+
when :text_done
|
|
47
|
+
yielder << Riffer::StreamEvents::TextDone.new(chunk.content)
|
|
48
|
+
when :tool_call
|
|
49
|
+
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
50
|
+
item_id: chunk.id,
|
|
51
|
+
call_id: chunk.id,
|
|
52
|
+
name: chunk.name,
|
|
53
|
+
arguments: chunk.arguments
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def convert_messages(messages)
|
|
61
|
+
messages.map do |msg|
|
|
62
|
+
case msg
|
|
63
|
+
when Riffer::Messages::System
|
|
64
|
+
{role: "system", content: msg.content}
|
|
65
|
+
when Riffer::Messages::User
|
|
66
|
+
{role: "user", content: msg.content}
|
|
67
|
+
when Riffer::Messages::Assistant
|
|
68
|
+
convert_assistant(msg)
|
|
69
|
+
when Riffer::Messages::Tool
|
|
70
|
+
{role: "tool", tool_call_id: msg.tool_call_id, content: msg.content}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def convert_assistant(msg)
|
|
76
|
+
# Handle tool calls if present
|
|
77
|
+
{role: "assistant", content: msg.content, tool_calls: msg.tool_calls}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_tool_calls(response)
|
|
81
|
+
return [] unless response.tool_calls
|
|
82
|
+
|
|
83
|
+
response.tool_calls.map do |tc|
|
|
84
|
+
{
|
|
85
|
+
id: tc.id,
|
|
86
|
+
call_id: tc.id,
|
|
87
|
+
name: tc.name,
|
|
88
|
+
arguments: tc.arguments
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Using depends_on
|
|
96
|
+
|
|
97
|
+
For lazy loading of external gems:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
101
|
+
def initialize(**options)
|
|
102
|
+
depends_on "my_provider_gem" # Only loaded when provider is used
|
|
103
|
+
|
|
104
|
+
@client = ::MyProviderGem::Client.new(**options)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Registering Your Provider
|
|
110
|
+
|
|
111
|
+
Add your provider to the repository:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# In lib/riffer/providers/repository.rb or your own code
|
|
115
|
+
|
|
116
|
+
Riffer::Providers::Repository::REPO[:my_provider] = -> { Riffer::Providers::MyProvider }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or create a custom repository:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
module MyApp
|
|
123
|
+
module Providers
|
|
124
|
+
def self.find(identifier)
|
|
125
|
+
case identifier.to_sym
|
|
126
|
+
when :my_provider
|
|
127
|
+
Riffer::Providers::MyProvider
|
|
128
|
+
else
|
|
129
|
+
Riffer::Providers::Repository.find(identifier)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Using Your Provider
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class MyAgent < Riffer::Agent
|
|
140
|
+
model 'my_provider/model-name'
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Tool Support
|
|
145
|
+
|
|
146
|
+
Convert tools to your provider's format:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
def perform_generate_text(messages, model:, tools: nil, **options)
|
|
150
|
+
params = {
|
|
151
|
+
model: model,
|
|
152
|
+
messages: convert_messages(messages)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if tools && !tools.empty?
|
|
156
|
+
params[:tools] = tools.map { |t| convert_tool(t) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
response = @client.generate(**params)
|
|
160
|
+
# ...
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def convert_tool(tool)
|
|
164
|
+
{
|
|
165
|
+
name: tool.name,
|
|
166
|
+
description: tool.description,
|
|
167
|
+
parameters: tool.parameters_schema
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Stream Events
|
|
173
|
+
|
|
174
|
+
Use the appropriate stream event classes:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Text streaming
|
|
178
|
+
Riffer::StreamEvents::TextDelta.new("chunk of text")
|
|
179
|
+
Riffer::StreamEvents::TextDone.new("complete text")
|
|
180
|
+
|
|
181
|
+
# Tool calls
|
|
182
|
+
Riffer::StreamEvents::ToolCallDelta.new(
|
|
183
|
+
item_id: "id",
|
|
184
|
+
name: "tool_name",
|
|
185
|
+
arguments_delta: '{"partial":'
|
|
186
|
+
)
|
|
187
|
+
Riffer::StreamEvents::ToolCallDone.new(
|
|
188
|
+
item_id: "id",
|
|
189
|
+
call_id: "call_id",
|
|
190
|
+
name: "tool_name",
|
|
191
|
+
arguments: '{"complete":"args"}'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Reasoning (if supported)
|
|
195
|
+
Riffer::StreamEvents::ReasoningDelta.new("thinking...")
|
|
196
|
+
Riffer::StreamEvents::ReasoningDone.new("complete reasoning")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Error Handling
|
|
200
|
+
|
|
201
|
+
Raise appropriate Riffer errors:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
def perform_generate_text(messages, model:, **options)
|
|
205
|
+
response = @client.generate(...)
|
|
206
|
+
|
|
207
|
+
if response.error?
|
|
208
|
+
raise Riffer::Error, "Provider error: #{response.error_message}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# ...
|
|
212
|
+
rescue MyProviderGem::AuthError => e
|
|
213
|
+
raise Riffer::ArgumentError, "Authentication failed: #{e.message}"
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Complete Example
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# lib/riffer/providers/anthropic.rb
|
|
221
|
+
|
|
222
|
+
class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
223
|
+
def initialize(**options)
|
|
224
|
+
depends_on "anthropic"
|
|
225
|
+
|
|
226
|
+
api_key = options[:api_key] || ENV['ANTHROPIC_API_KEY']
|
|
227
|
+
@client = ::Anthropic::Client.new(api_key: api_key)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def perform_generate_text(messages, model:, tools: nil, **options)
|
|
233
|
+
system_message = extract_system(messages)
|
|
234
|
+
conversation = messages.reject { |m| m.is_a?(Riffer::Messages::System) }
|
|
235
|
+
|
|
236
|
+
params = {
|
|
237
|
+
model: model,
|
|
238
|
+
messages: convert_messages(conversation),
|
|
239
|
+
system: system_message,
|
|
240
|
+
max_tokens: options[:max_tokens] || 4096
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if tools && !tools.empty?
|
|
244
|
+
params[:tools] = tools.map { |t| convert_tool(t) }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
response = @client.messages.create(**params)
|
|
248
|
+
extract_assistant_message(response)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def perform_stream_text(messages, model:, tools: nil, **options)
|
|
252
|
+
Enumerator.new do |yielder|
|
|
253
|
+
# Similar implementation with streaming
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def extract_system(messages)
|
|
258
|
+
system_msg = messages.find { |m| m.is_a?(Riffer::Messages::System) }
|
|
259
|
+
system_msg&.content
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def convert_messages(messages)
|
|
263
|
+
messages.map do |msg|
|
|
264
|
+
case msg
|
|
265
|
+
when Riffer::Messages::User
|
|
266
|
+
{role: "user", content: msg.content}
|
|
267
|
+
when Riffer::Messages::Assistant
|
|
268
|
+
{role: "assistant", content: msg.content}
|
|
269
|
+
when Riffer::Messages::Tool
|
|
270
|
+
{role: "user", content: [{type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content}]}
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def convert_tool(tool)
|
|
276
|
+
{
|
|
277
|
+
name: tool.name,
|
|
278
|
+
description: tool.description,
|
|
279
|
+
input_schema: tool.parameters_schema
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def extract_assistant_message(response)
|
|
284
|
+
text = ""
|
|
285
|
+
tool_calls = []
|
|
286
|
+
|
|
287
|
+
response.content.each do |block|
|
|
288
|
+
case block.type
|
|
289
|
+
when "text"
|
|
290
|
+
text = block.text
|
|
291
|
+
when "tool_use"
|
|
292
|
+
tool_calls << {
|
|
293
|
+
id: block.id,
|
|
294
|
+
call_id: block.id,
|
|
295
|
+
name: block.name,
|
|
296
|
+
arguments: block.input.to_json
|
|
297
|
+
}
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
Riffer::Messages::Assistant.new(text, tool_calls: tool_calls)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -5,10 +5,18 @@ require "json"
|
|
|
5
5
|
# Riffer::Agent is the base class for all agents in the Riffer framework.
|
|
6
6
|
#
|
|
7
7
|
# Provides orchestration for LLM calls, tool use, and message management.
|
|
8
|
+
# Subclass this to create your own agents.
|
|
9
|
+
#
|
|
10
|
+
# See Riffer::Messages and Riffer::Providers.
|
|
11
|
+
#
|
|
12
|
+
# class MyAgent < Riffer::Agent
|
|
13
|
+
# model 'openai/gpt-4o'
|
|
14
|
+
# instructions 'You are a helpful assistant.'
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# agent = MyAgent.new
|
|
18
|
+
# agent.generate('Hello!')
|
|
8
19
|
#
|
|
9
|
-
# @abstract
|
|
10
|
-
# @see Riffer::Messages
|
|
11
|
-
# @see Riffer::Providers
|
|
12
20
|
class Riffer::Agent
|
|
13
21
|
include Riffer::Messages::Converter
|
|
14
22
|
|
|
@@ -16,79 +24,97 @@ class Riffer::Agent
|
|
|
16
24
|
include Riffer::Helpers::ClassNameConverter
|
|
17
25
|
include Riffer::Helpers::Validations
|
|
18
26
|
|
|
19
|
-
# Gets or sets the agent identifier
|
|
20
|
-
#
|
|
21
|
-
#
|
|
27
|
+
# Gets or sets the agent identifier.
|
|
28
|
+
#
|
|
29
|
+
# value:: String or nil - the identifier to set, or nil to get
|
|
30
|
+
#
|
|
31
|
+
# Returns String - the agent identifier.
|
|
22
32
|
def identifier(value = nil)
|
|
23
33
|
return @identifier || class_name_to_path(name) if value.nil?
|
|
24
34
|
@identifier = value.to_s
|
|
25
35
|
end
|
|
26
36
|
|
|
27
|
-
# Gets or sets the model string (e.g., "openai/gpt-
|
|
28
|
-
#
|
|
29
|
-
#
|
|
37
|
+
# Gets or sets the model string (e.g., "openai/gpt-4o").
|
|
38
|
+
#
|
|
39
|
+
# model_string:: String or nil - the model string to set, or nil to get
|
|
40
|
+
#
|
|
41
|
+
# Returns String - the model string.
|
|
30
42
|
def model(model_string = nil)
|
|
31
43
|
return @model if model_string.nil?
|
|
32
44
|
validate_is_string!(model_string, "model")
|
|
33
45
|
@model = model_string
|
|
34
46
|
end
|
|
35
47
|
|
|
36
|
-
# Gets or sets the agent instructions
|
|
37
|
-
#
|
|
38
|
-
#
|
|
48
|
+
# Gets or sets the agent instructions.
|
|
49
|
+
#
|
|
50
|
+
# instructions_text:: String or nil - the instructions to set, or nil to get
|
|
51
|
+
#
|
|
52
|
+
# Returns String - the agent instructions.
|
|
39
53
|
def instructions(instructions_text = nil)
|
|
40
54
|
return @instructions if instructions_text.nil?
|
|
41
55
|
validate_is_string!(instructions_text, "instructions")
|
|
42
56
|
@instructions = instructions_text
|
|
43
57
|
end
|
|
44
58
|
|
|
45
|
-
# Gets or sets provider options passed to the provider client
|
|
46
|
-
#
|
|
47
|
-
#
|
|
59
|
+
# Gets or sets provider options passed to the provider client.
|
|
60
|
+
#
|
|
61
|
+
# options:: Hash or nil - the options to set, or nil to get
|
|
62
|
+
#
|
|
63
|
+
# Returns Hash - the provider options.
|
|
48
64
|
def provider_options(options = nil)
|
|
49
65
|
return @provider_options || {} if options.nil?
|
|
50
66
|
@provider_options = options
|
|
51
67
|
end
|
|
52
68
|
|
|
53
|
-
# Gets or sets model options passed to generate_text/stream_text
|
|
54
|
-
#
|
|
55
|
-
#
|
|
69
|
+
# Gets or sets model options passed to generate_text/stream_text.
|
|
70
|
+
#
|
|
71
|
+
# options:: Hash or nil - the options to set, or nil to get
|
|
72
|
+
#
|
|
73
|
+
# Returns Hash - the model options.
|
|
56
74
|
def model_options(options = nil)
|
|
57
75
|
return @model_options || {} if options.nil?
|
|
58
76
|
@model_options = options
|
|
59
77
|
end
|
|
60
78
|
|
|
61
|
-
# Gets or sets the tools used by this agent
|
|
62
|
-
#
|
|
63
|
-
#
|
|
79
|
+
# Gets or sets the tools used by this agent.
|
|
80
|
+
#
|
|
81
|
+
# tools_or_lambda:: Array of Tool classes, Proc, or nil - tools array or lambda returning tools
|
|
82
|
+
#
|
|
83
|
+
# Returns Array, Proc, or nil - the tools configuration.
|
|
64
84
|
def uses_tools(tools_or_lambda = nil)
|
|
65
85
|
return @tools_config if tools_or_lambda.nil?
|
|
66
86
|
@tools_config = tools_or_lambda
|
|
67
87
|
end
|
|
68
88
|
|
|
69
|
-
# Finds an agent class by identifier
|
|
70
|
-
#
|
|
71
|
-
#
|
|
89
|
+
# Finds an agent class by identifier.
|
|
90
|
+
#
|
|
91
|
+
# identifier:: String - the identifier to search for
|
|
92
|
+
#
|
|
93
|
+
# Returns Class or nil - the agent class, or nil if not found.
|
|
72
94
|
def find(identifier)
|
|
73
95
|
subclasses.find { |agent_class| agent_class.identifier == identifier.to_s }
|
|
74
96
|
end
|
|
75
97
|
|
|
76
|
-
# Returns all agent subclasses
|
|
77
|
-
#
|
|
98
|
+
# Returns all agent subclasses.
|
|
99
|
+
#
|
|
100
|
+
# Returns Array of Class - all agent subclasses.
|
|
78
101
|
def all
|
|
79
102
|
subclasses
|
|
80
103
|
end
|
|
81
104
|
end
|
|
82
105
|
|
|
83
|
-
# The message history for the agent
|
|
84
|
-
#
|
|
106
|
+
# The message history for the agent.
|
|
107
|
+
#
|
|
108
|
+
# Returns Array of Riffer::Messages::Base.
|
|
85
109
|
attr_reader :messages
|
|
86
110
|
|
|
87
|
-
# Initializes a new agent
|
|
88
|
-
#
|
|
89
|
-
#
|
|
111
|
+
# Initializes a new agent.
|
|
112
|
+
#
|
|
113
|
+
# Raises Riffer::ArgumentError if the configured model string is invalid
|
|
114
|
+
# (must be "provider/model" format).
|
|
90
115
|
def initialize
|
|
91
116
|
@messages = []
|
|
117
|
+
@message_callbacks = []
|
|
92
118
|
@model_string = self.class.model
|
|
93
119
|
@instructions_text = self.class.instructions
|
|
94
120
|
|
|
@@ -100,10 +126,12 @@ class Riffer::Agent
|
|
|
100
126
|
@model_name = model_name
|
|
101
127
|
end
|
|
102
128
|
|
|
103
|
-
# Generates a response from the agent
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
129
|
+
# Generates a response from the agent.
|
|
130
|
+
#
|
|
131
|
+
# prompt_or_messages:: String or Array - a string prompt or array of message hashes/objects
|
|
132
|
+
# tool_context:: Object or nil - optional context object passed to all tool calls
|
|
133
|
+
#
|
|
134
|
+
# Returns String - the final response content.
|
|
107
135
|
def generate(prompt_or_messages, tool_context: nil)
|
|
108
136
|
@tool_context = tool_context
|
|
109
137
|
@resolved_tools = nil
|
|
@@ -111,7 +139,7 @@ class Riffer::Agent
|
|
|
111
139
|
|
|
112
140
|
loop do
|
|
113
141
|
response = call_llm
|
|
114
|
-
|
|
142
|
+
add_message(response)
|
|
115
143
|
|
|
116
144
|
break unless has_tool_calls?(response)
|
|
117
145
|
|
|
@@ -121,10 +149,12 @@ class Riffer::Agent
|
|
|
121
149
|
extract_final_response
|
|
122
150
|
end
|
|
123
151
|
|
|
124
|
-
# Streams a response from the agent
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
152
|
+
# Streams a response from the agent.
|
|
153
|
+
#
|
|
154
|
+
# prompt_or_messages:: String or Array - a string prompt or array of message hashes/objects
|
|
155
|
+
# tool_context:: Object or nil - optional context object passed to all tool calls
|
|
156
|
+
#
|
|
157
|
+
# Returns Enumerator - an enumerator yielding stream events.
|
|
128
158
|
def stream(prompt_or_messages, tool_context: nil)
|
|
129
159
|
@tool_context = tool_context
|
|
130
160
|
@resolved_tools = nil
|
|
@@ -160,7 +190,7 @@ class Riffer::Agent
|
|
|
160
190
|
end
|
|
161
191
|
|
|
162
192
|
response = Riffer::Messages::Assistant.new(accumulated_content, tool_calls: accumulated_tool_calls)
|
|
163
|
-
|
|
193
|
+
add_message(response)
|
|
164
194
|
|
|
165
195
|
break unless has_tool_calls?(response)
|
|
166
196
|
|
|
@@ -169,8 +199,26 @@ class Riffer::Agent
|
|
|
169
199
|
end
|
|
170
200
|
end
|
|
171
201
|
|
|
202
|
+
# Registers a callback to be invoked when messages are added during generation.
|
|
203
|
+
#
|
|
204
|
+
# block:: Block - callback receiving a Riffer::Messages::Base subclass
|
|
205
|
+
#
|
|
206
|
+
# Raises Riffer::ArgumentError if no block is given.
|
|
207
|
+
#
|
|
208
|
+
# Returns self for method chaining.
|
|
209
|
+
def on_message(&block)
|
|
210
|
+
raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
|
|
211
|
+
@message_callbacks << block
|
|
212
|
+
self
|
|
213
|
+
end
|
|
214
|
+
|
|
172
215
|
private
|
|
173
216
|
|
|
217
|
+
def add_message(message)
|
|
218
|
+
@messages << message
|
|
219
|
+
@message_callbacks.each { |callback| callback.call(message) }
|
|
220
|
+
end
|
|
221
|
+
|
|
174
222
|
def initialize_messages(prompt_or_messages)
|
|
175
223
|
@messages = []
|
|
176
224
|
@messages << Riffer::Messages::System.new(@instructions_text) if @instructions_text
|
|
@@ -217,13 +265,13 @@ class Riffer::Agent
|
|
|
217
265
|
def execute_tool_calls(response)
|
|
218
266
|
response.tool_calls.each do |tool_call|
|
|
219
267
|
result = execute_tool_call(tool_call)
|
|
220
|
-
|
|
221
|
-
result
|
|
268
|
+
add_message(Riffer::Messages::Tool.new(
|
|
269
|
+
result.content,
|
|
222
270
|
tool_call_id: tool_call[:id],
|
|
223
271
|
name: tool_call[:name],
|
|
224
|
-
error: result
|
|
225
|
-
error_type: result
|
|
226
|
-
)
|
|
272
|
+
error: result.error_message,
|
|
273
|
+
error_type: result.error_type
|
|
274
|
+
))
|
|
227
275
|
end
|
|
228
276
|
end
|
|
229
277
|
|
|
@@ -231,31 +279,23 @@ class Riffer::Agent
|
|
|
231
279
|
tool_class = find_tool_class(tool_call[:name])
|
|
232
280
|
|
|
233
281
|
if tool_class.nil?
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
282
|
+
return Riffer::Tools::Response.error(
|
|
283
|
+
"Unknown tool '#{tool_call[:name]}'",
|
|
284
|
+
type: :unknown_tool
|
|
285
|
+
)
|
|
239
286
|
end
|
|
240
287
|
|
|
241
288
|
tool_instance = tool_class.new
|
|
242
289
|
arguments = parse_tool_arguments(tool_call[:arguments])
|
|
243
290
|
|
|
244
291
|
begin
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
tool_instance.call_with_validation(context: @tool_context, **arguments)
|
|
293
|
+
rescue Riffer::TimeoutError => e
|
|
294
|
+
Riffer::Tools::Response.error(e.message, type: :timeout_error)
|
|
247
295
|
rescue Riffer::ValidationError => e
|
|
248
|
-
|
|
249
|
-
content: "Validation error: #{e.message}",
|
|
250
|
-
error: e.message,
|
|
251
|
-
error_type: :validation_error
|
|
252
|
-
}
|
|
296
|
+
Riffer::Tools::Response.error(e.message, type: :validation_error)
|
|
253
297
|
rescue => e
|
|
254
|
-
{
|
|
255
|
-
content: "Error executing tool: #{e.message}",
|
|
256
|
-
error: e.message,
|
|
257
|
-
error_type: :execution_error
|
|
258
|
-
}
|
|
298
|
+
Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
|
|
259
299
|
end
|
|
260
300
|
end
|
|
261
301
|
|
data/lib/riffer/config.rb
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Configuration for the Riffer framework
|
|
3
|
+
# Configuration for the Riffer framework.
|
|
4
4
|
#
|
|
5
5
|
# Provides configuration options for AI providers and other settings.
|
|
6
6
|
#
|
|
7
|
-
# @example Setting the OpenAI API key
|
|
8
7
|
# Riffer.config.openai.api_key = "sk-..."
|
|
9
8
|
#
|
|
10
|
-
# @example Setting Amazon Bedrock configuration
|
|
11
9
|
# Riffer.config.amazon_bedrock.region = "us-east-1"
|
|
12
10
|
# Riffer.config.amazon_bedrock.api_token = "..."
|
|
11
|
+
#
|
|
12
|
+
# Riffer.config.anthropic.api_key = "sk-ant-..."
|
|
13
|
+
#
|
|
13
14
|
class Riffer::Config
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Amazon Bedrock configuration
|
|
19
|
-
# @return [Struct]
|
|
15
|
+
# Amazon Bedrock configuration (Struct with +api_token+ and +region+).
|
|
16
|
+
#
|
|
17
|
+
# Returns Struct.
|
|
20
18
|
attr_reader :amazon_bedrock
|
|
21
19
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
20
|
+
# Anthropic configuration (Struct with +api_key+).
|
|
21
|
+
#
|
|
22
|
+
# Returns Struct.
|
|
23
|
+
attr_reader :anthropic
|
|
24
|
+
|
|
25
|
+
# OpenAI configuration (Struct with +api_key+).
|
|
26
|
+
#
|
|
27
|
+
# Returns Struct.
|
|
28
|
+
attr_reader :openai
|
|
29
|
+
|
|
30
|
+
# Initializes the configuration.
|
|
24
31
|
def initialize
|
|
25
|
-
@openai = Struct.new(:api_key).new
|
|
26
32
|
@amazon_bedrock = Struct.new(:api_token, :region).new
|
|
33
|
+
@anthropic = Struct.new(:api_key).new
|
|
34
|
+
@openai = Struct.new(:api_key).new
|
|
27
35
|
end
|
|
28
36
|
end
|
data/lib/riffer/core.rb
CHANGED
|
@@ -6,21 +6,21 @@ require "logger"
|
|
|
6
6
|
#
|
|
7
7
|
# Handles logging and configuration for the framework.
|
|
8
8
|
class Riffer::Core
|
|
9
|
-
# The logger instance for Riffer
|
|
10
|
-
#
|
|
9
|
+
# The logger instance for Riffer.
|
|
10
|
+
#
|
|
11
|
+
# Returns Logger.
|
|
11
12
|
attr_reader :logger
|
|
12
13
|
|
|
13
|
-
# Initializes the core object and logger
|
|
14
|
-
# @return [void]
|
|
14
|
+
# Initializes the core object and logger.
|
|
15
15
|
def initialize
|
|
16
16
|
@logger = Logger.new($stdout)
|
|
17
17
|
@logger.level = Logger::INFO
|
|
18
18
|
@storage_registry = {}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Yields self for configuration
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# Yields self for configuration.
|
|
22
|
+
#
|
|
23
|
+
# Yields core (Riffer::Core) to the block.
|
|
24
24
|
def configure
|
|
25
25
|
yield self if block_given?
|
|
26
26
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Helper module for converting class names.
|
|
3
4
|
module Riffer::Helpers::ClassNameConverter
|
|
4
|
-
# Converts a class name to snake_case path format
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Converts a class name to snake_case path format.
|
|
6
|
+
#
|
|
7
|
+
# class_name:: String - the class name (e.g., "Riffer::Agent")
|
|
8
|
+
#
|
|
9
|
+
# Returns String - the snake_case path (e.g., "riffer/agent").
|
|
7
10
|
def class_name_to_path(class_name)
|
|
8
11
|
class_name
|
|
9
12
|
.to_s
|