riffer 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +18 -11
  3. data/.agents/code-style.md +1 -1
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.agents/testing.md +9 -5
  6. data/.release-please-manifest.json +1 -1
  7. data/AGENTS.md +17 -10
  8. data/CHANGELOG.md +19 -0
  9. data/README.md +17 -18
  10. data/Steepfile +7 -1
  11. data/docs/03_AGENTS.md +34 -3
  12. data/docs/04_AGENT_LIFECYCLE.md +87 -86
  13. data/docs/05_AGENT_LOOP.md +2 -2
  14. data/docs/06_TOOLS.md +9 -4
  15. data/docs/07_TOOL_ADVANCED.md +17 -17
  16. data/docs/08_MESSAGES.md +25 -32
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +7 -18
  19. data/docs/providers/01_PROVIDERS.md +6 -0
  20. data/docs/providers/06_MOCK_PROVIDER.md +2 -1
  21. data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
  22. data/docs/providers/08_GEMINI.md +2 -2
  23. data/docs/providers/09_OPENROUTER.md +242 -0
  24. data/lib/riffer/agent/config.rb +173 -0
  25. data/lib/riffer/agent/context.rb +125 -0
  26. data/lib/riffer/agent/run.rb +308 -0
  27. data/lib/riffer/agent/session/repair.rb +112 -0
  28. data/lib/riffer/agent/session.rb +268 -0
  29. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  30. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  31. data/lib/riffer/agent.rb +234 -923
  32. data/lib/riffer/config.rb +14 -7
  33. data/lib/riffer/evals/evaluator.rb +13 -3
  34. data/lib/riffer/evals/judge.rb +2 -2
  35. data/lib/riffer/evals/run_result.rb +2 -1
  36. data/lib/riffer/evals/scenario_result.rb +2 -1
  37. data/lib/riffer/guardrails/runner.rb +3 -2
  38. data/lib/riffer/helpers/call_or_value.rb +16 -0
  39. data/lib/riffer/helpers.rb +0 -1
  40. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  41. data/lib/riffer/mcp/client.rb +1 -1
  42. data/lib/riffer/mcp/registration.rb +2 -3
  43. data/lib/riffer/mcp/registry.rb +3 -1
  44. data/lib/riffer/mcp/tool_factory.rb +5 -0
  45. data/lib/riffer/messages/assistant.rb +9 -3
  46. data/lib/riffer/messages/base.rb +22 -0
  47. data/lib/riffer/messages/converter.rb +6 -6
  48. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  49. data/lib/riffer/messages/tool.rb +1 -1
  50. data/lib/riffer/messages/user.rb +4 -4
  51. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  52. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  53. data/lib/riffer/params.rb +27 -21
  54. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  55. data/lib/riffer/providers/anthropic.rb +27 -28
  56. data/lib/riffer/providers/base.rb +10 -9
  57. data/lib/riffer/providers/gemini.rb +15 -12
  58. data/lib/riffer/providers/mock.rb +41 -13
  59. data/lib/riffer/providers/open_ai.rb +24 -22
  60. data/lib/riffer/providers/open_router.rb +318 -0
  61. data/lib/riffer/providers/repository.rb +1 -0
  62. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  63. data/lib/riffer/providers.rb +1 -0
  64. data/lib/riffer/runner/fibers.rb +4 -3
  65. data/lib/riffer/runner/sequential.rb +1 -1
  66. data/lib/riffer/runner/threaded.rb +1 -1
  67. data/lib/riffer/runner.rb +1 -1
  68. data/lib/riffer/skills/activate_tool.rb +4 -3
  69. data/lib/riffer/skills/config.rb +1 -1
  70. data/lib/riffer/skills/context.rb +3 -3
  71. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  72. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  73. data/lib/riffer/skills/xml_adapter.rb +1 -1
  74. data/lib/riffer/stream_events/interrupt.rb +1 -1
  75. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  76. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  77. data/lib/riffer/tool.rb +3 -3
  78. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  79. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  80. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  81. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
  82. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  83. data/lib/riffer/version.rb +1 -1
  84. data/lib/riffer.rb +2 -1
  85. data/sig/generated/riffer/agent/config.rbs +119 -0
  86. data/sig/generated/riffer/agent/context.rbs +91 -0
  87. data/sig/generated/riffer/agent/run.rbs +144 -0
  88. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  89. data/sig/generated/riffer/agent/session.rbs +145 -0
  90. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  91. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  92. data/sig/generated/riffer/agent.rbs +143 -342
  93. data/sig/generated/riffer/config.rbs +17 -5
  94. data/sig/generated/riffer/evals/judge.rbs +2 -2
  95. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  96. data/sig/generated/riffer/helpers.rbs +0 -1
  97. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  98. data/sig/generated/riffer/messages/base.rbs +18 -0
  99. data/sig/generated/riffer/messages/converter.rbs +4 -4
  100. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  101. data/sig/generated/riffer/messages/user.rbs +4 -4
  102. data/sig/generated/riffer/params/boolean.rbs +10 -0
  103. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  104. data/sig/generated/riffer/params.rbs +15 -15
  105. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  106. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  107. data/sig/generated/riffer/providers/base.rbs +10 -10
  108. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  109. data/sig/generated/riffer/providers/mock.rbs +25 -5
  110. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  111. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  112. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  113. data/sig/generated/riffer/providers.rbs +1 -0
  114. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  115. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  116. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  117. data/sig/generated/riffer/runner.rbs +2 -2
  118. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  119. data/sig/generated/riffer/skills/config.rbs +1 -1
  120. data/sig/generated/riffer/skills/context.rbs +2 -2
  121. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  122. data/sig/generated/riffer/tool.rbs +5 -5
  123. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  124. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  125. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  126. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
  127. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  128. data/sig/stubs/agent_ivars.rbs +7 -0
  129. data/sig/stubs/async.rbs +24 -0
  130. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  131. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  132. data/sig/stubs/extend_self.rbs +11 -0
  133. data/sig/stubs/lib_ivars.rbs +101 -0
  134. data/sig/stubs/mcp_sdk.rbs +22 -0
  135. data/sig/stubs/provider_ivars.rbs +36 -0
  136. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  137. data/sig/stubs/zeitwerk.rbs +12 -0
  138. metadata +54 -33
  139. data/lib/riffer/core.rb +0 -28
  140. data/lib/riffer/helpers/validations.rb +0 -18
  141. data/sig/generated/riffer/boolean.rbs +0 -10
  142. data/sig/generated/riffer/core.rbs +0 -19
  143. 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::ToolRuntime subclass, a Riffer::ToolRuntime instance,
