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
data/lib/riffer/agent.rb CHANGED
@@ -1,12 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  # Riffer::Agent is the base class for all agents in the Riffer framework.
4
6
  #
5
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!')
6
19
  #
7
- # @abstract
8
- # @see Riffer::Messages
9
- # @see Riffer::Providers
10
20
  class Riffer::Agent
11
21
  include Riffer::Messages::Converter
12
22
 
@@ -14,64 +24,99 @@ class Riffer::Agent
14
24
  include Riffer::Helpers::ClassNameConverter
15
25
  include Riffer::Helpers::Validations
16
26
 
17
- # Gets or sets the agent identifier
18
- # @param value [String, nil] the identifier to set, or nil to get
19
- # @return [String] the agent identifier
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.
20
32
  def identifier(value = nil)
21
33
  return @identifier || class_name_to_path(name) if value.nil?
22
34
  @identifier = value.to_s
23
35
  end
24
36
 
25
- # Gets or sets the model string (e.g., "openai/gpt-4")
26
- # @param model_string [String, nil] the model string to set, or nil to get
27
- # @return [String] the model string
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.
28
42
  def model(model_string = nil)
29
43
  return @model if model_string.nil?
30
44
  validate_is_string!(model_string, "model")
31
45
  @model = model_string
32
46
  end
33
47
 
34
- # Gets or sets the agent instructions
35
- # @param instructions_text [String, nil] the instructions to set, or nil to get
36
- # @return [String] the agent instructions
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.
37
53
  def instructions(instructions_text = nil)
38
54
  return @instructions if instructions_text.nil?
39
55
  validate_is_string!(instructions_text, "instructions")
40
56
  @instructions = instructions_text
41
57
  end
42
58
 
43
- def reasoning(level = nil)
44
- return @reasoning if level.nil?
45
- validate_is_string!(level, "reasoning")
46
- @reasoning = level
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.
64
+ def provider_options(options = nil)
65
+ return @provider_options || {} if options.nil?
66
+ @provider_options = options
67
+ end
68
+
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.
74
+ def model_options(options = nil)
75
+ return @model_options || {} if options.nil?
76
+ @model_options = options
77
+ end
78
+
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.
84
+ def uses_tools(tools_or_lambda = nil)
85
+ return @tools_config if tools_or_lambda.nil?
86
+ @tools_config = tools_or_lambda
47
87
  end
48
88
 
49
- # Finds an agent class by identifier
50
- # @param identifier [String] the identifier to search for
51
- # @return [Class, nil] the agent class, or nil if not found
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.
52
94
  def find(identifier)
53
95
  subclasses.find { |agent_class| agent_class.identifier == identifier.to_s }
54
96
  end
55
97
 
56
- # Returns all agent subclasses
57
- # @return [Array<Class>] all agent subclasses
98
+ # Returns all agent subclasses.
99
+ #
100
+ # Returns Array of Class - all agent subclasses.
58
101
  def all
59
102
  subclasses
60
103
  end
61
104
  end
62
105
 
63
- # The message history for the agent
64
- # @return [Array<Riffer::Messages::Base>]
106
+ # The message history for the agent.
107
+ #
108
+ # Returns Array of Riffer::Messages::Base.
65
109
  attr_reader :messages
66
110
 
67
- # Initializes a new agent
68
- # @raise [Riffer::ArgumentError] if the configured model string is invalid (must be "provider/model")
69
- # @return [void]
111
+ # Initializes a new agent.
112
+ #
113
+ # Raises Riffer::ArgumentError if the configured model string is invalid
114
+ # (must be "provider/model" format).
70
115
  def initialize
71
116
  @messages = []
117
+ @message_callbacks = []
72
118
  @model_string = self.class.model
73
119
  @instructions_text = self.class.instructions
74
- @reasoning = self.class.reasoning
75
120
 
76
121
  provider_name, model_name = @model_string.split("/", 2)
77
122
 
@@ -81,15 +126,20 @@ class Riffer::Agent
81
126
  @model_name = model_name
82
127
  end
83
128
 
84
- # Generates a response from the agent
85
- # @param prompt_or_messages [String, Array<Hash, Riffer::Messages::Base>]
86
- # @return [String]
87
- def generate(prompt_or_messages)
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.
135
+ def generate(prompt_or_messages, tool_context: nil)
136
+ @tool_context = tool_context
137
+ @resolved_tools = nil
88
138
  initialize_messages(prompt_or_messages)
89
139
 
90
140
  loop do
