rasti-ai 2.0.2 → 3.0.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/.github/workflows/ci.yml +4 -20
- data/AGENTS.md +614 -0
- data/README.md +133 -25
- data/Rakefile +2 -0
- data/lib/rasti/ai/anthropic/assistant.rb +139 -0
- data/lib/rasti/ai/anthropic/client.rb +58 -0
- data/lib/rasti/ai/anthropic/roles.rb +12 -0
- data/lib/rasti/ai/assistant.rb +8 -3
- data/lib/rasti/ai/gemini/assistant.rb +42 -12
- data/lib/rasti/ai/mcp/client.rb +60 -9
- data/lib/rasti/ai/mcp/{errors.rb → constants.rb} +4 -1
- data/lib/rasti/ai/mcp/server.rb +42 -47
- data/lib/rasti/ai/mcp/tools_registry.rb +64 -0
- data/lib/rasti/ai/open_ai/assistant.rb +9 -4
- data/lib/rasti/ai/open_ai/client.rb +3 -2
- data/lib/rasti/ai/tool_serializer.rb +35 -62
- data/lib/rasti/ai/version.rb +1 -1
- data/lib/rasti/ai.rb +10 -0
- data/rasti-ai.gemspec +4 -1
- data/spec/anthropic/assistant_spec.rb +349 -0
- data/spec/anthropic/client_spec.rb +203 -0
- data/spec/gemini/assistant_spec.rb +15 -0
- data/spec/mcp/client_spec.rb +3 -1
- data/spec/mcp/server_spec.rb +195 -136
- data/spec/mcp/tools_registry_spec.rb +226 -0
- data/spec/minitest_helper.rb +29 -0
- data/spec/open_ai/assistant_spec.rb +20 -4
- data/spec/resources/anthropic/basic_request.json +1 -0
- data/spec/resources/anthropic/basic_response.json +20 -0
- data/spec/resources/anthropic/tool_request.json +1 -0
- data/spec/resources/anthropic/tool_response.json +22 -0
- data/spec/tool_serializer_spec.rb +31 -6
- data/tasks/assistant.rake +94 -0
- metadata +46 -6
data/spec/minitest_helper.rb
CHANGED
|
@@ -22,6 +22,9 @@ Rasti::AI.configure do |config|
|
|
|
22
22
|
|
|
23
23
|
config.gemini_api_key = 'test_gemini_api_key'
|
|
24
24
|
config.gemini_default_model = 'gemini-test'
|
|
25
|
+
|
|
26
|
+
config.anthropic_api_key = 'test_anthropic_api_key'
|
|
27
|
+
config.anthropic_default_model = 'claude-test'
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
|
|
@@ -32,6 +35,32 @@ class Minitest::Test
|
|
|
32
35
|
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
class HelloWorldTool < Rasti::AI::Tool
|
|
39
|
+
def self.description
|
|
40
|
+
'Hello World'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def execute(form)
|
|
44
|
+
{text: 'Hello world'}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class SumTool < Rasti::AI::Tool
|
|
49
|
+
class Form < Rasti::Form
|
|
50
|
+
attribute :number_a, Rasti::Types::Float
|
|
51
|
+
attribute :number_b, Rasti::Types::Float
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.description
|
|
55
|
+
'Sum two numbers'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute(form)
|
|
59
|
+
{result: form.number_a + form.number_b}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
|
|
35
64
|
class GoalsByPlayer
|
|
36
65
|
def self.form
|
|
37
66
|
Rasti::Form[player: Rasti::Types::String, team: Rasti::Types::String]
|
|
@@ -35,15 +35,16 @@ describe Rasti::AI::OpenAI::Assistant do
|
|
|
35
35
|
it 'Client' do
|
|
36
36
|
client_arguments = [
|
|
37
37
|
{
|
|
38
|
-
model:
|
|
39
|
-
tools:
|
|
38
|
+
model: nil,
|
|
39
|
+
tools: [],
|
|
40
40
|
messages: [
|
|
41
41
|
{
|
|
42
|
-
role:
|
|
42
|
+
role: Rasti::AI::OpenAI::Roles::USER,
|
|
43
43
|
content: question
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
|
-
response_format:
|
|
46
|
+
response_format: nil,
|
|
47
|
+
reasoning_effort: nil
|
|
47
48
|
}
|
|
48
49
|
]
|
|
49
50
|
|
|
@@ -111,6 +112,21 @@ describe Rasti::AI::OpenAI::Assistant do
|
|
|
111
112
|
assert_equal answer, response
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
it 'Thinking' do
|
|
116
|
+
body = read_json_resource('open_ai/basic_request.json', model: Rasti::AI.openai_default_model, prompt: question)
|
|
117
|
+
body['reasoning_effort'] = 'medium'
|
|
118
|
+
|
|
119
|
+
stub_request(:post, api_url)
|
|
120
|
+
.with(body: JSON.dump(body))
|
|
121
|
+
.to_return(body: read_resource('open_ai/basic_response.json', content: answer))
|
|
122
|
+
|
|
123
|
+
assistant = Rasti::AI::OpenAI::Assistant.new thinking: 'medium'
|
|
124
|
+
|
|
125
|
+
response = assistant.call question
|
|
126
|
+
|
|
127
|
+
assert_equal answer, response
|
|
128
|
+
end
|
|
129
|
+
|
|
114
130
|
it 'JSON Schema' do
|
|
115
131
|
json_schema = {answer: 'Response answer'}
|
|
116
132
|
json_answer = "{\\\"answer\\\": \\\"#{answer}\\\"}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"model":"<%= model %>","max_tokens":4096,"messages":[{"role":"user","content":"<%= prompt %>"}]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
|
|
3
|
+
"type": "message",
|
|
4
|
+
"role": "assistant",
|
|
5
|
+
"content": [
|
|
6
|
+
{
|
|
7
|
+
"type": "text",
|
|
8
|
+
"text": "<%= content %>"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"model": "claude-test",
|
|
12
|
+
"stop_reason": "end_turn",
|
|
13
|
+
"stop_sequence": null,
|
|
14
|
+
"usage": {
|
|
15
|
+
"input_tokens": 25,
|
|
16
|
+
"output_tokens": 11,
|
|
17
|
+
"cache_creation_input_tokens": 0,
|
|
18
|
+
"cache_read_input_tokens": 0
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"model":"<%= model %>","max_tokens":4096,"messages":[{"role":"user","content":"<%= prompt %>"}],"tools":<%= JSON.dump(tools) %>,"tool_choice":{"type":"auto"}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "msg_01HqBZkZSz4zKijpCKB8AMKM",
|
|
3
|
+
"type": "message",
|
|
4
|
+
"role": "assistant",
|
|
5
|
+
"content": [
|
|
6
|
+
{
|
|
7
|
+
"type": "tool_use",
|
|
8
|
+
"id": "toolu_01A09q90qw90lq917835lq9",
|
|
9
|
+
"name": "<%= name %>",
|
|
10
|
+
"input": <%= JSON.dump(arguments) %>
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"model": "claude-test",
|
|
14
|
+
"stop_reason": "tool_use",
|
|
15
|
+
"stop_sequence": null,
|
|
16
|
+
"usage": {
|
|
17
|
+
"input_tokens": 82,
|
|
18
|
+
"output_tokens": 29,
|
|
19
|
+
"cache_creation_input_tokens": 0,
|
|
20
|
+
"cache_read_input_tokens": 0
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -274,16 +274,41 @@ describe Rasti::AI::ToolSerializer do
|
|
|
274
274
|
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
-
it '
|
|
277
|
+
it 'Custom registered type' do
|
|
278
|
+
custom_type = Class.new
|
|
279
|
+
Rasti::Model::Schema.register_type_serializer(custom_type) { {type: :string} }
|
|
280
|
+
|
|
281
|
+
form_class = Rasti::Form[value: custom_type]
|
|
282
|
+
tool_class = build_tool_class form_class
|
|
283
|
+
|
|
284
|
+
serialization = serializer.serialize tool_class
|
|
285
|
+
|
|
286
|
+
expected_serialization = build_serializaton param_name: 'value', param_type: 'string'
|
|
287
|
+
|
|
288
|
+
assert_equal expected_serialization, serialization
|
|
289
|
+
|
|
290
|
+
tool_class.verify
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'Unknown type serializes without constraints' do
|
|
278
294
|
form_class = Rasti::Form[obj: Object]
|
|
279
295
|
tool_class = build_tool_class form_class
|
|
280
296
|
|
|
281
|
-
|
|
282
|
-
serializer.serialize tool_class
|
|
283
|
-
end
|
|
297
|
+
serialization = serializer.serialize tool_class
|
|
284
298
|
|
|
285
|
-
|
|
286
|
-
|
|
299
|
+
expected_serialization = {
|
|
300
|
+
name: 'call_custom_function',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
obj: {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
assert_equal expected_serialization, serialization
|
|
310
|
+
|
|
311
|
+
tool_class.verify
|
|
287
312
|
end
|
|
288
313
|
|
|
289
314
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
# Interactive CLI to chat with an AI assistant backed by a public weather MCP server.
|
|
7
|
+
#
|
|
8
|
+
# The Pipeworx weather MCP is free, requires no auth key, and exposes:
|
|
9
|
+
# get_weather — current conditions for any city or lat/lon
|
|
10
|
+
# get_forecast — daily forecast up to 16 days ahead
|
|
11
|
+
# get_historical — historical weather back to 1940
|
|
12
|
+
# Docs: https://pipeworx.io
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# rake assistant:openai # requires OPENAI_API_KEY (+ optionally OPENAI_DEFAULT_MODEL)
|
|
16
|
+
# rake assistant:gemini # requires GEMINI_API_KEY (+ optionally GEMINI_DEFAULT_MODEL)
|
|
17
|
+
# rake assistant:anthropic # requires ANTHROPIC_API_KEY (+ optionally ANTHROPIC_DEFAULT_MODEL)
|
|
18
|
+
|
|
19
|
+
WEATHER_MCP_URL = 'https://gateway.pipeworx.io/weather/mcp'.freeze
|
|
20
|
+
|
|
21
|
+
PROVIDERS = {
|
|
22
|
+
'openai' => {key: 'OPENAI_API_KEY', klass: -> { Rasti::AI::OpenAI::Assistant }},
|
|
23
|
+
'gemini' => {key: 'GEMINI_API_KEY', klass: -> { Rasti::AI::Gemini::Assistant }},
|
|
24
|
+
'anthropic' => {key: 'ANTHROPIC_API_KEY', klass: -> { Rasti::AI::Anthropic::Assistant }}
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def build_weather_mcp
|
|
28
|
+
Rasti::AI::MCP::Client.new(
|
|
29
|
+
url: WEATHER_MCP_URL,
|
|
30
|
+
allowed_tools: ['get_weather', 'get_forecast', 'get_historical']
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print_banner(provider, tool_names)
|
|
35
|
+
puts
|
|
36
|
+
puts '=' * 60
|
|
37
|
+
puts " Rasti AI Chat — #{provider}"
|
|
38
|
+
puts " MCP: #{WEATHER_MCP_URL}"
|
|
39
|
+
puts " Tools: #{tool_names.join(', ')}"
|
|
40
|
+
puts '=' * 60
|
|
41
|
+
puts " Type your message and press Enter."
|
|
42
|
+
puts " Type 'exit' or press Ctrl+C to quit."
|
|
43
|
+
puts '=' * 60
|
|
44
|
+
puts
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def chat_loop(assistant)
|
|
48
|
+
loop do
|
|
49
|
+
print 'You: '
|
|
50
|
+
$stdout.flush
|
|
51
|
+
|
|
52
|
+
input = $stdin.gets&.chomp
|
|
53
|
+
break if input.nil? || input.strip.downcase == 'exit'
|
|
54
|
+
next if input.strip.empty?
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
response = assistant.call(input.strip)
|
|
58
|
+
puts
|
|
59
|
+
puts "Assistant: #{response}"
|
|
60
|
+
puts
|
|
61
|
+
rescue Interrupt
|
|
62
|
+
break
|
|
63
|
+
rescue => e
|
|
64
|
+
puts "\n[Error] #{e.message}\n"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "\nGoodbye!"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start_assistant(provider, env_key, assistant_klass)
|
|
72
|
+
require 'rasti-ai'
|
|
73
|
+
|
|
74
|
+
abort "[Error] #{env_key} is not set." unless ENV[env_key]
|
|
75
|
+
|
|
76
|
+
FileUtils.mkdir_p 'log'
|
|
77
|
+
Rasti::AI.configure { |c| c.logger = Logger.new("log/#{provider}.log") }
|
|
78
|
+
|
|
79
|
+
mcp = build_weather_mcp
|
|
80
|
+
print_banner provider.capitalize, mcp.list_tools.map { |t| t['name'] }
|
|
81
|
+
|
|
82
|
+
chat_loop assistant_klass.call.new(mcp_servers: {weather: mcp})
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
namespace :assistant do
|
|
86
|
+
|
|
87
|
+
PROVIDERS.each do |provider, config|
|
|
88
|
+
desc "Chat with #{provider.capitalize} assistant using Pipeworx weather MCP (requires #{config[:key]})"
|
|
89
|
+
task provider.to_sym do
|
|
90
|
+
start_assistant(provider, config[:key], config[:klass])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rasti-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Naiman
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: multi_require
|
|
@@ -94,6 +94,26 @@ dependencies:
|
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '12.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rack
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '1.3'
|
|
104
|
+
- - "<"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '3'
|
|
107
|
+
type: :development
|
|
108
|
+
prerelease: false
|
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '1.3'
|
|
114
|
+
- - "<"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3'
|
|
97
117
|
- !ruby/object:Gem::Dependency
|
|
98
118
|
name: rack-test
|
|
99
119
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -188,14 +208,14 @@ dependencies:
|
|
|
188
208
|
name: pry-nav
|
|
189
209
|
requirement: !ruby/object:Gem::Requirement
|
|
190
210
|
requirements:
|
|
191
|
-
- - "
|
|
211
|
+
- - ">="
|
|
192
212
|
- !ruby/object:Gem::Version
|
|
193
213
|
version: '0.2'
|
|
194
214
|
type: :development
|
|
195
215
|
prerelease: false
|
|
196
216
|
version_requirements: !ruby/object:Gem::Requirement
|
|
197
217
|
requirements:
|
|
198
|
-
- - "
|
|
218
|
+
- - ">="
|
|
199
219
|
- !ruby/object:Gem::Version
|
|
200
220
|
version: '0.2'
|
|
201
221
|
- !ruby/object:Gem::Dependency
|
|
@@ -223,12 +243,16 @@ files:
|
|
|
223
243
|
- ".gitignore"
|
|
224
244
|
- ".ruby-gemset"
|
|
225
245
|
- ".ruby-version"
|
|
246
|
+
- AGENTS.md
|
|
226
247
|
- Gemfile
|
|
227
248
|
- LICENSE.txt
|
|
228
249
|
- README.md
|
|
229
250
|
- Rakefile
|
|
230
251
|
- lib/rasti-ai.rb
|
|
231
252
|
- lib/rasti/ai.rb
|
|
253
|
+
- lib/rasti/ai/anthropic/assistant.rb
|
|
254
|
+
- lib/rasti/ai/anthropic/client.rb
|
|
255
|
+
- lib/rasti/ai/anthropic/roles.rb
|
|
232
256
|
- lib/rasti/ai/assistant.rb
|
|
233
257
|
- lib/rasti/ai/assistant_state.rb
|
|
234
258
|
- lib/rasti/ai/client.rb
|
|
@@ -237,8 +261,9 @@ files:
|
|
|
237
261
|
- lib/rasti/ai/gemini/client.rb
|
|
238
262
|
- lib/rasti/ai/gemini/roles.rb
|
|
239
263
|
- lib/rasti/ai/mcp/client.rb
|
|
240
|
-
- lib/rasti/ai/mcp/
|
|
264
|
+
- lib/rasti/ai/mcp/constants.rb
|
|
241
265
|
- lib/rasti/ai/mcp/server.rb
|
|
266
|
+
- lib/rasti/ai/mcp/tools_registry.rb
|
|
242
267
|
- lib/rasti/ai/open_ai/assistant.rb
|
|
243
268
|
- lib/rasti/ai/open_ai/client.rb
|
|
244
269
|
- lib/rasti/ai/open_ai/roles.rb
|
|
@@ -248,14 +273,21 @@ files:
|
|
|
248
273
|
- lib/rasti/ai/version.rb
|
|
249
274
|
- log/.gitkeep
|
|
250
275
|
- rasti-ai.gemspec
|
|
276
|
+
- spec/anthropic/assistant_spec.rb
|
|
277
|
+
- spec/anthropic/client_spec.rb
|
|
251
278
|
- spec/coverage_helper.rb
|
|
252
279
|
- spec/gemini/assistant_spec.rb
|
|
253
280
|
- spec/gemini/client_spec.rb
|
|
254
281
|
- spec/mcp/client_spec.rb
|
|
255
282
|
- spec/mcp/server_spec.rb
|
|
283
|
+
- spec/mcp/tools_registry_spec.rb
|
|
256
284
|
- spec/minitest_helper.rb
|
|
257
285
|
- spec/open_ai/assistant_spec.rb
|
|
258
286
|
- spec/open_ai/client_spec.rb
|
|
287
|
+
- spec/resources/anthropic/basic_request.json
|
|
288
|
+
- spec/resources/anthropic/basic_response.json
|
|
289
|
+
- spec/resources/anthropic/tool_request.json
|
|
290
|
+
- spec/resources/anthropic/tool_response.json
|
|
259
291
|
- spec/resources/gemini/basic_request.json
|
|
260
292
|
- spec/resources/gemini/basic_response.json
|
|
261
293
|
- spec/resources/gemini/tool_request.json
|
|
@@ -267,6 +299,7 @@ files:
|
|
|
267
299
|
- spec/support/helpers/erb.rb
|
|
268
300
|
- spec/support/helpers/resources.rb
|
|
269
301
|
- spec/tool_serializer_spec.rb
|
|
302
|
+
- tasks/assistant.rake
|
|
270
303
|
homepage: https://github.com/gabynaiman/rasti-ai
|
|
271
304
|
licenses:
|
|
272
305
|
- MIT
|
|
@@ -279,7 +312,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
279
312
|
requirements:
|
|
280
313
|
- - ">="
|
|
281
314
|
- !ruby/object:Gem::Version
|
|
282
|
-
version: '
|
|
315
|
+
version: '2.3'
|
|
283
316
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
284
317
|
requirements:
|
|
285
318
|
- - ">="
|
|
@@ -291,14 +324,21 @@ signing_key:
|
|
|
291
324
|
specification_version: 4
|
|
292
325
|
summary: AI for apps
|
|
293
326
|
test_files:
|
|
327
|
+
- spec/anthropic/assistant_spec.rb
|
|
328
|
+
- spec/anthropic/client_spec.rb
|
|
294
329
|
- spec/coverage_helper.rb
|
|
295
330
|
- spec/gemini/assistant_spec.rb
|
|
296
331
|
- spec/gemini/client_spec.rb
|
|
297
332
|
- spec/mcp/client_spec.rb
|
|
298
333
|
- spec/mcp/server_spec.rb
|
|
334
|
+
- spec/mcp/tools_registry_spec.rb
|
|
299
335
|
- spec/minitest_helper.rb
|
|
300
336
|
- spec/open_ai/assistant_spec.rb
|
|
301
337
|
- spec/open_ai/client_spec.rb
|
|
338
|
+
- spec/resources/anthropic/basic_request.json
|
|
339
|
+
- spec/resources/anthropic/basic_response.json
|
|
340
|
+
- spec/resources/anthropic/tool_request.json
|
|
341
|
+
- spec/resources/anthropic/tool_response.json
|
|
302
342
|
- spec/resources/gemini/basic_request.json
|
|
303
343
|
- spec/resources/gemini/basic_response.json
|
|
304
344
|
- spec/resources/gemini/tool_request.json
|