111
- # or a Proc. Defaults to <tt>Riffer::ToolRuntime::Inline.new</tt>.
112
- attr_reader :tool_runtime #: (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)
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::ToolRuntime) | Riffer::ToolRuntime | Proc)) -> void
126
+ #: ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> void
121
127
  def tool_runtime=(value)
122
- valid = (value.is_a?(Class) && value < Riffer::ToolRuntime) || value.is_a?(Riffer::ToolRuntime) || value.is_a?(Proc)
123
- raise Riffer::ArgumentError, "tool_runtime must be a Riffer::ToolRuntime subclass, instance, or a Proc" unless valid
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::ToolRuntime::Inline.new
212
+ @tool_runtime = Riffer::Tools::Runtime::Inline.new
206
213
  @skills = Skills.new
207
214
  @message_id_strategy = :none
208
215
  @experimental_history_healing = false
@@ -31,7 +31,11 @@ class Riffer::Evals::Evaluator
31
31
  #--
32
32
  #: (?bool?) -> bool
33
33
  def higher_is_better(value = nil)
34
- return @higher_is_better.nil? || @higher_is_better if value.nil?
34
+ if value.nil?
35
+ current = @higher_is_better
36
+ return true if current.nil?
37
+ return current
38
+ end
35
39
  @higher_is_better = value
36
40
  end
37
41
 
@@ -87,8 +91,14 @@ class Riffer::Evals::Evaluator
87
91
  return input if input.is_a?(String)
88
92
 
89
93
  input.map do |msg|