91
141
  response = call_llm
92
- @messages << response
142
+ add_message(response)
93
143
 
94
144
  break unless has_tool_calls?(response)
95
145
 
@@ -99,33 +149,76 @@ class Riffer::Agent
99
149
  extract_final_response
100
150
  end
101
151
 
102
- # Streams a response from the agent
103
- # @param prompt_or_messages [String, Array<Hash, Riffer::Messages::Base>]
104
- # @return [Enumerator] an enumerator yielding stream events
105
- def stream(prompt_or_messages)
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.
158
+ def stream(prompt_or_messages, tool_context: nil)
159
+ @tool_context = tool_context
160
+ @resolved_tools = nil
106
161
  initialize_messages(prompt_or_messages)
107
162
 
108
163
  Enumerator.new do |yielder|
109
- accumulated_content = ""
164
+ loop do
165
+ accumulated_content = ""
166
+ accumulated_tool_calls = []
167
+ current_tool_call = nil
168
+
169
+ call_llm_stream.each do |event|
170
+ yielder << event
171
+
172
+ case event
173
+ when Riffer::StreamEvents::TextDelta
174
+ accumulated_content += event.content
175
+ when Riffer::StreamEvents::TextDone
176
+ accumulated_content = event.content
177
+ when Riffer::StreamEvents::ToolCallDelta
178
+ current_tool_call ||= {item_id: event.item_id, name: event.name, arguments: ""}
179
+ current_tool_call[:arguments] += event.arguments_delta
180
+ current_tool_call[:name] ||= event.name
181
+ when Riffer::StreamEvents::ToolCallDone
182
+ accumulated_tool_calls << {
183
+ id: event.item_id,
184
+ call_id: event.call_id,
185
+ name: event.name,
186
+ arguments: event.arguments
187
+ }
188
+ current_tool_call = nil
189
+ end
190
+ end
110
191
 
111
- call_llm_stream.each do |event|
112
- yielder << event
192
+ response = Riffer::Messages::Assistant.new(accumulated_content, tool_calls: accumulated_tool_calls)
193
+ add_message(response)
113
194
 
114
- case event
115
- when Riffer::StreamEvents::TextDelta
116
- accumulated_content += event.content
117
- when Riffer::StreamEvents::TextDone
118
- accumulated_content = event.content
119
- end
120
- end
195
+ break unless has_tool_calls?(response)
121
196
 
122
- response = Riffer::Messages::Assistant.new(accumulated_content)
123
- @messages << response
197
+ execute_tool_calls(response)
198
+ end
124
199
  end
125
200
  end
126
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
+
127
215
  private
128
216
 
217
+ def add_message(message)
218
+ @messages << message
219
+ @message_callbacks.each { |callback| callback.call(message) }
220
+ end
221
+
129
222
  def initialize_messages(prompt_or_messages)
130
223
  @messages = []
131
224
  @messages << Riffer::Messages::System.new(@instructions_text) if @instructions_text
@@ -140,18 +233,28 @@ class Riffer::Agent
140
233
  end
141
234
 
142
235
  def call_llm
143
- provider_instance.generate_text(messages: @messages, model: @model_name, reasoning: @reasoning)
236
+ provider_instance.generate_text(
237
+ messages: @messages,
238
+ model: @model_name,
239
+ tools: resolved_tools,
240
+ **self.class.model_options
241
+ )
144
242
  end
145
243
 
146
244
  def call_llm_stream
147
- provider_instance.stream_text(messages: @messages, model: @model_name, reasoning: @reasoning)
245
+ provider_instance.stream_text(
246
+ messages: @messages,
247
+ model: @model_name,
248
+ tools: resolved_tools,
249
+ **self.class.model_options
250
+ )
148
251
  end
149
252
 
150
253
  def provider_instance
151
254
  @provider_instance ||= begin
152
255
  provider_class = Riffer::Providers::Repository.find(@provider_name)
153
256
  raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
154
- provider_class.new
257
+ provider_class.new(**self.class.provider_options)
155
258
  end
156
259
  end
157
260
 
@@ -161,17 +264,77 @@ class Riffer::Agent
161
264
 
162
265
  def execute_tool_calls(response)
163
266
  response.tool_calls.each do |tool_call|
164
- tool_result = execute_tool_call(tool_call)
165
- @messages << Riffer::Messages::Tool.new(
166
- tool_result,
267
+ result = execute_tool_call(tool_call)
268
+ add_message(Riffer::Messages::Tool.new(
269
+ result[:content],
167
270
  tool_call_id: tool_call[:id],
168
- name: tool_call[:name]
169
- )
271
+ name: tool_call[:name],
272
+ error: result[:error],
273
+ error_type: result[:error_type]
274
+ ))
170
275
  end
