riffer 0.42.0 → 0.44.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +19 -0
- data/Steepfile +1 -0
- data/docs/AGENTS.md +23 -2
- data/docs/AGENT_LOOP.md +6 -4
- data/docs/CONFIGURATION.md +32 -0
- data/docs/MESSAGES.md +10 -0
- data/docs/OVERVIEW.md +1 -0
- data/docs/TESTING.md +129 -0
- data/docs/TOOLS.md +27 -3
- data/docs/TOOL_ADVANCED.md +20 -7
- data/docs/TRACING.md +5 -4
- data/docs/providers/AMAZON_BEDROCK.md +17 -0
- data/docs/providers/GEMINI.md +2 -3
- data/docs-site/manifest.yml +3 -0
- data/lib/riffer/config.rb +94 -0
- data/lib/riffer/files/downloader.rb +78 -0
- data/lib/riffer/files/resolver.rb +97 -0
- data/lib/riffer/files.rb +5 -0
- data/lib/riffer/helpers/identifier.rb +11 -3
- data/lib/riffer/messages/file_part.rb +61 -9
- data/lib/riffer/messages/tool.rb +2 -1
- data/lib/riffer/providers/amazon_bedrock.rb +7 -7
- data/lib/riffer/providers/base.rb +8 -0
- data/lib/riffer/providers/gemini.rb +6 -5
- data/lib/riffer/providers/open_ai.rb +6 -0
- data/lib/riffer/providers/open_router.rb +6 -0
- data/lib/riffer/registrable.rb +105 -22
- data/lib/riffer/testing/minitest.rb +21 -0
- data/lib/riffer/testing/rspec.rb +11 -0
- data/lib/riffer/testing.rb +130 -0
- data/lib/riffer/tool.rb +24 -9
- data/lib/riffer/tools/response.rb +10 -5
- data/lib/riffer/tools/runtime.rb +20 -11
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +34 -1
- data/sig/_private/minitest.rbs +9 -0
- data/sig/_private/riffer/testing/minitest.rbs +6 -0
- data/sig/_private/rspec.rbs +7 -0
- data/sig/_private/zeitwerk.rbs +2 -0
- data/sig/generated/riffer/config.rbs +64 -0
- data/sig/generated/riffer/files/downloader.rbs +16 -0
- data/sig/generated/riffer/files/resolver.rbs +36 -0
- data/sig/generated/riffer/files.rbs +4 -0
- data/sig/generated/riffer/helpers/identifier.rbs +8 -0
- data/sig/generated/riffer/messages/file_part.rbs +38 -6
- data/sig/generated/riffer/messages/tool.rbs +2 -1
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +4 -0
- data/sig/generated/riffer/providers/base.rbs +4 -0
- data/sig/generated/riffer/providers/gemini.rbs +4 -0
- data/sig/generated/riffer/providers/open_ai.rbs +4 -0
- data/sig/generated/riffer/providers/open_router.rbs +4 -0
- data/sig/generated/riffer/registrable.rbs +57 -9
- data/sig/generated/riffer/testing/rspec.rbs +2 -0
- data/sig/generated/riffer/testing.rbs +75 -0
- data/sig/generated/riffer/tool.rbs +4 -5
- data/sig/generated/riffer/tools/response.rbs +8 -4
- data/sig/generated/riffer/tools/runtime.rbs +5 -4
- data/sig/generated/riffer.rbs +35 -1
- data/sig/manifest.yaml +1 -0
- data/sig/manual/riffer/testing.rbs +5 -0
- metadata +31 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Builds throwaway agents and tools a test suite can resolve by identifier or
|
|
5
|
+
# constant name, and removes them again. Require <tt>riffer/testing/rspec</tt>
|
|
6
|
+
# or <tt>riffer/testing/minitest</tt> to get +stub_agent+/+stub_tool+ in every
|
|
7
|
+
# example plus per-test cleanup; otherwise include this module and call +reset!+
|
|
8
|
+
# from your own teardown, or call the methods on the module directly.
|
|
9
|
+
#
|
|
10
|
+
# Tracking is not synchronized — stub from a single-threaded test, before
|
|
11
|
+
# concurrent lookups begin.
|
|
12
|
+
module Riffer::Testing
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
# @rbs self.@registrations: Array[[Class, String?]]?
|
|
16
|
+
|
|
17
|
+
CONST_NAME_PATTERN = /\A[A-Z][A-Za-z0-9_]*\z/ #: Regexp
|
|
18
|
+
private_constant :CONST_NAME_PATTERN
|
|
19
|
+
|
|
20
|
+
# Builds an agent class, evaluates the optional body in it, and makes it
|
|
21
|
+
# resolvable until the next +reset!+. A +name+ assigns a top-level constant
|
|
22
|
+
# and derives the identifier; the body may set +identifier+ like any other
|
|
23
|
+
# agent config.
|
|
24
|
+
#
|
|
25
|
+
# agent = stub_agent("SupportAgent") { model "mock/gpt-5-mini" }
|
|
26
|
+
#
|
|
27
|
+
# Raises Riffer::ArgumentError when the constant is already defined or the
|
|
28
|
+
# stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
|
|
29
|
+
# another agent already holds the identifier.
|
|
30
|
+
#
|
|
31
|
+
#--
|
|
32
|
+
#: (?(String | Symbol)?, ?base: singleton(Riffer::Agent)) ?{ () [self: singleton(Riffer::Agent)] -> void } -> singleton(Riffer::Agent)
|
|
33
|
+
def stub_agent(name = nil, base: Riffer::Agent, &body)
|
|
34
|
+
build_stub(name, base: base, &body) #: singleton(Riffer::Agent)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Builds a tool class, evaluates the optional body in it, and makes it
|
|
38
|
+
# resolvable until the next +reset!+. A +name+ assigns a top-level constant
|
|
39
|
+
# and derives the identifier; the body may set +identifier+ like any other
|
|
40
|
+
# tool config.
|
|
41
|
+
#
|
|
42
|
+
# tool = stub_tool("KbSearch") { def call(context:, **) = text("stubbed") }
|
|
43
|
+
#
|
|
44
|
+
# Raises Riffer::ArgumentError when the constant is already defined or the
|
|
45
|
+
# stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
|
|
46
|
+
# another tool already holds the identifier.
|
|
47
|
+
#
|
|
48
|
+
#--
|
|
49
|
+
#: (?(String | Symbol)?, ?base: singleton(Riffer::Tool)) ?{ () [self: singleton(Riffer::Tool)] -> void } -> singleton(Riffer::Tool)
|
|
50
|
+
def stub_tool(name = nil, base: Riffer::Tool, &body)
|
|
51
|
+
build_stub(name, base: base, &body) #: singleton(Riffer::Tool)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removes every stub built since the last reset — its registration and any
|
|
55
|
+
# constant it created — newest first, and forgets them. A no-op when nothing
|
|
56
|
+
# has been stubbed.
|
|
57
|
+
#--
|
|
58
|
+
#: () -> void
|
|
59
|
+
def reset!
|
|
60
|
+
tracked = Riffer::Testing.registrations
|
|
61
|
+
tracked.reverse_each do |stub, const_name|
|
|
62
|
+
registrable = stub.superclass #: untyped
|
|
63
|
+
registrable.unregister(stub)
|
|
64
|
+
remove_stub_const(const_name, stub) if const_name
|
|
65
|
+
end
|
|
66
|
+
tracked.clear
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The stub classes awaiting cleanup. Lives on the module rather than the
|
|
70
|
+
# caller so an including test case and a direct
|
|
71
|
+
# <tt>Riffer::Testing.stub_*</tt> call share one list.
|
|
72
|
+
#--
|
|
73
|
+
#: () -> Array[[Class, String?]]
|
|
74
|
+
def self.registrations # :nodoc:
|
|
75
|
+
@registrations ||= []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
#--
|
|
81
|
+
#: ((String | Symbol)?, base: Class) ?{ () [self: untyped] -> void } -> Class
|
|
82
|
+
def build_stub(name, base:, &body)
|
|
83
|
+
const_name = validate_const_name(name)
|
|
84
|
+
stub = Class.new(base)
|
|
85
|
+
configurable = stub #: untyped
|
|
86
|
+
configurable.identifier(Riffer::Helpers::Identifier.derive(const_name)) if const_name
|
|
87
|
+
configurable.class_eval(&body) if body
|
|
88
|
+
if configurable.identifier.to_s.strip.empty?
|
|
89
|
+
raise Riffer::ArgumentError, "a stub needs a name, or an identifier set in its block"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The class must be registered while still anonymous: naming it first makes
|
|
93
|
+
# it implicitly live, and +register+ rejects an identifier the registry
|
|
94
|
+
# already resolves — even to this same class.
|
|
95
|
+
registrable = base #: untyped
|
|
96
|
+
registrable.register(stub)
|
|
97
|
+
Object.const_set(const_name, stub) if const_name
|
|
98
|
+
Riffer::Testing.registrations << [stub, const_name]
|
|
99
|
+
|
|
100
|
+
stub
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#--
|
|
104
|
+
#: ((String | Symbol)?) -> String?
|
|
105
|
+
def validate_const_name(name)
|
|
106
|
+
return nil if name.nil?
|
|
107
|
+
|
|
108
|
+
const_name = name.to_s
|
|
109
|
+
unless CONST_NAME_PATTERN.match?(const_name)
|
|
110
|
+
raise Riffer::ArgumentError, "#{const_name.inspect} is not a simple top-level constant name"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# True of a pending autoload too — the name is taken either way.
|
|
114
|
+
if Object.const_defined?(const_name, false)
|
|
115
|
+
raise Riffer::ArgumentError, "#{const_name} is already defined; use stub_const to replace a real class"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
const_name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A test may have removed or replaced the constant itself, so never clobber
|
|
122
|
+
# one that no longer points at the stub.
|
|
123
|
+
#--
|
|
124
|
+
#: (String, Class) -> void
|
|
125
|
+
def remove_stub_const(const_name, stub)
|
|
126
|
+
return unless Object.const_defined?(const_name, false) && Object.const_get(const_name, false).equal?(stub)
|
|
127
|
+
|
|
128
|
+
Object.send(:remove_const, const_name)
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -56,19 +56,23 @@ class Riffer::Tool
|
|
|
56
56
|
Riffer::Tools::Response.error(message, type: type)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Executes the tool with validation and timeout
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# Raises Riffer::Error if the tool does not return a Response object.
|
|
59
|
+
# Executes the tool with validation and timeout, folding every +StandardError+
|
|
60
|
+
# into an error Response. Anything outside +StandardError+ — an unimplemented
|
|
61
|
+
# +#call+ above all — still propagates, because a broken tool is a broken
|
|
62
|
+
# deploy rather than a bad request.
|
|
64
63
|
#
|
|
65
64
|
#--
|
|
66
65
|
#: (context: Riffer::Agent::Context?, **untyped) -> Riffer::Tools::Response
|
|
67
66
|
def call_with_validation(context:, **kwargs)
|
|
68
67
|
params_builder = self.class.params
|
|
69
|
-
validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
begin
|
|
70
|
+
validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
|
|
71
|
+
rescue Riffer::ValidationError => e
|
|
72
|
+
return Riffer::Tools::Response.error(e.message, type: :validation_error)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
result = Timeout.timeout(self.class.timeout, Riffer::TimeoutError) do
|
|
72
76
|
call(context: context, **validated_args) #: untyped
|
|
73
77
|
end
|
|
74
78
|
|
|
@@ -77,7 +81,18 @@ class Riffer::Tool
|
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
result
|
|
80
|
-
rescue
|
|
81
|
-
|
|
84
|
+
rescue Riffer::TimeoutError
|
|
85
|
+
Riffer::Tools::Response.error(
|
|
86
|
+
"Tool execution timed out after #{self.class.timeout} seconds",
|
|
87
|
+
type: :timeout_error,
|
|
88
|
+
)
|
|
89
|
+
rescue Riffer::ToolExecutionError => e
|
|
90
|
+
Riffer::Tools::Response.error(e.message, type: :execution_error)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Riffer::Tools::Response.error(
|
|
93
|
+
"Error executing tool: #{e.class}: #{e.message}",
|
|
94
|
+
type: :unhandled_error,
|
|
95
|
+
exception: e,
|
|
96
|
+
)
|
|
82
97
|
end
|
|
83
98
|
end
|
|
@@ -28,6 +28,10 @@ class Riffer::Tools::Response
|
|
|
28
28
|
# The error type, or +nil+ on success.
|
|
29
29
|
attr_reader :error_type #: Symbol?
|
|
30
30
|
|
|
31
|
+
# The exception an unhandled failure was folded from, or +nil+. Kept out of
|
|
32
|
+
# every serialized form so it never reaches an LLM or a message payload.
|
|
33
|
+
attr_reader :exception #: Exception?
|
|
34
|
+
|
|
31
35
|
# Creates a success response.
|
|
32
36
|
#
|
|
33
37
|
# Raises Riffer::ArgumentError if format is invalid.
|
|
@@ -62,9 +66,9 @@ class Riffer::Tools::Response
|
|
|
62
66
|
# Creates an error response.
|
|
63
67
|
#
|
|
64
68
|
#--
|
|
65
|
-
#: (String, ?type: Symbol) -> Riffer::Tools::Response
|
|
66
|
-
def self.error(message, type: :execution_error)
|
|
67
|
-
new(content: message, success: false, error_message: message, error_type: type)
|
|
69
|
+
#: (String, ?type: Symbol, ?exception: Exception?) -> Riffer::Tools::Response
|
|
70
|
+
def self.error(message, type: :execution_error, exception: nil)
|
|
71
|
+
new(content: message, success: false, error_message: message, error_type: type, exception: exception)
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
# Returns true if the tool execution succeeded.
|
|
@@ -88,11 +92,12 @@ class Riffer::Tools::Response
|
|
|
88
92
|
private
|
|
89
93
|
|
|
90
94
|
#--
|
|
91
|
-
#: (content: String, success: bool, ?error_message: String?, ?error_type: Symbol?) -> void
|
|
92
|
-
def initialize(content:, success:, error_message: nil, error_type: nil)
|
|
95
|
+
#: (content: String, success: bool, ?error_message: String?, ?error_type: Symbol?, ?exception: Exception?) -> void
|
|
96
|
+
def initialize(content:, success:, error_message: nil, error_type: nil, exception: nil)
|
|
93
97
|
@content = content
|
|
94
98
|
@success = success
|
|
95
99
|
@error_message = error_message
|
|
96
100
|
@error_type = error_type
|
|
101
|
+
@exception = exception
|
|
97
102
|
end
|
|
98
103
|
end
|
data/lib/riffer/tools/runtime.rb
CHANGED
|
@@ -88,19 +88,20 @@ class Riffer::Tools::Runtime
|
|
|
88
88
|
tool_instance = tool_class.new
|
|
89
89
|
arguments = parse_arguments(tool_call.arguments)
|
|
90
90
|
|
|
91
|
+
unless arguments.is_a?(Hash)
|
|
92
|
+
return Riffer::Tools::Response.error(
|
|
93
|
+
"Invalid JSON in tool arguments: expected an object, got #{arguments.class}",
|
|
94
|
+
type: :validation_error,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
91
98
|
tool_instance.call_with_validation(context: context, **arguments)
|
|
92
|
-
rescue
|
|
93
|
-
Riffer::Tools::Response.error(e.message, type: :
|
|
94
|
-
rescue Riffer::ValidationError => e
|
|
95
|
-
Riffer::Tools::Response.error(e.message, type: :validation_error)
|
|
96
|
-
rescue Riffer::ToolExecutionError => e
|
|
97
|
-
Riffer::Tools::Response.error(e.message, type: :execution_error)
|
|
98
|
-
rescue RuntimeError => e
|
|
99
|
-
Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
|
|
99
|
+
rescue JSON::ParserError => e
|
|
100
|
+
Riffer::Tools::Response.error("Invalid JSON in tool arguments: #{e.message}", type: :validation_error)
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
#--
|
|
103
|
-
#: (String?) ->
|
|
104
|
+
#: (String?) -> untyped
|
|
104
105
|
def parse_arguments(arguments)
|
|
105
106
|
return {} if arguments.nil? || arguments.empty?
|
|
106
107
|
|
|
@@ -144,13 +145,21 @@ class Riffer::Tools::Runtime
|
|
|
144
145
|
tags.transform_keys { |key| "riffer.tag.#{key}" }
|
|
145
146
|
end
|
|
146
147
|
|
|
147
|
-
# A
|
|
148
|
-
#
|
|
148
|
+
# A deliberate error Response is a handled outcome, so its status stays unset.
|
|
149
|
+
# An error status is reserved for a Response carrying the exception it was
|
|
150
|
+
# folded from — the tool failed for a reason nobody anticipated.
|
|
149
151
|
#--
|
|
150
152
|
#: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void
|
|
151
153
|
def record_tool_outcome(span, result)
|
|
152
154
|
error_type = result.error_type
|
|
153
155
|
span.set_attribute("error.type", error_type.to_s) if error_type
|
|
156
|
+
|
|
157
|
+
exception = result.exception
|
|
158
|
+
if exception
|
|
159
|
+
span.record_exception(exception)
|
|
160
|
+
span.error!(exception.message)
|
|
161
|
+
end
|
|
162
|
+
|
|
154
163
|
capture_tool_result(span, result)
|
|
155
164
|
end
|
|
156
165
|
|
data/lib/riffer/version.rb
CHANGED
data/lib/riffer.rb
CHANGED
|
@@ -10,6 +10,13 @@ loader.inflector.inflect(
|
|
|
10
10
|
"azure_open_ai" => "AzureOpenAI",
|
|
11
11
|
"open_router" => "OpenRouter",
|
|
12
12
|
)
|
|
13
|
+
# Test-framework wiring a consumer requires by hand; neither file defines the
|
|
14
|
+
# constant its path implies, and both reference framework constants riffer
|
|
15
|
+
# never loads itself.
|
|
16
|
+
loader.ignore(
|
|
17
|
+
"#{__dir__}/riffer/testing/rspec.rb",
|
|
18
|
+
"#{__dir__}/riffer/testing/minitest.rb",
|
|
19
|
+
)
|
|
13
20
|
loader.setup
|
|
14
21
|
|
|
15
22
|
module Riffer
|
|
@@ -26,12 +33,38 @@ module Riffer
|
|
|
26
33
|
# Raised when tool parameter validation fails.
|
|
27
34
|
class ValidationError < Error; end
|
|
28
35
|
|
|
29
|
-
# Raised
|
|
36
|
+
# Raised inside a tool's +call+ when execution exceeds the configured
|
|
37
|
+
# timeout. Rescue it in the tool to clean up; otherwise it becomes a
|
|
38
|
+
# +:timeout_error+ response.
|
|
30
39
|
class TimeoutError < Error; end
|
|
31
40
|
|
|
32
41
|
# Raised when a tool encounters an expected execution error.
|
|
33
42
|
class ToolExecutionError < Error; end
|
|
34
43
|
|
|
44
|
+
# Base error for a file attachment that can't be resolved for the provider.
|
|
45
|
+
class FileError < Error; end
|
|
46
|
+
|
|
47
|
+
# Raised when the provider can't accept the file at all.
|
|
48
|
+
class FileUnsupportedError < FileError; end
|
|
49
|
+
|
|
50
|
+
# Raised when a download is required but Riffer.config.files.allow_downloads is false.
|
|
51
|
+
class FileDownloadsDisabledError < FileError; end
|
|
52
|
+
|
|
53
|
+
# Raised when a message carries more files than Riffer.config.files.max_per_message allows.
|
|
54
|
+
class TooManyFilesError < FileError; end
|
|
55
|
+
|
|
56
|
+
# Raised when there's an issue downloading a file
|
|
57
|
+
class FileDownloadError < FileError; end
|
|
58
|
+
|
|
59
|
+
# Raised when a file size exceeds Riffer.config.files.max_bytes
|
|
60
|
+
class FileTooLargeError < FileError; end
|
|
61
|
+
|
|
62
|
+
# Raised when a downloaded or inline file's sha256 doesn't match.
|
|
63
|
+
class FileChecksumMismatchError < FileError; end
|
|
64
|
+
|
|
65
|
+
# Raised when a FilePart's data isn't valid base64
|
|
66
|
+
class FileEncodingError < FileError; end
|
|
67
|
+
|
|
35
68
|
# Raised when two registered subclasses share the same identifier.
|
|
36
69
|
class DuplicateIdentifierError < Error; end
|
|
37
70
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Minimal signatures for `minitest`, a development-only framework riffer never
|
|
2
|
+
# depends on. Only the surface `lib/riffer/testing/minitest.rb` touches is
|
|
3
|
+
# declared; the file is required by hand from a consumer's test_helper, so this
|
|
4
|
+
# stub keeps Steep aware of the constants without a `library` dependency.
|
|
5
|
+
module Minitest
|
|
6
|
+
class Test
|
|
7
|
+
def after_teardown: () -> void
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Hand-written because the signature names `Minitest`, an optional
|
|
2
|
+
# development-only framework that must never appear in a shipped signature —
|
|
3
|
+
# `lib/riffer/testing/minitest.rb` therefore leaves rbs-inline disabled.
|
|
4
|
+
module Riffer::Testing::MinitestCleanup : Minitest::Test
|
|
5
|
+
def after_teardown: () -> void
|
|
6
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Minimal signatures for `rspec`, a development-only framework riffer never
|
|
2
|
+
# depends on. Only the surface `lib/riffer/testing/rspec.rb` touches is
|
|
3
|
+
# declared; the file is required by hand from a consumer's spec_helper, so this
|
|
4
|
+
# stub keeps Steep aware of the constants without a `library` dependency.
|
|
5
|
+
module RSpec
|
|
6
|
+
def self.configure: () { (untyped) -> void } -> void
|
|
7
|
+
end
|
data/sig/_private/zeitwerk.rbs
CHANGED
|
@@ -147,6 +147,68 @@ class Riffer::Config
|
|
|
147
147
|
def backend=: (untyped) -> void
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# File-attachment-download policy for +Riffer::Messages::FilePart+ URL sources
|
|
151
|
+
class Files
|
|
152
|
+
# Allow file attachments to be downloaded to send to providers.
|
|
153
|
+
attr_reader allow_downloads: bool
|
|
154
|
+
|
|
155
|
+
# Maximum file size to download before failing.
|
|
156
|
+
attr_reader max_bytes: Integer
|
|
157
|
+
|
|
158
|
+
# Maximum amount of time to spend downloading a file before failing.
|
|
159
|
+
attr_reader timeout: Integer
|
|
160
|
+
|
|
161
|
+
# Maximum number of files to include in an individual message.
|
|
162
|
+
attr_reader max_per_message: Integer?
|
|
163
|
+
|
|
164
|
+
# Execution pattern for downloading files.
|
|
165
|
+
attr_reader runner: Riffer::Runner
|
|
166
|
+
|
|
167
|
+
# The object used to fetch a URL source's bytes
|
|
168
|
+
attr_reader downloader: untyped
|
|
169
|
+
|
|
170
|
+
# --
|
|
171
|
+
# : () -> void
|
|
172
|
+
def initialize: () -> void
|
|
173
|
+
|
|
174
|
+
# Sets the allow_downloads flag, coercing boolean-ish values so an env-var
|
|
175
|
+
# +"false"+ (truthy in Ruby) doesn't silently enable downloads. Raises
|
|
176
|
+
# Riffer::ArgumentError on an unrecognized value.
|
|
177
|
+
# --
|
|
178
|
+
# : (untyped) -> void
|
|
179
|
+
def allow_downloads=: (untyped) -> void
|
|
180
|
+
|
|
181
|
+
# Sets max_bytes, provided value is a positive integer.
|
|
182
|
+
# Raises Riffer::ArgumentError if value is not an Integer or less than or equal to 0.
|
|
183
|
+
# --
|
|
184
|
+
# : (untyped) -> void
|
|
185
|
+
def max_bytes=: (untyped) -> void
|
|
186
|
+
|
|
187
|
+
# Sets timeout, provided value is a positive integer.
|
|
188
|
+
# Raises Riffer::ArgumentError if value is not an Integer or is less than or equal to 0.
|
|
189
|
+
# --
|
|
190
|
+
# : (untyped) -> void
|
|
191
|
+
def timeout=: (untyped) -> void
|
|
192
|
+
|
|
193
|
+
# Sets max_per_message, provided value is either nil or a positive integer.
|
|
194
|
+
# Raises Riffer::ArgumentError if value is not an Integer or nil, or is less than or equal to 0.
|
|
195
|
+
# --
|
|
196
|
+
# : (untyped) -> void
|
|
197
|
+
def max_per_message=: (untyped) -> void
|
|
198
|
+
|
|
199
|
+
# Sets the runner used to process file downloads, provided value is a Riffer::Runner.
|
|
200
|
+
# Raises Riffer::ArgumentError if value is not a Riffer::Runner.
|
|
201
|
+
# --
|
|
202
|
+
# : (untyped) -> void
|
|
203
|
+
def runner=: (untyped) -> void
|
|
204
|
+
|
|
205
|
+
# Sets the object used to download bytes from a URL.
|
|
206
|
+
# Raises Riffer::ArgumentError if value does not respond to +#call+.
|
|
207
|
+
# --
|
|
208
|
+
# : (untyped) -> void
|
|
209
|
+
def downloader=: (untyped) -> void
|
|
210
|
+
end
|
|
211
|
+
|
|
150
212
|
# Consumer-configured token pricing, keyed by +provider/model+ id. Riffer
|
|
151
213
|
# ships no price table, so an unconfigured model carries no cost.
|
|
152
214
|
class Pricing
|
|
@@ -256,6 +318,8 @@ class Riffer::Config
|
|
|
256
318
|
# Tracing-related global configuration.
|
|
257
319
|
attr_reader tracing: Riffer::Config::Tracing
|
|
258
320
|
|
|
321
|
+
attr_reader files: Riffer::Config::Files
|
|
322
|
+
|
|
259
323
|
# Consumer-configured per-model token pricing.
|
|
260
324
|
attr_reader pricing: Riffer::Config::Pricing
|
|
261
325
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated from lib/riffer/files/downloader.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class Riffer::Files::Downloader
|
|
4
|
+
MAX_REDIRECTS: Integer
|
|
5
|
+
|
|
6
|
+
# : (String, max_bytes: Integer, timeout: Integer) -> String
|
|
7
|
+
def call: (String, max_bytes: Integer, timeout: Integer) -> String
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# : (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
|
|
12
|
+
def fetch: (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
|
|
13
|
+
|
|
14
|
+
# : (Net::HTTPResponse, max_bytes: Integer) -> String
|
|
15
|
+
def read_capped: (Net::HTTPResponse, max_bytes: Integer) -> String
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated from lib/riffer/files/resolver.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class Riffer::Files::Resolver
|
|
4
|
+
@provider: Riffer::Providers::Base
|
|
5
|
+
|
|
6
|
+
@config: Riffer::Config::Files
|
|
7
|
+
|
|
8
|
+
# : (provider: Riffer::Providers::Base) -> void
|
|
9
|
+
def initialize: (provider: Riffer::Providers::Base) -> void
|
|
10
|
+
|
|
11
|
+
# Resolves every file in every User message in place - downloading,
|
|
12
|
+
# verifying, and caching as the provider's capability and each file's
|
|
13
|
+
# sha256 require. Raises Riffer::FileError on any file that can't be
|
|
14
|
+
# resolved
|
|
15
|
+
# : (Array[Riffer::Messages::Base]) -> void
|
|
16
|
+
def resolve!: (Array[Riffer::Messages::Base]) -> void
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# : (Riffer::Messages::User) -> void
|
|
21
|
+
def check_file_count!: (Riffer::Messages::User) -> void
|
|
22
|
+
|
|
23
|
+
# : (Riffer::Messages::FilePart) -> void
|
|
24
|
+
def resolve_file!: (Riffer::Messages::FilePart) -> void
|
|
25
|
+
|
|
26
|
+
# : (Riffer::Messages::FilePart) -> void
|
|
27
|
+
def verify_inline!: (Riffer::Messages::FilePart) -> void
|
|
28
|
+
|
|
29
|
+
# +cache:+ is false for a :url-delivery provider verifying a sha256 — the
|
|
30
|
+
# request still sends the URL, never the downloaded bytes, so caching them
|
|
31
|
+
# would hold memory nothing reads and let later turns skip re-verifying.
|
|
32
|
+
# : (Riffer::Messages::FilePart, cache: (false | Symbol)) -> void
|
|
33
|
+
def download!: (Riffer::Messages::FilePart, cache: false | Symbol) -> void
|
|
34
|
+
|
|
35
|
+
def verify_bytes!: (untyped bytes, untyped sha256) -> untyped
|
|
36
|
+
end
|
|
@@ -16,4 +16,12 @@ module Riffer::Helpers::Identifier
|
|
|
16
16
|
# --
|
|
17
17
|
# : (Module) -> String
|
|
18
18
|
def for: (Module) -> String
|
|
19
|
+
|
|
20
|
+
# Returns the class-path name of a class or module, or +nil+ when anonymous.
|
|
21
|
+
# Tool classes shadow Module#name with the identifier DSL, so the real name
|
|
22
|
+
# must come from Module's own implementation.
|
|
23
|
+
#
|
|
24
|
+
# --
|
|
25
|
+
# : (Module) -> String?
|
|
26
|
+
def real_name: (Module) -> String?
|
|
19
27
|
end
|
|
@@ -3,29 +3,40 @@
|
|
|
3
3
|
# Represents a file attachment (image or document) — from a URL (+from_url+) or
|
|
4
4
|
# raw base64 data (+new+).
|
|
5
5
|
class Riffer::Messages::FilePart
|
|
6
|
+
@data_bytes: String?
|
|
7
|
+
|
|
8
|
+
@downloaded_data: String?
|
|
9
|
+
|
|
10
|
+
@data: String?
|
|
11
|
+
|
|
6
12
|
@url_string: String?
|
|
7
13
|
|
|
8
14
|
MEDIA_TYPES: Hash[String, String]
|
|
9
15
|
|
|
10
16
|
SUPPORTED_MEDIA_TYPES: Array[String]
|
|
11
17
|
|
|
18
|
+
SHA256_PATTERN: Regexp
|
|
19
|
+
|
|
12
20
|
# The MIME type of the file.
|
|
13
21
|
attr_reader media_type: String
|
|
14
22
|
|
|
15
23
|
# The filename, if available.
|
|
16
24
|
attr_reader filename: String?
|
|
17
25
|
|
|
26
|
+
# The expected SHA-256 of the file contents, if the caller supplied one.
|
|
27
|
+
attr_reader sha256: String?
|
|
28
|
+
|
|
18
29
|
# Raises Riffer::ArgumentError unless +data+ or +url+ is given and
|
|
19
30
|
# +media_type+ is supported.
|
|
20
31
|
# --
|
|
21
|
-
# : (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
|
|
22
|
-
def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
|
|
32
|
+
# : (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
|
|
33
|
+
def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
|
|
23
34
|
|
|
24
35
|
# Creates a FilePart from a URL, detecting +media_type+ from the path
|
|
25
36
|
# extension when omitted. Raises Riffer::ArgumentError if it can't be detected.
|
|
26
37
|
# --
|
|
27
|
-
# : (String, ?media_type: String?) -> Riffer::Messages::FilePart
|
|
28
|
-
def self.from_url: (String, ?media_type: String?) -> Riffer::Messages::FilePart
|
|
38
|
+
# : (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
|
|
39
|
+
def self.from_url: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
|
|
29
40
|
|
|
30
41
|
# Builds a FilePart from a +{url:, media_type:}+ or +{data:, media_type:}+ hash,
|
|
31
42
|
# or returns +file+ unchanged when it is already a FilePart. Raises
|
|
@@ -34,8 +45,29 @@ class Riffer::Messages::FilePart
|
|
|
34
45
|
# : ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
|
|
35
46
|
def self.from_hash: (Hash[Symbol, untyped] | Riffer::Messages::FilePart) -> Riffer::Messages::FilePart
|
|
36
47
|
|
|
37
|
-
#
|
|
38
|
-
|
|
48
|
+
# The base64-encoded contents - caller-supplied, or filled in by the file
|
|
49
|
+
# resolver after a download. Nil for a URL source riffer hasn't fetched.
|
|
50
|
+
# --
|
|
51
|
+
# : () -> String?
|
|
52
|
+
def data: () -> String?
|
|
53
|
+
|
|
54
|
+
# : () -> String?
|
|
55
|
+
def data_bytes: () -> String?
|
|
56
|
+
|
|
57
|
+
# : (String) -> void
|
|
58
|
+
def cache_data_bytes: (String) -> void
|
|
59
|
+
|
|
60
|
+
# Whether data was supplied directly, as opposed to filled in later by the
|
|
61
|
+
# file resolver after a download
|
|
62
|
+
# : () -> bool
|
|
63
|
+
def inline_data?: () -> bool
|
|
64
|
+
|
|
65
|
+
# Caches bytes fetched for a URL source. Deliberately absent from +to_h+:
|
|
66
|
+
# the agent loop re-sends history on every turn, so the cache saves refreshing
|
|
67
|
+
# the same file, while persisted history stays free of megabytes of base64
|
|
68
|
+
# --
|
|
69
|
+
# : (String) -> void
|
|
70
|
+
def cache_downloaded_data: (String) -> void
|
|
39
71
|
|
|
40
72
|
# Returns the URL if the source was a URL, nil otherwise.
|
|
41
73
|
#
|
|
@@ -11,7 +11,8 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
|
11
11
|
# The error message if the tool execution failed.
|
|
12
12
|
attr_reader error: String?
|
|
13
13
|
|
|
14
|
-
# The type of error (:unknown_tool, :validation_error, :execution_error,
|
|
14
|
+
# The type of error (:unknown_tool, :validation_error, :execution_error,
|
|
15
|
+
# :timeout_error, :unhandled_error).
|
|
15
16
|
attr_reader error_type: Symbol?
|
|
16
17
|
|
|
17
18
|
# --
|
|
@@ -24,6 +24,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
24
24
|
# : () -> void
|
|
25
25
|
def initialize: () -> void
|
|
26
26
|
|
|
27
|
+
# --
|
|
28
|
+
# : (Riffer::Messages::FilePart) -> Symbol
|
|
29
|
+
def file_delivery: (Riffer::Messages::FilePart) -> Symbol
|
|
30
|
+
|
|
27
31
|
private
|
|
28
32
|
|
|
29
33
|
# --
|
|
@@ -41,6 +41,10 @@ class Riffer::Providers::Base
|
|
|
41
41
|
# : (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
42
42
|
def stream_text: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
43
43
|
|
|
44
|
+
# --
|
|
45
|
+
# : (Riffer::Messages::FilePart) -> Symbol
|
|
46
|
+
def file_delivery: (Riffer::Messages::FilePart) -> Symbol
|
|
47
|
+
|
|
44
48
|
private
|
|
45
49
|
|
|
46
50
|
# : (String) -> true
|
|
@@ -11,6 +11,10 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
11
11
|
# : () -> String
|
|
12
12
|
def self.semconv_provider_name: () -> String
|
|
13
13
|
|
|
14
|
+
# --
|
|
15
|
+
# : (Riffer::Messages::FilePart) -> Symbol
|
|
16
|
+
def file_delivery: (Riffer::Messages::FilePart) -> Symbol
|
|
17
|
+
|
|
14
18
|
private
|
|
15
19
|
|
|
16
20
|
# --
|