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.
@@ -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: nil,
39
- tools: [],
38
+ model: nil,
39
+ tools: [],
40
40
  messages: [
41
41
  {
42
- role: Rasti::AI::OpenAI::Roles::USER,
42
+ role: Rasti::AI::OpenAI::Roles::USER,
43
43
  content: question
44
44
  }
45
45
  ],
46
- response_format: nil
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 'Invalid tool' do
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
- error = assert_raises(Rasti::AI::Errors::ToolSerializationError) do
282
- serializer.serialize tool_class
283
- end
297
+ serialization = serializer.serialize tool_class
284
298
 
285
- assert_equal "Tool serialization error: #{tool_class}", error.message
286
- assert_equal 'Type not serializable Object', error.cause.message
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: 2.0.2
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-03-16 00:00:00.000000000 Z
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/errors.rb
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: '0'
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