90
- role = msg.is_a?(Hash) ? (msg[:role] || msg["role"]) : msg.role
91
- content = msg.is_a?(Hash) ? (msg[:content] || msg["content"]) : msg.content
94
+ if msg.is_a?(Hash)
95
+ hash = msg #: Hash[untyped, untyped]
96
+ role = hash[:role] || hash["role"]
97
+ content = hash[:content] || hash["content"]
98
+ else
99
+ role = msg.role
100
+ content = msg.content
101
+ end
92
102
  "#{role}: #{content}"
93
103
  end.join("\n\n")
94
104
  end
@@ -30,7 +30,7 @@ class Riffer::Evals::Judge
30
30
  end
31
31
 
32
32
  #--
33
- #: (context: Hash[Symbol, untyped]?, score: Float, reason: String) -> Riffer::Tools::Response
33
+ #: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
34
34
  def call(context:, score:, reason:)
35
35
  json({score: score, reason: reason})
36
36
  end
@@ -94,7 +94,7 @@ class Riffer::Evals::Judge
94
94
  #--
95
95
  #: (input: String, output: String, ?ground_truth: String?) -> String
96
96
  def build_user_message(input:, output:, ground_truth: nil)
97
- parts = []
97
+ parts = [] #: Array[String]
98
98
  parts << "## Input\n\n#{input}"
99
99
  parts << "## Output\n\n#{output}"
100
100
  parts << "## Ground Truth\n\n#{ground_truth}" if ground_truth
@@ -40,7 +40,8 @@ class Riffer::Evals::RunResult
40
40
  end
41
41
  end
42
42
 
43
- totals.each_with_object({}) do |(evaluator, total), hash|
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
- results.each_with_object({}) do |result, hash|
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[:class].new(**config[:options])
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
@@ -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
@@ -23,7 +23,7 @@ class Riffer::Mcp::Client
23
23
  depends_on "faraday"
24
24
 
25
25
  @client = client || begin
26
- resolved_headers = headers.is_a?(Proc) ? headers.call : headers
26
+ resolved_headers = Riffer::Helpers::CallOrValue.resolve(headers)
27
27
  transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
28
28
  MCP::Client.new(transport: transport)
29
29
  end
@@ -23,7 +23,7 @@ class Riffer::Mcp::Registration
23
23
  def initialize(manifest)
24
24
  @manifest = manifest
25
25
  @cancelled = false
26
- @tools = []
26
+ @tools = [] #: Array[singleton(Riffer::Tool)]
27
27
  @mutex = Mutex.new
28
28
  run_discovery
29
29
  end
@@ -62,8 +62,7 @@ class Riffer::Mcp::Registration
62
62
  tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
63
63
 
64
64
  @mutex.synchronize do
65
- next if @cancelled
66
- @tools = tools.freeze
65
+ @tools = tools.freeze unless @cancelled
67
66
  end
68
67
  end
69
68
  end
@@ -18,7 +18,9 @@ module Riffer::Mcp::Registry
18
18
  #--
19
19
  #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
20
20
  def register(manifest_or_hash)
21
- manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash)
21
+ # steep cannot verify that an untyped Hash splat supplies Manifest's
22
+ # required name:/endpoint: keywords; Manifest validates them at runtime.
23
+ manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
22
24
  registration = Riffer::Mcp::Registration.new(manifest)
23
25
  old = @mutex.synchronize do
24
26
  previous = @store[manifest.name]
@@ -31,7 +31,11 @@ module Riffer::Mcp::ToolFactory
31
31
  private_class_method def self.build_tool_class(manifest_name, client, td)
32
32
  prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
33
33
 
34
+ # steep cannot type the body of a dynamically created anonymous class:
35
+ # its ivars and `self` inside define_method are unresolvable, so the
36
+ # block is ignored wholesale (cf. AuthenticatedTool.wrap_one).
34
37
  Class.new(Riffer::Tool) do
38
+ # steep:ignore:start
35
39
  @mcp_client = client
36
40
  @mcp_server_tool_name = td[:name]
37
41
  # Set @identifier directly so .identifier does not fall back to
