riffer 0.6.1 → 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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +113 -0
  3. data/.agents/code-style.md +42 -0
  4. data/.agents/providers.md +46 -0
  5. data/.agents/rdoc.md +51 -0
  6. data/.agents/testing.md +56 -0
  7. data/.release-please-manifest.json +1 -1
  8. data/AGENTS.md +28 -0
  9. data/CHANGELOG.md +17 -0
  10. data/README.md +26 -36
  11. data/Rakefile +1 -1
  12. data/docs/01_OVERVIEW.md +106 -0
  13. data/docs/02_GETTING_STARTED.md +128 -0
  14. data/docs/03_AGENTS.md +226 -0
  15. data/docs/04_TOOLS.md +251 -0
  16. data/docs/05_MESSAGES.md +173 -0
  17. data/docs/06_STREAM_EVENTS.md +191 -0
  18. data/docs/07_CONFIGURATION.md +195 -0
  19. data/docs_providers/01_PROVIDERS.md +168 -0
  20. data/docs_providers/02_AMAZON_BEDROCK.md +196 -0
  21. data/docs_providers/03_ANTHROPIC.md +211 -0
  22. data/docs_providers/04_OPENAI.md +157 -0
  23. data/docs_providers/05_TEST_PROVIDER.md +163 -0
  24. data/docs_providers/06_CUSTOM_PROVIDERS.md +304 -0
  25. data/lib/riffer/agent.rb +220 -57
  26. data/lib/riffer/config.rb +20 -12
  27. data/lib/riffer/core.rb +7 -7
  28. data/lib/riffer/helpers/class_name_converter.rb +6 -3
  29. data/lib/riffer/helpers/dependencies.rb +18 -0
  30. data/lib/riffer/helpers/validations.rb +9 -0
  31. data/lib/riffer/messages/assistant.rb +23 -1
  32. data/lib/riffer/messages/base.rb +15 -0
  33. data/lib/riffer/messages/converter.rb +15 -5
  34. data/lib/riffer/messages/system.rb +8 -1
  35. data/lib/riffer/messages/tool.rb +58 -4
  36. data/lib/riffer/messages/user.rb +8 -1
  37. data/lib/riffer/messages.rb +7 -0
  38. data/lib/riffer/providers/amazon_bedrock.rb +128 -13
  39. data/lib/riffer/providers/anthropic.rb +209 -0
  40. data/lib/riffer/providers/base.rb +23 -18
  41. data/lib/riffer/providers/open_ai.rb +119 -39
  42. data/lib/riffer/providers/repository.rb +9 -4
  43. data/lib/riffer/providers/test.rb +78 -24
  44. data/lib/riffer/providers.rb +6 -0
  45. data/lib/riffer/stream_events/base.rb +13 -1
  46. data/lib/riffer/stream_events/reasoning_delta.rb +15 -1
  47. data/lib/riffer/stream_events/reasoning_done.rb +15 -1
  48. data/lib/riffer/stream_events/text_delta.rb +14 -1
  49. data/lib/riffer/stream_events/text_done.rb +14 -1
  50. data/lib/riffer/stream_events/tool_call_delta.rb +35 -0
  51. data/lib/riffer/stream_events/tool_call_done.rb +40 -0
  52. data/lib/riffer/stream_events.rb +9 -0
  53. data/lib/riffer/tool.rb +120 -0
  54. data/lib/riffer/tools/param.rb +68 -0
  55. data/lib/riffer/tools/params.rb +118 -0
  56. data/lib/riffer/tools.rb +9 -0
  57. data/lib/riffer/version.rb +1 -1
  58. data/lib/riffer.rb +23 -19
  59. metadata +41 -2
  60. data/CLAUDE.md +0 -73
