riffer 0.28.0 → 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 +19 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +87 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +17 -17
- data/docs/08_MESSAGES.md +25 -32
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +7 -18
- 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/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 +234 -923
- data/lib/riffer/config.rb +14 -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 +1 -1
- 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} +9 -9
- 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/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 +143 -342
- data/sig/generated/riffer/config.rbs +17 -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/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} +12 -12
- 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
|
|
|
@@ -200,9 +206,10 @@ class Riffer::Config
|
|
|
200
206
|
@azure_openai = AzureOpenAI.new
|
|
201
207
|
@gemini = Gemini.new
|
|
202
208
|
@openai = OpenAI.new
|
|
209
|
+
@openrouter = OpenRouter.new
|
|
203
210
|
@evals = Evals.new
|
|
204
211
|
@mcp = Mcp.new(credentials: nil, discovery_runner: Riffer::Runner::Sequential.new)
|
|
205
|
-
@tool_runtime = Riffer::
|
|
212
|
+
@tool_runtime = Riffer::Tools::Runtime::Inline.new
|
|
206
213
|
@skills = Skills.new
|
|
207
214
|
@message_id_strategy = :none
|
|
208
215
|
@experimental_history_healing = false
|
|
@@ -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
|
|
data/lib/riffer/params.rb
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# end
|
|
13
13
|
#
|
|
14
14
|
class Riffer::Params
|
|
15
|
-
attr_reader :parameters #: Array[Riffer::Param]
|
|
15
|
+
attr_reader :parameters #: Array[Riffer::Params::Param]
|
|
16
16
|
|
|
17
17
|
#--
|
|
18
18
|
#: () -> void
|
|
@@ -23,10 +23,10 @@ class Riffer::Params
|
|
|
23
23
|
# Defines a required parameter.
|
|
24
24
|
#
|
|
25
25
|
#--
|
|
26
|
-
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ () -> void } -> void
|
|
26
|
+
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
27
27
|
def required(name, type, description: nil, enum: nil, of: nil, &block)
|
|
28
28
|
nested = build_nested(type, of, &block)
|
|
29
|
-
@parameters << Riffer::Param.new(
|
|
29
|
+
@parameters << Riffer::Params::Param.new(
|
|
30
30
|
name: name,
|
|
31
31
|
type: type,
|
|
32
32
|
required: true,
|
|
@@ -40,10 +40,10 @@ class Riffer::Params
|
|
|
40
40
|
# Defines an optional parameter.
|
|
41
41
|
#
|
|
42
42
|
#--
|
|
43
|
-
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ () -> void } -> void
|
|
43
|
+
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
44
44
|
def optional(name, type, description: nil, enum: nil, default: nil, of: nil, &block)
|
|
45
45
|
nested = build_nested(type, of, &block)
|
|
46
|
-
@parameters << Riffer::Param.new(
|
|
46
|
+
@parameters << Riffer::Params::Param.new(
|
|
47
47
|
name: name,
|
|
48
48
|
type: type,
|
|
49
49
|
required: false,
|
|
@@ -62,8 +62,8 @@ class Riffer::Params
|
|
|
62
62
|
#--
|
|
63
63
|
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
64
64
|
def validate(arguments)
|
|
65
|
-
validated = {}
|
|
66
|
-
errors = []
|
|
65
|
+
validated = {} #: Hash[Symbol, untyped]
|
|
66
|
+
errors = [] #: Array[String]
|
|
67
67
|
|
|
68
68
|
@parameters.each do |param|
|
|
69
69
|
value = arguments[param.name]
|
|
@@ -107,8 +107,8 @@ class Riffer::Params
|
|
|
107
107
|
#--
|
|
108
108
|
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
109
109
|
def to_json_schema(strict: false)
|
|
110
|
-
properties = {}
|
|
111
|
-
required_params = []
|
|
110
|
+
properties = {} #: Hash[String, untyped]
|
|
111
|
+
required_params = [] #: Array[String]
|
|
112
112
|
|
|
113
113
|
@parameters.each do |param|
|
|
114
114
|
properties[param.name.to_s] = param.to_json_schema(strict: strict)
|
|
@@ -126,7 +126,7 @@ class Riffer::Params
|
|
|
126
126
|
private
|
|
127
127
|
|
|
128
128
|
#--
|
|
129
|
-
#: (Class, Class?) ?{ () -> void } -> Riffer::Params?
|
|
129
|
+
#: (Class, Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
130
130
|
def build_nested(type, of, &block)
|
|
131
131
|
if of && block
|
|
132
132
|
raise Riffer::ArgumentError, "cannot use both of: and a block"
|
|
@@ -136,9 +136,9 @@ class Riffer::Params
|
|
|
136
136
|
unless type == Array
|
|
137
137
|
raise Riffer::ArgumentError, "of: can only be used with Array type, got #{type}"
|
|
138
138
|
end
|
|
139
|
-
unless Riffer::Param::PRIMITIVE_TYPES.include?(of)
|
|
139
|
+
unless Riffer::Params::Param::PRIMITIVE_TYPES.include?(of)
|
|
140
140
|
raise Riffer::ArgumentError,
|
|
141
|
-
"of: must be a primitive type (#{Riffer::Param::PRIMITIVE_TYPES.map(&:name).join(", ")}), got #{of}"
|
|
141
|
+
"of: must be a primitive type (#{Riffer::Params::Param::PRIMITIVE_TYPES.map(&:name).join(", ")}), got #{of}"
|
|
142
142
|
end
|
|
143
143
|
return nil
|
|
144
144
|
end
|
|
@@ -154,7 +154,7 @@ class Riffer::Params
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
#--
|
|
157
|
-
#: (Riffer::Param, untyped, Array[String]) -> untyped
|
|
157
|
+
#: (Riffer::Params::Param, untyped, Array[String]) -> untyped
|
|
158
158
|
def validate_nested(param, value, errors)
|
|
159
159
|
if param.type == Hash && param.nested_params
|
|
160
160
|
validate_nested_hash(param, value, errors)
|
|
@@ -169,9 +169,11 @@ class Riffer::Params
|
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
#--
|
|
172
|
-
#: (Riffer::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
172
|
+
#: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
173
173
|
def validate_nested_hash(param, value, errors)
|
|
174
|
-
param.nested_params
|
|
174
|
+
nested = param.nested_params
|
|
175
|
+
return value unless nested
|
|
176
|
+
nested.validate(value)
|
|
175
177
|
rescue Riffer::ValidationError => e
|
|
176
178
|
e.message.split("; ").each do |msg|
|
|
177
179
|
errors << "#{param.name}.#{msg}"
|
|
@@ -180,14 +182,16 @@ class Riffer::Params
|
|
|
180
182
|
end
|
|
181
183
|
|
|
182
184
|
#--
|
|
183
|
-
#: (Riffer::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
185
|
+
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
184
186
|
def validate_nested_array_of_objects(param, value, errors)
|
|
187
|
+
nested = param.nested_params
|
|
188
|
+
return value unless nested
|
|
185
189
|
value.map.with_index do |item, i|
|
|
186
190
|
unless item.is_a?(Hash)
|
|
187
191
|
errors << "#{param.name}[#{i}] must be an object"
|
|
188
192
|
next item
|
|
189
193
|
end
|
|
190
|
-
|
|
194
|
+
nested.validate(item)
|
|
191
195
|
rescue Riffer::ValidationError => e
|
|
192
196
|
e.message.split("; ").each do |msg|
|
|
193
197
|
errors << "#{param.name}[#{i}].#{msg}"
|
|
@@ -197,14 +201,16 @@ class Riffer::Params
|
|
|
197
201
|
end
|
|
198
202
|
|
|
199
203
|
#--
|
|
200
|
-
#: (Riffer::Param, Array[untyped], Array[String]) -> void
|
|
204
|
+
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> void
|
|
201
205
|
def validate_typed_array(param, value, errors)
|
|
202
|
-
|
|
206
|
+
item_type = param.item_type
|
|
207
|
+
return unless item_type
|
|
208
|
+
type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
|
|
203
209
|
value.each_with_index do |item, i|
|
|
204
|
-
valid = if
|
|
210
|
+
valid = if item_type == Riffer::Params::Boolean || item_type == TrueClass || item_type == FalseClass
|
|
205
211
|
item == true || item == false
|
|
206
212
|
else
|
|
207
|
-
item.is_a?(
|
|
213
|
+
item.is_a?(item_type)
|
|
208
214
|
end
|
|
209
215
|
errors << "#{param.name}[#{i}] must be a #{type_name}" unless valid
|
|
210
216
|
end
|