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 +4 -4
- data/.standard.yml +1 -2
- data/CHANGELOG.md +7 -0
- data/Rakefile +3 -3
- data/lib/riffer/agent.rb +144 -0
- data/lib/riffer/config.rb +14 -6
- data/lib/riffer/core.rb +19 -11
- data/lib/riffer/{dependency_helper.rb → helpers/dependencies.rb} +1 -2
- data/lib/riffer/helpers/validations.rb +10 -0
- data/lib/riffer/messages/assistant.rb +13 -15
- data/lib/riffer/messages/base.rb +10 -12
- data/lib/riffer/messages/converter.rb +40 -0
- data/lib/riffer/messages/system.rb +3 -5
- data/lib/riffer/messages/tool.rb +12 -14
- data/lib/riffer/messages/user.rb +3 -5
- data/lib/riffer/providers/base.rb +72 -84
- data/lib/riffer/providers/open_ai.rb +70 -73
- data/lib/riffer/providers/test.rb +31 -33
- data/lib/riffer/providers.rb +0 -2
- data/lib/riffer/stream_events/base.rb +7 -9
- data/lib/riffer/stream_events/text_delta.rb +8 -10
- data/lib/riffer/stream_events/text_done.rb +8 -10
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +29 -4
- metadata +17 -37
- data/.rspec +0 -3
- data/.rubocop_rspec.yml +0 -6
- data/Guardfile +0 -70
- data/lib/riffer/agents/base.rb +0 -95
- data/lib/riffer/agents.rb +0 -6
- data/lib/riffer/storage/base.rb +0 -21
- data/lib/riffer/storage.rb +0 -5
- data/lib/riffer/tools/base.rb +0 -31
- data/lib/riffer/tools.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8dc7ed9ce34f57a7b72ebbe859953e93fd147e90930943e92279d77d94cba2f
|
|
4
|
+
data.tar.gz: b8b1784bba856ad9207b1582bd75885c18f13b9ff38c6049f1278912417d02fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 849e232aa92045fcacbe0ac98433297a1c769c1ff471c91bab4507b72f4a81a59b6d0fd2de1cb354615ac5910de27466ef9f96c3f72cf2bc8e5fa668f510f923
|
|
7
|
+
data.tar.gz: bfefcc497f720b13cd3032d4a03acff0c26e8fd1b83cf19b15da925aee5c375afa3f3577fb1086c82db857a82d65af60c148110be5c2730e2695e096f10d8906
|
data/.standard.yml
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
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 "
|
|
4
|
+
require "minitest/test_task"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Minitest::TestTask.create
|
|
7
7
|
|
|
8
8
|
require "standard/rake"
|
|
9
9
|
|
|
10
|
-
task default: %i[
|
|
10
|
+
task default: %i[test standard]
|
data/lib/riffer/agent.rb
ADDED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
attr_reader :tool_calls
|
|
3
|
+
class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
4
|
+
attr_reader :tool_calls
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
def initialize(content, tool_calls: [])
|
|
7
|
+
super(content)
|
|
8
|
+
@tool_calls = tool_calls
|
|
9
|
+
end
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def role
|
|
12
|
+
"assistant"
|
|
13
|
+
end
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
attr_reader :content
|
|
3
|
+
class Riffer::Messages::Base
|
|
4
|
+
attr_reader :content
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def initialize(content)
|
|
7
|
+
@content = content
|
|
8
|
+
end
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def to_h
|
|
11
|
+
{role: role, content: content}
|
|
12
|
+
end
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
data/lib/riffer/messages/tool.rb
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
def role
|
|
13
|
+
"tool"
|
|
14
|
+
end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
data/lib/riffer/messages/user.rb
CHANGED
|
@@ -1,107 +1,95 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
class Riffer::Providers::Base
|
|
4
|
+
include Riffer::Helpers::Dependencies
|
|
5
|
+
include Riffer::Messages::Converter
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
class << self
|
|
8
|
+
def identifier(value = nil)
|
|
9
|
+
return @identifier if value.nil?
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
22
|
+
private
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def ensure_providers_loaded
|
|
25
|
+
return if @providers_loaded
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
Zeitwerk::Loader.eager_load_namespace(Riffer::Providers)
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
end
|
|
29
|
+
@providers_loaded = true
|
|
33
30
|
end
|
|
31
|
+
end
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
61
|
+
private
|
|
48
62
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
def perform_generate_text(messages, model: nil)
|
|
64
|
+
raise NotImplementedError, "Subclasses must implement #perform_generate_text"
|
|
65
|
+
end
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
def perform_stream_text(messages, model: nil)
|
|
68
|
+
raise NotImplementedError, "Subclasses must implement #perform_stream_text"
|
|
69
|
+
end
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|