@@ -0,0 +1,128 @@
1
+ # Getting Started
2
+
3
+ This guide walks you through installing Riffer and creating your first AI agent.
4
+
5
+ ## Installation
6
+
7
+ Add Riffer to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'riffer'
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install directly:
20
+
21
+ ```bash
22
+ gem install riffer
23
+ ```
24
+
25
+ ## Provider Setup
26
+
27
+ Riffer requires an LLM provider. Install the provider gem for your chosen service:
28
+
29
+ ### OpenAI
30
+
31
+ ```ruby
32
+ gem 'openai'
33
+ ```
34
+
35
+ Configure your API key:
36
+
37
+ ```ruby
38
+ Riffer.configure do |config|
39
+ config.openai.api_key = ENV['OPENAI_API_KEY']
40
+ end
41
+ ```
42
+
43
+ ### Amazon Bedrock
44
+
45
+ ```ruby
46
+ gem 'aws-sdk-bedrockruntime'
47
+ ```
48
+
49
+ Configure your credentials:
50
+
51
+ ```ruby
52
+ Riffer.configure do |config|
53
+ config.amazon_bedrock.region = 'us-east-1'
54
+ # Optional: Use bearer token auth instead of IAM
55
+ config.amazon_bedrock.api_token = ENV['BEDROCK_API_TOKEN']
56
+ end
57
+ ```
58
+
59
+ ## Creating Your First Agent
60
+
61
+ Define an agent by subclassing `Riffer::Agent`:
62
+
63
+ ```ruby
64
+ require 'riffer'
65
+
66
+ Riffer.configure do |config|
67
+ config.openai.api_key = ENV['OPENAI_API_KEY']
68
+ end
69
+
70
+ class GreetingAgent < Riffer::Agent
71
+ model 'openai/gpt-4o'
72
+ instructions 'You are a friendly assistant. Greet the user warmly.'
73
+ end
74
+
75
+ agent = GreetingAgent.new
76
+ response = agent.generate('Hello!')
77
+ puts response
78
+ # => "Hello! It's wonderful to meet you..."
79
+ ```
80
+
81
+ ## Streaming Responses
82
+
83
+ Use `stream` for real-time output:
84
+
85
+ ```ruby
86
+ agent = GreetingAgent.new
87
+
88
+ agent.stream('Tell me a story').each do |event|
89
+ case event
90
+ when Riffer::StreamEvents::TextDelta
91
+ print event.content
92
+ when Riffer::StreamEvents::TextDone
93
+ puts "\n[Done]"
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Adding Tools
99
+
100
+ Tools let agents interact with external systems:
101
+
102
+ ```ruby
103
+ class TimeTool < Riffer::Tool
104
+ description "Gets the current time"
105
+
106
+ def call(context:)
107
+ Time.now.strftime('%Y-%m-%d %H:%M:%S')
108
+ end
109
+ end
110
+
111
+ class TimeAgent < Riffer::Agent
112
+ model 'openai/gpt-4o'
113
+ instructions 'You can tell the user the current time.'
114
+ uses_tools [TimeTool]
115
+ end
116
+
117
+ agent = TimeAgent.new
118
+ puts agent.generate("What time is it?")
119
+ # => "The current time is 2024-01-15 14:30:00."
120
+ ```
121
+
122
+ ## Next Steps
123
+
124
+ - [Agents](03_AGENTS.md) - Agent configuration options
125
+ - [Tools](04_TOOLS.md) - Creating tools with parameters
126
+ - [Messages](05_MESSAGES.md) - Message types and history
127
+ - [Stream Events](06_STREAM_EVENTS.md) - Streaming event types
128
+ - [Providers](providers/01_PROVIDERS.md) - Provider-specific guides
data/docs/03_AGENTS.md ADDED
@@ -0,0 +1,226 @@
1
+ # Agents
2
+
3
+ Agents are the central orchestrator in Riffer. They manage the conversation flow, call LLM providers, and handle tool execution.
4
+
5
+ ## Defining an Agent
6
+
7
+ Create an agent by subclassing `Riffer::Agent`:
8
+
9
+ ```ruby
10
+ class MyAgent < Riffer::Agent
11
+ model 'openai/gpt-4o'
12
+ instructions 'You are a helpful assistant.'
13
+ end
14
+ ```
15
+
16
+ ## Configuration Methods
17
+
18
+ ### model
19
+
20
+ Sets the provider and model in `provider/model` format:
21
+
22
+ ```ruby
23
+ class MyAgent < Riffer::Agent
24
+ model 'openai/gpt-4o' # OpenAI
25
+ # or
26
+ model 'amazon_bedrock/anthropic.claude-3-sonnet-20240229-v1:0' # Bedrock
27
+ # or
28
+ model 'test/any' # Test provider
29
+ end
30
+ ```
31
+
32
+ ### instructions
33
+
34
+ Sets system instructions for the agent:
35
+
36
+ ```ruby
37
+ class MyAgent < Riffer::Agent
38
+ model 'openai/gpt-4o'
39
+ instructions 'You are an expert Ruby programmer. Provide concise answers.'
40
+ end
41
+ ```
42
+
43
+ ### identifier
44
+
45
+ Sets a custom identifier (defaults to snake_case class name):
46
+
47
+ ```ruby
48
+ class MyAgent < Riffer::Agent
49
+ model 'openai/gpt-4o'
50
+ identifier 'custom_agent_name'
51
+ end
52
+
53
+ MyAgent.identifier # => "custom_agent_name"
54
+ ```
55
+
56
+ ### uses_tools
57
+
58
+ Registers tools the agent can use:
59
+
60
+ ```ruby
61
+ class MyAgent < Riffer::Agent
62
+ model 'openai/gpt-4o'
63
+ uses_tools [WeatherTool, TimeTool]
64
+ end
65
+ ```
66
+
67
+ Tools can also be resolved dynamically with a lambda:
68
+
69
+ ```ruby
70
+ class MyAgent < Riffer::Agent
71
+ model 'openai/gpt-4o'
72
+
73
+ uses_tools ->(context) {
74
+ tools = [PublicTool]
75
+ tools << AdminTool if context&.dig(:user)&.admin?
76
+ tools
77
+ }
78
+ end
79
+ ```
80
+
81
+ ### provider_options
82
+
83
+ Passes options to the provider client:
84
+
85
+ ```ruby
86
+ class MyAgent < Riffer::Agent
87
+ model 'openai/gpt-4o'
88
+ provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
89
+ end
90
+ ```
91
+
92
+ ### model_options
93
+
94
+ Passes options to each LLM request:
95
+
96
+ ```ruby
97
+ class MyAgent < Riffer::Agent
98
+ model 'openai/gpt-4o'
99
+ model_options reasoning: 'medium', temperature: 0.7
100
+ end
101
+ ```
102
+
103
+ ## Instance Methods
104
+
105
+ ### generate
106
+
107
+ Generates a response synchronously:
108
+
109
+ ```ruby
110
+ agent = MyAgent.new
111
+
112
+ # With a string prompt
113
+ response = agent.generate('Hello')
114
+
115
+ # With message objects/hashes
116
+ response = agent.generate([
117
+ {role: 'user', content: 'Hello'},
118
+ {role: 'assistant', content: 'Hi there!'},
119
+ {role: 'user', content: 'How are you?'}
120
+ ])
121
+
122
+ # With tool context
123
+ response = agent.generate('Look up my orders', tool_context: {user_id: 123})
124
+ ```
125
+
126
+ ### stream
127
+
128
+ Streams a response as an Enumerator:
129
+
130
+ ```ruby
131
+ agent = MyAgent.new
132
+
133
+ agent.stream('Tell me a story').each do |event|
134
+ case event
135
+ when Riffer::StreamEvents::TextDelta
136
+ print event.content
137
+ when Riffer::StreamEvents::TextDone
138
+ puts "\n"
139
+ when Riffer::StreamEvents::ToolCallDone
140
+ puts "[Tool: #{event.name}]"
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### messages
146
+
147
+ Access the message history after a generate/stream call:
148
+
149
+ ```ruby
150
+ agent = MyAgent.new
151
+ agent.generate('Hello')
152
+
153
+ agent.messages.each do |msg|
154
+ puts "#{msg.role}: #{msg.content}"
155
+ end
156
+ ```
157
+
158
+ ### on_message
159
+
160
+ Registers a callback to receive messages as they're added during generation:
161
+
162
+ ```ruby
163
+ agent.on_message do |message|
164
+ case message.role
165
+ when :assistant
166
+ puts "[Assistant] #{message.content}"
167
+ when :tool
168
+ puts "[Tool:#{message.name}] #{message.content}"
169
+ end
170
+ end
171
+ ```
172
+
173
+ Multiple callbacks can be registered. Returns `self` for method chaining:
174
+
175
+ ```ruby
176
+ agent
177
+ .on_message { |msg| persist_message(msg) }
178
+ .on_message { |msg| log_message(msg) }
179
+ .generate('Hello')
180
+ ```
181
+
182
+ Works with both `generate` and `stream`. Only emits agent-generated messages (Assistant, Tool), not inputs (System, User).
183
+
184
+ ## Class Methods
185
+
186
+ ### find
187
+
188
+ Find an agent class by identifier:
189
+
190
+ ```ruby
191
+ agent_class = Riffer::Agent.find('my_agent')
192
+ agent = agent_class.new
193
+ ```
194
+
195
+ ### all
196
+
197
+ List all agent subclasses:
198
+
199
+ ```ruby
200
+ Riffer::Agent.all.each do |agent_class|
201
+ puts agent_class.identifier
202
+ end
203
+ ```
204
+
205
+ ## Tool Execution Flow
206
+
207
+ When an agent receives a response with tool calls:
208
+
209
+ 1. Agent detects `tool_calls` in the assistant message
210
+ 2. For each tool call:
211
+ - Finds the matching tool class
212
+ - Validates arguments against the tool's parameter schema
213
+ - Calls the tool's `call` method with `context` and arguments
214
+ - Creates a Tool message with the result
215
+ 3. Sends the updated message history back to the LLM
216
+ 4. Repeats until no more tool calls
217
+
218
+ ## Error Handling
219
+
220
+ Tool execution errors are captured and sent back to the LLM:
221
+
222
+ - `unknown_tool` - Tool not found in registered tools
223
+ - `validation_error` - Arguments failed validation
224
+ - `execution_error` - Tool raised an exception
225
+
226
+ The LLM can use this information to retry or respond appropriately.
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.).