171
276
  end
172
277
 
173
278
  def execute_tool_call(tool_call)
174
- "Tool execution not implemented yet"
279
+ tool_class = find_tool_class(tool_call[:name])
280
+
281
+ if tool_class.nil?
282
+ return {
283
+ content: "Error: Unknown tool '#{tool_call[:name]}'",
284
+ error: "Unknown tool '#{tool_call[:name]}'",
285
+ error_type: :unknown_tool
286
+ }
287
+ end
288
+
289
+ tool_instance = tool_class.new
290
+ arguments = parse_tool_arguments(tool_call[:arguments])
291
+
292
+ begin
293
+ result = tool_instance.call_with_validation(context: @tool_context, **arguments)
294
+ {content: result.to_s, error: nil, error_type: nil}
295
+ rescue Riffer::TimeoutError => e
296
+ {
297
+ content: "Error: #{e.message}",
298
+ error: e.message,
299
+ error_type: :timeout_error
300
+ }
301
+ rescue Riffer::ValidationError => e
302
+ {
303
+ content: "Validation error: #{e.message}",
304
+ error: e.message,
305
+ error_type: :validation_error
306
+ }
307
+ rescue => e
308
+ {
309
+ content: "Error executing tool: #{e.message}",
310
+ error: e.message,
311
+ error_type: :execution_error
312
+ }
313
+ end
314
+ end
315
+
316
+ def resolved_tools
317
+ @resolved_tools ||= begin
318
+ config = self.class.uses_tools
319
+ return [] if config.nil?
320
+
321
+ if config.is_a?(Proc)
322
+ (config.arity == 0) ? config.call : config.call(@tool_context)
323
+ else
324
+ config
325
+ end
326
+ end
327
+ end
328
+
329
+ def find_tool_class(name)
330
+ resolved_tools.find { |tool_class| tool_class.name == name }
331
+ end
332
+
333
+ def parse_tool_arguments(arguments)
334
+ return {} if arguments.nil? || arguments.empty?
335
+
336
+ args = arguments.is_a?(String) ? JSON.parse(arguments) : arguments
337
+ args.transform_keys(&:to_sym)
175
338
  end
176
339
 
177
340
  def extract_final_response
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
- # OpenAI configuration
15
- # @return [Struct]
16
- attr_reader :openai
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
- # Initializes the configuration
23
- # @return [void]
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
- # @return [Logger]
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
- # @yieldparam core [Riffer::Core] the core object
23
- # @return [void]
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
- # @param class_name [String] the class name (e.g., "Riffer::Agent")
6
- # @return [String] the snake_case path (e.g., "riffer/agent")
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
@@ -1,9 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Helper module for lazy loading gem dependencies.
4
+ #
5
+ # Used by providers to load their required gems only when needed.
3
6
  module Riffer::Helpers::Dependencies
7
+ # Raised when a required gem cannot be loaded.
4
8
  class LoadError < ::LoadError; end
9
+
10
+ # Raised when a gem version requirement is not satisfied.
5
11
  class VersionError < ScriptError; end
6
12
 
13
+ # Declares a dependency on a gem.
14
+ #
15
+ # Verifies the gem is installed and satisfies version requirements,
16
+ # then requires it.
17
+ #
18
+ # gem_name:: String - the gem name
19
+ # req:: Boolean or String - true to require the gem, false to skip, or String to require a different lib
20
+ #
21
+ # Returns true if successful.
22
+ #
23
+ # Raises LoadError if the gem is not installed.
24
+ # Raises VersionError if the gem version does not satisfy requirements.
7
25
  def depends_on(gem_name, req: true)
8
26
  gem(gem_name)
9
27
 
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Helper module for input validation.
3
4
  module Riffer::Helpers::Validations
5
+ # Validates that a value is a non-empty string.
6
+ #
7
+ # value:: Object - the value to validate
8
+ # name:: String - the name of the value for error messages
9
+ #
10
+ # Returns true if valid.
11
+ #
12
+ # Raises Riffer::ArgumentError if the value is not a string or is empty.
4
13
  def validate_is_string!(value, name = "value")
5
14
  raise Riffer::ArgumentError, "#{name} must be a String" unless value.is_a?(String)
6
15
  raise Riffer::ArgumentError, "#{name} cannot be empty" if value.strip.empty?
