riffer 0.47.1 → 0.48.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/.claude/rules/rbs-inline.md +5 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +31 -0
- data/Rakefile +1 -1
- data/Steepfile +1 -15
- data/docs/AGENTS.md +37 -3
- data/docs/AGENT_LIFECYCLE.md +1 -0
- data/docs/CONFIGURATION.md +1 -1
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +31 -4
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +1 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +58 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/lib/riffer/agent/config.rb +40 -16
- data/lib/riffer/agent/outcome.rb +2 -2
- data/lib/riffer/agent/response.rb +16 -9
- data/lib/riffer/agent/run.rb +24 -9
- data/lib/riffer/agent/session.rb +2 -1
- data/lib/riffer/agent/structured_output/result.rb +2 -2
- data/lib/riffer/agent/structured_output.rb +1 -1
- data/lib/riffer/agent.rb +40 -16
- data/lib/riffer/config.rb +31 -31
- data/lib/riffer/evals/evaluator.rb +29 -2
- data/lib/riffer/evals/judge.rb +13 -8
- data/lib/riffer/evals/result.rb +6 -6
- data/lib/riffer/evals/run_result.rb +1 -1
- data/lib/riffer/evals/scenario_result.rb +6 -6
- data/lib/riffer/files/resolver.rb +3 -3
- data/lib/riffer/guardrails/modification.rb +3 -3
- data/lib/riffer/guardrails/result.rb +3 -3
- data/lib/riffer/guardrails/runner.rb +4 -4
- data/lib/riffer/guardrails/tripwire.rb +4 -4
- data/lib/riffer/helpers/deep_dup.rb +42 -0
- data/lib/riffer/mcp/manifest.rb +5 -5
- data/lib/riffer/mcp/registration.rb +1 -1
- data/lib/riffer/mcp/search_tool.rb +1 -1
- data/lib/riffer/messages/assistant/reasoning_part.rb +90 -0
- data/lib/riffer/messages/assistant/tool_call.rb +63 -0
- data/lib/riffer/messages/assistant.rb +55 -7
- data/lib/riffer/messages/base.rb +7 -25
- data/lib/riffer/messages/system.rb +10 -0
- data/lib/riffer/messages/tool.rb +21 -4
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +18 -17
- data/lib/riffer/messages/user.rb +13 -2
- data/lib/riffer/params/param.rb +20 -8
- data/lib/riffer/params.rb +10 -1
- data/lib/riffer/providers/amazon_bedrock.rb +34 -6
- data/lib/riffer/providers/anthropic.rb +9 -2
- data/lib/riffer/providers/base.rb +28 -10
- data/lib/riffer/providers/finish_reason.rb +2 -2
- data/lib/riffer/providers/gemini.rb +2 -2
- data/lib/riffer/providers/mock.rb +19 -3
- data/lib/riffer/providers/open_ai.rb +14 -5
- data/lib/riffer/providers/open_router.rb +6 -5
- data/lib/riffer/providers/repository.rb +0 -4
- data/lib/riffer/providers/token_usage.rb +21 -5
- data/lib/riffer/skills/adapter.rb +1 -1
- data/lib/riffer/skills/config.rb +12 -0
- data/lib/riffer/skills/context.rb +4 -7
- data/lib/riffer/skills/filesystem_backend.rb +1 -2
- data/lib/riffer/skills/frontmatter.rb +4 -4
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/base.rb +1 -1
- data/lib/riffer/stream_events/finish_reason_done.rb +2 -2
- data/lib/riffer/stream_events/guardrail_modification.rb +1 -1
- data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +2 -2
- data/lib/riffer/stream_events/reasoning_delta.rb +1 -1
- data/lib/riffer/stream_events/reasoning_done.rb +10 -8
- data/lib/riffer/stream_events/skill_activation.rb +1 -1
- data/lib/riffer/stream_events/text_delta.rb +1 -1
- data/lib/riffer/stream_events/text_done.rb +1 -1
- data/lib/riffer/stream_events/token_usage_done.rb +1 -1
- data/lib/riffer/stream_events/tool_call_delta.rb +3 -3
- data/lib/riffer/stream_events/tool_call_done.rb +4 -4
- data/lib/riffer/stream_events/web_search_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +3 -3
- data/lib/riffer/tools/response.rb +4 -4
- data/lib/riffer/tools/runtime.rb +3 -4
- data/lib/riffer/tracing/capture.rb +2 -4
- data/lib/riffer/tracing/stream_recorder.rb +6 -6
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +7 -0
- data/rbs_collection.lock.yaml +320 -0
- data/rbs_collection.yaml +12 -0
- data/sig/_private/anthropic.rbs +5 -4
- data/sig/_private/aws-sdk-core/event_error.rbs +15 -0
- data/sig/generated/riffer/agent/config.rbs +19 -5
- data/sig/generated/riffer/agent/response.rbs +6 -1
- data/sig/generated/riffer/agent/run.rbs +15 -8
- data/sig/generated/riffer/agent.rbs +25 -9
- data/sig/generated/riffer/config.rbs +1 -1
- data/sig/generated/riffer/evals/evaluator.rbs +20 -1
- data/sig/generated/riffer/evals/judge.rbs +5 -2
- data/sig/generated/riffer/files/resolver.rbs +6 -6
- data/sig/generated/riffer/helpers/deep_dup.rbs +21 -0
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +58 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +42 -0
- data/sig/generated/riffer/messages/assistant.rbs +24 -10
- data/sig/generated/riffer/messages/system.rbs +6 -0
- data/sig/generated/riffer/messages/tool.rbs +6 -0
- data/sig/generated/riffer/messages/{file_part.rbs → user/file_part.rbs} +11 -6
- data/sig/generated/riffer/messages/user.rbs +9 -3
- data/sig/generated/riffer/params/param.rbs +7 -0
- data/sig/generated/riffer/params.rbs +6 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +14 -5
- data/sig/generated/riffer/providers/anthropic.rbs +2 -2
- data/sig/generated/riffer/providers/base.rbs +22 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +7 -2
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +4 -4
- data/sig/generated/riffer/providers/repository.rbs +0 -2
- data/sig/generated/riffer/providers/token_usage.rbs +6 -0
- data/sig/generated/riffer/skills/config.rbs +9 -0
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +8 -6
- data/sig/generated/riffer/tools/runtime.rbs +2 -2
- data/sig/generated/riffer/tracing/capture.rbs +4 -4
- data/sig/generated/riffer.rbs +8 -0
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- metadata +13 -5
- data/sig/_private/async.rbs +0 -28
- data/sig/_private/minitest.rbs +0 -9
|
@@ -15,8 +15,12 @@ class Riffer::Evals::Evaluator
|
|
|
15
15
|
# @rbs self.@instructions: String?
|
|
16
16
|
# @rbs self.@higher_is_better: bool?
|
|
17
17
|
# @rbs self.@judge_model: String?
|
|
18
|
+
# @rbs self.@identifier: String?
|
|
18
19
|
# @rbs @judge: Riffer::Evals::Judge?
|
|
19
20
|
|
|
21
|
+
# The identifier for an anonymous evaluator class.
|
|
22
|
+
DEFAULT_IDENTIFIER = "riffer/judge" #: String
|
|
23
|
+
|
|
20
24
|
class << self
|
|
21
25
|
# Gets or sets the evaluation instructions (criteria and scoring rubric).
|
|
22
26
|
#
|
|
@@ -51,6 +55,28 @@ class Riffer::Evals::Evaluator
|
|
|
51
55
|
|
|
52
56
|
@judge_model = value.to_s
|
|
53
57
|
end
|
|
58
|
+
|
|
59
|
+
# Gets or sets the evaluator identifier, sent as the +agent+ tag on judge
|
|
60
|
+
# calls. Defaults to the snake_cased class name, or DEFAULT_IDENTIFIER for
|
|
61
|
+
# an anonymous class.
|
|
62
|
+
#
|
|
63
|
+
#--
|
|
64
|
+
#: (?String?) -> String
|
|
65
|
+
def identifier(value = nil)
|
|
66
|
+
return @identifier = value.to_s if value
|
|
67
|
+
|
|
68
|
+
@identifier || derived_identifier
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Anonymous classes derive an empty identifier.
|
|
74
|
+
#--
|
|
75
|
+
#: () -> String
|
|
76
|
+
def derived_identifier
|
|
77
|
+
derived = Riffer::Helpers::Identifier.for(self)
|
|
78
|
+
derived.empty? ? DEFAULT_IDENTIFIER : derived
|
|
79
|
+
end
|
|
54
80
|
end
|
|
55
81
|
|
|
56
82
|
# Evaluates an input/output pair. The default calls the judge with the
|
|
@@ -94,7 +120,8 @@ class Riffer::Evals::Evaluator
|
|
|
94
120
|
|
|
95
121
|
protected
|
|
96
122
|
|
|
97
|
-
# Returns a Judge instance configured for this evaluator.
|
|
123
|
+
# Returns a Judge instance configured for this evaluator. Its calls carry
|
|
124
|
+
# the +kind+ (+"judge"+) and +agent+ (the evaluator identifier) tags.
|
|
98
125
|
#
|
|
99
126
|
#--
|
|
100
127
|
#: () -> Riffer::Evals::Judge
|
|
@@ -106,7 +133,7 @@ class Riffer::Evals::Evaluator
|
|
|
106
133
|
"No judge model configured. Set judge_model on the evaluator or Riffer.config.evals.judge_model"
|
|
107
134
|
end
|
|
108
135
|
|
|
109
|
-
Riffer::Evals::Judge.new(model: model)
|
|
136
|
+
Riffer::Evals::Judge.new(model: model, tags: { "kind" => "judge", "agent" => self.class.identifier })
|
|
110
137
|
end
|
|
111
138
|
end
|
|
112
139
|
|
data/lib/riffer/evals/judge.rb
CHANGED
|
@@ -28,18 +28,22 @@ class Riffer::Evals::Judge
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# The model string (provider/model format).
|
|
31
|
-
attr_reader :model #: String
|
|
31
|
+
attr_reader :model #: String # @dynamic model
|
|
32
|
+
|
|
33
|
+
# The tags passed to the provider on every call.
|
|
34
|
+
attr_reader :tags #: Hash[String, String] # @dynamic tags
|
|
32
35
|
|
|
33
36
|
# Raises Riffer::ArgumentError unless +model+ is "provider/model" format.
|
|
34
37
|
#--
|
|
35
|
-
#: (model: String) -> void
|
|
36
|
-
def initialize(model:)
|
|
38
|
+
#: (model: String, ?tags: Hash[String, String]) -> void
|
|
39
|
+
def initialize(model:, tags: {})
|
|
37
40
|
provider_name, model_name = model.split("/", 2)
|
|
38
41
|
unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
|
|
39
42
|
raise Riffer::ArgumentError, "Invalid model string: #{model}"
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
@model = model
|
|
46
|
+
@tags = tags
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
# Evaluates an input/output pair using the configured LLM.
|
|
@@ -49,12 +53,14 @@ class Riffer::Evals::Judge
|
|
|
49
53
|
system_message = build_system_message(instructions)
|
|
50
54
|
user_message = build_user_message(input: input, output: output, ground_truth: ground_truth)
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
options = {
|
|
53
57
|
system: system_message,
|
|
54
58
|
prompt: user_message,
|
|
55
59
|
model: model_name,
|
|
56
60
|
tools: [EvaluationTool],
|
|
57
|
-
|
|
61
|
+
} #: Hash[Symbol, untyped]
|
|
62
|
+
options[:tags] = tags unless tags.empty?
|
|
63
|
+
response = provider_instance.generate_text(**options)
|
|
58
64
|
|
|
59
65
|
parse_tool_response(response)
|
|
60
66
|
end
|
|
@@ -109,10 +115,9 @@ class Riffer::Evals::Judge
|
|
|
109
115
|
#--
|
|
110
116
|
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
111
117
|
def parse_tool_response(response)
|
|
112
|
-
tool_call = response.tool_calls.
|
|
113
|
-
raise Riffer::Error, "Invalid judge response: no tool call found" unless tool_call
|
|
118
|
+
tool_call = response.tool_calls.fetch(0) { raise Riffer::Error, "Invalid judge response: no tool call found" }
|
|
114
119
|
|
|
115
|
-
parsed = JSON.parse(tool_call
|
|
120
|
+
parsed = JSON.parse(tool_call.arguments, symbolize_names: true)
|
|
116
121
|
score = parsed[:score]
|
|
117
122
|
reason = parsed[:reason]
|
|
118
123
|
|
data/lib/riffer/evals/result.rb
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
# Represents the result of a single evaluation.
|
|
5
5
|
class Riffer::Evals::Result
|
|
6
6
|
# The evaluator class that produced this result.
|
|
7
|
-
attr_reader :evaluator #: singleton(Riffer::Evals::Evaluator)
|
|
7
|
+
attr_reader :evaluator #: singleton(Riffer::Evals::Evaluator) # @dynamic evaluator
|
|
8
8
|
|
|
9
9
|
# The evaluation score (0.0 to 1.0).
|
|
10
|
-
attr_reader :score #: Float
|
|
10
|
+
attr_reader :score #: Float # @dynamic score
|
|
11
11
|
|
|
12
12
|
# Human-readable explanation of the score.
|
|
13
|
-
attr_reader :reason #: String?
|
|
13
|
+
attr_reader :reason #: String? # @dynamic reason
|
|
14
14
|
|
|
15
15
|
# Additional metadata from the evaluation.
|
|
16
|
-
attr_reader :metadata #: Hash[Symbol, untyped]
|
|
16
|
+
attr_reader :metadata #: Hash[Symbol, untyped] # @dynamic metadata
|
|
17
17
|
|
|
18
18
|
# Whether higher scores are better for this evaluator.
|
|
19
|
-
attr_reader :higher_is_better #: bool
|
|
19
|
+
attr_reader :higher_is_better #: bool # @dynamic higher_is_better
|
|
20
20
|
|
|
21
21
|
# Token usage for the judge call that produced this result, when the
|
|
22
22
|
# evaluator used an LLM. Nil for rule-based evaluators.
|
|
23
|
-
attr_reader :token_usage #: Riffer::Providers::TokenUsage?
|
|
23
|
+
attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
|
|
24
24
|
|
|
25
25
|
# Raises Riffer::ArgumentError if +score+ is not between 0.0 and 1.0.
|
|
26
26
|
#--
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Represents the complete result of an evaluation run across multiple scenarios.
|
|
5
5
|
class Riffer::Evals::RunResult
|
|
6
6
|
# Per-scenario evaluation results.
|
|
7
|
-
attr_reader :scenario_results #: Array[Riffer::Evals::ScenarioResult]
|
|
7
|
+
attr_reader :scenario_results #: Array[Riffer::Evals::ScenarioResult] # @dynamic scenario_results
|
|
8
8
|
|
|
9
9
|
#--
|
|
10
10
|
#: (scenario_results: Array[Riffer::Evals::ScenarioResult]) -> void
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
# Represents the result of evaluating a single scenario.
|
|
5
5
|
class Riffer::Evals::ScenarioResult
|
|
6
6
|
# The input that was evaluated.
|
|
7
|
-
attr_reader :input #: String
|
|
7
|
+
attr_reader :input #: String # @dynamic input
|
|
8
8
|
|
|
9
9
|
# The agent output for this scenario.
|
|
10
|
-
attr_reader :output #: String
|
|
10
|
+
attr_reader :output #: String # @dynamic output
|
|
11
11
|
|
|
12
12
|
# The ground truth used during evaluation.
|
|
13
|
-
attr_reader :ground_truth #: String?
|
|
13
|
+
attr_reader :ground_truth #: String? # @dynamic ground_truth
|
|
14
14
|
|
|
15
15
|
# Individual evaluation results.
|
|
16
|
-
attr_reader :results #: Array[Riffer::Evals::Result]
|
|
16
|
+
attr_reader :results #: Array[Riffer::Evals::Result] # @dynamic results
|
|
17
17
|
|
|
18
18
|
# The full message history from the agent conversation.
|
|
19
|
-
attr_reader :messages #: Array[Riffer::Messages::Base]
|
|
19
|
+
attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
|
|
20
20
|
|
|
21
21
|
# Token usage the agent under test spent generating this scenario's output.
|
|
22
|
-
attr_reader :token_usage #: Riffer::Providers::TokenUsage?
|
|
22
|
+
attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
|
|
23
23
|
|
|
24
24
|
#--
|
|
25
25
|
#: (input: String, output: String, ground_truth: String?, results: Array[Riffer::Evals::Result], ?messages: Array[Riffer::Messages::Base], ?token_usage: Riffer::Providers::TokenUsage?) -> void
|
|
@@ -40,7 +40,7 @@ class Riffer::Files::Resolver
|
|
|
40
40
|
raise Riffer::TooManyFilesError, "Too many files specified in user message"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
#: (Riffer::Messages::FilePart) -> void
|
|
43
|
+
#: (Riffer::Messages::User::FilePart) -> void
|
|
44
44
|
def resolve_file!(file)
|
|
45
45
|
delivery = @provider.file_delivery(file)
|
|
46
46
|
if delivery == :unsupported
|
|
@@ -62,7 +62,7 @@ class Riffer::Files::Resolver
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
#: (Riffer::Messages::FilePart) -> void
|
|
65
|
+
#: (Riffer::Messages::User::FilePart) -> void
|
|
66
66
|
def verify_inline!(file)
|
|
67
67
|
return unless file.sha256
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ class Riffer::Files::Resolver
|
|
|
72
72
|
# +cache:+ is false for a :url-delivery provider verifying a sha256 — the
|
|
73
73
|
# request still sends the URL, never the downloaded bytes, so caching them
|
|
74
74
|
# would hold memory nothing reads and let later turns skip re-verifying.
|
|
75
|
-
#: (Riffer::Messages::FilePart, cache: (false | Symbol)) -> void
|
|
75
|
+
#: (Riffer::Messages::User::FilePart, cache: (false | Symbol)) -> void
|
|
76
76
|
def download!(file, cache:)
|
|
77
77
|
raise Riffer::FileDownloadsDisabledError, "File attachments are disabled" unless @config.allow_downloads
|
|
78
78
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
# Records a guardrail transformation event.
|
|
5
5
|
class Riffer::Guardrails::Modification
|
|
6
6
|
# The guardrail class that transformed data.
|
|
7
|
-
attr_reader :guardrail #: singleton(Riffer::Guardrail)
|
|
7
|
+
attr_reader :guardrail #: singleton(Riffer::Guardrail) # @dynamic guardrail
|
|
8
8
|
|
|
9
9
|
# The phase when the transformation occurred (:before or :after).
|
|
10
|
-
attr_reader :phase #: Symbol
|
|
10
|
+
attr_reader :phase #: Symbol # @dynamic phase
|
|
11
11
|
|
|
12
12
|
# The indices of messages that were changed.
|
|
13
|
-
attr_reader :message_indices #: Array[Integer]
|
|
13
|
+
attr_reader :message_indices #: Array[Integer] # @dynamic message_indices
|
|
14
14
|
|
|
15
15
|
#--
|
|
16
16
|
#: (guardrail: singleton(Riffer::Guardrail), phase: Symbol, message_indices: Array[Integer]) -> void
|
|
@@ -7,13 +7,13 @@ class Riffer::Guardrails::Result
|
|
|
7
7
|
TYPES = %i[pass transform block].freeze #: Array[Symbol]
|
|
8
8
|
|
|
9
9
|
# The result type (:pass, :transform, or :block).
|
|
10
|
-
attr_reader :type #: Symbol
|
|
10
|
+
attr_reader :type #: Symbol # @dynamic type
|
|
11
11
|
|
|
12
12
|
# The data (for pass/transform) or reason (for block).
|
|
13
|
-
attr_reader :data #: untyped
|
|
13
|
+
attr_reader :data #: untyped # @dynamic data
|
|
14
14
|
|
|
15
15
|
# Optional metadata for block results.
|
|
16
|
-
attr_reader :metadata #: Hash[Symbol, untyped]?
|
|
16
|
+
attr_reader :metadata #: Hash[Symbol, untyped]? # @dynamic metadata
|
|
17
17
|
|
|
18
18
|
class << self
|
|
19
19
|
# Creates a pass result that continues with unchanged data.
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
# any blocks, execution stops and a tripwire is returned.
|
|
6
6
|
class Riffer::Guardrails::Runner
|
|
7
7
|
# The guardrail configs to execute.
|
|
8
|
-
attr_reader :guardrail_configs #: Array[Hash[Symbol, untyped]]
|
|
8
|
+
attr_reader :guardrail_configs #: Array[Hash[Symbol, untyped]] # @dynamic guardrail_configs
|
|
9
9
|
|
|
10
10
|
# The execution phase (:before or :after).
|
|
11
|
-
attr_reader :phase #: Symbol
|
|
11
|
+
attr_reader :phase #: Symbol # @dynamic phase
|
|
12
12
|
|
|
13
13
|
# The context passed to guardrails.
|
|
14
|
-
attr_reader :context #: untyped
|
|
14
|
+
attr_reader :context #: untyped # @dynamic context
|
|
15
15
|
|
|
16
16
|
# The normalized per-call tags, stamped as +riffer.tag.*+ on guardrail spans.
|
|
17
|
-
attr_reader :tags #: Hash[String, String]
|
|
17
|
+
attr_reader :tags #: Hash[String, String] # @dynamic tags
|
|
18
18
|
|
|
19
19
|
#--
|
|
20
20
|
#: (Array[Hash[Symbol, untyped]], phase: Symbol, ?context: untyped, ?tags: Hash[String, String]) -> void
|
|
@@ -6,16 +6,16 @@ class Riffer::Guardrails::Tripwire
|
|
|
6
6
|
PHASES = Riffer::Guardrails::PHASES #: Array[Symbol]
|
|
7
7
|
|
|
8
8
|
# The reason for blocking.
|
|
9
|
-
attr_reader :reason #: String
|
|
9
|
+
attr_reader :reason #: String # @dynamic reason
|
|
10
10
|
|
|
11
11
|
# The guardrail class that triggered the block.
|
|
12
|
-
attr_reader :guardrail #: singleton(Riffer::Guardrail)
|
|
12
|
+
attr_reader :guardrail #: singleton(Riffer::Guardrail) # @dynamic guardrail
|
|
13
13
|
|
|
14
14
|
# The phase when the block occurred (:before or :after).
|
|
15
|
-
attr_reader :phase #: Symbol
|
|
15
|
+
attr_reader :phase #: Symbol # @dynamic phase
|
|
16
16
|
|
|
17
17
|
# Optional metadata about the block.
|
|
18
|
-
attr_reader :metadata #: Hash[Symbol, untyped]?
|
|
18
|
+
attr_reader :metadata #: Hash[Symbol, untyped]? # @dynamic metadata
|
|
19
19
|
|
|
20
20
|
# Raises Riffer::ArgumentError if +phase+ is invalid.
|
|
21
21
|
#--
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Copying for the nested Hashes and Arrays a configuration object holds.
|
|
5
|
+
module Riffer::Helpers::DeepDup
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Returns +value+ with every Hash and Array rebuilt, so a copy shares no
|
|
9
|
+
# collection with its source.
|
|
10
|
+
#
|
|
11
|
+
# Anything else is returned as-is, which a Class or a Proc needs: +Class#dup+
|
|
12
|
+
# answers a new anonymous class. One source collection maps to one copy, so
|
|
13
|
+
# references shared within +value+ stay shared in the result — an +:around+
|
|
14
|
+
# guardrail registered under two phases is still one registration afterwards.
|
|
15
|
+
#--
|
|
16
|
+
#: (untyped) -> untyped
|
|
17
|
+
def call(value)
|
|
18
|
+
seen = {} #: Hash[untyped, untyped]
|
|
19
|
+
|
|
20
|
+
rebuild(value, seen.compare_by_identity)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
#--
|
|
26
|
+
#: (untyped, Hash[untyped, untyped]) -> untyped
|
|
27
|
+
def rebuild(value, seen)
|
|
28
|
+
return seen[value] if seen.key?(value)
|
|
29
|
+
|
|
30
|
+
case value
|
|
31
|
+
when Hash
|
|
32
|
+
copy = seen[value] = {}
|
|
33
|
+
value.each { |key, entry| copy[key] = rebuild(entry, seen) }
|
|
34
|
+
copy
|
|
35
|
+
when Array
|
|
36
|
+
copy = seen[value] = []
|
|
37
|
+
value.each { |entry| copy << rebuild(entry, seen) }
|
|
38
|
+
copy
|
|
39
|
+
else value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/riffer/mcp/manifest.rb
CHANGED
|
@@ -6,20 +6,20 @@ require "uri"
|
|
|
6
6
|
# Holds the configuration for a single MCP server.
|
|
7
7
|
class Riffer::Mcp::Manifest
|
|
8
8
|
# Identifier used as the registration key and generated-agent identifier.
|
|
9
|
-
attr_reader :name #: String
|
|
9
|
+
attr_reader :name #: String # @dynamic name
|
|
10
10
|
|
|
11
11
|
# Tags for matching +use_mcp+.
|
|
12
|
-
attr_reader :tags #: Array[Symbol]
|
|
12
|
+
attr_reader :tags #: Array[Symbol] # @dynamic tags
|
|
13
13
|
|
|
14
14
|
# HTTPS URL passed to the MCP transport.
|
|
15
|
-
attr_reader :endpoint #: String
|
|
15
|
+
attr_reader :endpoint #: String # @dynamic endpoint
|
|
16
16
|
|
|
17
17
|
# Headers (or a Proc) resolved once when building the discovery client.
|
|
18
|
-
attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)?
|
|
18
|
+
attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)? # @dynamic discovery_headers
|
|
19
19
|
|
|
20
20
|
# Optional hint (+:global+/+:tenant+/+:user+) for whether invocation
|
|
21
21
|
# credentials depend on tenant/user keys in +context+.
|
|
22
|
-
attr_reader :credentials_scope #: Symbol?
|
|
22
|
+
attr_reader :credentials_scope #: Symbol? # @dynamic credentials_scope
|
|
23
23
|
|
|
24
24
|
# Raises Riffer::ArgumentError unless +name+ is present and +endpoint+ is a
|
|
25
25
|
# valid HTTPS URL.
|
|
@@ -9,7 +9,7 @@ class Riffer::Mcp::Registration
|
|
|
9
9
|
# @rbs @mutex: Thread::Mutex
|
|
10
10
|
|
|
11
11
|
# The manifest that describes this server.
|
|
12
|
-
attr_reader :manifest #: Riffer::Mcp::Manifest
|
|
12
|
+
attr_reader :manifest #: Riffer::Mcp::Manifest # @dynamic manifest
|
|
13
13
|
|
|
14
14
|
# Generated Riffer::Mcp::Tool subclasses.
|
|
15
15
|
#
|
|
@@ -8,7 +8,7 @@ class Riffer::Mcp::SearchTool < Riffer::Tool
|
|
|
8
8
|
# Successful search response carrying the matched tool classes.
|
|
9
9
|
class Result < Riffer::Tools::Response
|
|
10
10
|
# Tool classes that matched the search query.
|
|
11
|
-
attr_reader :discovered_tools #: Array[singleton(Riffer::Tool)]
|
|
11
|
+
attr_reader :discovered_tools #: Array[singleton(Riffer::Tool)] # @dynamic discovered_tools
|
|
12
12
|
|
|
13
13
|
#--
|
|
14
14
|
#: (String, Array[singleton(Riffer::Tool)]) -> void
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Represents one block of model reasoning attached to an assistant message.
|
|
5
|
+
# riffer stores and replays a part verbatim rather than interpreting it, so a
|
|
6
|
+
# provider that requires its own thinking blocks back gets them unchanged.
|
|
7
|
+
class Riffer::Messages::Assistant::ReasoningPart
|
|
8
|
+
TYPES = %i[text summary encrypted].freeze #: Array[Symbol]
|
|
9
|
+
|
|
10
|
+
# What the part carries: readable reasoning (+:text+), a provider-condensed
|
|
11
|
+
# digest (+:summary+), or an opaque payload (+:encrypted+).
|
|
12
|
+
attr_reader :type #: Symbol # @dynamic type
|
|
13
|
+
|
|
14
|
+
# The reasoning prose, for +:text+ and +:summary+ parts.
|
|
15
|
+
attr_reader :text #: String? # @dynamic text
|
|
16
|
+
|
|
17
|
+
# The opaque payload, for +:encrypted+ parts.
|
|
18
|
+
attr_reader :data #: String? # @dynamic data
|
|
19
|
+
|
|
20
|
+
# The provider's signature over the part, when it issues one.
|
|
21
|
+
attr_reader :signature #: String? # @dynamic signature
|
|
22
|
+
|
|
23
|
+
# The provider's identifier for the part, when it issues one.
|
|
24
|
+
attr_reader :id #: String? # @dynamic id
|
|
25
|
+
|
|
26
|
+
# The wire format of the part, owned by the provider adapter that produced it
|
|
27
|
+
# (e.g. <tt>"anthropic-claude-v1"</tt>). Adapters replay only the formats they
|
|
28
|
+
# recognize, so it is never validated here.
|
|
29
|
+
attr_reader :format #: String? # @dynamic format
|
|
30
|
+
|
|
31
|
+
# Builds a ReasoningPart from a hash, or returns +part+ unchanged when it is
|
|
32
|
+
# already a ReasoningPart.
|
|
33
|
+
#--
|
|
34
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart)) -> Riffer::Messages::Assistant::ReasoningPart
|
|
35
|
+
def self.from_hash(part)
|
|
36
|
+
return part if part.is_a?(Riffer::Messages::Assistant::ReasoningPart)
|
|
37
|
+
|
|
38
|
+
new(
|
|
39
|
+
type: part[:type].to_sym,
|
|
40
|
+
text: part[:text],
|
|
41
|
+
data: part[:data],
|
|
42
|
+
signature: part[:signature],
|
|
43
|
+
id: part[:id],
|
|
44
|
+
format: part[:format],
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Raises Riffer::ArgumentError on a +type+ outside TYPES.
|
|
49
|
+
#--
|
|
50
|
+
#: (type: Symbol, ?text: String?, ?data: String?, ?signature: String?, ?id: String?, ?format: String?) -> void
|
|
51
|
+
def initialize(type:, text: nil, data: nil, signature: nil, id: nil, format: nil)
|
|
52
|
+
unless TYPES.include?(type)
|
|
53
|
+
raise Riffer::ArgumentError,
|
|
54
|
+
"type must be one of #{TYPES.inspect}, got #{type.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@type = type
|
|
58
|
+
@text = text
|
|
59
|
+
@data = data
|
|
60
|
+
@signature = signature
|
|
61
|
+
@id = id
|
|
62
|
+
@format = format
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Serializes the part to a hash, omitting the fields it doesn't carry.
|
|
66
|
+
#
|
|
67
|
+
#--
|
|
68
|
+
#: () -> Hash[Symbol, untyped]
|
|
69
|
+
def to_h
|
|
70
|
+
{ type: type, text: text, data: data, signature: signature, id: id, format: format }.compact
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#--
|
|
74
|
+
#: (untyped) -> bool
|
|
75
|
+
def ==(other)
|
|
76
|
+
other.is_a?(Riffer::Messages::Assistant::ReasoningPart) && to_h == other.to_h
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#--
|
|
80
|
+
#: (untyped) -> bool
|
|
81
|
+
def eql?(other)
|
|
82
|
+
self == other
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
#--
|
|
86
|
+
#: () -> Integer
|
|
87
|
+
def hash
|
|
88
|
+
to_h.hash
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Represents one tool invocation the model requested on an assistant message.
|
|
5
|
+
class Riffer::Messages::Assistant::ToolCall
|
|
6
|
+
# The provider's identifier for the call, echoed back on the tool result.
|
|
7
|
+
attr_reader :call_id #: String # @dynamic call_id
|
|
8
|
+
|
|
9
|
+
# The name of the tool to invoke.
|
|
10
|
+
attr_reader :name #: String # @dynamic name
|
|
11
|
+
|
|
12
|
+
# The JSON-encoded arguments, exactly as the provider emitted them.
|
|
13
|
+
attr_reader :arguments #: String # @dynamic arguments
|
|
14
|
+
|
|
15
|
+
# Builds a ToolCall from a hash, or returns +call+ unchanged when it is
|
|
16
|
+
# already a ToolCall. Raises Riffer::ArgumentError when the hash is missing
|
|
17
|
+
# a field.
|
|
18
|
+
#--
|
|
19
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant::ToolCall)) -> Riffer::Messages::Assistant::ToolCall
|
|
20
|
+
def self.from_hash(call)
|
|
21
|
+
return call if call.is_a?(Riffer::Messages::Assistant::ToolCall)
|
|
22
|
+
|
|
23
|
+
if call.values_at(:call_id, :name, :arguments).any?(&:nil?)
|
|
24
|
+
raise Riffer::ArgumentError, "Tool call hash must include :call_id, :name, and :arguments"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
new(call_id: call[:call_id], name: call[:name], arguments: call[:arguments])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#--
|
|
31
|
+
#: (call_id: String, name: String, arguments: String) -> void
|
|
32
|
+
def initialize(call_id:, name:, arguments:)
|
|
33
|
+
@call_id = call_id
|
|
34
|
+
@name = name
|
|
35
|
+
@arguments = arguments
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Serializes the call to a hash.
|
|
39
|
+
#
|
|
40
|
+
#--
|
|
41
|
+
#: () -> Hash[Symbol, untyped]
|
|
42
|
+
def to_h
|
|
43
|
+
{ call_id: call_id, name: name, arguments: arguments }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#--
|
|
47
|
+
#: (untyped) -> bool
|
|
48
|
+
def ==(other)
|
|
49
|
+
other.is_a?(Riffer::Messages::Assistant::ToolCall) && to_h == other.to_h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#--
|
|
53
|
+
#: (untyped) -> bool
|
|
54
|
+
def eql?(other)
|
|
55
|
+
self == other
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#--
|
|
59
|
+
#: () -> Integer
|
|
60
|
+
def hash
|
|
61
|
+
to_h.hash
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -4,24 +4,49 @@
|
|
|
4
4
|
# Represents an assistant (LLM) message in a conversation; may include tool
|
|
5
5
|
# calls when the LLM requests tool execution.
|
|
6
6
|
class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
7
|
-
|
|
7
|
+
# The reasoning part types +reasoning_text+ reads; the rest carry no prose.
|
|
8
|
+
REASONING_TEXT_TYPES = %i[text summary].freeze #: Array[Symbol]
|
|
9
|
+
|
|
10
|
+
# Builds an Assistant message from a hash, or returns +msg+ unchanged when it
|
|
11
|
+
# is already an Assistant message. Raises Riffer::ArgumentError on an invalid
|
|
12
|
+
# tool call, reasoning part, or +finish_reason+.
|
|
13
|
+
#--
|
|
14
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::Assistant)) -> Riffer::Messages::Assistant
|
|
15
|
+
def self.from_hash(msg)
|
|
16
|
+
return msg if msg.is_a?(Riffer::Messages::Assistant)
|
|
17
|
+
|
|
18
|
+
new(
|
|
19
|
+
msg[:content],
|
|
20
|
+
id: msg[:id],
|
|
21
|
+
tool_calls: (msg[:tool_calls] || []).map { |tc| Riffer::Messages::Assistant::ToolCall.from_hash(tc) },
|
|
22
|
+
reasoning: (msg[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
|
|
23
|
+
token_usage: msg[:token_usage] && Riffer::Providers::TokenUsage.from_hash(msg[:token_usage]),
|
|
24
|
+
structured_output: msg[:structured_output],
|
|
25
|
+
finish_reason: msg[:finish_reason]&.to_sym,
|
|
26
|
+
finish_reason_raw: msg[:finish_reason_raw],
|
|
27
|
+
)
|
|
28
|
+
end
|
|
8
29
|
|
|
9
30
|
# Array of tool calls requested by the assistant.
|
|
10
|
-
attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
31
|
+
attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall] # @dynamic tool_calls
|
|
32
|
+
|
|
33
|
+
# The model's reasoning blocks for this response, in the order the provider
|
|
34
|
+
# emitted them.
|
|
35
|
+
attr_reader :reasoning #: Array[Riffer::Messages::Assistant::ReasoningPart] # @dynamic reasoning
|
|
11
36
|
|
|
12
37
|
# Token usage data for this response.
|
|
13
|
-
attr_reader :token_usage #: Riffer::Providers::TokenUsage?
|
|
38
|
+
attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
|
|
14
39
|
|
|
15
40
|
# Parsed structured output hash, or nil when not applicable.
|
|
16
|
-
attr_reader :structured_output #: Hash[Symbol, untyped]?
|
|
41
|
+
attr_reader :structured_output #: Hash[Symbol, untyped]? # @dynamic structured_output
|
|
17
42
|
|
|
18
43
|
# Normalized reason the provider finished this response, when reported (see
|
|
19
44
|
# <tt>Riffer::Providers::FinishReason::VALUES</tt>).
|
|
20
|
-
attr_reader :finish_reason #: Symbol?
|
|
45
|
+
attr_reader :finish_reason #: Symbol? # @dynamic finish_reason
|
|
21
46
|
|
|
22
47
|
# The provider's raw finish-reason value behind +finish_reason+, when one
|
|
23
48
|
# exists on the wire.
|
|
24
|
-
attr_reader :finish_reason_raw #: String?
|
|
49
|
+
attr_reader :finish_reason_raw #: String? # @dynamic finish_reason_raw
|
|
25
50
|
|
|
26
51
|
# Raises Riffer::ArgumentError when +finish_reason+ is outside the
|
|
27
52
|
# normalized vocabulary.
|
|
@@ -30,6 +55,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
30
55
|
# String,
|
|
31
56
|
# ?id: String?,
|
|
32
57
|
# ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall],
|
|
58
|
+
# ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
|
|
33
59
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
34
60
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
35
61
|
# ?finish_reason: Symbol?,
|
|
@@ -39,6 +65,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
39
65
|
content,
|
|
40
66
|
id: nil,
|
|
41
67
|
tool_calls: [],
|
|
68
|
+
reasoning: [],
|
|
42
69
|
token_usage: nil,
|
|
43
70
|
structured_output: nil,
|
|
44
71
|
finish_reason: nil,
|
|
@@ -51,6 +78,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
51
78
|
|
|
52
79
|
super(content, id: id)
|
|
53
80
|
@tool_calls = tool_calls
|
|
81
|
+
@reasoning = reasoning
|
|
54
82
|
@token_usage = token_usage
|
|
55
83
|
@structured_output = structured_output
|
|
56
84
|
@finish_reason = finish_reason
|
|
@@ -75,10 +103,29 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
75
103
|
!@tool_calls.empty?
|
|
76
104
|
end
|
|
77
105
|
|
|
106
|
+
#--
|
|
107
|
+
#: () -> bool
|
|
108
|
+
def reasoning?
|
|
109
|
+
!@reasoning.empty?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The readable reasoning across this message's +:text+ and +:summary+ parts,
|
|
113
|
+
# joined by blank lines, or nil when it carries none.
|
|
114
|
+
#--
|
|
115
|
+
#: () -> String?
|
|
116
|
+
def reasoning_text
|
|
117
|
+
texts = reasoning.filter_map { |part| part.text if REASONING_TEXT_TYPES.include?(part.type) }
|
|
118
|
+
texts.empty? ? nil : texts.join("\n\n")
|
|
119
|
+
end
|
|
120
|
+
|
|
78
121
|
#--
|
|
79
122
|
#: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
|
|
80
123
|
def +(other)
|
|
81
|
-
self.class.new(
|
|
124
|
+
self.class.new(
|
|
125
|
+
"#{content}\n\n#{other.content}",
|
|
126
|
+
tool_calls: tool_calls + other.tool_calls,
|
|
127
|
+
reasoning: reasoning + other.reasoning,
|
|
128
|
+
)
|
|
82
129
|
end
|
|
83
130
|
|
|
84
131
|
# Converts the message to a hash.
|
|
@@ -89,6 +136,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
89
136
|
hash = { role: role, content: content } #: Hash[Symbol, untyped]
|
|
90
137
|
hash[:id] = id if id
|
|
91
138
|
hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
|
|
139
|
+
hash[:reasoning] = reasoning.map(&:to_h) if reasoning?
|
|
92
140
|
hash[:token_usage] = token_usage.to_h if token_usage
|
|
93
141
|
hash[:structured_output] = structured_output if structured_output?
|
|
94
142
|
hash[:finish_reason] = finish_reason if finish_reason
|