@@ -49,6 +53,7 @@ module Riffer::Mcp::ToolFactory
49
53
  )
50
54
  text(result)
51
55
  end
56
+ # steep:ignore:end
52
57
  end
53
58
  end
54
59
  end
@@ -17,13 +17,13 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
17
17
  attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
18
18
 
19
19
  # Token usage data for this response.
20
- attr_reader :token_usage #: Riffer::TokenUsage?
20
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage?
21
21
 
22
22
  # Parsed structured output hash, or nil when not applicable.
23
23
  attr_reader :structured_output #: Hash[Symbol, untyped]?
24
24
 
25
25
  #--
26
- #: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
26
+ #: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
27
27
  def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil)
28
28
  super(content, id: id)
29
29
  @tool_calls = tool_calls
@@ -43,6 +43,12 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
43
43
  !@structured_output.nil?
44
44
  end
45
45
 
46
+ #--
47
+ #: () -> bool
48
+ def has_tool_calls?
49
+ !@tool_calls.empty?
50
+ end
51
+
46
52
  #--
47
53
  #: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
48
54
  def +(other)
@@ -54,7 +60,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
54
60
  #--
55
61
  #: () -> Hash[Symbol, untyped]
56
62
  def to_h
57
- hash = {role: role, content: content}
63
+ hash = {role: role, content: content} #: Hash[Symbol, untyped]
58
64
  hash[:id] = id unless id.nil?
59
65
  hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
60
66
  hash[:token_usage] = token_usage.to_h if token_usage
@@ -40,6 +40,28 @@ class Riffer::Messages::Base
40
40
  raise NotImplementedError, "Subclasses must implement #role"
41
41
  end
42
42
 
43
+ # Whether this message carries pending tool calls. Defaults to +false+;
44
+ # +Riffer::Messages::Assistant+ overrides this when its +tool_calls+
45
+ # array is non-empty.
46
+ #
47
+ #--
48
+ #: () -> bool
49
+ def has_tool_calls?
50
+ false
51
+ end
52
+
53
+ # Merges another same-role message into this one.
54
+ #
55
+ # Raises NotImplementedError unless implemented by subclass. Mergeable
56
+ # message types (+User+, +Assistant+, +System+) override this; +Tool+
57
+ # messages are never merged.
58
+ #
59
+ #--
60
+ #: (untyped) -> Riffer::Messages::Base
61
+ def +(other)
62
+ raise NotImplementedError, "Subclasses must implement #+"
63
+ end
64
+
43
65
  private
44
66
 
45
67
  #: () -> String?
@@ -21,19 +21,19 @@ module Riffer::Messages::Converter
21
21
  convert_hash_to_message(msg)
22
22
  end
23
23
 
24
- # Converts a hash or FilePart object to a Riffer::FilePart.
24
+ # Converts a hash or FilePart object to a Riffer::Messages::FilePart.
25
25
  #
26
26
  # Accepts:
27
- # - +Riffer::FilePart+ objects (passed through)
27
+ # - +Riffer::Messages::FilePart+ objects (passed through)
28
28
  # - <tt>{url: "https://...", media_type: "..."}</tt> (URL source)
29
29
  # - <tt>{data: "...", media_type: "..."}</tt> (raw base64)
30
30
  #
31
31
  # Raises Riffer::ArgumentError if the hash format is invalid.
32
32
  #
33
33
  #--
34
- #: ((Hash[Symbol, untyped] | Riffer::FilePart)) -> Riffer::FilePart
34
+ #: ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
35
35
  def convert_to_file_part(file)
36
- return file if file.is_a?(Riffer::FilePart)
36
+ return file if file.is_a?(Riffer::Messages::FilePart)
37
37
 
38
38
  unless file.is_a?(Hash)
39
39
  raise Riffer::ArgumentError, "File must be a Hash or FilePart object, got #{file.class}"
@@ -45,9 +45,9 @@ module Riffer::Messages::Converter
45
45
  filename = file[:filename]
46
46
 
47
47
  if url
