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
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -12,40 +12,22 @@ class Riffer::Messages::Base
|
|
|
12
12
|
def self.from_hash(msg)
|
|
13
13
|
return msg if msg.is_a?(Riffer::Messages::Base)
|
|
14
14
|
|
|
15
|
-
raise Riffer::ArgumentError, "Message must be a Hash or Message object, got #{msg.class}" unless msg.is_a?(Hash)
|
|
16
|
-
|
|
17
15
|
raise Riffer::ArgumentError, "Message hash must include a 'role' key" if msg[:role].nil? || msg[:role].empty?
|
|
18
16
|
|
|
19
17
|
case msg[:role].to_sym
|
|
20
|
-
when :user
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
when :
|
|
24
|
-
|
|
25
|
-
tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
|
|
26
|
-
end
|
|
27
|
-
Riffer::Messages::Assistant.new(
|
|
28
|
-
msg[:content],
|
|
29
|
-
id: msg[:id],
|
|
30
|
-
tool_calls: tool_calls,
|
|
31
|
-
structured_output: msg[:structured_output],
|
|
32
|
-
finish_reason: msg[:finish_reason]&.to_sym,
|
|
33
|
-
finish_reason_raw: msg[:finish_reason_raw],
|
|
34
|
-
)
|
|
35
|
-
when :system
|
|
36
|
-
Riffer::Messages::System.new(msg[:content], id: msg[:id])
|
|
37
|
-
when :tool
|
|
38
|
-
Riffer::Messages::Tool.new(msg[:content], id: msg[:id], tool_call_id: msg[:tool_call_id], name: msg[:name])
|
|
39
|
-
else
|
|
40
|
-
raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
18
|
+
when :user then Riffer::Messages::User.from_hash(msg)
|
|
19
|
+
when :assistant then Riffer::Messages::Assistant.from_hash(msg)
|
|
20
|
+
when :system then Riffer::Messages::System.from_hash(msg)
|
|
21
|
+
when :tool then Riffer::Messages::Tool.from_hash(msg)
|
|
22
|
+
else raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
41
23
|
end
|
|
42
24
|
end
|
|
43
25
|
|
|
44
26
|
# The message content.
|
|
45
|
-
attr_reader :content #: String
|
|
27
|
+
attr_reader :content #: String # @dynamic content
|
|
46
28
|
|
|
47
29
|
# The message id, or nil when +Riffer.config.message_id_strategy+ is +:none+.
|
|
48
|
-
attr_reader :id #: String?
|
|
30
|
+
attr_reader :id #: String? # @dynamic id
|
|
49
31
|
|
|
50
32
|
#--
|
|
51
33
|
#: (String, ?id: String?) -> void
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a system message (instructions) in a conversation.
|
|
5
5
|
class Riffer::Messages::System < Riffer::Messages::Base
|
|
6
|
+
# Builds a System message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a System message.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::System)) -> Riffer::Messages::System
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::System)
|
|
12
|
+
|
|
13
|
+
new(msg[:content], id: msg[:id])
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
#--
|
|
7
17
|
#: () -> Symbol
|
|
8
18
|
def role
|
data/lib/riffer/messages/tool.rb
CHANGED
|
@@ -3,18 +3,35 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a tool execution result in a conversation.
|
|
5
5
|
class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
6
|
+
# Builds a Tool message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a Tool message.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::Tool)) -> Riffer::Messages::Tool
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::Tool)
|
|
12
|
+
|
|
13
|
+
new(
|
|
14
|
+
msg[:content],
|
|
15
|
+
id: msg[:id],
|
|
16
|
+
tool_call_id: msg[:tool_call_id],
|
|
17
|
+
name: msg[:name],
|
|
18
|
+
error: msg[:error],
|
|
19
|
+
error_type: msg[:error_type]&.to_sym,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
6
23
|
# The ID of the tool call this result responds to.
|
|
7
|
-
attr_reader :tool_call_id #: String
|
|
24
|
+
attr_reader :tool_call_id #: String # @dynamic tool_call_id
|
|
8
25
|
|
|
9
26
|
# The name of the tool that was called.
|
|
10
|
-
attr_reader :name #: String
|
|
27
|
+
attr_reader :name #: String # @dynamic name
|
|
11
28
|
|
|
12
29
|
# The error message if the tool execution failed.
|
|
13
|
-
attr_reader :error #: String?
|
|
30
|
+
attr_reader :error #: String? # @dynamic error
|
|
14
31
|
|
|
15
32
|
# The type of error (:unknown_tool, :validation_error, :execution_error,
|
|
16
33
|
# :timeout_error, :unhandled_error).
|
|
17
|
-
attr_reader :error_type #: Symbol?
|
|
34
|
+
attr_reader :error_type #: Symbol? # @dynamic error_type
|
|
18
35
|
|
|
19
36
|
#--
|
|
20
37
|
#: (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
|
|
@@ -6,7 +6,7 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
# Represents a file attachment (image or document) — from a URL (+from_url+) or
|
|
8
8
|
# raw base64 data (+new+).
|
|
9
|
-
class Riffer::Messages::FilePart
|
|
9
|
+
class Riffer::Messages::User::FilePart
|
|
10
10
|
# @rbs @url_string: String?
|
|
11
11
|
# @rbs @data: String?
|
|
12
12
|
# @rbs @downloaded_data: String?
|
|
@@ -29,13 +29,13 @@ class Riffer::Messages::FilePart
|
|
|
29
29
|
SHA256_PATTERN = /\A[0-9a-f]{64}\z/i #: Regexp
|
|
30
30
|
|
|
31
31
|
# The MIME type of the file.
|
|
32
|
-
attr_reader :media_type #: String
|
|
32
|
+
attr_reader :media_type #: String # @dynamic media_type
|
|
33
33
|
|
|
34
34
|
# The filename, if available.
|
|
35
|
-
attr_reader :filename #: String?
|
|
35
|
+
attr_reader :filename #: String? # @dynamic filename
|
|
36
36
|
|
|
37
37
|
# The expected SHA-256 of the file contents, if the caller supplied one.
|
|
38
|
-
attr_reader :sha256 #: String?
|
|
38
|
+
attr_reader :sha256 #: String? # @dynamic sha256
|
|
39
39
|
|
|
40
40
|
# Raises Riffer::ArgumentError unless +data+ or +url+ is given and
|
|
41
41
|
# +media_type+ is supported.
|
|
@@ -62,26 +62,18 @@ class Riffer::Messages::FilePart
|
|
|
62
62
|
# Creates a FilePart from a URL, detecting +media_type+ from the path
|
|
63
63
|
# extension when omitted. Raises Riffer::ArgumentError if it can't be detected.
|
|
64
64
|
#--
|
|
65
|
-
#: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
|
|
65
|
+
#: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::User::FilePart
|
|
66
66
|
def self.from_url(url, media_type: nil, filename: nil, sha256: nil)
|
|
67
|
-
|
|
68
|
-
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
69
|
-
media_type = MEDIA_TYPES[ext]
|
|
70
|
-
raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
new(url: url, media_type: media_type, filename: filename, sha256: sha256)
|
|
67
|
+
new(url: url, media_type: media_type || detect_media_type(url), filename: filename, sha256: sha256)
|
|
74
68
|
end
|
|
75
69
|
|
|
76
70
|
# Builds a FilePart from a +{url:, media_type:}+ or +{data:, media_type:}+ hash,
|
|
77
71
|
# or returns +file+ unchanged when it is already a FilePart. Raises
|
|
78
72
|
# Riffer::ArgumentError on an invalid hash.
|
|
79
73
|
#--
|
|
80
|
-
#: ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
|
|
74
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::User::FilePart)) -> Riffer::Messages::User::FilePart
|
|
81
75
|
def self.from_hash(file)
|
|
82
|
-
return file if file.is_a?(Riffer::Messages::FilePart)
|
|
83
|
-
|
|
84
|
-
raise Riffer::ArgumentError, "File must be a Hash or FilePart object, got #{file.class}" unless file.is_a?(Hash)
|
|
76
|
+
return file if file.is_a?(Riffer::Messages::User::FilePart)
|
|
85
77
|
|
|
86
78
|
url = file[:url]
|
|
87
79
|
data = file[:data]
|
|
@@ -90,7 +82,7 @@ class Riffer::Messages::FilePart
|
|
|
90
82
|
sha256 = file[:sha256]
|
|
91
83
|
|
|
92
84
|
if url
|
|
93
|
-
|
|
85
|
+
new(url: url, data: data, media_type: media_type || detect_media_type(url), filename: filename, sha256: sha256)
|
|
94
86
|
elsif data && media_type
|
|
95
87
|
new(data: data, media_type: media_type, filename: filename, sha256: sha256)
|
|
96
88
|
else
|
|
@@ -98,6 +90,15 @@ class Riffer::Messages::FilePart
|
|
|
98
90
|
end
|
|
99
91
|
end
|
|
100
92
|
|
|
93
|
+
# Raises Riffer::ArgumentError when the URL's extension isn't a known media type.
|
|
94
|
+
#--
|
|
95
|
+
#: (String) -> String
|
|
96
|
+
def self.detect_media_type(url)
|
|
97
|
+
ext = ::File.extname(URI.parse(url).path.to_s).downcase
|
|
98
|
+
MEDIA_TYPES.fetch(ext) { raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" }
|
|
99
|
+
end
|
|
100
|
+
private_class_method :detect_media_type
|
|
101
|
+
|
|
101
102
|
# The base64-encoded contents - caller-supplied, or filled in by the file
|
|
102
103
|
# resolver after a download. Nil for a URL source riffer hasn't fetched.
|
|
103
104
|
#--
|
data/lib/riffer/messages/user.rb
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
# Represents a user message in a conversation.
|
|
5
5
|
class Riffer::Messages::User < Riffer::Messages::Base
|
|
6
|
+
# Builds a User message from a hash, or returns +msg+ unchanged when it is
|
|
7
|
+
# already a User message. Raises Riffer::ArgumentError on an invalid file.
|
|
8
|
+
#--
|
|
9
|
+
#: ((Hash[Symbol, untyped] | Riffer::Messages::User)) -> Riffer::Messages::User
|
|
10
|
+
def self.from_hash(msg)
|
|
11
|
+
return msg if msg.is_a?(Riffer::Messages::User)
|
|
12
|
+
|
|
13
|
+
files = (msg[:files] || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
14
|
+
new(msg[:content], id: msg[:id], files: files)
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
# File attachments for this message.
|
|
7
|
-
attr_reader :files #: Array[Riffer::Messages::FilePart]
|
|
18
|
+
attr_reader :files #: Array[Riffer::Messages::User::FilePart] # @dynamic files
|
|
8
19
|
|
|
9
20
|
#--
|
|
10
|
-
#: (String, ?id: String?, ?files: Array[Riffer::Messages::FilePart]) -> void
|
|
21
|
+
#: (String, ?id: String?, ?files: Array[Riffer::Messages::User::FilePart]) -> void
|
|
11
22
|
def initialize(content, id: nil, files: [])
|
|
12
23
|
super(content, id: id)
|
|
13
24
|
@files = files
|
data/lib/riffer/params/param.rb
CHANGED
|
@@ -31,28 +31,28 @@ class Riffer::Params::Param
|
|
|
31
31
|
}.freeze #: Hash[String, Module]
|
|
32
32
|
|
|
33
33
|
# The parameter name.
|
|
34
|
-
attr_reader :name #: Symbol
|
|
34
|
+
attr_reader :name #: Symbol # @dynamic name
|
|
35
35
|
|
|
36
36
|
# The Ruby type.
|
|
37
|
-
attr_reader :type #: Module
|
|
37
|
+
attr_reader :type #: Module # @dynamic type
|
|
38
38
|
|
|
39
39
|
# Whether the parameter is required.
|
|
40
|
-
attr_reader :required #: bool
|
|
40
|
+
attr_reader :required #: bool # @dynamic required
|
|
41
41
|
|
|
42
42
|
# The parameter description, if any.
|
|
43
|
-
attr_reader :description #: String?
|
|
43
|
+
attr_reader :description #: String? # @dynamic description
|
|
44
44
|
|
|
45
45
|
# Allowed values, if constrained.
|
|
46
|
-
attr_reader :enum #: Array[untyped]?
|
|
46
|
+
attr_reader :enum #: Array[untyped]? # @dynamic enum
|
|
47
47
|
|
|
48
48
|
# The default value, if any.
|
|
49
|
-
attr_reader :default #: untyped
|
|
49
|
+
attr_reader :default #: untyped # @dynamic default
|
|
50
50
|
|
|
51
51
|
# Element type for a typed array (+of:+).
|
|
52
|
-
attr_reader :item_type #: Module?
|
|
52
|
+
attr_reader :item_type #: Module? # @dynamic item_type
|
|
53
53
|
|
|
54
54
|
# Nested Params for object / array-of-object types.
|
|
55
|
-
attr_reader :nested_params #: Riffer::Params?
|
|
55
|
+
attr_reader :nested_params #: Riffer::Params? # @dynamic nested_params
|
|
56
56
|
|
|
57
57
|
# Reconstructs a Param from a single JSON Schema property. Raises
|
|
58
58
|
# Riffer::ArgumentError on a type outside the Params-expressible subset.
|
|
@@ -181,6 +181,18 @@ class Riffer::Params::Param
|
|
|
181
181
|
|
|
182
182
|
private
|
|
183
183
|
|
|
184
|
+
# +dup+ would leave the copy sharing this one's nested Params, enum list and
|
|
185
|
+
# default, so defining a parameter or editing either value on one would reach
|
|
186
|
+
# the other.
|
|
187
|
+
#--
|
|
188
|
+
#: (Riffer::Params::Param) -> void
|
|
189
|
+
def initialize_copy(source)
|
|
190
|
+
super
|
|
191
|
+
@nested_params = source.nested_params&.dup
|
|
192
|
+
@enum = Riffer::Helpers::DeepDup.call(source.enum)
|
|
193
|
+
@default = Riffer::Helpers::DeepDup.call(source.default)
|
|
194
|
+
end
|
|
195
|
+
|
|
184
196
|
#--
|
|
185
197
|
#: () -> void
|
|
186
198
|
def validate_strict_shape!
|
data/lib/riffer/params.rb
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
class Riffer::Params
|
|
13
13
|
# The defined parameters.
|
|
14
|
-
attr_reader :parameters #: Array[Riffer::Params::Param]
|
|
14
|
+
attr_reader :parameters #: Array[Riffer::Params::Param] # @dynamic parameters
|
|
15
15
|
|
|
16
16
|
#--
|
|
17
17
|
#: () -> void
|
|
@@ -151,6 +151,15 @@ class Riffer::Params
|
|
|
151
151
|
|
|
152
152
|
private
|
|
153
153
|
|
|
154
|
+
# +dup+ would leave the copy sharing this one's parameters, so a caller
|
|
155
|
+
# defining a parameter on either would reach the other.
|
|
156
|
+
#--
|
|
157
|
+
#: (Riffer::Params) -> void
|
|
158
|
+
def initialize_copy(source)
|
|
159
|
+
super
|
|
160
|
+
@parameters = source.parameters.map(&:dup)
|
|
161
|
+
end
|
|
162
|
+
|
|
154
163
|
#--
|
|
155
164
|
#: (Module, Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
156
165
|
def build_nested(type, of, &block)
|
|
@@ -47,7 +47,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
#--
|
|
50
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
50
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
51
51
|
def file_delivery(file)
|
|
52
52
|
file.url&.start_with?("s3://") ? :url : :bytes
|
|
53
53
|
end
|
|
@@ -98,8 +98,9 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
98
98
|
|
|
99
99
|
# requestMetadata is a flat String=>String map used to filter invocation
|
|
100
100
|
# logs; every tag (including the reserved user_id) rides along, since
|
|
101
|
-
# Converse has no dedicated end-user field.
|
|
102
|
-
|
|
101
|
+
# Converse has no dedicated end-user field. Merged over any request_metadata
|
|
102
|
+
# set in model_options; a tag wins on a shared key.
|
|
103
|
+
params[:request_metadata] = (params[:request_metadata] || {}).merge(tags) unless tags.empty?
|
|
103
104
|
|
|
104
105
|
if tools && !tools.empty?
|
|
105
106
|
params[:tool_config] = {
|
|
@@ -154,7 +155,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
154
155
|
#: (untyped) -> Hash[Symbol, untyped]
|
|
155
156
|
def build_cache_point(cache_control)
|
|
156
157
|
point = { type: "default" } #: Hash[Symbol, untyped]
|
|
157
|
-
ttl = cache_control.is_a?(Hash)
|
|
158
|
+
ttl = cache_control[:ttl] if cache_control.is_a?(Hash)
|
|
158
159
|
point[:ttl] = ttl if ttl
|
|
159
160
|
point
|
|
160
161
|
end
|
|
@@ -252,6 +253,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
252
253
|
tool_call: nil,
|
|
253
254
|
} #: Hash[Symbol, untyped]
|
|
254
255
|
|
|
256
|
+
stream_completed = false
|
|
257
|
+
|
|
255
258
|
client.converse_stream(**params) do |stream|
|
|
256
259
|
stream.on_event do |event|
|
|
257
260
|
case event
|
|
@@ -264,20 +267,45 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
264
267
|
handle_content_block_stop_text_delta(event, state: current_state, yielder: yielder) if current_state[:text]
|
|
265
268
|
handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if current_state[:tool_call]
|
|
266
269
|
when Aws::BedrockRuntime::Types::MessageStopEvent
|
|
270
|
+
stream_completed = true
|
|
267
271
|
yield_finish_reason(yielder, build_finish_reason(event.stop_reason))
|
|
268
272
|
when Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent
|
|
269
273
|
handle_metadata_usage(event, state: current_state, yielder: yielder) if event.usage
|
|
274
|
+
when Aws::Errors::EventError
|
|
275
|
+
# The SDK turns an event-stream +:message-type: error+ frame into an
|
|
276
|
+
# EventError instance and hands it to this block as an event; it is
|
|
277
|
+
# never raised. Re-raise it here so the failure surfaces instead of
|
|
278
|
+
# truncating the stream.
|
|
279
|
+
raise_stream_event_error!(event)
|
|
270
280
|
else
|
|
271
281
|
raise_if_stream_exception!(event)
|
|
272
282
|
end
|
|
273
283
|
end
|
|
274
284
|
end
|
|
285
|
+
|
|
286
|
+
return if stream_completed
|
|
287
|
+
|
|
288
|
+
raise Riffer::IncompleteStreamError, "Bedrock ConverseStream ended without a messageStop event"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Re-raises an +Aws::Errors::EventError+ event with a message built from its
|
|
292
|
+
# error code and message. The SDK's own +#message+ is just the class name,
|
|
293
|
+
# so without this the failure would be unreadable.
|
|
294
|
+
#--
|
|
295
|
+
#: (untyped) -> void
|
|
296
|
+
def raise_stream_event_error!(event)
|
|
297
|
+
details = [event.error_code, event.error_message].compact.join(": ")
|
|
298
|
+
details = "Bedrock ConverseStream error event" if details.empty?
|
|
299
|
+
|
|
300
|
+
raise event.exception(details)
|
|
275
301
|
end
|
|
276
302
|
|
|
277
303
|
# Re-raises a Bedrock stream-exception event as the matching
|
|
278
304
|
# +Aws::BedrockRuntime::Errors+ class. ConverseStream delivers API errors on
|
|
279
305
|
# the same channel as content, so without this a mid-stream failure would
|
|
280
|
-
# silently end the stream with no content.
|
|
306
|
+
# silently end the stream with no content. Non-exception events that we do
|
|
307
|
+
# not consume (including the SDK's +:unknown_event+ struct) are ignored for
|
|
308
|
+
# forward compatibility.
|
|
281
309
|
#--
|
|
282
310
|
#: (untyped) -> void
|
|
283
311
|
def raise_if_stream_exception!(event)
|
|
@@ -422,7 +450,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
422
450
|
end
|
|
423
451
|
|
|
424
452
|
#--
|
|
425
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
453
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
426
454
|
def convert_file_part_to_bedrock_format(file)
|
|
427
455
|
format = bedrock_format(file.media_type)
|
|
428
456
|
|
|
@@ -211,6 +211,8 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
211
211
|
web_search_query: nil,
|
|
212
212
|
} #: Hash[Symbol, untyped]
|
|
213
213
|
|
|
214
|
+
stream_completed = false
|
|
215
|
+
|
|
214
216
|
# Workaround for anthropics/anthropic-sdk-ruby#182: force identity
|
|
215
217
|
# encoding so Net::HTTP/Zlib doesn't buffer SSE chunks until EOF.
|
|
216
218
|
stream = client.messages.stream(
|
|
@@ -247,6 +249,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
247
249
|
handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder)
|
|
248
250
|
end
|
|
249
251
|
when ::Anthropic::Helpers::Streaming::MessageStopEvent
|
|
252
|
+
stream_completed = true
|
|
250
253
|
handle_message_stop(event, accumulated_message: stream.accumulated_message, yielder: yielder)
|
|
251
254
|
end
|
|
252
255
|
end
|
|
@@ -256,6 +259,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
256
259
|
# socket leaks until GC. close is idempotent and a no-op after EOF.
|
|
257
260
|
stream.close
|
|
258
261
|
end
|
|
262
|
+
|
|
263
|
+
return if stream_completed
|
|
264
|
+
|
|
265
|
+
raise Riffer::IncompleteStreamError, "Anthropic stream ended without a message_stop event"
|
|
259
266
|
end
|
|
260
267
|
|
|
261
268
|
#--
|
|
@@ -331,7 +338,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
331
338
|
#--
|
|
332
339
|
#: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
|
|
333
340
|
def handle_content_block_stop_thinking(_event, state:, yielder:)
|
|
334
|
-
yielder
|
|
341
|
+
yield_reasoning_done(yielder, state[:reasoning])
|
|
335
342
|
state[:reasoning] = nil
|
|
336
343
|
end
|
|
337
344
|
|
|
@@ -437,7 +444,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
437
444
|
end
|
|
438
445
|
|
|
439
446
|
#--
|
|
440
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
447
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
441
448
|
def convert_file_part_to_anthropic_format(file)
|
|
442
449
|
type = file.image? ? "image" : "document"
|
|
443
450
|
|
|
@@ -30,10 +30,9 @@ class Riffer::Providers::Base
|
|
|
30
30
|
#--
|
|
31
31
|
#: () -> String
|
|
32
32
|
def self.semconv_provider_name
|
|
33
|
-
class_name = name
|
|
34
33
|
# Anonymous classes stay uncached: a class assigned to a constant
|
|
35
34
|
# later must pick up its real name, not a frozen "unknown".
|
|
36
|
-
return "unknown"
|
|
35
|
+
class_name = name or return "unknown"
|
|
37
36
|
|
|
38
37
|
@semconv_provider_name ||= Riffer::Helpers::Identifier.derive(class_name.split("::").last)
|
|
39
38
|
end
|
|
@@ -41,7 +40,7 @@ class Riffer::Providers::Base
|
|
|
41
40
|
# Generates text using the provider.
|
|
42
41
|
#
|
|
43
42
|
#--
|
|
44
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
43
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Riffer::Messages::Assistant
|
|
45
44
|
def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
46
45
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
47
46
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -57,6 +56,7 @@ class Riffer::Providers::Base
|
|
|
57
56
|
|
|
58
57
|
content = extract_content(response)
|
|
59
58
|
tool_calls = extract_tool_calls(response)
|
|
59
|
+
reasoning = extract_reasoning(response)
|
|
60
60
|
token_usage = extract_token_usage(response)
|
|
61
61
|
finish_reason = extract_finish_reason(response)
|
|
62
62
|
structured_output = parse_structured_output(content) if options[:structured_output] && tool_calls.empty?
|
|
@@ -68,6 +68,7 @@ class Riffer::Providers::Base
|
|
|
68
68
|
Riffer::Messages::Assistant.new(
|
|
69
69
|
content,
|
|
70
70
|
tool_calls: tool_calls,
|
|
71
|
+
reasoning: reasoning,
|
|
71
72
|
token_usage: token_usage,
|
|
72
73
|
structured_output: structured_output,
|
|
73
74
|
finish_reason: finish_reason&.reason,
|
|
@@ -79,7 +80,7 @@ class Riffer::Providers::Base
|
|
|
79
80
|
# Streams text from the provider.
|
|
80
81
|
#
|
|
81
82
|
#--
|
|
82
|
-
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
83
|
+
#: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
|
|
83
84
|
def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
|
|
84
85
|
validate_input!(prompt: prompt, system: system, messages: messages)
|
|
85
86
|
@current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
|
|
@@ -106,7 +107,7 @@ class Riffer::Providers::Base
|
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
#--
|
|
109
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
110
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
110
111
|
def file_delivery(_file)
|
|
111
112
|
:url
|
|
112
113
|
end
|
|
@@ -225,6 +226,14 @@ class Riffer::Providers::Base
|
|
|
225
226
|
nil
|
|
226
227
|
end
|
|
227
228
|
|
|
229
|
+
# Defaults to no parts rather than raising — reasoning parts are optional, so
|
|
230
|
+
# providers that don't expose replayable reasoning stay valid.
|
|
231
|
+
#--
|
|
232
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
233
|
+
def extract_reasoning(_response)
|
|
234
|
+
[]
|
|
235
|
+
end
|
|
236
|
+
|
|
228
237
|
#--
|
|
229
238
|
#: (untyped) -> String
|
|
230
239
|
def extract_content(response)
|
|
@@ -349,6 +358,15 @@ class Riffer::Providers::Base
|
|
|
349
358
|
Riffer.config.tracing.capture_messages && span.recording?
|
|
350
359
|
end
|
|
351
360
|
|
|
361
|
+
# Wraps reasoning text that an adapter cannot yet replay in a +:text+ part
|
|
362
|
+
# with no +format+, so it persists for display but is never sent back.
|
|
363
|
+
#--
|
|
364
|
+
#: (Riffer::Providers::_EventSink, String) -> void
|
|
365
|
+
def yield_reasoning_done(yielder, text)
|
|
366
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: text)
|
|
367
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
368
|
+
end
|
|
369
|
+
|
|
352
370
|
#--
|
|
353
371
|
#: (Riffer::Providers::_EventSink, Riffer::Providers::FinishReason?) -> void
|
|
354
372
|
def yield_finish_reason(yielder, finish_reason)
|
|
@@ -369,11 +387,11 @@ class Riffer::Providers::Base
|
|
|
369
387
|
end
|
|
370
388
|
|
|
371
389
|
#--
|
|
372
|
-
#: (
|
|
390
|
+
#: (String) -> Hash[String, untyped]
|
|
373
391
|
def parse_tool_arguments(arguments)
|
|
374
|
-
return {} if arguments.
|
|
392
|
+
return {} if arguments.empty?
|
|
375
393
|
|
|
376
|
-
|
|
394
|
+
JSON.parse(arguments)
|
|
377
395
|
end
|
|
378
396
|
|
|
379
397
|
#--
|
|
@@ -398,7 +416,7 @@ class Riffer::Providers::Base
|
|
|
398
416
|
end
|
|
399
417
|
|
|
400
418
|
#--
|
|
401
|
-
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
419
|
+
#: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> Array[Riffer::Messages::Base]
|
|
402
420
|
def normalize_messages(prompt:, system:, messages:, files: nil)
|
|
403
421
|
if messages && files && !files.empty?
|
|
404
422
|
raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead"
|
|
@@ -408,7 +426,7 @@ class Riffer::Providers::Base
|
|
|
408
426
|
|
|
409
427
|
result = [] #: Array[Riffer::Messages::Base]
|
|
410
428
|
result << Riffer::Messages::System.new(system) if system
|
|
411
|
-
file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
429
|
+
file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
|
|
412
430
|
prompt_text = prompt #: String
|
|
413
431
|
result << Riffer::Messages::User.new(prompt_text, files: file_parts)
|
|
414
432
|
result
|
|
@@ -8,10 +8,10 @@ class Riffer::Providers::FinishReason
|
|
|
8
8
|
VALUES = %i[stop length tool_calls content_filter context_window malformed_output error other].freeze #: Array[Symbol]
|
|
9
9
|
|
|
10
10
|
# The normalized reason.
|
|
11
|
-
attr_reader :reason #: Symbol
|
|
11
|
+
attr_reader :reason #: Symbol # @dynamic reason
|
|
12
12
|
|
|
13
13
|
# The provider's raw finish-reason value, when one exists on the wire.
|
|
14
|
-
attr_reader :raw #: String?
|
|
14
|
+
attr_reader :raw #: String? # @dynamic raw
|
|
15
15
|
|
|
16
16
|
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
17
17
|
#--
|
|
@@ -37,7 +37,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
#--
|
|
40
|
-
#: (Riffer::Messages::FilePart) -> Symbol
|
|
40
|
+
#: (Riffer::Messages::User::FilePart) -> Symbol
|
|
41
41
|
def file_delivery(_file)
|
|
42
42
|
:base64
|
|
43
43
|
end
|
|
@@ -287,7 +287,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
#--
|
|
290
|
-
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
290
|
+
#: (Riffer::Messages::User::FilePart) -> Hash[Symbol, untyped]
|
|
291
291
|
def convert_file_part_to_gemini_format(file)
|
|
292
292
|
{ inlineData: { mimeType: file.media_type, data: file.data } }
|
|
293
293
|
end
|
|
@@ -25,7 +25,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Array of recorded method calls for assertions.
|
|
28
|
-
attr_reader :calls #: Array[Hash[Symbol, untyped]]
|
|
28
|
+
attr_reader :calls #: Array[Hash[Symbol, untyped]] # @dynamic calls
|
|
29
29
|
|
|
30
30
|
# +responses:+ pre-configures canned responses (same shape as
|
|
31
31
|
# +#stub_response+) for standalone use; agent tests queue responses on
|
|
@@ -54,15 +54,17 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
54
54
|
# provider.stub_response("Final response",
|
|
55
55
|
# token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
56
56
|
# provider.stub_response("Truncated...", finish_reason: :length)
|
|
57
|
+
# provider.stub_response("Answer", reasoning: [{type: :text, text: "Thinking...", format: "mock-v1"}])
|
|
57
58
|
#
|
|
58
59
|
#--
|
|
59
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?) -> void
|
|
60
|
-
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil)
|
|
60
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?, ?finish_reason: Symbol?, ?reasoning: Array[Hash[Symbol, untyped] | Riffer::Messages::Assistant::ReasoningPart]) -> void
|
|
61
|
+
def stub_response(content, tool_calls: [], token_usage: nil, finish_reason: nil, reasoning: [])
|
|
61
62
|
@stubbed_responses << normalize_response(
|
|
62
63
|
content: content,
|
|
63
64
|
tool_calls: tool_calls,
|
|
64
65
|
token_usage: token_usage,
|
|
65
66
|
finish_reason: finish_reason,
|
|
67
|
+
reasoning: reasoning,
|
|
66
68
|
)
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -92,6 +94,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
92
94
|
role: response[:role] || "assistant",
|
|
93
95
|
content: response[:content] || "",
|
|
94
96
|
tool_calls: formatted_tool_calls,
|
|
97
|
+
reasoning: (response[:reasoning] || []).map { |part| Riffer::Messages::Assistant::ReasoningPart.from_hash(part) },
|
|
95
98
|
token_usage: response[:token_usage],
|
|
96
99
|
finish_reason: response[:finish_reason] || (formatted_tool_calls.empty? ? :stop : :tool_calls),
|
|
97
100
|
}
|
|
@@ -129,6 +132,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
129
132
|
reason ? Riffer::Providers::FinishReason.new(reason: reason) : nil
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
#--
|
|
136
|
+
#: (untyped) -> Array[Riffer::Messages::Assistant::ReasoningPart]
|
|
137
|
+
def extract_reasoning(response)
|
|
138
|
+
response[:reasoning] || []
|
|
139
|
+
end
|
|
140
|
+
|
|
132
141
|
#--
|
|
133
142
|
#: (untyped) -> String
|
|
134
143
|
def extract_content(response)
|
|
@@ -149,6 +158,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
149
158
|
tool_calls = response[:tool_calls] || []
|
|
150
159
|
token_usage = response[:token_usage]
|
|
151
160
|
web_search = response[:web_search]
|
|
161
|
+
reasoning = response[:reasoning] || []
|
|
152
162
|
|
|
153
163
|
if web_search
|
|
154
164
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("in_progress")
|
|
@@ -161,6 +171,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
161
171
|
)
|
|
162
172
|
end
|
|
163
173
|
|
|
174
|
+
reasoning.each do |part|
|
|
175
|
+
text = part.text
|
|
176
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(text) if text
|
|
177
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
178
|
+
end
|
|
179
|
+
|
|
164
180
|
unless full_content.empty?
|
|
165
181
|
content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
|
|
166
182
|
content_parts.each do |part|
|