riffer 0.28.0 → 0.29.1
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 +53 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +26 -0
- data/README.md +17 -18
- data/Steepfile +8 -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 +127 -0
- data/lib/riffer/agent/response.rb +2 -0
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +270 -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 +236 -923
- data/lib/riffer/config.rb +14 -7
- data/lib/riffer/evals/evaluator.rb +18 -3
- data/lib/riffer/evals/judge.rb +7 -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 +3 -1
- data/lib/riffer/mcp/registration.rb +6 -3
- data/lib/riffer/mcp/registry.rb +6 -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} +7 -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 +37 -31
- data/lib/riffer/providers/anthropic.rb +39 -36
- data/lib/riffer/providers/base.rb +12 -9
- data/lib/riffer/providers/gemini.rb +19 -12
- data/lib/riffer/providers/mock.rb +45 -13
- data/lib/riffer/providers/open_ai.rb +34 -29
- data/lib/riffer/providers/open_router.rb +325 -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 +6 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +3 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +6 -1
- data/lib/riffer/skills/context.rb +6 -3
- data/lib/riffer/skills/filesystem_backend.rb +10 -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/tools/response.rb +2 -0
- 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} +11 -9
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +19 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +4 -1
- data/sig/_private/anthropic.rbs +16 -0
- data/sig/_private/async.rbs +24 -0
- data/sig/_private/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/_private/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/_private/mcp.rbs +22 -0
- data/sig/_private/openai.rbs +29 -0
- data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -0
- data/sig/_private/riffer/providers/anthropic.rbs +4 -0
- data/sig/_private/riffer/providers/open_ai.rbs +4 -0
- data/sig/_private/riffer/providers/open_router.rbs +4 -0
- data/sig/_private/zeitwerk.rbs +12 -0
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +93 -0
- data/sig/generated/riffer/agent/response.rbs +2 -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 +147 -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 +145 -342
- data/sig/generated/riffer/config.rbs +17 -5
- data/sig/generated/riffer/evals/evaluator.rbs +8 -0
- data/sig/generated/riffer/evals/judge.rbs +10 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/mcp/client.rbs +2 -0
- data/sig/generated/riffer/mcp/registration.rbs +6 -0
- data/sig/generated/riffer/mcp/registry.rbs +4 -0
- 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} +7 -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 +12 -12
- data/sig/generated/riffer/providers/base.rbs +12 -10
- data/sig/generated/riffer/providers/gemini.rbs +10 -4
- data/sig/generated/riffer/providers/mock.rbs +31 -5
- data/sig/generated/riffer/providers/open_ai.rbs +10 -10
- 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 +4 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +4 -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 +9 -1
- data/sig/generated/riffer/skills/context.rbs +6 -2
- data/sig/generated/riffer/skills/filesystem_backend.rbs +4 -0
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/tools/response.rbs +2 -0
- 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} +14 -12
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +18 -6
- data/sig/generated/riffer.rbs +2 -0
- data/sig/manifest.yaml +3 -0
- data/sig/manual/riffer/agent/run.rbs +5 -0
- data/sig/manual/riffer/helpers/call_or_value.rbs +5 -0
- data/sig/manual/riffer/tools/toolable.rbs +6 -0
- metadata +59 -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
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
# end
|
|
17
17
|
#
|
|
18
18
|
class Riffer::Evals::Evaluator
|
|
19
|
+
# @rbs self.@instructions: String?
|
|
20
|
+
# @rbs self.@higher_is_better: bool?
|
|
21
|
+
# @rbs self.@judge_model: String?
|
|
22
|
+
# @rbs @judge: Riffer::Evals::Judge?
|
|
23
|
+
|
|
19
24
|
class << self
|
|
20
25
|
# Gets or sets the evaluation instructions (criteria and scoring rubric).
|
|
21
26
|
#
|
|
@@ -31,7 +36,11 @@ class Riffer::Evals::Evaluator
|
|
|
31
36
|
#--
|
|
32
37
|
#: (?bool?) -> bool
|
|
33
38
|
def higher_is_better(value = nil)
|
|
34
|
-
|
|
39
|
+
if value.nil?
|
|
40
|
+
current = @higher_is_better
|
|
41
|
+
return true if current.nil?
|
|
42
|
+
return current
|
|
43
|
+
end
|
|
35
44
|
@higher_is_better = value
|
|
36
45
|
end
|
|
37
46
|
|
|
@@ -87,8 +96,14 @@ class Riffer::Evals::Evaluator
|
|
|
87
96
|
return input if input.is_a?(String)
|
|
88
97
|
|
|
89
98
|
input.map do |msg|
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
if msg.is_a?(Hash)
|
|
100
|
+
hash = msg #: Hash[untyped, untyped]
|
|
101
|
+
role = hash[:role] || hash["role"]
|
|
102
|
+
content = hash[:content] || hash["content"]
|
|
103
|
+
else
|
|
104
|
+
role = msg.role
|
|
105
|
+
content = msg.content
|
|
106
|
+
end
|
|
92
107
|
"#{role}: #{content}"
|
|
93
108
|
end.join("\n\n")
|
|
94
109
|
end
|
data/lib/riffer/evals/judge.rb
CHANGED
|
@@ -19,6 +19,11 @@ require "json"
|
|
|
19
19
|
# result[:reason] # => "The response is relevant..."
|
|
20
20
|
#
|
|
21
21
|
class Riffer::Evals::Judge
|
|
22
|
+
# @rbs @provider_options: Hash[Symbol, untyped]
|
|
23
|
+
# @rbs @provider_instance: Riffer::Providers::Base?
|
|
24
|
+
# @rbs @provider_name: String?
|
|
25
|
+
# @rbs @model_name: String?
|
|
26
|
+
|
|
22
27
|
# Internal tool for structured evaluation output.
|
|
23
28
|
class EvaluationTool < Riffer::Tool
|
|
24
29
|
identifier "evaluation"
|
|
@@ -30,7 +35,7 @@ class Riffer::Evals::Judge
|
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
#--
|
|
33
|
-
#: (context:
|
|
38
|
+
#: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
|
|
34
39
|
def call(context:, score:, reason:)
|
|
35
40
|
json({score: score, reason: reason})
|
|
36
41
|
end
|
|
@@ -94,7 +99,7 @@ class Riffer::Evals::Judge
|
|
|
94
99
|
#--
|
|
95
100
|
#: (input: String, output: String, ?ground_truth: String?) -> String
|
|
96
101
|
def build_user_message(input:, output:, ground_truth: nil)
|
|
97
|
-
parts = []
|
|
102
|
+
parts = [] #: Array[String]
|
|
98
103
|
parts << "## Input\n\n#{input}"
|
|
99
104
|
parts << "## Output\n\n#{output}"
|
|
100
105
|
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
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
class Riffer::Mcp::Client
|
|
17
17
|
include Riffer::Helpers::Dependencies
|
|
18
18
|
|
|
19
|
+
# @rbs @client: untyped
|
|
20
|
+
|
|
19
21
|
#--
|
|
20
22
|
#: (endpoint: String, ?headers: (Hash[String, String] | Proc), ?client: untyped?) -> void
|
|
21
23
|
def initialize(endpoint:, headers: {}, client: nil)
|
|
@@ -23,7 +25,7 @@ class Riffer::Mcp::Client
|
|
|
23
25
|
depends_on "faraday"
|
|
24
26
|
|
|
25
27
|
@client = client || begin
|
|
26
|
-
resolved_headers =
|
|
28
|
+
resolved_headers = Riffer::Helpers::CallOrValue.resolve(headers)
|
|
27
29
|
transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
|
|
28
30
|
MCP::Client.new(transport: transport)
|
|
29
31
|
end
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
# +tools/list+ call, then generates tool classes.
|
|
8
8
|
#
|
|
9
9
|
class Riffer::Mcp::Registration
|
|
10
|
+
# @rbs @cancelled: bool
|
|
11
|
+
# @rbs @tools: Array[singleton(Riffer::Tool)]
|
|
12
|
+
# @rbs @mutex: Thread::Mutex
|
|
13
|
+
|
|
10
14
|
# The manifest that describes this server.
|
|
11
15
|
attr_reader :manifest #: Riffer::Mcp::Manifest
|
|
12
16
|
|
|
@@ -23,7 +27,7 @@ class Riffer::Mcp::Registration
|
|
|
23
27
|
def initialize(manifest)
|
|
24
28
|
@manifest = manifest
|
|
25
29
|
@cancelled = false
|
|
26
|
-
@tools = []
|
|
30
|
+
@tools = [] #: Array[singleton(Riffer::Tool)]
|
|
27
31
|
@mutex = Mutex.new
|
|
28
32
|
run_discovery
|
|
29
33
|
end
|
|
@@ -62,8 +66,7 @@ class Riffer::Mcp::Registration
|
|
|
62
66
|
tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
|
|
63
67
|
|
|
64
68
|
@mutex.synchronize do
|
|
65
|
-
|
|
66
|
-
@tools = tools.freeze
|
|
69
|
+
@tools = tools.freeze unless @cancelled
|
|
67
70
|
end
|
|
68
71
|
end
|
|
69
72
|
end
|
data/lib/riffer/mcp/registry.rb
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
# Keyed by manifest name. All public methods are mutex-guarded.
|
|
7
7
|
#
|
|
8
8
|
module Riffer::Mcp::Registry
|
|
9
|
+
# @rbs self.@mutex: Thread::Mutex
|
|
10
|
+
# @rbs self.@store: Hash[String, Riffer::Mcp::Registration]
|
|
11
|
+
|
|
9
12
|
@mutex = Mutex.new
|
|
10
13
|
@store = {} #: Hash[String, Riffer::Mcp::Registration]
|
|
11
14
|
|
|
@@ -18,7 +21,9 @@ module Riffer::Mcp::Registry
|
|
|
18
21
|
#--
|
|
19
22
|
#: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
|
|
20
23
|
def register(manifest_or_hash)
|
|
21
|
-
|
|
24
|
+
# steep cannot verify that an untyped Hash splat supplies Manifest's
|
|
25
|
+
# required name:/endpoint: keywords; Manifest validates them at runtime.
|
|
26
|
+
manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
|
|
22
27
|
registration = Riffer::Mcp::Registration.new(manifest)
|
|
23
28
|
old = @mutex.synchronize do
|
|
24
29
|
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,13 @@ 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
|
+
# @rbs @url_string: String?
|
|
19
|
+
|
|
18
20
|
MEDIA_TYPES = {
|
|
19
21
|
".jpg" => "image/jpeg",
|
|
20
22
|
".jpeg" => "image/jpeg",
|
|
@@ -63,10 +65,10 @@ class Riffer::FilePart
|
|
|
63
65
|
# Raises Riffer::ArgumentError if media_type cannot be detected.
|
|
64
66
|
#
|
|
65
67
|
#--
|
|
66
|
-
#: (String, ?media_type: String?) -> Riffer::FilePart
|
|
68
|
+
#: (String, ?media_type: String?) -> Riffer::Messages::FilePart
|
|
67
69
|
def self.from_url(url, media_type: nil)
|
|
68
70
|
unless media_type
|
|
69
|
-
ext = ::File.extname(URI.parse(url).path).downcase
|
|
71
|
+
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
70
72
|
media_type = MEDIA_TYPES[ext]
|
|
71
73
|
raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
|
|
72
74
|
end
|
|
@@ -114,7 +116,7 @@ class Riffer::FilePart
|
|
|
114
116
|
#--
|
|
115
117
|
#: () -> Hash[Symbol, untyped]
|
|
116
118
|
def to_h
|
|
117
|
-
hash = {media_type: media_type}
|
|
119
|
+
hash = {media_type: media_type} #: Hash[Symbol, untyped]
|
|
118
120
|
hash[:data] = @data if @data
|
|
119
121
|
hash[:url] = @url_string if @url_string
|
|
120
122
|
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
|
|