@@ -1,17 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Represents an assistant (LLM) message in a conversation.
4
+ #
5
+ # May include tool calls when the LLM requests tool execution.
6
+ #
7
+ # msg = Riffer::Messages::Assistant.new("Hello!")
8
+ # msg.role # => :assistant
9
+ # msg.content # => "Hello!"
10
+ # msg.tool_calls # => []
11
+ #
3
12
  class Riffer::Messages::Assistant < Riffer::Messages::Base
13
+ # Array of tool calls requested by the assistant.
14
+ #
15
+ # Each tool call is a Hash with +:id+, +:call_id+, +:name+, and +:arguments+ keys.
16
+ #
17
+ # Returns Array of Hash.
4
18
  attr_reader :tool_calls
5
19
 
20
+ # Creates a new assistant message.
21
+ #
22
+ # content:: String - the message content
23
+ # tool_calls:: Array of Hash - optional tool calls
6
24
  def initialize(content, tool_calls: [])
7
25
  super(content)
8
26
  @tool_calls = tool_calls
9
27
  end
10
28
 
29
+ # Returns :assistant.
11
30
  def role
12
- "assistant"
31
+ :assistant
13
32
  end
14
33
 
34
+ # Converts the message to a hash.
35
+ #
36
+ # Returns Hash with +:role+, +:content+, and optionally +:tool_calls+.
15
37
  def to_h
16
38
  hash = {role: role, content: content}
17
39
  hash[:tool_calls] = tool_calls unless tool_calls.empty?
@@ -1,16 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Base class for all message types in the Riffer framework.
4
+ #
5
+ # Subclasses must implement the +role+ method.
3
6
  class Riffer::Messages::Base
7
+ # The message content.
8
+ #
9
+ # Returns String.
4
10
  attr_reader :content
5
11
 
12
+ # Creates a new message.
13
+ #
14
+ # content:: String - the message content
6
15
  def initialize(content)
7
16
  @content = content
8
17
  end
9
18
 
19
+ # Converts the message to a hash.
20
+ #
21
+ # Returns Hash with +:role+ and +:content+ keys.
10
22
  def to_h
11
23
  {role: role, content: content}
12
24
  end
13
25
 
26
+ # Returns the message role.
27
+ #
28
+ # Raises NotImplementedError if not implemented by subclass.
14
29
  def role
15
30
  raise NotImplementedError, "Subclasses must implement #role"
16
31
  end
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Module for converting hashes to message objects.
4
+ #
5
+ # Included in Agent and Provider classes to handle message normalization.
3
6
  module Riffer::Messages::Converter
7
+ # Converts a hash or message object to a Riffer::Messages::Base subclass.
8
+ #
9
+ # msg:: Hash or Riffer::Messages::Base - the message to convert
10
+ #
11
+ # Returns Riffer::Messages::Base subclass.
12
+ #
13
+ # Raises Riffer::ArgumentError if the message format is invalid.
4
14
  def convert_to_message_object(msg)
5
15
  return msg if msg.is_a?(Riffer::Messages::Base)
6
16
 
@@ -21,15 +31,15 @@ module Riffer::Messages::Converter
21
31
  raise Riffer::ArgumentError, "Message hash must include a 'role' key"
22
32
  end
23
33
 
24
- case role
25
- when "user"
34
+ case role.to_sym
35
+ when :user
26
36
  Riffer::Messages::User.new(content)
27
- when "assistant"
37
+ when :assistant
28
38
  tool_calls = hash[:tool_calls] || hash["tool_calls"] || []
29
39
  Riffer::Messages::Assistant.new(content, tool_calls: tool_calls)
30
- when "system"
40
+ when :system
31
41
  Riffer::Messages::System.new(content)
32
- when "tool"
42
+ when :tool
33
43
  tool_call_id = hash[:tool_call_id] || hash["tool_call_id"]
34
44
  name = hash[:name] || hash["name"]
35
45
  Riffer::Messages::Tool.new(content, tool_call_id: tool_call_id, name: name)
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Represents a system message (instructions) in a conversation.
4
+ #
5
+ # msg = Riffer::Messages::System.new("You are a helpful assistant.")
6
+ # msg.role # => :system
7
+ # msg.content # => "You are a helpful assistant."
8
+ #
3
9
  class Riffer::Messages::System < Riffer::Messages::Base
10
+ # Returns :system.
4
11
  def role
5
- "system"
12
+ :system
6
13
  end
7
14
  end