48
- Riffer::FilePart.from_url(url, media_type: media_type)
48
+ Riffer::Messages::FilePart.from_url(url, media_type: media_type)
49
49
  elsif data && media_type
50
- Riffer::FilePart.new(data: data, media_type: media_type, filename: filename)
50
+ Riffer::Messages::FilePart.new(data: data, media_type: media_type, filename: filename)
51
51
  else
52
52
  raise Riffer::ArgumentError, "File hash must include :url or :data with :media_type"
53
53
  end
@@ -10,11 +10,11 @@ require "uri"
10
10
  # - URLs (stored and passed to providers that support them via +from_url+)
11
11
  # - Raw base64 data (via +new+)
12
12
  #
13
- # file = Riffer::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
13
+ # file = Riffer::Messages::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
14
14
  # file.url? # => true
15
15
  # file.document? # => true
16
16
  #
17
- class Riffer::FilePart
17
+ class Riffer::Messages::FilePart
18
18
  MEDIA_TYPES = {
19
19
  ".jpg" => "image/jpeg",
20
20
  ".jpeg" => "image/jpeg",
@@ -63,10 +63,10 @@ class Riffer::FilePart
63
63
  # Raises Riffer::ArgumentError if media_type cannot be detected.
64
64
  #
65
65
  #--
66
- #: (String, ?media_type: String?) -> Riffer::FilePart
66
+ #: (String, ?media_type: String?) -> Riffer::Messages::FilePart
67
67
  def self.from_url(url, media_type: nil)
68
68
  unless media_type
69
- ext = ::File.extname(URI.parse(url).path).downcase
69
+ ext = ::File.extname(URI.parse(url).path.to_s).downcase
70
70
  media_type = MEDIA_TYPES[ext]
71
71
  raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
72
72
  end
@@ -114,7 +114,7 @@ class Riffer::FilePart
114
114
  #--
115
115
  #: () -> Hash[Symbol, untyped]
116
116
  def to_h
117
- hash = {media_type: media_type}
117
+ hash = {media_type: media_type} #: Hash[Symbol, untyped]
118
118
  hash[:data] = @data if @data
119
119
  hash[:url] = @url_string if @url_string
120
120
  hash[:filename] = filename if filename
@@ -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
@@ -8,16 +8,16 @@
8
8
  # msg.content # => "Hello!"
9
9
  #
10
10
  # msg = Riffer::Messages::User.new("Describe this image", files: [file_part])
11
- # msg.files # => [#<Riffer::FilePart ...>]
11
+ # msg.files # => [#<Riffer::Messages::FilePart ...>]
12
12
  #
13
13
  class Riffer::Messages::User < Riffer::Messages::Base
14
14
  # File attachments for this message.
15
- attr_reader :files #: Array[Riffer::FilePart]
15
+ attr_reader :files #: Array[Riffer::Messages::FilePart]
16
16
 
17
17
  # Initializes a user message.
18
18
  #
19
19
  #--
20
- #: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
20
+ #: (String, ?id: String?, ?files: Array[Riffer::Messages::FilePart]) -> void
21
21
  def initialize(content, id: nil, files: [])
22
22
  super(content, id: id)
23
23
  @files = files
@@ -38,7 +38,7 @@ class Riffer::Messages::User < Riffer::Messages::Base
38
38
  #--
39
39
  #: () -> Hash[Symbol, untyped]
40
40
  def to_h
41
- hash = {role: role, content: content}
41
+ hash = {role: role, content: content} #: Hash[Symbol, untyped]
42
42
  hash[:id] = id unless id.nil?
43
43
  hash[:files] = files.map(&:to_h) unless files.empty?
44
44
  hash
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Boolean is a sentinel type for declaring boolean parameters.
4
+ # Riffer::Params::Boolean is a sentinel type for declaring boolean parameters.
5
5
  #
6
6
  # Ruby has no +Boolean+ class (+true+ is +TrueClass+, +false+ is +FalseClass+).
7
7
  # Use this module wherever you need a single type that means "boolean":
8
8
  #
9
- # required :verbose, Riffer::Boolean
9
+ # required :verbose, Riffer::Params::Boolean
10
10
  #
11
- module Riffer::Boolean
11
+ module Riffer::Params::Boolean
12
12
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Param represents a single parameter definition.
4
+ # Riffer::Params::Param represents a single parameter definition.
5
5
  #
6
6
  # Handles type validation and JSON Schema generation for individual parameters.
7
- class Riffer::Param
7
+ class Riffer::Params::Param
8
8
  # Maps Ruby types to JSON Schema type strings
9
9
  TYPE_MAPPINGS = {
10
10
  String => "string",
11
11
  Integer => "integer",
12
12
  Float => "number",
13
- Riffer::Boolean => "boolean",
13
+ Riffer::Params::Boolean => "boolean",
14
14
  TrueClass => "boolean",
15
15
  FalseClass => "boolean",
16
16
  Array => "array",
@@ -49,7 +49,7 @@ class Riffer::Param
49
49
  def valid_type?(value)
50
50
  return true if value.nil? && !required
51
51
 
52
- if type == Riffer::Boolean || type == TrueClass || type == FalseClass
52
+ if type == Riffer::Params::Boolean || type == TrueClass || type == FalseClass
53
53
  value == true || value == false
54
54
  else
55
55
  value.is_a?(type)
@@ -80,7 +80,7 @@ class Riffer::Param
80
80
  nullable = strict && !required
81
81
 
82
82
  if nullable && enum
83
- schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]}
83
+ schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]} #: Hash[Symbol, untyped]
84
84
  schema[:description] = description if description
