riffer 0.22.0 → 0.23.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/08_GEMINI.md +142 -0
- data/lib/riffer/config.rb +5 -0
- data/lib/riffer/providers/gemini.rb +323 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/skills/backend.rb +2 -0
- data/lib/riffer/tool.rb +2 -61
- data/lib/riffer/toolable.rb +140 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/config.rbs +14 -0
- data/sig/generated/riffer/providers/gemini.rbs +84 -0
- data/sig/generated/riffer/skills/backend.rbs +2 -0
- data/sig/generated/riffer/tool.rbs +1 -41
- data/sig/generated/riffer/toolable.rbs +99 -0
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c8de18bc9f5531012f3ccbf37098925f0f58a446b1ee528cc2bb60a13ef5989
|
|
4
|
+
data.tar.gz: 32343e215229cb1a02f19ffe61d5d0f128654f2d4bf128b2a23e7ef67908b0aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f8f6bdbe623eabacc481f8264f9d43472c1d152d2383736087102db93e6d55047506d2fd6e7ec63e6647ad03aafb6f08861fcfed5ae6e44d54acd1a722041f1
|
|
7
|
+
data.tar.gz: 2ca5412c0f1c6a4f86e53f7d53495dcf8073e85d6173a92e0dd58a40c111cd2ed1e7f658d92f1d1bbdcd194bbf8c5bc17d00fa890dfc9186ec86885baacf5991
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.23.0](https://github.com/janeapp/riffer/compare/riffer/v0.22.0...riffer/v0.23.0) (2026-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add Gemini provider ([#199](https://github.com/janeapp/riffer/issues/199)) ([d0f0823](https://github.com/janeapp/riffer/commit/d0f08237052258be64a8fb63e0d1c23508258176))
|
|
14
|
+
|
|
8
15
|
## [0.22.0](https://github.com/janeapp/riffer/compare/riffer/v0.21.0...riffer/v0.22.0) (2026-04-09)
|
|
9
16
|
|
|
10
17
|
|
|
@@ -10,6 +10,7 @@ Providers are adapters that connect Riffer to LLM services. They implement a com
|
|
|
10
10
|
| Azure OpenAI | `azure_openai` | `openai` |
|
|
11
11
|
| Amazon Bedrock | `amazon_bedrock` | `aws-sdk-bedrockruntime` |
|
|
12
12
|
| Anthropic | `anthropic` | `anthropic` |
|
|
13
|
+
| Gemini | `gemini` | None |
|
|
13
14
|
| Mock | `mock` | None |
|
|
14
15
|
|
|
15
16
|
## Model String Format
|
|
@@ -22,6 +23,7 @@ class MyAgent < Riffer::Agent
|
|
|
22
23
|
model 'azure_openai/gpt-5-mini' # Azure OpenAI
|
|
23
24
|
model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0' # Bedrock
|
|
24
25
|
model 'anthropic/claude-haiku-4-5-20251001' # Anthropic
|
|
26
|
+
model 'gemini/gemini-2.5-flash-lite' # Gemini
|
|
25
27
|
model 'mock/any' # Mock provider
|
|
26
28
|
end
|
|
27
29
|
```
|
|
@@ -160,6 +162,9 @@ Riffer::Providers::Repository.find(:amazon_bedrock)
|
|
|
160
162
|
Riffer::Providers::Repository.find(:anthropic)
|
|
161
163
|
# => Riffer::Providers::Anthropic
|
|
162
164
|
|
|
165
|
+
Riffer::Providers::Repository.find(:gemini)
|
|
166
|
+
# => Riffer::Providers::Gemini
|
|
167
|
+
|
|
163
168
|
Riffer::Providers::Repository.find(:mock)
|
|
164
169
|
# => Riffer::Providers::Mock
|
|
165
170
|
```
|
|
@@ -172,3 +177,4 @@ Riffer::Providers::Repository.find(:mock)
|
|
|
172
177
|
- [Azure OpenAI](05_AZURE_OPENAI.md) - GPT models via Azure
|
|
173
178
|
- [Mock](06_MOCK_PROVIDER.md) - Mock provider for testing
|
|
174
179
|
- [Custom Providers](07_CUSTOM_PROVIDERS.md) - Creating your own provider
|
|
180
|
+
- [Gemini](08_GEMINI.md) - Gemini models via Google GenAI API
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Gemini Provider
|
|
2
|
+
|
|
3
|
+
The Gemini provider connects to Google's Gemini models via the Gemini REST API.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Configure your Gemini API key:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Riffer.configure do |config|
|
|
11
|
+
config.gemini.api_key = ENV['GEMINI_API_KEY']
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or per-agent:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
class MyAgent < Riffer::Agent
|
|
19
|
+
model 'gemini/gemini-2.5-flash-lite'
|
|
20
|
+
provider_options api_key: ENV['GEMINI_API_KEY']
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Supported Models
|
|
25
|
+
|
|
26
|
+
Use Gemini model IDs in the `gemini/model` format:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
model 'gemini/gemini-2.5-flash-lite'
|
|
30
|
+
model 'gemini/gemini-2.5-pro'
|
|
31
|
+
model 'gemini/gemini-2.5-flash'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Model Options
|
|
35
|
+
|
|
36
|
+
### temperature
|
|
37
|
+
|
|
38
|
+
Controls randomness:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
model_options temperature: 0.7
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### maxOutputTokens
|
|
45
|
+
|
|
46
|
+
Maximum tokens in response:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
model_options maxOutputTokens: 4096
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### topP
|
|
53
|
+
|
|
54
|
+
Nucleus sampling:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
model_options topP: 0.9
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Basic Generation
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
provider = Riffer::Providers::Gemini.new(api_key: ENV['GEMINI_API_KEY'])
|
|
66
|
+
|
|
67
|
+
response = provider.generate_text(
|
|
68
|
+
prompt: "Hello!",
|
|
69
|
+
model: "gemini-2.5-flash-lite"
|
|
70
|
+
)
|
|
71
|
+
puts response.content
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Streaming
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
provider.stream_text(prompt: "Tell me a story", model: "gemini-2.5-flash-lite").each do |event|
|
|
78
|
+
case event
|
|
79
|
+
when Riffer::StreamEvents::TextDelta
|
|
80
|
+
print event.content
|
|
81
|
+
when Riffer::StreamEvents::TextDone
|
|
82
|
+
puts "\n---"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Structured Output
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
params = Riffer::Params.new
|
|
91
|
+
params.required(:sentiment, String)
|
|
92
|
+
params.required(:score, Float)
|
|
93
|
+
structured_output = Riffer::StructuredOutput.new(params)
|
|
94
|
+
|
|
95
|
+
response = provider.generate_text(
|
|
96
|
+
prompt: "Analyze: 'This is great!'",
|
|
97
|
+
model: "gemini-2.5-flash-lite",
|
|
98
|
+
structured_output: structured_output
|
|
99
|
+
)
|
|
100
|
+
puts response.structured_output
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Tool Calling
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class WeatherTool < Riffer::Tool
|
|
107
|
+
description "Gets weather"
|
|
108
|
+
params do
|
|
109
|
+
required :city, String
|
|
110
|
+
end
|
|
111
|
+
def call(context:, city:)
|
|
112
|
+
text("Sunny in #{city}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
response = provider.generate_text(
|
|
117
|
+
prompt: "What's the weather in Tokyo?",
|
|
118
|
+
model: "gemini-2.5-flash-lite",
|
|
119
|
+
tools: [WeatherTool]
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### File Support
|
|
124
|
+
|
|
125
|
+
Gemini supports inline base64-encoded files (images and documents):
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
file = Riffer::FilePart.new(data: base64_data, media_type: "image/png")
|
|
129
|
+
response = provider.generate_text(
|
|
130
|
+
prompt: "Describe this image",
|
|
131
|
+
model: "gemini-2.5-flash-lite",
|
|
132
|
+
files: [file]
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Note:** URL-based file references are not supported. Provide base64-encoded data instead.
|
|
137
|
+
|
|
138
|
+
## Limitations
|
|
139
|
+
|
|
140
|
+
- **No web search** - Gemini's standard API does not include a web search tool
|
|
141
|
+
- **No URL files** - Only base64 inline data is supported for file attachments
|
|
142
|
+
- **Tool call IDs** - Gemini does not return unique call IDs for tool invocations; IDs are generated client-side
|
data/lib/riffer/config.rb
CHANGED
|
@@ -18,6 +18,7 @@ class Riffer::Config
|
|
|
18
18
|
AmazonBedrock = Struct.new(:api_token, :region, keyword_init: true)
|
|
19
19
|
Anthropic = Struct.new(:api_key, keyword_init: true)
|
|
20
20
|
AzureOpenAI = Struct.new(:api_key, :endpoint, keyword_init: true)
|
|
21
|
+
Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
|
|
21
22
|
OpenAI = Struct.new(:api_key, keyword_init: true)
|
|
22
23
|
Evals = Struct.new(:judge_model, keyword_init: true)
|
|
23
24
|
|
|
@@ -30,6 +31,9 @@ class Riffer::Config
|
|
|
30
31
|
# Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
|
|
31
32
|
attr_reader :azure_openai #: Riffer::Config::AzureOpenAI
|
|
32
33
|
|
|
34
|
+
# Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
|
|
35
|
+
attr_reader :gemini #: Riffer::Config::Gemini
|
|
36
|
+
|
|
33
37
|
# OpenAI configuration (Struct with +api_key+).
|
|
34
38
|
attr_reader :openai #: Riffer::Config::OpenAI
|
|
35
39
|
|
|
@@ -61,6 +65,7 @@ class Riffer::Config
|
|
|
61
65
|
@amazon_bedrock = AmazonBedrock.new
|
|
62
66
|
@anthropic = Anthropic.new
|
|
63
67
|
@azure_openai = AzureOpenAI.new
|
|
68
|
+
@gemini = Gemini.new
|
|
64
69
|
@openai = OpenAI.new
|
|
65
70
|
@evals = Evals.new
|
|
66
71
|
@tool_runtime = Riffer::ToolRuntime::Inline.new
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
# Google Gemini provider for Gemini models via the Gemini REST API.
|
|
10
|
+
class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
11
|
+
BASE_URI = URI("https://generativelanguage.googleapis.com") #: URI::Generic
|
|
12
|
+
VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
|
|
13
|
+
DEFAULT_OPEN_TIMEOUT = 10 #: Integer
|
|
14
|
+
DEFAULT_READ_TIMEOUT = 60 #: Integer
|
|
15
|
+
|
|
16
|
+
# Initializes the Gemini provider.
|
|
17
|
+
#
|
|
18
|
+
#--
|
|
19
|
+
#: (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
|
|
20
|
+
def initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **options)
|
|
21
|
+
api_key ||= Riffer.config.gemini.api_key
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
@open_timeout = open_timeout || Riffer.config.gemini.open_timeout || DEFAULT_OPEN_TIMEOUT
|
|
24
|
+
@read_timeout = read_timeout || Riffer.config.gemini.read_timeout || DEFAULT_READ_TIMEOUT
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
#--
|
|
30
|
+
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
31
|
+
def build_request_params(messages, model, options)
|
|
32
|
+
partitioned = partition_messages(messages)
|
|
33
|
+
tools = options[:tools]
|
|
34
|
+
structured_output = options[:structured_output]
|
|
35
|
+
|
|
36
|
+
params = {
|
|
37
|
+
model: model,
|
|
38
|
+
contents: partitioned[:contents]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction]
|
|
42
|
+
|
|
43
|
+
if tools && !tools.empty?
|
|
44
|
+
params[:tools] = [{
|
|
45
|
+
functionDeclarations: tools.map { |t| convert_tool_to_gemini_format(t) }
|
|
46
|
+
}]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
generation_config = options.except(:tools, :structured_output)
|
|
50
|
+
|
|
51
|
+
if structured_output
|
|
52
|
+
generation_config[:responseMimeType] = "application/json"
|
|
53
|
+
generation_config[:responseSchema] = strip_additional_properties(structured_output.json_schema)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
params[:generationConfig] = generation_config unless generation_config.empty?
|
|
57
|
+
|
|
58
|
+
params
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#--
|
|
62
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
63
|
+
def execute_generate(params)
|
|
64
|
+
model = params[:model]
|
|
65
|
+
body = params.except(:model)
|
|
66
|
+
response = post_request(api_path(model, "generateContent"), body)
|
|
67
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
68
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#--
|
|
72
|
+
#: (Hash[Symbol, untyped]) -> String
|
|
73
|
+
def extract_content(response)
|
|
74
|
+
parts = response.dig(:candidates, 0, :content, :parts)
|
|
75
|
+
return "" unless parts
|
|
76
|
+
|
|
77
|
+
parts.filter_map { |part| part[:text] }.join
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#--
|
|
81
|
+
#: (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
82
|
+
def extract_tool_calls(response)
|
|
83
|
+
parts = response.dig(:candidates, 0, :content, :parts)
|
|
84
|
+
return [] unless parts
|
|
85
|
+
|
|
86
|
+
parts.filter_map do |part|
|
|
87
|
+
next unless part[:functionCall]
|
|
88
|
+
|
|
89
|
+
fc = part[:functionCall]
|
|
90
|
+
Riffer::Messages::Assistant::ToolCall.new(
|
|
91
|
+
call_id: "gemini_call_#{SecureRandom.hex(12)}",
|
|
92
|
+
name: fc[:name],
|
|
93
|
+
arguments: encode_tool_arguments(fc[:args])
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#--
|
|
99
|
+
#: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
|
|
100
|
+
def extract_token_usage(response)
|
|
101
|
+
usage = response[:usageMetadata]
|
|
102
|
+
return nil unless usage
|
|
103
|
+
|
|
104
|
+
Riffer::TokenUsage.new(
|
|
105
|
+
input_tokens: usage[:promptTokenCount] || 0,
|
|
106
|
+
output_tokens: usage[:candidatesTokenCount] || 0
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#--
|
|
111
|
+
#: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
|
|
112
|
+
def execute_stream(params, yielder)
|
|
113
|
+
model = params[:model]
|
|
114
|
+
body = params.except(:model)
|
|
115
|
+
|
|
116
|
+
uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse")
|
|
117
|
+
request = Net::HTTP::Post.new(uri)
|
|
118
|
+
request["Content-Type"] = "application/json"
|
|
119
|
+
request["x-goog-api-key"] = @api_key
|
|
120
|
+
request.body = body.to_json
|
|
121
|
+
|
|
122
|
+
full_text = +""
|
|
123
|
+
buffer = +""
|
|
124
|
+
|
|
125
|
+
process_chunk = lambda do |chunk|
|
|
126
|
+
buffer << chunk
|
|
127
|
+
|
|
128
|
+
while (match = buffer.match(/\r?\n\r?\n/))
|
|
129
|
+
frame = buffer.slice!(0, match.end(0)).strip
|
|
130
|
+
next unless frame.start_with?("data: ")
|
|
131
|
+
|
|
132
|
+
json_str = frame.delete_prefix("data: ").strip
|
|
133
|
+
next if json_str.empty?
|
|
134
|
+
|
|
135
|
+
parsed = JSON.parse(json_str, symbolize_names: true)
|
|
136
|
+
parts = parsed.dig(:candidates, 0, :content, :parts)
|
|
137
|
+
|
|
138
|
+
parts&.each do |part|
|
|
139
|
+
if part[:text]
|
|
140
|
+
full_text << part[:text]
|
|
141
|
+
yielder << Riffer::StreamEvents::TextDelta.new(part[:text])
|
|
142
|
+
elsif part[:functionCall]
|
|
143
|
+
fc = part[:functionCall]
|
|
144
|
+
call_id = "gemini_call_#{SecureRandom.hex(12)}"
|
|
145
|
+
arguments = encode_tool_arguments(fc[:args])
|
|
146
|
+
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
147
|
+
item_id: call_id,
|
|
148
|
+
call_id: call_id,
|
|
149
|
+
name: fc[:name],
|
|
150
|
+
arguments: arguments
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
usage = parsed[:usageMetadata]
|
|
156
|
+
if usage && usage[:candidatesTokenCount]
|
|
157
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
158
|
+
token_usage: Riffer::TokenUsage.new(
|
|
159
|
+
input_tokens: usage[:promptTokenCount] || 0,
|
|
160
|
+
output_tokens: usage[:candidatesTokenCount] || 0
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
|
|
168
|
+
http.request(request) do |response|
|
|
169
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
response.read_body { |chunk| process_chunk.call(chunk) }
|
|
173
|
+
rescue IOError
|
|
174
|
+
process_chunk.call(response.body)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
yielder << Riffer::StreamEvents::TextDone.new(full_text) unless full_text.empty?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#--
|
|
183
|
+
#: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
184
|
+
def partition_messages(messages)
|
|
185
|
+
system_parts = []
|
|
186
|
+
contents = []
|
|
187
|
+
|
|
188
|
+
messages.each do |message|
|
|
189
|
+
case message
|
|
190
|
+
when Riffer::Messages::System
|
|
191
|
+
system_parts << {text: message.content}
|
|
192
|
+
when Riffer::Messages::User
|
|
193
|
+
if message.files.empty?
|
|
194
|
+
contents << {role: "user", parts: [{text: message.content}]}
|
|
195
|
+
else
|
|
196
|
+
parts = [{text: message.content}]
|
|
197
|
+
message.files.each { |file| parts << convert_file_part_to_gemini_format(file) }
|
|
198
|
+
contents << {role: "user", parts: parts}
|
|
199
|
+
end
|
|
200
|
+
when Riffer::Messages::Assistant
|
|
201
|
+
contents << convert_assistant_to_gemini_format(message)
|
|
202
|
+
when Riffer::Messages::Tool
|
|
203
|
+
contents << {
|
|
204
|
+
role: "user",
|
|
205
|
+
parts: [{
|
|
206
|
+
functionResponse: {
|
|
207
|
+
name: message.name,
|
|
208
|
+
response: {result: message.content}
|
|
209
|
+
}
|
|
210
|
+
}]
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
result = {contents: contents}
|
|
216
|
+
result[:system_instruction] = {parts: system_parts} unless system_parts.empty?
|
|
217
|
+
result
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
#--
|
|
221
|
+
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
222
|
+
def convert_assistant_to_gemini_format(message)
|
|
223
|
+
parts = []
|
|
224
|
+
parts << {text: message.content} if message.content && !message.content.empty?
|
|
225
|
+
|
|
226
|
+
message.tool_calls.each do |tc|
|
|
227
|
+
parts << {
|
|
228
|
+
functionCall: {
|
|
229
|
+
name: tc.name,
|
|
230
|
+
args: parse_tool_arguments(tc.arguments)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
{role: "model", parts: parts}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
#--
|
|
239
|
+
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
240
|
+
def convert_file_part_to_gemini_format(file)
|
|
241
|
+
if file.url?
|
|
242
|
+
raise Riffer::ArgumentError,
|
|
243
|
+
"Gemini provider does not support URL-based file references. Provide base64-encoded data instead."
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
{inlineData: {mimeType: file.media_type, data: file.data}}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#--
|
|
250
|
+
#: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
|
|
251
|
+
def convert_tool_to_gemini_format(tool)
|
|
252
|
+
{
|
|
253
|
+
name: tool.name,
|
|
254
|
+
description: tool.description,
|
|
255
|
+
parameters: strip_additional_properties(tool.parameters_schema)
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
#--
|
|
260
|
+
#: (untyped) -> String
|
|
261
|
+
def encode_tool_arguments(args)
|
|
262
|
+
return "{}" unless args
|
|
263
|
+
|
|
264
|
+
args.is_a?(String) ? args : args.to_json
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
#--
|
|
268
|
+
#: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
|
|
269
|
+
def post_request(path, body)
|
|
270
|
+
uri = URI("#{BASE_URI}/#{path}")
|
|
271
|
+
request = Net::HTTP::Post.new(uri)
|
|
272
|
+
request["Content-Type"] = "application/json"
|
|
273
|
+
request["x-goog-api-key"] = @api_key
|
|
274
|
+
request.body = body.to_json
|
|
275
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
#--
|
|
279
|
+
#: (String, String) -> String
|
|
280
|
+
def api_path(model, method)
|
|
281
|
+
validate_model!(model)
|
|
282
|
+
"v1beta/models/#{model}:#{method}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
#--
|
|
286
|
+
#: (String) -> void
|
|
287
|
+
def validate_model!(model)
|
|
288
|
+
return if model.match?(VALID_MODEL_PATTERN)
|
|
289
|
+
|
|
290
|
+
raise Riffer::ArgumentError, "Invalid model name: #{model.inspect}. Model must contain only alphanumeric characters, hyphens, dots, and underscores."
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
#--
|
|
294
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
295
|
+
def strip_additional_properties(schema)
|
|
296
|
+
schema = schema.dup
|
|
297
|
+
schema.delete(:additionalProperties)
|
|
298
|
+
|
|
299
|
+
if schema[:properties]
|
|
300
|
+
schema[:properties] = schema[:properties].transform_values do |prop|
|
|
301
|
+
strip_additional_properties(prop)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if schema[:items].is_a?(Hash)
|
|
306
|
+
schema[:items] = strip_additional_properties(schema[:items])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
schema
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
#--
|
|
313
|
+
#: (Net::HTTPResponse) -> void
|
|
314
|
+
def handle_api_error!(response)
|
|
315
|
+
body = begin
|
|
316
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
317
|
+
rescue JSON::ParserError
|
|
318
|
+
{message: response.body}
|
|
319
|
+
end
|
|
320
|
+
error_message = body.dig(:error, :message) || body[:message] || response.body
|
|
321
|
+
raise Riffer::Error, "Gemini API error (#{response.code}): #{error_message}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
@@ -8,6 +8,7 @@ class Riffer::Providers::Repository
|
|
|
8
8
|
amazon_bedrock: -> { Riffer::Providers::AmazonBedrock },
|
|
9
9
|
anthropic: -> { Riffer::Providers::Anthropic },
|
|
10
10
|
azure_openai: -> { Riffer::Providers::AzureOpenAI },
|
|
11
|
+
gemini: -> { Riffer::Providers::Gemini },
|
|
11
12
|
openai: -> { Riffer::Providers::OpenAI },
|
|
12
13
|
mock: -> { Riffer::Providers::Mock }
|
|
13
14
|
}.freeze #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -24,68 +24,9 @@ require "timeout"
|
|
|
24
24
|
# end
|
|
25
25
|
#
|
|
26
26
|
class Riffer::Tool
|
|
27
|
-
|
|
27
|
+
extend Riffer::Toolable
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Gets or sets the tool description.
|
|
32
|
-
#
|
|
33
|
-
#--
|
|
34
|
-
#: (?String?) -> String?
|
|
35
|
-
def self.description(value = nil)
|
|
36
|
-
return @description if value.nil?
|
|
37
|
-
@description = value.to_s
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Gets or sets the tool identifier/name.
|
|
41
|
-
#
|
|
42
|
-
#--
|
|
43
|
-
#: (?String?) -> String
|
|
44
|
-
def self.identifier(value = nil)
|
|
45
|
-
return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
|
|
46
|
-
@identifier = value.to_s
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Alias for identifier - used by providers.
|
|
50
|
-
#
|
|
51
|
-
#--
|
|
52
|
-
#: (?String?) -> String
|
|
53
|
-
def self.name(value = nil)
|
|
54
|
-
return identifier(value) unless value.nil?
|
|
55
|
-
identifier
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Gets or sets the tool timeout in seconds.
|
|
59
|
-
#
|
|
60
|
-
#--
|
|
61
|
-
#: (?(Integer | Float)?) -> (Integer | Float)
|
|
62
|
-
def self.timeout(value = nil)
|
|
63
|
-
return @timeout || DEFAULT_TIMEOUT if value.nil?
|
|
64
|
-
@timeout = value.to_f
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Defines parameters using the Params DSL.
|
|
68
|
-
#
|
|
69
|
-
#--
|
|
70
|
-
#: () ?{ () -> void } -> Riffer::Params?
|
|
71
|
-
def self.params(&block)
|
|
72
|
-
return @params_builder if block.nil?
|
|
73
|
-
@params_builder = Riffer::Params.new
|
|
74
|
-
@params_builder.instance_eval(&block)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Returns the JSON Schema for the tool's parameters.
|
|
78
|
-
#
|
|
79
|
-
#--
|
|
80
|
-
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
81
|
-
def self.parameters_schema(strict: false)
|
|
82
|
-
@params_builder&.to_json_schema(strict: strict) || empty_schema
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def self.empty_schema # :nodoc:
|
|
86
|
-
{type: "object", properties: {}, required: [], additionalProperties: false}
|
|
87
|
-
end
|
|
88
|
-
private_class_method :empty_schema
|
|
29
|
+
kind :tool
|
|
89
30
|
|
|
90
31
|
# Executes the tool with the given arguments.
|
|
91
32
|
#
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Riffer::Toolable provides the shared class-level DSL for anything that can
|
|
5
|
+
# present as a tool to an LLM — tools today, and subagents/workflows in the
|
|
6
|
+
# future.
|
|
7
|
+
#
|
|
8
|
+
# Extend this module to make a class discoverable as a tool by LLM providers.
|
|
9
|
+
# Provides identifier, description, params, timeout, and JSON schema
|
|
10
|
+
# generation.
|
|
11
|
+
#
|
|
12
|
+
# Instance-level execution concerns (+call+, +call_with_validation+, etc.)
|
|
13
|
+
# are NOT part of Toolable — those belong on Riffer::Tool.
|
|
14
|
+
#
|
|
15
|
+
# class MyTool
|
|
16
|
+
# extend Riffer::Toolable
|
|
17
|
+
#
|
|
18
|
+
# description "Does something useful"
|
|
19
|
+
#
|
|
20
|
+
# params do
|
|
21
|
+
# required :input, String
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
module Riffer::Toolable
|
|
26
|
+
DEFAULT_TIMEOUT = 10 #: Integer
|
|
27
|
+
|
|
28
|
+
# Tracks all classes that extend Toolable.
|
|
29
|
+
#
|
|
30
|
+
#--
|
|
31
|
+
#: (Module) -> void
|
|
32
|
+
def self.extended(base)
|
|
33
|
+
base.extend Riffer::Helpers::ClassNameConverter
|
|
34
|
+
@extenders ||= []
|
|
35
|
+
@extenders << base
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns all classes that have extended Toolable.
|
|
39
|
+
#
|
|
40
|
+
#--
|
|
41
|
+
#: () -> Array[Module]
|
|
42
|
+
def self.all
|
|
43
|
+
@extenders || []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Gets or sets the tool description.
|
|
47
|
+
#
|
|
48
|
+
#--
|
|
49
|
+
#: (?String?) -> String?
|
|
50
|
+
def description(value = nil)
|
|
51
|
+
return @description if value.nil?
|
|
52
|
+
@description = value.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Gets or sets the tool identifier/name.
|
|
56
|
+
#
|
|
57
|
+
#--
|
|
58
|
+
#: (?String?) -> String
|
|
59
|
+
def identifier(value = nil)
|
|
60
|
+
return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
|
|
61
|
+
@identifier = value.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Alias for identifier — used by providers.
|
|
65
|
+
#
|
|
66
|
+
#--
|
|
67
|
+
#: (?String?) -> String
|
|
68
|
+
def name(value = nil)
|
|
69
|
+
return identifier(value) unless value.nil?
|
|
70
|
+
identifier
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Gets or sets the tool timeout in seconds.
|
|
74
|
+
#
|
|
75
|
+
#--
|
|
76
|
+
#: (?(Integer | Float)?) -> (Integer | Float)
|
|
77
|
+
def timeout(value = nil)
|
|
78
|
+
return @timeout || DEFAULT_TIMEOUT if value.nil?
|
|
79
|
+
@timeout = value.to_f
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Defines parameters using the Params DSL.
|
|
83
|
+
#
|
|
84
|
+
#--
|
|
85
|
+
#: () ?{ () -> void } -> Riffer::Params?
|
|
86
|
+
def params(&block)
|
|
87
|
+
return @params_builder if block.nil?
|
|
88
|
+
@params_builder = Riffer::Params.new
|
|
89
|
+
@params_builder.instance_eval(&block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the JSON Schema for the tool's parameters.
|
|
93
|
+
#
|
|
94
|
+
#--
|
|
95
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
96
|
+
def parameters_schema(strict: false)
|
|
97
|
+
@params_builder&.to_json_schema(strict: strict) || empty_schema
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the kind of toolable entity.
|
|
101
|
+
#
|
|
102
|
+
# Defaults to +:tool+. Extensible to +:agent+, +:workflow+, etc.
|
|
103
|
+
#
|
|
104
|
+
#--
|
|
105
|
+
#: (?Symbol?) -> Symbol
|
|
106
|
+
def kind(value = nil)
|
|
107
|
+
return @kind || :tool if value.nil?
|
|
108
|
+
@kind = value.to_sym
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns a provider-agnostic tool schema hash.
|
|
112
|
+
#
|
|
113
|
+
#--
|
|
114
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
115
|
+
def to_tool_schema(strict: false)
|
|
116
|
+
{
|
|
117
|
+
name: name,
|
|
118
|
+
description: description,
|
|
119
|
+
parameters_schema: parameters_schema(strict: strict)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Validates that the minimum required metadata is present for LLM tool use.
|
|
124
|
+
#
|
|
125
|
+
# Raises Riffer::ArgumentError if validation fails.
|
|
126
|
+
#
|
|
127
|
+
#--
|
|
128
|
+
#: () -> true
|
|
129
|
+
def validate_as_tool!
|
|
130
|
+
raise Riffer::ArgumentError, "#{self} must define a description" if description.nil? || description.to_s.strip.empty?
|
|
131
|
+
raise Riffer::ArgumentError, "#{self} must have an identifier" if identifier.nil? || identifier.to_s.strip.empty?
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def empty_schema # :nodoc:
|
|
138
|
+
{type: "object", properties: {}, required: [], additionalProperties: false}
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/riffer/version.rb
CHANGED
|
@@ -38,6 +38,17 @@ class Riffer::Config
|
|
|
38
38
|
| ({ ?api_key: untyped, ?endpoint: untyped }) -> instance
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
class Gemini < Struct[untyped]
|
|
42
|
+
attr_accessor api_key(): untyped
|
|
43
|
+
|
|
44
|
+
attr_accessor open_timeout(): untyped
|
|
45
|
+
|
|
46
|
+
attr_accessor read_timeout(): untyped
|
|
47
|
+
|
|
48
|
+
def self.new: (?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped) -> instance
|
|
49
|
+
| ({ ?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped }) -> instance
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
class OpenAI < Struct[untyped]
|
|
42
53
|
attr_accessor api_key(): untyped
|
|
43
54
|
|
|
@@ -61,6 +72,9 @@ class Riffer::Config
|
|
|
61
72
|
# Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
|
|
62
73
|
attr_reader azure_openai: Riffer::Config::AzureOpenAI
|
|
63
74
|
|
|
75
|
+
# Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
|
|
76
|
+
attr_reader gemini: Riffer::Config::Gemini
|
|
77
|
+
|
|
64
78
|
# OpenAI configuration (Struct with +api_key+).
|
|
65
79
|
attr_reader openai: Riffer::Config::OpenAI
|
|
66
80
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Generated from lib/riffer/providers/gemini.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Google Gemini provider for Gemini models via the Gemini REST API.
|
|
4
|
+
class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
5
|
+
BASE_URI: URI::Generic
|
|
6
|
+
|
|
7
|
+
VALID_MODEL_PATTERN: Regexp
|
|
8
|
+
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT: Integer
|
|
10
|
+
|
|
11
|
+
DEFAULT_READ_TIMEOUT: Integer
|
|
12
|
+
|
|
13
|
+
# Initializes the Gemini provider.
|
|
14
|
+
#
|
|
15
|
+
# --
|
|
16
|
+
# : (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
|
|
17
|
+
def initialize: (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# --
|
|
22
|
+
# : (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
23
|
+
def build_request_params: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
24
|
+
|
|
25
|
+
# --
|
|
26
|
+
# : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
27
|
+
def execute_generate: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
28
|
+
|
|
29
|
+
# --
|
|
30
|
+
# : (Hash[Symbol, untyped]) -> String
|
|
31
|
+
def extract_content: (Hash[Symbol, untyped]) -> String
|
|
32
|
+
|
|
33
|
+
# --
|
|
34
|
+
# : (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
35
|
+
def extract_tool_calls: (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
36
|
+
|
|
37
|
+
# --
|
|
38
|
+
# : (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
|
|
39
|
+
def extract_token_usage: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
|
|
40
|
+
|
|
41
|
+
# --
|
|
42
|
+
# : (Hash[Symbol, untyped], Enumerator::Yielder) -> void
|
|
43
|
+
def execute_stream: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
|
|
44
|
+
|
|
45
|
+
# --
|
|
46
|
+
# : (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
47
|
+
def partition_messages: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
48
|
+
|
|
49
|
+
# --
|
|
50
|
+
# : (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
51
|
+
def convert_assistant_to_gemini_format: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
52
|
+
|
|
53
|
+
# --
|
|
54
|
+
# : (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
55
|
+
def convert_file_part_to_gemini_format: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
56
|
+
|
|
57
|
+
# --
|
|
58
|
+
# : (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
|
|
59
|
+
def convert_tool_to_gemini_format: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
|
|
60
|
+
|
|
61
|
+
# --
|
|
62
|
+
# : (untyped) -> String
|
|
63
|
+
def encode_tool_arguments: (untyped) -> String
|
|
64
|
+
|
|
65
|
+
# --
|
|
66
|
+
# : (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
|
|
67
|
+
def post_request: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
|
|
68
|
+
|
|
69
|
+
# --
|
|
70
|
+
# : (String, String) -> String
|
|
71
|
+
def api_path: (String, String) -> String
|
|
72
|
+
|
|
73
|
+
# --
|
|
74
|
+
# : (String) -> void
|
|
75
|
+
def validate_model!: (String) -> void
|
|
76
|
+
|
|
77
|
+
# --
|
|
78
|
+
# : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
79
|
+
def strip_additional_properties: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
80
|
+
|
|
81
|
+
# --
|
|
82
|
+
# : (Net::HTTPResponse) -> void
|
|
83
|
+
def handle_api_error!: (Net::HTTPResponse) -> void
|
|
84
|
+
end
|
|
@@ -20,47 +20,7 @@
|
|
|
20
20
|
# end
|
|
21
21
|
# end
|
|
22
22
|
class Riffer::Tool
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
extend Riffer::Helpers::ClassNameConverter
|
|
26
|
-
|
|
27
|
-
# Gets or sets the tool description.
|
|
28
|
-
#
|
|
29
|
-
# --
|
|
30
|
-
# : (?String?) -> String?
|
|
31
|
-
def self.description: (?String?) -> String?
|
|
32
|
-
|
|
33
|
-
# Gets or sets the tool identifier/name.
|
|
34
|
-
#
|
|
35
|
-
# --
|
|
36
|
-
# : (?String?) -> String
|
|
37
|
-
def self.identifier: (?String?) -> String
|
|
38
|
-
|
|
39
|
-
# Alias for identifier - used by providers.
|
|
40
|
-
#
|
|
41
|
-
# --
|
|
42
|
-
# : (?String?) -> String
|
|
43
|
-
def self.name: (?String?) -> String
|
|
44
|
-
|
|
45
|
-
# Gets or sets the tool timeout in seconds.
|
|
46
|
-
#
|
|
47
|
-
# --
|
|
48
|
-
# : (?(Integer | Float)?) -> (Integer | Float)
|
|
49
|
-
def self.timeout: (?(Integer | Float)?) -> (Integer | Float)
|
|
50
|
-
|
|
51
|
-
# Defines parameters using the Params DSL.
|
|
52
|
-
#
|
|
53
|
-
# --
|
|
54
|
-
# : () ?{ () -> void } -> Riffer::Params?
|
|
55
|
-
def self.params: () ?{ () -> void } -> Riffer::Params?
|
|
56
|
-
|
|
57
|
-
# Returns the JSON Schema for the tool's parameters.
|
|
58
|
-
#
|
|
59
|
-
# --
|
|
60
|
-
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
61
|
-
def self.parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
62
|
-
|
|
63
|
-
def self.empty_schema: () -> untyped
|
|
23
|
+
extend Riffer::Toolable
|
|
64
24
|
|
|
65
25
|
# Executes the tool with the given arguments.
|
|
66
26
|
#
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Generated from lib/riffer/toolable.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Riffer::Toolable provides the shared class-level DSL for anything that can
|
|
4
|
+
# present as a tool to an LLM — tools today, and subagents/workflows in the
|
|
5
|
+
# future.
|
|
6
|
+
#
|
|
7
|
+
# Extend this module to make a class discoverable as a tool by LLM providers.
|
|
8
|
+
# Provides identifier, description, params, timeout, and JSON schema
|
|
9
|
+
# generation.
|
|
10
|
+
#
|
|
11
|
+
# Instance-level execution concerns (+call+, +call_with_validation+, etc.)
|
|
12
|
+
# are NOT part of Toolable — those belong on Riffer::Tool.
|
|
13
|
+
#
|
|
14
|
+
# class MyTool
|
|
15
|
+
# extend Riffer::Toolable
|
|
16
|
+
#
|
|
17
|
+
# description "Does something useful"
|
|
18
|
+
#
|
|
19
|
+
# params do
|
|
20
|
+
# required :input, String
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
module Riffer::Toolable
|
|
24
|
+
DEFAULT_TIMEOUT: Integer
|
|
25
|
+
|
|
26
|
+
# Tracks all classes that extend Toolable.
|
|
27
|
+
#
|
|
28
|
+
# --
|
|
29
|
+
# : (Module) -> void
|
|
30
|
+
def self.extended: (Module) -> void
|
|
31
|
+
|
|
32
|
+
# Returns all classes that have extended Toolable.
|
|
33
|
+
#
|
|
34
|
+
# --
|
|
35
|
+
# : () -> Array[Module]
|
|
36
|
+
def self.all: () -> Array[Module]
|
|
37
|
+
|
|
38
|
+
# Gets or sets the tool description.
|
|
39
|
+
#
|
|
40
|
+
# --
|
|
41
|
+
# : (?String?) -> String?
|
|
42
|
+
def description: (?String?) -> String?
|
|
43
|
+
|
|
44
|
+
# Gets or sets the tool identifier/name.
|
|
45
|
+
#
|
|
46
|
+
# --
|
|
47
|
+
# : (?String?) -> String
|
|
48
|
+
def identifier: (?String?) -> String
|
|
49
|
+
|
|
50
|
+
# Alias for identifier — used by providers.
|
|
51
|
+
#
|
|
52
|
+
# --
|
|
53
|
+
# : (?String?) -> String
|
|
54
|
+
def name: (?String?) -> String
|
|
55
|
+
|
|
56
|
+
# Gets or sets the tool timeout in seconds.
|
|
57
|
+
#
|
|
58
|
+
# --
|
|
59
|
+
# : (?(Integer | Float)?) -> (Integer | Float)
|
|
60
|
+
def timeout: (?(Integer | Float)?) -> (Integer | Float)
|
|
61
|
+
|
|
62
|
+
# Defines parameters using the Params DSL.
|
|
63
|
+
#
|
|
64
|
+
# --
|
|
65
|
+
# : () ?{ () -> void } -> Riffer::Params?
|
|
66
|
+
def params: () ?{ () -> void } -> Riffer::Params?
|
|
67
|
+
|
|
68
|
+
# Returns the JSON Schema for the tool's parameters.
|
|
69
|
+
#
|
|
70
|
+
# --
|
|
71
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
72
|
+
def parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
73
|
+
|
|
74
|
+
# Returns the kind of toolable entity.
|
|
75
|
+
#
|
|
76
|
+
# Defaults to +:tool+. Extensible to +:agent+, +:workflow+, etc.
|
|
77
|
+
#
|
|
78
|
+
# --
|
|
79
|
+
# : (?Symbol?) -> Symbol
|
|
80
|
+
def kind: (?Symbol?) -> Symbol
|
|
81
|
+
|
|
82
|
+
# Returns a provider-agnostic tool schema hash.
|
|
83
|
+
#
|
|
84
|
+
# --
|
|
85
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
86
|
+
def to_tool_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
87
|
+
|
|
88
|
+
# Validates that the minimum required metadata is present for LLM tool use.
|
|
89
|
+
#
|
|
90
|
+
# Raises Riffer::ArgumentError if validation fails.
|
|
91
|
+
#
|
|
92
|
+
# --
|
|
93
|
+
# : () -> true
|
|
94
|
+
def validate_as_tool!: () -> true
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def empty_schema: () -> untyped
|
|
99
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.23.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - "~>"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: 1.
|
|
38
|
+
version: 1.32.0
|
|
39
39
|
type: :development
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: 1.
|
|
45
|
+
version: 1.32.0
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: aws-sdk-bedrockruntime
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -63,14 +63,14 @@ dependencies:
|
|
|
63
63
|
requirements:
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: 0.
|
|
66
|
+
version: 0.58.0
|
|
67
67
|
type: :development
|
|
68
68
|
prerelease: false
|
|
69
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
|
71
71
|
- - "~>"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: 0.
|
|
73
|
+
version: 0.58.0
|
|
74
74
|
- !ruby/object:Gem::Dependency
|
|
75
75
|
name: async
|
|
76
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -254,6 +254,7 @@ files:
|
|
|
254
254
|
- docs/providers/05_AZURE_OPENAI.md
|
|
255
255
|
- docs/providers/06_MOCK_PROVIDER.md
|
|
256
256
|
- docs/providers/07_CUSTOM_PROVIDERS.md
|
|
257
|
+
- docs/providers/08_GEMINI.md
|
|
257
258
|
- lib/riffer.rb
|
|
258
259
|
- lib/riffer/agent.rb
|
|
259
260
|
- lib/riffer/agent/response.rb
|
|
@@ -292,6 +293,7 @@ files:
|
|
|
292
293
|
- lib/riffer/providers/anthropic.rb
|
|
293
294
|
- lib/riffer/providers/azure_open_ai.rb
|
|
294
295
|
- lib/riffer/providers/base.rb
|
|
296
|
+
- lib/riffer/providers/gemini.rb
|
|
295
297
|
- lib/riffer/providers/mock.rb
|
|
296
298
|
- lib/riffer/providers/open_ai.rb
|
|
297
299
|
- lib/riffer/providers/repository.rb
|
|
@@ -332,6 +334,7 @@ files:
|
|
|
332
334
|
- lib/riffer/tool_runtime/fibers.rb
|
|
333
335
|
- lib/riffer/tool_runtime/inline.rb
|
|
334
336
|
- lib/riffer/tool_runtime/threaded.rb
|
|
337
|
+
- lib/riffer/toolable.rb
|
|
335
338
|
- lib/riffer/tools.rb
|
|
336
339
|
- lib/riffer/tools/response.rb
|
|
337
340
|
- lib/riffer/version.rb
|
|
@@ -373,6 +376,7 @@ files:
|
|
|
373
376
|
- sig/generated/riffer/providers/anthropic.rbs
|
|
374
377
|
- sig/generated/riffer/providers/azure_open_ai.rbs
|
|
375
378
|
- sig/generated/riffer/providers/base.rbs
|
|
379
|
+
- sig/generated/riffer/providers/gemini.rbs
|
|
376
380
|
- sig/generated/riffer/providers/mock.rbs
|
|
377
381
|
- sig/generated/riffer/providers/open_ai.rbs
|
|
378
382
|
- sig/generated/riffer/providers/repository.rbs
|
|
@@ -413,6 +417,7 @@ files:
|
|
|
413
417
|
- sig/generated/riffer/tool_runtime/fibers.rbs
|
|
414
418
|
- sig/generated/riffer/tool_runtime/inline.rbs
|
|
415
419
|
- sig/generated/riffer/tool_runtime/threaded.rbs
|
|
420
|
+
- sig/generated/riffer/toolable.rbs
|
|
416
421
|
- sig/generated/riffer/tools.rbs
|
|
417
422
|
- sig/generated/riffer/tools/response.rbs
|
|
418
423
|
- sig/generated/riffer/version.rbs
|