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.
Files changed (59) 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 +21 -308
  9. data/CHANGELOG.md +10 -0
  10. data/README.md +21 -112
  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 +97 -43
  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 +45 -2
  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 +8 -4
  39. data/lib/riffer/providers/anthropic.rb +209 -0
  40. data/lib/riffer/providers/base.rb +17 -12
  41. data/lib/riffer/providers/open_ai.rb +7 -1
  42. data/lib/riffer/providers/repository.rb +9 -4
  43. data/lib/riffer/providers/test.rb +25 -7
  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 +18 -11
  51. data/lib/riffer/stream_events/tool_call_done.rb +22 -12
  52. data/lib/riffer/stream_events.rb +9 -0
  53. data/lib/riffer/tool.rb +57 -25
  54. data/lib/riffer/tools/param.rb +19 -16
  55. data/lib/riffer/tools/params.rb +28 -22
  56. data/lib/riffer/tools.rb +5 -0
  57. data/lib/riffer/version.rb +1 -1
  58. data/lib/riffer.rb +21 -21
  59. 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.).
@@ -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
+ ```