riffer 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28ba9aa31a3f79392de37c769531f765a1b32aff4272fd727efeef8eb1435996
4
- data.tar.gz: 97328dee069286c025a7478414816b032682e34ce3e1649d1f96634f2fbc0b43
3
+ metadata.gz: b8dc7ed9ce34f57a7b72ebbe859953e93fd147e90930943e92279d77d94cba2f
4
+ data.tar.gz: b8b1784bba856ad9207b1582bd75885c18f13b9ff38c6049f1278912417d02fa
5
5
  SHA512:
6
- metadata.gz: 961a2559a012e481273fe92ac2ae81b4cdb827a00433c74b62c6a3041a78b64b4b374a41ea380ff1874ea11e7c883bddda391276e32bef043134c6fdbefc245e
7
- data.tar.gz: dd824a10a31dc4eac9732dac2d864698ec6118f6753001b92a8fadddf8d695e8c28240adf250341d16c5c3f93c8523bd22d81e9f5a0f7f2a9889ef020673e9da
6
+ metadata.gz: 849e232aa92045fcacbe0ac98433297a1c769c1ff471c91bab4507b72f4a81a59b6d0fd2de1cb354615ac5910de27466ef9f96c3f72cf2bc8e5fa668f510f923
7
+ data.tar.gz: bfefcc497f720b13cd3032d4a03acff0c26e8fd1b83cf19b15da925aee5c375afa3f3577fb1086c82db857a82d65af60c148110be5c2730e2695e096f10d8906
data/.standard.yml CHANGED
@@ -1,2 +1 @@
1
- extend_config:
2
- - .rubocop_rspec.yml
1
+ # Standard Ruby configuration
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0](https://github.com/bottrall/riffer/compare/v0.1.0...v0.2.0) (2025-12-28)
9
+
10
+
11
+ ### Features
12
+
13
+ * add release and publish workflows ([#35](https://github.com/bottrall/riffer/issues/35)) ([3eb0389](https://github.com/bottrall/riffer/commit/3eb03897d0e96c01ef1857c04b2bafa53e37dde0))
14
+
8
15
  ## [0.1.0] - 2024-12-20
9
16
 
10
17
  ### Added
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
4
+ require "minitest/test_task"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ Minitest::TestTask.create
7
7
 
8
8
  require "standard/rake"
9
9
 
10
- task default: %i[spec standard]
10
+ task default: %i[test standard]
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Riffer::Agent is the base class for all agents in the Riffer framework.
4
+ #
5
+ # Provides orchestration for LLM calls, tool use, and message management.
6
+ #
7
+ # @abstract
8
+ # @see Riffer::Messages
9
+ # @see Riffer::Providers
10
+ class Riffer::Agent
11
+ include Riffer::Messages::Converter
12
+
13
+ class << self
14
+ include Riffer::Helpers::Validations
15
+
16
+ # Gets or sets the agent identifier
17
+ # @param value [String, nil] the identifier to set, or nil to get
18
+ # @return [String] the agent identifier
19
+ def identifier(value = nil)
20
+ return @identifier if value.nil?
21
+ @identifier = value.to_s
22
+ end
23
+
24
+ # Gets or sets the model string (e.g., "openai/gpt-4")
25
+ # @param model_string [String, nil] the model string to set, or nil to get
26
+ # @return [String] the model string
27
+ def model(model_string = nil)
28
+ return @model if model_string.nil?
29
+ validate_is_string!(model_string, "model")
30
+ @model = model_string
31
+ end
32
+
33
+ # Gets or sets the agent instructions
34
+ # @param instructions_text [String, nil] the instructions to set, or nil to get
35
+ # @return [String] the agent instructions
36
+ def instructions(instructions_text = nil)
37
+ return @instructions if instructions_text.nil?
38
+ validate_is_string!(instructions_text, "instructions")
39
+ @instructions = instructions_text
40
+ end
41
+
42
+ # Finds an agent class by identifier
43
+ # @param identifier [String] the identifier to search for
44
+ # @return [Class, nil] the agent class, or nil if not found
45
+ def find(identifier)
46
+ subclasses.find { |agent_class| agent_class.identifier == identifier.to_s }
47
+ end
48
+
49
+ # Returns all agent subclasses
50
+ # @return [Array<Class>] all agent subclasses
51
+ def all
52
+ subclasses
53
+ end
54
+ end
55
+
56
+ # The message history for the agent
57
+ # @return [Array<Riffer::Messages::Base>]
58
+ attr_reader :messages
59
+
60
+ # Initializes a new agent
61
+ # @raise [Riffer::ArgumentError] if the configured model string is invalid (must be "provider/model")
62
+ # @return [void]
63
+ def initialize
64
+ @messages = []
65
+ @model_string = self.class.model
66
+ @instructions_text = self.class.instructions
67
+
68
+ provider_name, model_name = @model_string.split("/", 2)
69
+
70
+ raise Riffer::ArgumentError, "Invalid model string: #{@model_string}" unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
71
+
72
+ @provider_name = provider_name
73
+ @model_name = model_name
74
+ end
75
+
76
+ # Generates a response from the agent
77
+ # @param prompt_or_messages [String, Array<Hash, Riffer::Messages::Base>]
78
+ # @return [String]
79
+ def generate(prompt_or_messages)
80
+ initialize_messages(prompt_or_messages)
81
+
82
+ loop do
83
+ response = call_llm
84
+ @messages << response
85
+
86
+ break unless has_tool_calls?(response)
87
+
88
+ execute_tool_calls(response)
89
+ end
90
+
91
+ extract_final_response
92
+ end
93
+
94
+ private
95
+
96
+ def initialize_messages(prompt_or_messages)
97
+ @messages = []
98
+ @messages << Riffer::Messages::System.new(@instructions_text) if @instructions_text
99
+
100
+ if prompt_or_messages.is_a?(Array)
101
+ prompt_or_messages.each do |item|
102
+ @messages << convert_to_message_object(item)
103
+ end
104
+ else
105
+ @messages << Riffer::Messages::User.new(prompt_or_messages)
106
+ end
107
+ end
108
+
109
+ def call_llm
110
+ provider_instance.generate_text(messages: @messages, model: @model_name)
111
+ end
112
+
113
+ def provider_instance
114
+ @provider_instance ||= begin
115
+ provider_class = Riffer::Providers::Base.find(@provider_name)
116
+ raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
117
+ provider_class.new
118
+ end
119
+ end
120
+
121
+ def has_tool_calls?(response)
122
+ response.is_a?(Riffer::Messages::Assistant) && !response.tool_calls.empty?
123
+ end
124
+
125
+ def execute_tool_calls(response)
126
+ response.tool_calls.each do |tool_call|
127
+ tool_result = execute_tool_call(tool_call)
128
+ @messages << Riffer::Messages::Tool.new(
129
+ tool_result,
130
+ tool_call_id: tool_call[:id],
131
+ name: tool_call[:name]
132
+ )
133
+ end
134
+ end
135
+
136
+ def execute_tool_call(tool_call)
137
+ "Tool execution not implemented yet"
138
+ end
139
+
140
+ def extract_final_response
141
+ last_assistant_message = @messages.reverse.find { |msg| msg.is_a?(Riffer::Messages::Assistant) }
142
+ last_assistant_message&.content || ""
143
+ end
144
+ end
data/lib/riffer/config.rb CHANGED
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer
4
- class Config
5
- attr_reader :openai
3
+ # Configuration for the Riffer framework
4
+ #
5
+ # Provides configuration options for AI providers and other settings.
6
+ #
7
+ # @example Setting the OpenAI API key
8
+ # Riffer.config.openai.api_key = "sk-..."
9
+ class Riffer::Config
10
+ # OpenAI configuration
11
+ # @return [Struct]
12
+ attr_reader :openai
6
13
 
7
- def initialize
8
- @openai = Struct.new(:api_key).new
9
- end
14
+ # Initializes the configuration
15
+ # @return [void]
16
+ def initialize
17
+ @openai = Struct.new(:api_key).new
10
18
  end
11
19
  end
data/lib/riffer/core.rb CHANGED
@@ -2,18 +2,26 @@
2
2
 
3
3
  require "logger"
4
4
 
5
- module Riffer
6
- class Core
7
- attr_reader :logger
5
+ # Riffer::Core provides core functionality for the Riffer framework.
6
+ #
7
+ # Handles logging and configuration for the framework.
8
+ class Riffer::Core
9
+ # The logger instance for Riffer
10
+ # @return [Logger]
11
+ attr_reader :logger
8
12
 
9
- def initialize
10
- @logger = Logger.new($stdout)
11
- @logger.level = Logger::INFO
12
- @storage_registry = {}
13
- end
13
+ # Initializes the core object and logger
14
+ # @return [void]
15
+ def initialize
16
+ @logger = Logger.new($stdout)
17
+ @logger.level = Logger::INFO
18
+ @storage_registry = {}
19
+ end
14
20
 
15
- def configure
16
- yield self if block_given?
17
- end
21
+ # Yields self for configuration
22
+ # @yieldparam core [Riffer::Core] the core object
23
+ # @return [void]
24
+ def configure
25
+ yield self if block_given?
18
26
  end
19
27
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::DependencyHelper
3
+ module Riffer::Helpers::Dependencies
4
4
  class LoadError < ::LoadError; end
5
-
6
5
  class VersionError < ScriptError; end
7
6
 
8
7
  def depends_on(gem_name, req: true)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Helpers::Validations
4
+ def validate_is_string!(value, name = "value")
5
+ raise Riffer::ArgumentError, "#{name} must be a String" unless value.is_a?(String)
6
+ raise Riffer::ArgumentError, "#{name} cannot be empty" if value.strip.empty?
7
+
8
+ true
9
+ end
10
+ end
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Messages
4
- class Assistant < Base
5
- attr_reader :tool_calls
3
+ class Riffer::Messages::Assistant < Riffer::Messages::Base
4
+ attr_reader :tool_calls
6
5
 
7
- def initialize(content, tool_calls: [])
8
- super(content)
9
- @tool_calls = tool_calls
10
- end
6
+ def initialize(content, tool_calls: [])
7
+ super(content)
8
+ @tool_calls = tool_calls
9
+ end
11
10
 
12
- def role
13
- "assistant"
14
- end
11
+ def role
12
+ "assistant"
13
+ end
15
14
 
16
- def to_h
17
- hash = {role: role, content: content}
18
- hash[:tool_calls] = tool_calls unless tool_calls.empty?
19
- hash
20
- end
15
+ def to_h
16
+ hash = {role: role, content: content}
17
+ hash[:tool_calls] = tool_calls unless tool_calls.empty?
18
+ hash
21
19
  end
22
20
  end
@@ -1,19 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Messages
4
- class Base
5
- attr_reader :content
3
+ class Riffer::Messages::Base
4
+ attr_reader :content
6
5
 
7
- def initialize(content)
8
- @content = content
9
- end
6
+ def initialize(content)
7
+ @content = content
8
+ end
10
9
 
11
- def to_h
12
- {role: role, content: content}
13
- end
10
+ def to_h
11
+ {role: role, content: content}
12
+ end
14
13
 
15
- def role
16
- raise NotImplementedError, "Subclasses must implement #role"
17
- end
14
+ def role
15
+ raise NotImplementedError, "Subclasses must implement #role"
18
16
  end
19
17
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Messages::Converter
4
+ def convert_to_message_object(msg)
5
+ return msg if msg.is_a?(Riffer::Messages::Base)
6
+
7
+ unless msg.is_a?(Hash)
8
+ raise Riffer::ArgumentError, "Message must be a Hash or Message object, got #{msg.class}"
9
+ end
10
+
11
+ convert_hash_to_message(msg)
12
+ end
13
+
14
+ private
15
+
16
+ def convert_hash_to_message(hash)
17
+ role = hash[:role] || hash["role"]
18
+ content = hash[:content] || hash["content"]
19
+
20
+ if role.nil? || role.empty?
21
+ raise Riffer::ArgumentError, "Message hash must include a 'role' key"
22
+ end
23
+
24
+ case role
25
+ when "user"
26
+ Riffer::Messages::User.new(content)
27
+ when "assistant"
28
+ tool_calls = hash[:tool_calls] || hash["tool_calls"] || []
29
+ Riffer::Messages::Assistant.new(content, tool_calls: tool_calls)
30
+ when "system"
31
+ Riffer::Messages::System.new(content)
32
+ when "tool"
33
+ tool_call_id = hash[:tool_call_id] || hash["tool_call_id"]
34
+ name = hash[:name] || hash["name"]
35
+ Riffer::Messages::Tool.new(content, tool_call_id: tool_call_id, name: name)
36
+ else
37
+ raise Riffer::ArgumentError, "Unknown message role: #{role}"
38
+ end
39
+ end
40
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Messages
4
- class System < Base
5
- def role
6
- "system"
7
- end
3
+ class Riffer::Messages::System < Riffer::Messages::Base
4
+ def role
5
+ "system"
8
6
  end
9
7
  end
@@ -1,21 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Messages
4
- class Tool < Base
5
- attr_reader :tool_call_id, :name
3
+ class Riffer::Messages::Tool < Riffer::Messages::Base
4
+ attr_reader :tool_call_id, :name
6
5
 
7
- def initialize(content, tool_call_id:, name:)
8
- super(content)
9
- @tool_call_id = tool_call_id
10
- @name = name
11
- end
6
+ def initialize(content, tool_call_id:, name:)
7
+ super(content)
8
+ @tool_call_id = tool_call_id
9
+ @name = name
10
+ end
12
11
 
13
- def role
14
- "tool"
15
- end
12
+ def role
13
+ "tool"
14
+ end
16
15
 
17
- def to_h
18
- {role: role, content: content, tool_call_id: tool_call_id, name: name}
19
- end
16
+ def to_h
17
+ {role: role, content: content, tool_call_id: tool_call_id, name: name}
20
18
  end
21
19
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Messages
4
- class User < Base
5
- def role
6
- "user"
7
- end
3
+ class Riffer::Messages::User < Riffer::Messages::Base
4
+ def role
5
+ "user"
8
6
  end
9
7
  end
@@ -1,107 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Riffer::Providers
4
- class Base
5
- include Riffer::DependencyHelper
3
+ class Riffer::Providers::Base
4
+ include Riffer::Helpers::Dependencies
5
+ include Riffer::Messages::Converter
6
6
 
7
- class << self
8
- def identifier(value = nil)
9
- return @identifier if value.nil?
7
+ class << self
8
+ def identifier(value = nil)
9
+ return @identifier if value.nil?
10
10
 
11
- @identifier = value
12
- end
13
-
14
- def find_provider(identifier)
15
- ensure_providers_loaded
16
-
17
- provider = subclasses.find { |provider_class| provider_class.identifier == identifier }
18
-
19
- raise InvalidInputError, "Provider not found for identifier: #{identifier}" if provider.nil?
11
+ @identifier = value
12
+ end
20
13
 
21
- provider
22
- end
14
+ # Finds a provider class by identifier
15
+ # @param identifier [String, Symbol] the identifier to search for
16
+ # @return [Class, nil] the provider class, or nil if not found
17
+ def find(identifier)
18
+ ensure_providers_loaded
19
+ subclasses.find { |provider_class| provider_class.identifier == identifier }
20
+ end
23
21
 
24
- private
22
+ private
25
23
 
26
- def ensure_providers_loaded
27
- return if @providers_loaded
24
+ def ensure_providers_loaded
25
+ return if @providers_loaded
28
26
 
29
- Zeitwerk::Loader.eager_load_namespace(Riffer::Providers)
27
+ Zeitwerk::Loader.eager_load_namespace(Riffer::Providers)
30
28
 
31
- @providers_loaded = true
32
- end
29
+ @providers_loaded = true
33
30
  end
31
+ end
34
32
 
35
- def generate_text(prompt: nil, system: nil, messages: nil, model: nil)
36
- validate_input!(prompt: prompt, system: system, messages: messages)
37
- normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
38
- perform_generate_text(normalized_messages, model: model)
39
- end
33
+ # Generates text using the provider.
34
+ #
35
+ # @param prompt [String, nil] the user prompt (required when `messages` is not provided)
36
+ # @param system [String, nil] an optional system message
37
+ # @param messages [Array<Hash, Riffer::Messages::Base>, nil] optional messages array
38
+ # @param model [String, nil] optional model string to override the configured model
39
+ # @return [Riffer::Messages::Assistant] the generated assistant message
40
+ def generate_text(prompt: nil, system: nil, messages: nil, model: nil)
41
+ validate_input!(prompt: prompt, system: system, messages: messages)
42
+ normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
43
+ validate_normalized_messages!(normalized_messages)
44
+ perform_generate_text(normalized_messages, model: model)
45
+ end
40
46
 
41
- def stream_text(prompt: nil, system: nil, messages: nil, model: nil)
42
- validate_input!(prompt: prompt, system: system, messages: messages)
43
- normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
44
- perform_stream_text(normalized_messages, model: model)
45
- end
47
+ # Streams text from the provider.
48
+ #
49
+ # @param prompt [String, nil] the user prompt (required when `messages` is not provided)
50
+ # @param system [String, nil] an optional system message
51
+ # @param messages [Array<Hash, Riffer::Messages::Base>, nil] optional messages array
52
+ # @param model [String, nil] optional model string to override the configured model
53
+ # @return [Enumerator] an enumerator yielding stream events or chunks (provider-specific)
54
+ def stream_text(prompt: nil, system: nil, messages: nil, model: nil)
55
+ validate_input!(prompt: prompt, system: system, messages: messages)
56
+ normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
57
+ validate_normalized_messages!(normalized_messages)
58
+ perform_stream_text(normalized_messages, model: model)
59
+ end
46
60
 
47
- private
61
+ private
48
62
 
49
- def perform_generate_text(messages, model: nil)
50
- raise NotImplementedError, "Subclasses must implement #perform_generate_text"
51
- end
63
+ def perform_generate_text(messages, model: nil)
64
+ raise NotImplementedError, "Subclasses must implement #perform_generate_text"
65
+ end
52
66
 
53
- def perform_stream_text(messages, model: nil)
54
- raise NotImplementedError, "Subclasses must implement #perform_stream_text"
55
- end
67
+ def perform_stream_text(messages, model: nil)
68
+ raise NotImplementedError, "Subclasses must implement #perform_stream_text"
69
+ end
56
70
 
57
- def validate_input!(prompt:, system:, messages:)
58
- if messages.nil?
59
- raise InvalidInputError, "prompt is required when messages is not provided" if prompt.nil?
60
- else
61
- raise InvalidInputError, "cannot provide both prompt and messages" unless prompt.nil?
62
- raise InvalidInputError, "cannot provide both system and messages" unless system.nil?
63
- raise InvalidInputError, "messages must include at least one user message" unless has_user_message?(messages)
64
- end
71
+ def validate_input!(prompt:, system:, messages:)
72
+ if messages.nil?
73
+ raise Riffer::ArgumentError, "prompt is required when messages is not provided" if prompt.nil?
74
+ else
75
+ raise Riffer::ArgumentError, "cannot provide both prompt and messages" unless prompt.nil?
76
+ raise Riffer::ArgumentError, "cannot provide both system and messages" unless system.nil?
65
77
  end
78
+ end
66
79
 
67
- def normalize_messages(prompt:, system:, messages:)
68
- if messages
69
- return messages.map { |msg| convert_to_message_object(msg) }
70
- end
71
-
72
- result = []
73
- result << Riffer::Messages::System.new(system) if system
74
- result << Riffer::Messages::User.new(prompt)
75
- result
80
+ def normalize_messages(prompt:, system:, messages:)
81
+ if messages
82
+ return messages.map { |msg| convert_to_message_object(msg) }
76
83
  end
77
84
 
78
- def convert_to_message_object(msg)
79
- if msg.is_a?(Riffer::Messages::Base)
80
- return msg
81
- end
82
-
83
- unless msg.is_a?(Hash)
84
- raise InvalidInputError, "Message must be a Hash or Message object, got #{msg.class}"
85
- end
86
-
87
- case msg[:role]
88
- when "user"
89
- Riffer::Messages::User.new(msg[:content])
90
- when "assistant"
91
- Riffer::Messages::Assistant.new(msg[:content], tool_calls: msg[:tool_calls] || [])
92
- when "system"
93
- Riffer::Messages::System.new(msg[:content])
94
- when "tool"
95
- Riffer::Messages::Tool.new(msg[:content], tool_call_id: msg[:tool_call_id], name: msg[:name])
96
- else
97
- raise InvalidInputError, "Unknown message role: #{msg[:role]}"
98
- end
99
- end
85
+ result = []
86
+ result << Riffer::Messages::System.new(system) if system
87
+ result << Riffer::Messages::User.new(prompt)
88
+ result
89
+ end
100
90
 
101
- def has_user_message?(messages)
102
- messages.any? do |msg|
103
- msg.is_a?(Riffer::Messages::User) || (msg.is_a?(Hash) && msg[:role] == "user")
104
- end
105
- end
91
+ def validate_normalized_messages!(messages)
92
+ has_user = messages.any? { |msg| msg.is_a?(Riffer::Messages::User) }
93
+ raise Riffer::ArgumentError, "messages must include at least one user message" unless has_user
106
94
  end
107
95
  end