riffer 0.27.2 → 0.29.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/.agents/architecture.md +18 -11
- data/.agents/code-style.md +1 -1
- data/.agents/rbs-inline.md +2 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +31 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +134 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +23 -19
- data/docs/08_MESSAGES.md +28 -31
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +25 -15
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/06_MOCK_PROVIDER.md +2 -1
- data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
- data/docs/providers/08_GEMINI.md +2 -2
- data/docs/providers/09_OPENROUTER.md +242 -0
- data/lib/riffer/agent/config.rb +173 -0
- data/lib/riffer/agent/context.rb +125 -0
- data/lib/riffer/agent/response.rb +11 -2
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +268 -0
- data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
- data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
- data/lib/riffer/agent.rb +246 -684
- data/lib/riffer/config.rb +56 -7
- data/lib/riffer/evals/evaluator.rb +13 -3
- data/lib/riffer/evals/judge.rb +2 -2
- data/lib/riffer/evals/run_result.rb +2 -1
- data/lib/riffer/evals/scenario_result.rb +2 -1
- data/lib/riffer/guardrails/runner.rb +3 -2
- data/lib/riffer/helpers/call_or_value.rb +16 -0
- data/lib/riffer/helpers.rb +0 -1
- data/lib/riffer/mcp/authenticated_tool.rb +4 -0
- data/lib/riffer/mcp/client.rb +1 -1
- data/lib/riffer/mcp/registration.rb +2 -3
- data/lib/riffer/mcp/registry.rb +3 -1
- data/lib/riffer/mcp/tool_factory.rb +5 -0
- data/lib/riffer/messages/assistant.rb +9 -3
- data/lib/riffer/messages/base.rb +22 -0
- data/lib/riffer/messages/converter.rb +6 -6
- data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
- data/lib/riffer/messages/tool.rb +1 -1
- data/lib/riffer/messages/user.rb +4 -4
- data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
- data/lib/riffer/{param.rb → params/param.rb} +6 -6
- data/lib/riffer/params.rb +27 -21
- data/lib/riffer/providers/amazon_bedrock.rb +19 -20
- data/lib/riffer/providers/anthropic.rb +27 -28
- data/lib/riffer/providers/base.rb +10 -9
- data/lib/riffer/providers/gemini.rb +15 -12
- data/lib/riffer/providers/mock.rb +41 -13
- data/lib/riffer/providers/open_ai.rb +24 -22
- data/lib/riffer/providers/open_router.rb +318 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
- data/lib/riffer/providers.rb +1 -0
- data/lib/riffer/runner/fibers.rb +4 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +1 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +1 -1
- data/lib/riffer/skills/context.rb +3 -3
- data/lib/riffer/skills/filesystem_backend.rb +7 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +10 -3
- data/lib/riffer/stream_events/token_usage_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +1 -1
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
- data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
- data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
- data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +21 -15
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -1
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +91 -0
- data/sig/generated/riffer/agent/response.rbs +10 -2
- data/sig/generated/riffer/agent/run.rbs +144 -0
- data/sig/generated/riffer/agent/session/repair.rbs +51 -0
- data/sig/generated/riffer/agent/session.rbs +145 -0
- data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
- data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
- data/sig/generated/riffer/agent.rbs +154 -225
- data/sig/generated/riffer/config.rbs +50 -5
- data/sig/generated/riffer/evals/judge.rbs +2 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/messages/assistant.rbs +7 -3
- data/sig/generated/riffer/messages/base.rbs +18 -0
- data/sig/generated/riffer/messages/converter.rbs +4 -4
- data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
- data/sig/generated/riffer/messages/user.rbs +4 -4
- data/sig/generated/riffer/params/boolean.rbs +10 -0
- data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
- data/sig/generated/riffer/params.rbs +15 -15
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +10 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +25 -5
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +85 -0
- data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
- data/sig/generated/riffer/providers.rbs +1 -0
- data/sig/generated/riffer/runner/fibers.rbs +2 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +2 -2
- data/sig/generated/riffer/runner.rbs +2 -2
- data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
- data/sig/generated/riffer/skills/config.rbs +1 -1
- data/sig/generated/riffer/skills/context.rbs +2 -2
- data/sig/generated/riffer/stream_events/interrupt.rbs +7 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +19 -13
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
- data/sig/stubs/agent_ivars.rbs +7 -0
- data/sig/stubs/async.rbs +24 -0
- data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/stubs/extend_self.rbs +11 -0
- data/sig/stubs/lib_ivars.rbs +101 -0
- data/sig/stubs/mcp_sdk.rbs +22 -0
- data/sig/stubs/provider_ivars.rbs +36 -0
- data/sig/stubs/provider_sdk_methods.rbs +50 -0
- data/sig/stubs/zeitwerk.rbs +12 -0
- metadata +54 -33
- data/lib/riffer/core.rb +0 -28
- data/lib/riffer/helpers/validations.rb +0 -18
- data/sig/generated/riffer/boolean.rbs +0 -10
- data/sig/generated/riffer/core.rbs +0 -19
- data/sig/generated/riffer/helpers/validations.rbs +0 -12
data/lib/riffer/config.rb
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
#
|
|
13
13
|
# Riffer.config.anthropic.api_key = "sk-ant-..."
|
|
14
14
|
#
|
|
15
|
+
# Riffer.config.openrouter.api_key = "sk-or-..."
|
|
16
|
+
#
|
|
15
17
|
# Riffer.config.evals.judge_model = "anthropic/claude-sonnet-4-20250514"
|
|
16
18
|
#
|
|
17
19
|
class Riffer::Config
|
|
@@ -20,6 +22,7 @@ class Riffer::Config
|
|
|
20
22
|
AzureOpenAI = Struct.new(:api_key, :endpoint, keyword_init: true)
|
|
21
23
|
Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
|
|
22
24
|
OpenAI = Struct.new(:api_key, keyword_init: true)
|
|
25
|
+
OpenRouter = Struct.new(:api_key, keyword_init: true)
|
|
23
26
|
Evals = Struct.new(:judge_model, keyword_init: true)
|
|
24
27
|
Mcp = Struct.new(:credentials, :discovery_runner, keyword_init: true)
|
|
25
28
|
|
|
@@ -91,6 +94,9 @@ class Riffer::Config
|
|
|
91
94
|
# OpenAI configuration (Struct with +api_key+).
|
|
92
95
|
attr_reader :openai #: Riffer::Config::OpenAI
|
|
93
96
|
|
|
97
|
+
# OpenRouter configuration (Struct with +api_key+).
|
|
98
|
+
attr_reader :openrouter #: Riffer::Config::OpenRouter
|
|
99
|
+
|
|
94
100
|
# Evals configuration (Struct with +judge_model+).
|
|
95
101
|
attr_reader :evals #: Riffer::Config::Evals
|
|
96
102
|
|
|
@@ -107,9 +113,9 @@ class Riffer::Config
|
|
|
107
113
|
|
|
108
114
|
# Global tool runtime configuration (experimental).
|
|
109
115
|
#
|
|
110
|
-
# Accepts a Riffer::
|
|
111
|
-
# or a Proc. Defaults to <tt>Riffer::
|
|
112
|
-
attr_reader :tool_runtime #: (singleton(Riffer::
|
|
116
|
+
# Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
|
|
117
|
+
# or a Proc. Defaults to <tt>Riffer::Tools::Runtime::Inline.new</tt>.
|
|
118
|
+
attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
113
119
|
|
|
114
120
|
# Sets the global tool runtime.
|
|
115
121
|
#
|
|
@@ -117,10 +123,10 @@ class Riffer::Config
|
|
|
117
123
|
# (ToolRuntime subclass, ToolRuntime instance, or Proc).
|
|
118
124
|
#
|
|
119
125
|
#--
|
|
120
|
-
#: ((singleton(Riffer::
|
|
126
|
+
#: ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> void
|
|
121
127
|
def tool_runtime=(value)
|
|
122
|
-
valid = (value.is_a?(Class) && value < Riffer::
|
|
123
|
-
raise Riffer::ArgumentError, "tool_runtime must be a Riffer::
|
|
128
|
+
valid = (value.is_a?(Class) && value < Riffer::Tools::Runtime) || value.is_a?(Riffer::Tools::Runtime) || value.is_a?(Proc)
|
|
129
|
+
raise Riffer::ArgumentError, "tool_runtime must be a Riffer::Tools::Runtime subclass, instance, or a Proc" unless valid
|
|
124
130
|
@tool_runtime = value
|
|
125
131
|
end
|
|
126
132
|
|
|
@@ -151,6 +157,47 @@ class Riffer::Config
|
|
|
151
157
|
@message_id_strategy = value
|
|
152
158
|
end
|
|
153
159
|
|
|
160
|
+
# Experimental: when +true+, riffer keeps the +tool_use+ ↔ +tool_result+
|
|
161
|
+
# invariant intact on its own.
|
|
162
|
+
#
|
|
163
|
+
# - On +Riffer::Agent#generate(messages_array)+, orphaned +tool_use+
|
|
164
|
+
# exchanges and parentless +Riffer::Messages::Tool+ messages are
|
|
165
|
+
# silently stripped from the seed. Pending tool calls on the resume
|
|
166
|
+
# boundary (last assistant whose tail is purely Tool results) are
|
|
167
|
+
# preserved for +execute_pending_tool_calls+.
|
|
168
|
+
# - On any interrupt (caller-issued +interrupt!+ or
|
|
169
|
+
# +INTERRUPT_MAX_STEPS+), riffer fills any orphaned +tool_use+ with a
|
|
170
|
+
# placeholder +Riffer::Messages::Tool+ carrying
|
|
171
|
+
# +error_type: :interrupted+, leaving history valid for the next turn.
|
|
172
|
+
# Filled call_ids are exposed on
|
|
173
|
+
# +Riffer::Agent::Response#healed_tool_call_ids+ (and the streaming
|
|
174
|
+
# +Riffer::StreamEvents::Interrupt+ event).
|
|
175
|
+
#
|
|
176
|
+
# Defaults to +false+ — the pre-healing behavior. Experimental: the
|
|
177
|
+
# surface and default may change without notice.
|
|
178
|
+
attr_reader :experimental_history_healing #: bool
|
|
179
|
+
|
|
180
|
+
# Sets the +experimental_history_healing+ flag.
|
|
181
|
+
#
|
|
182
|
+
# Coerces common boolean representations so values pulled from
|
|
183
|
+
# environment variables don't silently enable healing — the string
|
|
184
|
+
# +"false"+ is truthy in Ruby and would otherwise flip the flag on.
|
|
185
|
+
# Accepts +true+/+false+, +"true"+/+"false"+, +1+/+0+, +"1"+/+"0"+, and
|
|
186
|
+
# +nil+ (treated as +false+, the default). Raises
|
|
187
|
+
# +Riffer::ArgumentError+ for any other value.
|
|
188
|
+
#
|
|
189
|
+
#--
|
|
190
|
+
#: (untyped) -> void
|
|
191
|
+
def experimental_history_healing=(value)
|
|
192
|
+
@experimental_history_healing = case value
|
|
193
|
+
when true, "true", 1, "1" then true
|
|
194
|
+
when false, "false", 0, "0", nil then false
|
|
195
|
+
else
|
|
196
|
+
raise Riffer::ArgumentError,
|
|
197
|
+
"experimental_history_healing must be a boolean (or 'true'/'false'/'1'/'0'/1/0), got #{value.inspect}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
154
201
|
#--
|
|
155
202
|
#: () -> void
|
|
156
203
|
def initialize
|
|
@@ -159,10 +206,12 @@ class Riffer::Config
|
|
|
159
206
|
@azure_openai = AzureOpenAI.new
|
|
160
207
|
@gemini = Gemini.new
|
|
161
208
|
@openai = OpenAI.new
|
|
209
|
+
@openrouter = OpenRouter.new
|
|
162
210
|
@evals = Evals.new
|
|
163
211
|
@mcp = Mcp.new(credentials: nil, discovery_runner: Riffer::Runner::Sequential.new)
|
|
164
|
-
@tool_runtime = Riffer::
|
|
212
|
+
@tool_runtime = Riffer::Tools::Runtime::Inline.new
|
|
165
213
|
@skills = Skills.new
|
|
166
214
|
@message_id_strategy = :none
|
|
215
|
+
@experimental_history_healing = false
|
|
167
216
|
end
|
|
168
217
|
end
|
|
@@ -31,7 +31,11 @@ class Riffer::Evals::Evaluator
|
|
|
31
31
|
#--
|
|
32
32
|
#: (?bool?) -> bool
|
|
33
33
|
def higher_is_better(value = nil)
|
|
34
|
-
|
|
34
|
+
if value.nil?
|
|
35
|
+
current = @higher_is_better
|
|
36
|
+
return true if current.nil?
|
|
37
|
+
return current
|
|
38
|
+
end
|
|
35
39
|
@higher_is_better = value
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -87,8 +91,14 @@ class Riffer::Evals::Evaluator
|
|
|
87
91
|
return input if input.is_a?(String)
|
|
88
92
|
|
|
89
93
|
input.map do |msg|
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
if msg.is_a?(Hash)
|
|
95
|
+
hash = msg #: Hash[untyped, untyped]
|
|
96
|
+
role = hash[:role] || hash["role"]
|
|
97
|
+
content = hash[:content] || hash["content"]
|
|
98
|
+
else
|
|
99
|
+
role = msg.role
|
|
100
|
+
content = msg.content
|
|
101
|
+
end
|
|
92
102
|
"#{role}: #{content}"
|
|
93
103
|
end.join("\n\n")
|
|
94
104
|
end
|
data/lib/riffer/evals/judge.rb
CHANGED
|
@@ -30,7 +30,7 @@ class Riffer::Evals::Judge
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
#--
|
|
33
|
-
#: (context:
|
|
33
|
+
#: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
|
|
34
34
|
def call(context:, score:, reason:)
|
|
35
35
|
json({score: score, reason: reason})
|
|
36
36
|
end
|
|
@@ -94,7 +94,7 @@ class Riffer::Evals::Judge
|
|
|
94
94
|
#--
|
|
95
95
|
#: (input: String, output: String, ?ground_truth: String?) -> String
|
|
96
96
|
def build_user_message(input:, output:, ground_truth: nil)
|
|
97
|
-
parts = []
|
|
97
|
+
parts = [] #: Array[String]
|
|
98
98
|
parts << "## Input\n\n#{input}"
|
|
99
99
|
parts << "## Output\n\n#{output}"
|
|
100
100
|
parts << "## Ground Truth\n\n#{ground_truth}" if ground_truth
|
|
@@ -40,7 +40,8 @@ class Riffer::Evals::RunResult
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
averages = {} #: Hash[singleton(Riffer::Evals::Evaluator), Float]
|
|
44
|
+
totals.each_with_object(averages) do |(evaluator, total), hash|
|
|
44
45
|
hash[evaluator] = total / counts[evaluator]
|
|
45
46
|
end
|
|
46
47
|
end
|
|
@@ -47,7 +47,8 @@ class Riffer::Evals::ScenarioResult
|
|
|
47
47
|
#--
|
|
48
48
|
#: () -> Hash[singleton(Riffer::Evals::Evaluator), Float]
|
|
49
49
|
def scores
|
|
50
|
-
|
|
50
|
+
acc = {} #: Hash[singleton(Riffer::Evals::Evaluator), Float]
|
|
51
|
+
results.each_with_object(acc) do |result, hash|
|
|
51
52
|
hash[result.evaluator] = result.score
|
|
52
53
|
end
|
|
53
54
|
end
|
|
@@ -80,7 +80,8 @@ class Riffer::Guardrails::Runner
|
|
|
80
80
|
#--
|
|
81
81
|
#: (Hash[Symbol, untyped]) -> Riffer::Guardrail
|
|
82
82
|
def instantiate_guardrail(config)
|
|
83
|
-
config[:
|
|
83
|
+
options = config[:options] #: Hash[Symbol, untyped]
|
|
84
|
+
config[:class].new(**options)
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
#--
|
|
@@ -101,7 +102,7 @@ class Riffer::Guardrails::Runner
|
|
|
101
102
|
when :before
|
|
102
103
|
guardrail.process_input(data, context: context)
|
|
103
104
|
when :after
|
|
104
|
-
guardrail.process_output(data, messages: messages, context: context)
|
|
105
|
+
guardrail.process_output(data, messages: messages || [], context: context)
|
|
105
106
|
else
|
|
106
107
|
raise Riffer::Error, "Unexpected guardrail phase: #{phase}. Valid phases: #{Riffer::Guardrails::PHASES.join(", ")}"
|
|
107
108
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Resolves the "Proc-or-value" idiom: if +thing+ is a Proc, calls it
|
|
5
|
+
# (passing +context+ when its arity is non-zero); otherwise returns
|
|
6
|
+
# +thing+ unchanged. When +thing+ is +nil+, returns +default+.
|
|
7
|
+
module Riffer::Helpers::CallOrValue
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
#: (untyped, ?context: untyped, ?default: untyped) -> untyped
|
|
11
|
+
def resolve(thing, context: nil, default: nil)
|
|
12
|
+
return default if thing.nil?
|
|
13
|
+
return thing unless thing.is_a?(Proc)
|
|
14
|
+
thing.arity.zero? ? thing.call : thing.call(context)
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/riffer/helpers.rb
CHANGED
|
@@ -6,6 +6,5 @@
|
|
|
6
6
|
# Helpers provide reusable functionality across the library:
|
|
7
7
|
# - Riffer::Helpers::ClassNameConverter - Class name to path conversion
|
|
8
8
|
# - Riffer::Helpers::Dependencies - Lazy gem dependency loading
|
|
9
|
-
# - Riffer::Helpers::Validations - Input validation
|
|
10
9
|
module Riffer::Helpers
|
|
11
10
|
end
|
|
@@ -23,6 +23,9 @@ module Riffer::Mcp::AuthenticatedTool
|
|
|
23
23
|
tags = matched_tags
|
|
24
24
|
|
|
25
25
|
Class.new(Riffer::Tool) do
|
|
26
|
+
# steep cannot type the body of a dynamically created anonymous class:
|
|
27
|
+
# its ivars and `self` inside define_method are unresolvable.
|
|
28
|
+
# steep:ignore:start
|
|
26
29
|
@identifier = inner.identifier
|
|
27
30
|
|
|
28
31
|
define_singleton_method(:name) { inner.name }
|
|
@@ -60,6 +63,7 @@ module Riffer::Mcp::AuthenticatedTool
|
|
|
60
63
|
client = build_call_client(man.endpoint, headers)
|
|
61
64
|
text(client.tools_call(inner.mcp_server_tool_name, kwargs))
|
|
62
65
|
end
|
|
66
|
+
# steep:ignore:end
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
end
|
data/lib/riffer/mcp/client.rb
CHANGED
|
@@ -23,7 +23,7 @@ class Riffer::Mcp::Client
|
|
|
23
23
|
depends_on "faraday"
|
|
24
24
|
|
|
25
25
|
@client = client || begin
|
|
26
|
-
resolved_headers =
|
|
26
|
+
resolved_headers = Riffer::Helpers::CallOrValue.resolve(headers)
|
|
27
27
|
transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
|
|
28
28
|
MCP::Client.new(transport: transport)
|
|
29
29
|
end
|
|
@@ -23,7 +23,7 @@ class Riffer::Mcp::Registration
|
|
|
23
23
|
def initialize(manifest)
|
|
24
24
|
@manifest = manifest
|
|
25
25
|
@cancelled = false
|
|
26
|
-
@tools = []
|
|
26
|
+
@tools = [] #: Array[singleton(Riffer::Tool)]
|
|
27
27
|
@mutex = Mutex.new
|
|
28
28
|
run_discovery
|
|
29
29
|
end
|
|
@@ -62,8 +62,7 @@ class Riffer::Mcp::Registration
|
|
|
62
62
|
tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
|
|
63
63
|
|
|
64
64
|
@mutex.synchronize do
|
|
65
|
-
|
|
66
|
-
@tools = tools.freeze
|
|
65
|
+
@tools = tools.freeze unless @cancelled
|
|
67
66
|
end
|
|
68
67
|
end
|
|
69
68
|
end
|
data/lib/riffer/mcp/registry.rb
CHANGED
|
@@ -18,7 +18,9 @@ module Riffer::Mcp::Registry
|
|
|
18
18
|
#--
|
|
19
19
|
#: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
|
|
20
20
|
def register(manifest_or_hash)
|
|
21
|
-
|
|
21
|
+
# steep cannot verify that an untyped Hash splat supplies Manifest's
|
|
22
|
+
# required name:/endpoint: keywords; Manifest validates them at runtime.
|
|
23
|
+
manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
|
|
22
24
|
registration = Riffer::Mcp::Registration.new(manifest)
|
|
23
25
|
old = @mutex.synchronize do
|
|
24
26
|
previous = @store[manifest.name]
|
|
@@ -31,7 +31,11 @@ module Riffer::Mcp::ToolFactory
|
|
|
31
31
|
private_class_method def self.build_tool_class(manifest_name, client, td)
|
|
32
32
|
prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
|
|
33
33
|
|
|
34
|
+
# steep cannot type the body of a dynamically created anonymous class:
|
|
35
|
+
# its ivars and `self` inside define_method are unresolvable, so the
|
|
36
|
+
# block is ignored wholesale (cf. AuthenticatedTool.wrap_one).
|
|
34
37
|
Class.new(Riffer::Tool) do
|
|
38
|
+
# steep:ignore:start
|
|
35
39
|
@mcp_client = client
|
|
36
40
|
@mcp_server_tool_name = td[:name]
|
|
37
41
|
# Set @identifier directly so .identifier does not fall back to
|
|
@@ -49,6 +53,7 @@ module Riffer::Mcp::ToolFactory
|
|
|
49
53
|
)
|
|
50
54
|
text(result)
|
|
51
55
|
end
|
|
56
|
+
# steep:ignore:end
|
|
52
57
|
end
|
|
53
58
|
end
|
|
54
59
|
end
|
|
@@ -17,13 +17,13 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
17
17
|
attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
18
18
|
|
|
19
19
|
# Token usage data for this response.
|
|
20
|
-
attr_reader :token_usage #: Riffer::TokenUsage?
|
|
20
|
+
attr_reader :token_usage #: Riffer::Providers::TokenUsage?
|
|
21
21
|
|
|
22
22
|
# Parsed structured output hash, or nil when not applicable.
|
|
23
23
|
attr_reader :structured_output #: Hash[Symbol, untyped]?
|
|
24
24
|
|
|
25
25
|
#--
|
|
26
|
-
#: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
26
|
+
#: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
27
27
|
def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil)
|
|
28
28
|
super(content, id: id)
|
|
29
29
|
@tool_calls = tool_calls
|
|
@@ -43,6 +43,12 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
43
43
|
!@structured_output.nil?
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
#--
|
|
47
|
+
#: () -> bool
|
|
48
|
+
def has_tool_calls?
|
|
49
|
+
!@tool_calls.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
#--
|
|
47
53
|
#: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
|
|
48
54
|
def +(other)
|
|
@@ -54,7 +60,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
54
60
|
#--
|
|
55
61
|
#: () -> Hash[Symbol, untyped]
|
|
56
62
|
def to_h
|
|
57
|
-
hash = {role: role, content: content}
|
|
63
|
+
hash = {role: role, content: content} #: Hash[Symbol, untyped]
|
|
58
64
|
hash[:id] = id unless id.nil?
|
|
59
65
|
hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
|
|
60
66
|
hash[:token_usage] = token_usage.to_h if token_usage
|
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -40,6 +40,28 @@ class Riffer::Messages::Base
|
|
|
40
40
|
raise NotImplementedError, "Subclasses must implement #role"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Whether this message carries pending tool calls. Defaults to +false+;
|
|
44
|
+
# +Riffer::Messages::Assistant+ overrides this when its +tool_calls+
|
|
45
|
+
# array is non-empty.
|
|
46
|
+
#
|
|
47
|
+
#--
|
|
48
|
+
#: () -> bool
|
|
49
|
+
def has_tool_calls?
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Merges another same-role message into this one.
|
|
54
|
+
#
|
|
55
|
+
# Raises NotImplementedError unless implemented by subclass. Mergeable
|
|
56
|
+
# message types (+User+, +Assistant+, +System+) override this; +Tool+
|
|
57
|
+
# messages are never merged.
|
|
58
|
+
#
|
|
59
|
+
#--
|
|
60
|
+
#: (untyped) -> Riffer::Messages::Base
|
|
61
|
+
def +(other)
|
|
62
|
+
raise NotImplementedError, "Subclasses must implement #+"
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
private
|
|
44
66
|
|
|
45
67
|
#: () -> String?
|
|
@@ -21,19 +21,19 @@ module Riffer::Messages::Converter
|
|
|
21
21
|
convert_hash_to_message(msg)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Converts a hash or FilePart object to a Riffer::FilePart.
|
|
24
|
+
# Converts a hash or FilePart object to a Riffer::Messages::FilePart.
|
|
25
25
|
#
|
|
26
26
|
# Accepts:
|
|
27
|
-
# - +Riffer::FilePart+ objects (passed through)
|
|
27
|
+
# - +Riffer::Messages::FilePart+ objects (passed through)
|
|
28
28
|
# - <tt>{url: "https://...", media_type: "..."}</tt> (URL source)
|
|
29
29
|
# - <tt>{data: "...", media_type: "..."}</tt> (raw base64)
|
|
30
30
|
#
|
|
31
31
|
# Raises Riffer::ArgumentError if the hash format is invalid.
|
|
32
32
|
#
|
|
33
33
|
#--
|
|
34
|
-
#: ((Hash[Symbol, untyped] | Riffer::FilePart)) -> Riffer::FilePart
|
|
34
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
|
|
35
35
|
def convert_to_file_part(file)
|
|
36
|
-
return file if file.is_a?(Riffer::FilePart)
|
|
36
|
+
return file if file.is_a?(Riffer::Messages::FilePart)
|
|
37
37
|
|
|
38
38
|
unless file.is_a?(Hash)
|
|
39
39
|
raise Riffer::ArgumentError, "File must be a Hash or FilePart object, got #{file.class}"
|
|
@@ -45,9 +45,9 @@ module Riffer::Messages::Converter
|
|
|
45
45
|
filename = file[:filename]
|
|
46
46
|
|
|
47
47
|
if url
|
|
48
|
-
Riffer::FilePart.from_url(url, media_type: media_type)
|
|
48
|
+
Riffer::Messages::FilePart.from_url(url, media_type: media_type)
|
|
49
49
|
elsif data && media_type
|
|
50
|
-
Riffer::FilePart.new(data: data, media_type: media_type, filename: filename)
|
|
50
|
+
Riffer::Messages::FilePart.new(data: data, media_type: media_type, filename: filename)
|
|
51
51
|
else
|
|
52
52
|
raise Riffer::ArgumentError, "File hash must include :url or :data with :media_type"
|
|
53
53
|
end
|
|
@@ -10,11 +10,11 @@ require "uri"
|
|
|
10
10
|
# - URLs (stored and passed to providers that support them via +from_url+)
|
|
11
11
|
# - Raw base64 data (via +new+)
|
|
12
12
|
#
|
|
13
|
-
# file = Riffer::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
|
|
13
|
+
# file = Riffer::Messages::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
|
|
14
14
|
# file.url? # => true
|
|
15
15
|
# file.document? # => true
|
|
16
16
|
#
|
|
17
|
-
class Riffer::FilePart
|
|
17
|
+
class Riffer::Messages::FilePart
|
|
18
18
|
MEDIA_TYPES = {
|
|
19
19
|
".jpg" => "image/jpeg",
|
|
20
20
|
".jpeg" => "image/jpeg",
|
|
@@ -63,10 +63,10 @@ class Riffer::FilePart
|
|
|
63
63
|
# Raises Riffer::ArgumentError if media_type cannot be detected.
|
|
64
64
|
#
|
|
65
65
|
#--
|
|
66
|
-
#: (String, ?media_type: String?) -> Riffer::FilePart
|
|
66
|
+
#: (String, ?media_type: String?) -> Riffer::Messages::FilePart
|
|
67
67
|
def self.from_url(url, media_type: nil)
|
|
68
68
|
unless media_type
|
|
69
|
-
ext = ::File.extname(URI.parse(url).path).downcase
|
|
69
|
+
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
70
70
|
media_type = MEDIA_TYPES[ext]
|
|
71
71
|
raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
|
|
72
72
|
end
|
|
@@ -114,7 +114,7 @@ class Riffer::FilePart
|
|
|
114
114
|
#--
|
|
115
115
|
#: () -> Hash[Symbol, untyped]
|
|
116
116
|
def to_h
|
|
117
|
-
hash = {media_type: media_type}
|
|
117
|
+
hash = {media_type: media_type} #: Hash[Symbol, untyped]
|
|
118
118
|
hash[:data] = @data if @data
|
|
119
119
|
hash[:url] = @url_string if @url_string
|
|
120
120
|
hash[:filename] = filename if filename
|
data/lib/riffer/messages/tool.rb
CHANGED
|
@@ -54,7 +54,7 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
|
54
54
|
#--
|
|
55
55
|
#: () -> Hash[Symbol, untyped]
|
|
56
56
|
def to_h
|
|
57
|
-
hash = {role: role, content: content, tool_call_id: tool_call_id, name: name}
|
|
57
|
+
hash = {role: role, content: content, tool_call_id: tool_call_id, name: name} #: Hash[Symbol, untyped]
|
|
58
58
|
hash[:id] = id unless id.nil?
|
|
59
59
|
if error?
|
|
60
60
|
hash[:error] = error
|
data/lib/riffer/messages/user.rb
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
# msg.content # => "Hello!"
|
|
9
9
|
#
|
|
10
10
|
# msg = Riffer::Messages::User.new("Describe this image", files: [file_part])
|
|
11
|
-
# msg.files # => [#<Riffer::FilePart ...>]
|
|
11
|
+
# msg.files # => [#<Riffer::Messages::FilePart ...>]
|
|
12
12
|
#
|
|
13
13
|
class Riffer::Messages::User < Riffer::Messages::Base
|
|
14
14
|
# File attachments for this message.
|
|
15
|
-
attr_reader :files #: Array[Riffer::FilePart]
|
|
15
|
+
attr_reader :files #: Array[Riffer::Messages::FilePart]
|
|
16
16
|
|
|
17
17
|
# Initializes a user message.
|
|
18
18
|
#
|
|
19
19
|
#--
|
|
20
|
-
#: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
|
|
20
|
+
#: (String, ?id: String?, ?files: Array[Riffer::Messages::FilePart]) -> void
|
|
21
21
|
def initialize(content, id: nil, files: [])
|
|
22
22
|
super(content, id: id)
|
|
23
23
|
@files = files
|
|
@@ -38,7 +38,7 @@ class Riffer::Messages::User < Riffer::Messages::Base
|
|
|
38
38
|
#--
|
|
39
39
|
#: () -> Hash[Symbol, untyped]
|
|
40
40
|
def to_h
|
|
41
|
-
hash = {role: role, content: content}
|
|
41
|
+
hash = {role: role, content: content} #: Hash[Symbol, untyped]
|
|
42
42
|
hash[:id] = id unless id.nil?
|
|
43
43
|
hash[:files] = files.map(&:to_h) unless files.empty?
|
|
44
44
|
hash
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Riffer::Boolean is a sentinel type for declaring boolean parameters.
|
|
4
|
+
# Riffer::Params::Boolean is a sentinel type for declaring boolean parameters.
|
|
5
5
|
#
|
|
6
6
|
# Ruby has no +Boolean+ class (+true+ is +TrueClass+, +false+ is +FalseClass+).
|
|
7
7
|
# Use this module wherever you need a single type that means "boolean":
|
|
8
8
|
#
|
|
9
|
-
# required :verbose, Riffer::Boolean
|
|
9
|
+
# required :verbose, Riffer::Params::Boolean
|
|
10
10
|
#
|
|
11
|
-
module Riffer::Boolean
|
|
11
|
+
module Riffer::Params::Boolean
|
|
12
12
|
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Riffer::Param represents a single parameter definition.
|
|
4
|
+
# Riffer::Params::Param represents a single parameter definition.
|
|
5
5
|
#
|
|
6
6
|
# Handles type validation and JSON Schema generation for individual parameters.
|
|
7
|
-
class Riffer::Param
|
|
7
|
+
class Riffer::Params::Param
|
|
8
8
|
# Maps Ruby types to JSON Schema type strings
|
|
9
9
|
TYPE_MAPPINGS = {
|
|
10
10
|
String => "string",
|
|
11
11
|
Integer => "integer",
|
|
12
12
|
Float => "number",
|
|
13
|
-
Riffer::Boolean => "boolean",
|
|
13
|
+
Riffer::Params::Boolean => "boolean",
|
|
14
14
|
TrueClass => "boolean",
|
|
15
15
|
FalseClass => "boolean",
|
|
16
16
|
Array => "array",
|
|
@@ -49,7 +49,7 @@ class Riffer::Param
|
|
|
49
49
|
def valid_type?(value)
|
|
50
50
|
return true if value.nil? && !required
|
|
51
51
|
|
|
52
|
-
if type == Riffer::Boolean || type == TrueClass || type == FalseClass
|
|
52
|
+
if type == Riffer::Params::Boolean || type == TrueClass || type == FalseClass
|
|
53
53
|
value == true || value == false
|
|
54
54
|
else
|
|
55
55
|
value.is_a?(type)
|
|
@@ -80,7 +80,7 @@ class Riffer::Param
|
|
|
80
80
|
nullable = strict && !required
|
|
81
81
|
|
|
82
82
|
if nullable && enum
|
|
83
|
-
schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]}
|
|
83
|
+
schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]} #: Hash[Symbol, untyped]
|
|
84
84
|
schema[:description] = description if description
|
|
85
85
|
return schema
|
|
86
86
|
end
|
|
@@ -88,7 +88,7 @@ class Riffer::Param
|
|
|
88
88
|
type = type_name
|
|
89
89
|
type = [type, "null"] if nullable
|
|
90
90
|
|
|
91
|
-
schema = {type: type}
|
|
91
|
+
schema = {type: type} #: Hash[Symbol, untyped]
|
|
92
92
|
schema[:description] = description if description
|
|
93
93
|
schema[:enum] = enum if enum
|
|
94
94
|
|