85
85
  return schema
86
86
  end
@@ -88,7 +88,7 @@ class Riffer::Param
88
88
  type = type_name
89
89
  type = [type, "null"] if nullable
90
90
 
91
- schema = {type: type}
91
+ schema = {type: type} #: Hash[Symbol, untyped]
92
92
  schema[:description] = description if description
93
93
  schema[:enum] = enum if enum
94
94
 
data/lib/riffer/params.rb CHANGED
@@ -12,7 +12,7 @@
12
12
  # end
13
13
  #
14
14
  class Riffer::Params
15
- attr_reader :parameters #: Array[Riffer::Param]
15
+ attr_reader :parameters #: Array[Riffer::Params::Param]
16
16
 
17
17
  #--
18
18
  #: () -> void
@@ -23,10 +23,10 @@ class Riffer::Params
23
23
  # Defines a required parameter.
24
24
  #
25
25
  #--
26
- #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ () -> void } -> void
26
+ #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
27
27
  def required(name, type, description: nil, enum: nil, of: nil, &block)
28
28
  nested = build_nested(type, of, &block)
29
- @parameters << Riffer::Param.new(
29
+ @parameters << Riffer::Params::Param.new(
30
30
  name: name,
31
31
  type: type,
32
32
  required: true,
@@ -40,10 +40,10 @@ class Riffer::Params
40
40
  # Defines an optional parameter.
41
41
  #
42
42
  #--
43
- #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ () -> void } -> void
43
+ #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
44
44
  def optional(name, type, description: nil, enum: nil, default: nil, of: nil, &block)
45
45
  nested = build_nested(type, of, &block)
46
- @parameters << Riffer::Param.new(
46
+ @parameters << Riffer::Params::Param.new(
47
47
  name: name,
48
48
  type: type,
49
49
  required: false,
@@ -62,8 +62,8 @@ class Riffer::Params
62
62
  #--
63
63
  #: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
64
64
  def validate(arguments)
65
- validated = {}
66
- errors = []
65
+ validated = {} #: Hash[Symbol, untyped]
66
+ errors = [] #: Array[String]
67
67
 
68
68
  @parameters.each do |param|
69
69
  value = arguments[param.name]
@@ -107,8 +107,8 @@ class Riffer::Params
107
107
  #--
108
108
  #: (?strict: bool) -> Hash[Symbol, untyped]
109
109
  def to_json_schema(strict: false)
110
- properties = {}
111
- required_params = []
110
+ properties = {} #: Hash[String, untyped]
111
+ required_params = [] #: Array[String]
112
112
 
113
113
  @parameters.each do |param|
114
114
  properties[param.name.to_s] = param.to_json_schema(strict: strict)
@@ -126,7 +126,7 @@ class Riffer::Params
126
126
  private
127
127
 
128
128
  #--
129
- #: (Class, Class?) ?{ () -> void } -> Riffer::Params?
129
+ #: (Class, Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
130
130
  def build_nested(type, of, &block)
131
131
  if of && block
132
132
  raise Riffer::ArgumentError, "cannot use both of: and a block"
@@ -136,9 +136,9 @@ class Riffer::Params
136
136
  unless type == Array
137
137
  raise Riffer::ArgumentError, "of: can only be used with Array type, got #{type}"
138
138
  end
139
- unless Riffer::Param::PRIMITIVE_TYPES.include?(of)
139
+ unless Riffer::Params::Param::PRIMITIVE_TYPES.include?(of)
140
140
  raise Riffer::ArgumentError,
141
- "of: must be a primitive type (#{Riffer::Param::PRIMITIVE_TYPES.map(&:name).join(", ")}), got #{of}"
141
+ "of: must be a primitive type (#{Riffer::Params::Param::PRIMITIVE_TYPES.map(&:name).join(", ")}), got #{of}"
142
142
  end
143
143
  return nil
144
144
  end
@@ -154,7 +154,7 @@ class Riffer::Params
154
154
  end
155
155
 
156
156
  #--
157
- #: (Riffer::Param, untyped, Array[String]) -> untyped
157
+ #: (Riffer::Params::Param, untyped, Array[String]) -> untyped
158
158
  def validate_nested(param, value, errors)
159
159
  if param.type == Hash && param.nested_params
160
160
  validate_nested_hash(param, value, errors)
@@ -169,9 +169,11 @@ class Riffer::Params
169
169
  end
170
170
 
171
171
  #--
172
- #: (Riffer::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
172
+ #: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
173
173
  def validate_nested_hash(param, value, errors)
174
- param.nested_params.validate(value)
174
+ nested = param.nested_params
175
+ return value unless nested
176
+ nested.validate(value)
175
177
  rescue Riffer::ValidationError => e
176
178
  e.message.split("; ").each do |msg|
177
179
  errors << "#{param.name}.#{msg}"
@@ -180,14 +182,16 @@ class Riffer::Params
180
182
  end
181
183
 
182
184
  #--
183
- #: (Riffer::Param, Array[untyped], Array[String]) -> Array[untyped]
185
+ #: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
184
186
  def validate_nested_array_of_objects(param, value, errors)
187
+ nested = param.nested_params
188
+ return value unless nested
185
189
  value.map.with_index do |item, i|
186
190
  unless item.is_a?(Hash)
187
191
  errors << "#{param.name}[#{i}] must be an object"
188
192
  next item
189
193
  end
190
- param.nested_params.validate(item)
194
+ nested.validate(item)
191
195
  rescue Riffer::ValidationError => e
192
196
  e.message.split("; ").each do |msg|
193
197
  errors << "#{param.name}[#{i}].#{msg}"
@@ -197,14 +201,16 @@ class Riffer::Params
197
201
  end
198
202
 
199
203
  #--
200
- #: (Riffer::Param, Array[untyped], Array[String]) -> void
204
+ #: (Riffer::Params::Param, Array[untyped], Array[String]) -> void
201
205
  def validate_typed_array(param, value, errors)
202
- type_name = Riffer::Param::TYPE_MAPPINGS[param.item_type]
206
+ item_type = param.item_type
207
+ return unless item_type
208
+ type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
203
209
  value.each_with_index do |item, i|
204
- valid = if param.item_type == Riffer::Boolean || param.item_type == TrueClass || param.item_type == FalseClass
210
+ valid = if item_type == Riffer::Params::Boolean || item_type == TrueClass || item_type == FalseClass
205
211
  item == true || item == false
206
212
  else
207
- item.is_a?(param.item_type)
213
+ item.is_a?(item_type)
208
214
  end
209
215
  errors << "#{param.name}[#{i}] must be a #{type_name}" unless valid
210
216
  end