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
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
require 'minitest_helper'
|
|
2
|
+
|
|
3
|
+
describe Rasti::AI::Anthropic::Assistant do
|
|
4
|
+
|
|
5
|
+
let(:api_url) { 'https://api.anthropic.com/v1/messages' }
|
|
6
|
+
|
|
7
|
+
let(:question) { 'How many goals has Messi scored for Barca?' }
|
|
8
|
+
|
|
9
|
+
let(:answer) { 'Lionel Messi scored 672 goals in 778 official matches for FC Barcelona.' }
|
|
10
|
+
|
|
11
|
+
def stub_anthropic_messages(question:, answer:, model:nil, json_schema:nil)
|
|
12
|
+
model ||= Rasti::AI.anthropic_default_model
|
|
13
|
+
|
|
14
|
+
body = read_json_resource('anthropic/basic_request.json', model: model, prompt: question)
|
|
15
|
+
|
|
16
|
+
if json_schema
|
|
17
|
+
body['tools'] = [{
|
|
18
|
+
'name' => 'structured_output',
|
|
19
|
+
'description' => 'Return the structured response',
|
|
20
|
+
'input_schema' => {'type' => 'object', 'properties' => json_schema}
|
|
21
|
+
}]
|
|
22
|
+
body['tool_choice'] = {'type' => 'tool', 'name' => 'structured_output'}
|
|
23
|
+
|
|
24
|
+
stub_request(:post, api_url)
|
|
25
|
+
.with(body: JSON.dump(body))
|
|
26
|
+
.to_return(body: read_resource('anthropic/tool_response.json', name: 'structured_output', arguments: json_schema))
|
|
27
|
+
else
|
|
28
|
+
stub_request(:post, api_url)
|
|
29
|
+
.with(body: JSON.dump(body))
|
|
30
|
+
.to_return(body: read_resource('anthropic/basic_response.json', content: answer))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'Default' do
|
|
35
|
+
stub_anthropic_messages question: question, answer: answer
|
|
36
|
+
|
|
37
|
+
assistant = Rasti::AI::Anthropic::Assistant.new
|
|
38
|
+
|
|
39
|
+
response = assistant.call question
|
|
40
|
+
|
|
41
|
+
assert_equal answer, response
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe 'Customized' do
|
|
45
|
+
|
|
46
|
+
it 'Client' do
|
|
47
|
+
client_arguments = [
|
|
48
|
+
{
|
|
49
|
+
model: nil,
|
|
50
|
+
system: nil,
|
|
51
|
+
tools: [],
|
|
52
|
+
tool_choice: nil,
|
|
53
|
+
thinking: nil,
|
|
54
|
+
messages: [
|
|
55
|
+
{
|
|
56
|
+
role: Rasti::AI::Anthropic::Roles::USER,
|
|
57
|
+
content: question
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
client_response = read_json_resource 'anthropic/basic_response.json', content: answer
|
|
64
|
+
|
|
65
|
+
client = Minitest::Mock.new
|
|
66
|
+
client.expect :messages, client_response, client_arguments
|
|
67
|
+
|
|
68
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client
|
|
69
|
+
|
|
70
|
+
response = assistant.call question
|
|
71
|
+
|
|
72
|
+
assert_equal answer, response
|
|
73
|
+
|
|
74
|
+
client.verify
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'State' do
|
|
78
|
+
context = 'Act as sports journalist'
|
|
79
|
+
state = Rasti::AI::AssistantState.new context: context
|
|
80
|
+
|
|
81
|
+
request_body = {
|
|
82
|
+
model: Rasti::AI.anthropic_default_model,
|
|
83
|
+
max_tokens: 4096,
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: Rasti::AI::Anthropic::Roles::USER,
|
|
87
|
+
content: question
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
system: context
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
stub_request(:post, api_url)
|
|
94
|
+
.with(body: JSON.dump(request_body))
|
|
95
|
+
.to_return(body: read_resource('anthropic/basic_response.json', content: answer))
|
|
96
|
+
|
|
97
|
+
assistant = Rasti::AI::Anthropic::Assistant.new state: state
|
|
98
|
+
|
|
99
|
+
response = assistant.call question
|
|
100
|
+
|
|
101
|
+
expected_assistant_message = {
|
|
102
|
+
role: Rasti::AI::Anthropic::Roles::ASSISTANT,
|
|
103
|
+
content: answer
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
assert_equal answer, response
|
|
107
|
+
assert_equal 2, state.messages.count
|
|
108
|
+
assert_equal expected_assistant_message, state.messages.last
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'Model' do
|
|
112
|
+
model = SecureRandom.uuid
|
|
113
|
+
|
|
114
|
+
stub_anthropic_messages question: question, answer: answer, model: model
|
|
115
|
+
|
|
116
|
+
assistant = Rasti::AI::Anthropic::Assistant.new model: model
|
|
117
|
+
|
|
118
|
+
response = assistant.call question
|
|
119
|
+
|
|
120
|
+
assert_equal answer, response
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'Thinking' do
|
|
124
|
+
body = read_json_resource('anthropic/basic_request.json', model: Rasti::AI.anthropic_default_model, prompt: question)
|
|
125
|
+
body['thinking'] = {'type' => 'enabled', 'budget_tokens' => 8_000}
|
|
126
|
+
|
|
127
|
+
stub_request(:post, api_url)
|
|
128
|
+
.with(body: JSON.dump(body))
|
|
129
|
+
.to_return(body: read_resource('anthropic/basic_response.json', content: answer))
|
|
130
|
+
|
|
131
|
+
assistant = Rasti::AI::Anthropic::Assistant.new thinking: 'medium'
|
|
132
|
+
|
|
133
|
+
response = assistant.call question
|
|
134
|
+
|
|
135
|
+
assert_equal answer, response
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'JSON Schema' do
|
|
139
|
+
json_schema = {'answer' => 'Response answer'}
|
|
140
|
+
|
|
141
|
+
stub_anthropic_messages question: question, answer: answer, json_schema: json_schema
|
|
142
|
+
|
|
143
|
+
assistant = Rasti::AI::Anthropic::Assistant.new json_schema: json_schema
|
|
144
|
+
|
|
145
|
+
response = assistant.call question
|
|
146
|
+
|
|
147
|
+
assert_equal 'Response answer', JSON.parse(response)['answer']
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
describe 'Tools' do
|
|
155
|
+
|
|
156
|
+
let(:client) { Minitest::Mock.new }
|
|
157
|
+
|
|
158
|
+
let(:tool_response) do
|
|
159
|
+
read_json_resource(
|
|
160
|
+
'anthropic/tool_response.json',
|
|
161
|
+
name: 'goals_by_player',
|
|
162
|
+
arguments: {
|
|
163
|
+
player: 'Lionel Messi',
|
|
164
|
+
team: 'Barcelona'
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
let(:tool_result) { '672' }
|
|
170
|
+
|
|
171
|
+
let(:error_message) { 'There was an error using a tool' }
|
|
172
|
+
|
|
173
|
+
def basic_response(content)
|
|
174
|
+
read_json_resource 'anthropic/basic_response.json', content: content
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def stub_client_request(role:, content:, response:, tools:[])
|
|
178
|
+
serialized_tools = tools.map do |tool|
|
|
179
|
+
raw = Rasti::AI::ToolSerializer.serialize(tool.class)
|
|
180
|
+
result = raw.dup
|
|
181
|
+
if result.key?(:inputSchema)
|
|
182
|
+
result[:input_schema] = result.delete(:inputSchema)
|
|
183
|
+
end
|
|
184
|
+
result
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
client.expect :messages, response do |params|
|
|
188
|
+
last_message = params[:messages].last
|
|
189
|
+
last_message[:role] == role &&
|
|
190
|
+
last_message[:content] == content &&
|
|
191
|
+
params[:tools] == serialized_tools
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'Call function' do
|
|
196
|
+
tool = GoalsByPlayer.new
|
|
197
|
+
|
|
198
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
199
|
+
content: question,
|
|
200
|
+
tools: [tool],
|
|
201
|
+
response: tool_response
|
|
202
|
+
|
|
203
|
+
expected_tool_result_content = [{
|
|
204
|
+
type: 'tool_result',
|
|
205
|
+
tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
|
|
206
|
+
content: tool_result
|
|
207
|
+
}]
|
|
208
|
+
|
|
209
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
210
|
+
content: expected_tool_result_content,
|
|
211
|
+
tools: [tool],
|
|
212
|
+
response: basic_response(answer)
|
|
213
|
+
|
|
214
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
|
|
215
|
+
|
|
216
|
+
response = assistant.call question
|
|
217
|
+
|
|
218
|
+
assert_equal answer, response
|
|
219
|
+
|
|
220
|
+
client.verify
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'Tool failure' do
|
|
224
|
+
tool = GoalsByPlayer.new
|
|
225
|
+
tool.define_singleton_method :call do |*args|
|
|
226
|
+
raise 'Broken tool'
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
230
|
+
content: question,
|
|
231
|
+
tools: [tool],
|
|
232
|
+
response: tool_response
|
|
233
|
+
|
|
234
|
+
expected_tool_result_content = [{
|
|
235
|
+
type: 'tool_result',
|
|
236
|
+
tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
|
|
237
|
+
content: 'Error: Broken tool'
|
|
238
|
+
}]
|
|
239
|
+
|
|
240
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
241
|
+
content: expected_tool_result_content,
|
|
242
|
+
tools: [tool],
|
|
243
|
+
response: basic_response(error_message)
|
|
244
|
+
|
|
245
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
|
|
246
|
+
|
|
247
|
+
response = assistant.call question
|
|
248
|
+
|
|
249
|
+
assert_equal error_message, response
|
|
250
|
+
|
|
251
|
+
client.verify
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'Undefined tool' do
|
|
255
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
256
|
+
content: question,
|
|
257
|
+
response: tool_response
|
|
258
|
+
|
|
259
|
+
expected_tool_result_content = [{
|
|
260
|
+
type: 'tool_result',
|
|
261
|
+
tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
|
|
262
|
+
content: 'Error: Undefined tool goals_by_player'
|
|
263
|
+
}]
|
|
264
|
+
|
|
265
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
266
|
+
content: expected_tool_result_content,
|
|
267
|
+
response: basic_response(error_message)
|
|
268
|
+
|
|
269
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: []
|
|
270
|
+
|
|
271
|
+
response = assistant.call question
|
|
272
|
+
|
|
273
|
+
assert_equal error_message, response
|
|
274
|
+
|
|
275
|
+
client.verify
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'Cached result' do
|
|
279
|
+
mock = Minitest::Mock.new
|
|
280
|
+
mock.expect :call, tool_result, [{'player' => 'Lionel Messi', 'team' => 'Barcelona'}]
|
|
281
|
+
|
|
282
|
+
tool = GoalsByPlayer.new
|
|
283
|
+
tool.define_singleton_method :call do |*args|
|
|
284
|
+
mock.call(*args)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
expected_tool_result_content = [{
|
|
288
|
+
type: 'tool_result',
|
|
289
|
+
tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
|
|
290
|
+
content: tool_result
|
|
291
|
+
}]
|
|
292
|
+
|
|
293
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool]
|
|
294
|
+
|
|
295
|
+
5.times do
|
|
296
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
297
|
+
content: question,
|
|
298
|
+
tools: [tool],
|
|
299
|
+
response: tool_response
|
|
300
|
+
|
|
301
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
302
|
+
content: expected_tool_result_content,
|
|
303
|
+
tools: [tool],
|
|
304
|
+
response: basic_response(answer)
|
|
305
|
+
|
|
306
|
+
response = assistant.call question
|
|
307
|
+
|
|
308
|
+
assert_equal answer, response
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
client.verify
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'Custom logger' do
|
|
315
|
+
log_output = StringIO.new
|
|
316
|
+
logger = Logger.new log_output
|
|
317
|
+
|
|
318
|
+
tool = GoalsByPlayer.new
|
|
319
|
+
|
|
320
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
321
|
+
content: question,
|
|
322
|
+
tools: [tool],
|
|
323
|
+
response: tool_response
|
|
324
|
+
|
|
325
|
+
expected_tool_result_content = [{
|
|
326
|
+
type: 'tool_result',
|
|
327
|
+
tool_use_id: 'toolu_01A09q90qw90lq917835lq9',
|
|
328
|
+
content: tool_result
|
|
329
|
+
}]
|
|
330
|
+
|
|
331
|
+
stub_client_request role: Rasti::AI::Anthropic::Roles::USER,
|
|
332
|
+
content: expected_tool_result_content,
|
|
333
|
+
tools: [tool],
|
|
334
|
+
response: basic_response(answer)
|
|
335
|
+
|
|
336
|
+
assistant = Rasti::AI::Anthropic::Assistant.new client: client, tools: [tool], logger: logger
|
|
337
|
+
|
|
338
|
+
response = assistant.call question
|
|
339
|
+
|
|
340
|
+
assert_equal answer, response
|
|
341
|
+
|
|
342
|
+
refute_empty log_output.string
|
|
343
|
+
|
|
344
|
+
client.verify
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
require 'minitest_helper'
|
|
2
|
+
|
|
3
|
+
describe Rasti::AI::Anthropic::Client do
|
|
4
|
+
|
|
5
|
+
let(:api_url) { 'https://api.anthropic.com/v1/messages' }
|
|
6
|
+
|
|
7
|
+
def user_message(content)
|
|
8
|
+
{
|
|
9
|
+
role: Rasti::AI::Anthropic::Roles::USER,
|
|
10
|
+
content: content
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe 'Basic message' do
|
|
15
|
+
|
|
16
|
+
let(:question) { 'who is Messi?' }
|
|
17
|
+
|
|
18
|
+
let(:answer) { 'Lionel Messi is the best player ever' }
|
|
19
|
+
|
|
20
|
+
def stub_anthropic_messages(api_key:nil, model:nil)
|
|
21
|
+
api_key ||= Rasti::AI.anthropic_api_key
|
|
22
|
+
model ||= Rasti::AI.anthropic_default_model
|
|
23
|
+
|
|
24
|
+
stub_request(:post, api_url)
|
|
25
|
+
.with(
|
|
26
|
+
headers: {
|
|
27
|
+
'x-api-key' => api_key,
|
|
28
|
+
'anthropic-version' => '2023-06-01'
|
|
29
|
+
},
|
|
30
|
+
body: read_resource('anthropic/basic_request.json', model: model, prompt: question)
|
|
31
|
+
)
|
|
32
|
+
.to_return(body: read_resource('anthropic/basic_response.json', content: answer))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def assert_response_content(response, expected_content)
|
|
36
|
+
text_block = response['content'].find { |b| b['type'] == 'text' }
|
|
37
|
+
assert_equal expected_content, text_block['text']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'Default API key, model and logger' do
|
|
41
|
+
stub_anthropic_messages
|
|
42
|
+
|
|
43
|
+
client = Rasti::AI::Anthropic::Client.new
|
|
44
|
+
|
|
45
|
+
response = client.messages messages: [user_message(question)]
|
|
46
|
+
|
|
47
|
+
assert_response_content response, answer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'Custom API key' do
|
|
51
|
+
custom_api_key = SecureRandom.uuid
|
|
52
|
+
|
|
53
|
+
stub_anthropic_messages api_key: custom_api_key
|
|
54
|
+
|
|
55
|
+
client = Rasti::AI::Anthropic::Client.new api_key: custom_api_key
|
|
56
|
+
|
|
57
|
+
response = client.messages messages: [user_message(question)]
|
|
58
|
+
|
|
59
|
+
assert_response_content response, answer
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'Custom model' do
|
|
63
|
+
custom_model = SecureRandom.uuid
|
|
64
|
+
|
|
65
|
+
stub_anthropic_messages model: custom_model
|
|
66
|
+
|
|
67
|
+
client = Rasti::AI::Anthropic::Client.new
|
|
68
|
+
|
|
69
|
+
response = client.messages messages: [user_message(question)], model: custom_model
|
|
70
|
+
|
|
71
|
+
assert_response_content response, answer
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'Custom logger' do
|
|
75
|
+
log_output = StringIO.new
|
|
76
|
+
logger = Logger.new log_output
|
|
77
|
+
|
|
78
|
+
stub_anthropic_messages
|
|
79
|
+
|
|
80
|
+
client = Rasti::AI::Anthropic::Client.new logger: logger
|
|
81
|
+
|
|
82
|
+
response = client.messages messages: [user_message(question)]
|
|
83
|
+
|
|
84
|
+
assert_response_content response, answer
|
|
85
|
+
|
|
86
|
+
refute_empty log_output.string
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe 'Usage tracker' do
|
|
90
|
+
|
|
91
|
+
it 'Track usage' do
|
|
92
|
+
stub_anthropic_messages
|
|
93
|
+
|
|
94
|
+
tracked = []
|
|
95
|
+
tracker = ->(usage) { tracked << usage }
|
|
96
|
+
|
|
97
|
+
client = Rasti::AI::Anthropic::Client.new usage_tracker: tracker
|
|
98
|
+
|
|
99
|
+
client.messages messages: [user_message(question)]
|
|
100
|
+
|
|
101
|
+
assert_equal 1, tracked.count
|
|
102
|
+
|
|
103
|
+
expected_raw = {
|
|
104
|
+
'input_tokens' => 25,
|
|
105
|
+
'output_tokens' => 11,
|
|
106
|
+
'cache_creation_input_tokens' => 0,
|
|
107
|
+
'cache_read_input_tokens' => 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
usage = tracked[0]
|
|
111
|
+
assert_instance_of Rasti::AI::Usage, usage
|
|
112
|
+
assert_equal 'anthropic', usage.provider
|
|
113
|
+
assert_equal 'claude-test', usage.model
|
|
114
|
+
assert_equal 25, usage.input_tokens
|
|
115
|
+
assert_equal 11, usage.output_tokens
|
|
116
|
+
assert_equal 0, usage.cached_tokens
|
|
117
|
+
assert_equal 0, usage.reasoning_tokens
|
|
118
|
+
assert_equal expected_raw, usage.raw
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'Without tracker' do
|
|
122
|
+
stub_anthropic_messages
|
|
123
|
+
|
|
124
|
+
client = Rasti::AI::Anthropic::Client.new
|
|
125
|
+
|
|
126
|
+
response = client.messages messages: [user_message(question)]
|
|
127
|
+
|
|
128
|
+
assert_response_content response, answer
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'Request error' do
|
|
136
|
+
stub_request(:post, api_url)
|
|
137
|
+
.to_return(status: 400, body: '{"type":"error","error":{"type":"invalid_request_error","message":"Test error"}}')
|
|
138
|
+
|
|
139
|
+
client = Rasti::AI::Anthropic::Client.new
|
|
140
|
+
|
|
141
|
+
error = assert_raises(Rasti::AI::Errors::RequestFail) do
|
|
142
|
+
client.messages messages: ['invalid message']
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
assert_includes error.message, 'Response: 400'
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'Tool call' do
|
|
149
|
+
question = 'how many goals did messi for barca'
|
|
150
|
+
tool_name = 'player_goals'
|
|
151
|
+
|
|
152
|
+
tool = {
|
|
153
|
+
name: tool_name,
|
|
154
|
+
description: 'Gets the number of goals scored by a player for a specific team',
|
|
155
|
+
input_schema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
name: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'Full name of the player'
|
|
161
|
+
},
|
|
162
|
+
team: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'Name of the team the player was part of'
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
required: ['name', 'team']
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
arguments = {
|
|
172
|
+
name: 'Lionel Messi',
|
|
173
|
+
team: 'FC Barcelona'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
stub_request(:post, api_url)
|
|
177
|
+
.with(
|
|
178
|
+
headers: {
|
|
179
|
+
'x-api-key' => Rasti::AI.anthropic_api_key,
|
|
180
|
+
'anthropic-version' => '2023-06-01'
|
|
181
|
+
},
|
|
182
|
+
body: read_resource(
|
|
183
|
+
'anthropic/tool_request.json',
|
|
184
|
+
model: Rasti::AI.anthropic_default_model,
|
|
185
|
+
prompt: question,
|
|
186
|
+
tools: [tool]
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
.to_return(body: read_resource('anthropic/tool_response.json', name: tool_name, arguments: arguments))
|
|
190
|
+
|
|
191
|
+
client = Rasti::AI::Anthropic::Client.new
|
|
192
|
+
|
|
193
|
+
response = client.messages messages: [{role: 'user', content: question}],
|
|
194
|
+
tools: [tool],
|
|
195
|
+
tool_choice: {type: 'auto'}
|
|
196
|
+
|
|
197
|
+
tool_use = response['content'].find { |b| b['type'] == 'tool_use' }
|
|
198
|
+
|
|
199
|
+
assert_equal tool_name, tool_use['name']
|
|
200
|
+
assert_equal({'name' => 'Lionel Messi', 'team' => 'FC Barcelona'}, tool_use['input'])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
end
|
|
@@ -110,6 +110,21 @@ describe Rasti::AI::Gemini::Assistant do
|
|
|
110
110
|
assert_equal answer, response
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
it 'Thinking' do
|
|
114
|
+
body = read_json_resource('gemini/basic_request.json', prompt: question)
|
|
115
|
+
body['generation_config'] = {'thinking_config' => {'thinking_budget' => 8_192}}
|
|
116
|
+
|
|
117
|
+
stub_request(:post, api_url)
|
|
118
|
+
.with(body: JSON.dump(body))
|
|
119
|
+
.to_return(body: read_resource('gemini/basic_response.json', content: answer))
|
|
120
|
+
|
|
121
|
+
assistant = Rasti::AI::Gemini::Assistant.new thinking: 'medium'
|
|
122
|
+
|
|
123
|
+
response = assistant.call question
|
|
124
|
+
|
|
125
|
+
assert_equal answer, response
|
|
126
|
+
end
|
|
127
|
+
|
|
113
128
|
it 'JSON Schema' do
|
|
114
129
|
json_schema = {answer: 'Response answer'}
|
|
115
130
|
json_answer = "{\\\"answer\\\": \\\"#{answer}\\\"}"
|
data/spec/mcp/client_spec.rb
CHANGED
|
@@ -11,12 +11,14 @@ describe Rasti::AI::MCP::Client do
|
|
|
11
11
|
def stub_mcp_request(method, params:{}, result:{}, error:nil)
|
|
12
12
|
request_body = {
|
|
13
13
|
jsonrpc: '2.0',
|
|
14
|
+
id: 1,
|
|
14
15
|
method: method,
|
|
15
16
|
params: params
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
response_body = {
|
|
19
|
-
jsonrpc: '2.0'
|
|
20
|
+
jsonrpc: '2.0',
|
|
21
|
+
id: 1
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
if error
|