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/lib/rasti/ai/mcp/client.rb
CHANGED
|
@@ -4,13 +4,14 @@ module Rasti
|
|
|
4
4
|
class Client
|
|
5
5
|
|
|
6
6
|
def initialize(url:, allowed_tools:nil, logger:nil)
|
|
7
|
-
@url
|
|
7
|
+
@url = url
|
|
8
8
|
@allowed_tools = allowed_tools
|
|
9
|
-
@logger
|
|
9
|
+
@logger = logger || Rasti::AI.logger
|
|
10
|
+
@session_id = nil
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def list_tools
|
|
13
|
-
result =
|
|
14
|
+
result = request_with_session 'tools/list'
|
|
14
15
|
tools = result['tools']
|
|
15
16
|
if allowed_tools
|
|
16
17
|
tools.select { |tool| allowed_tools.include? tool['name'] }
|
|
@@ -21,15 +22,55 @@ module Rasti
|
|
|
21
22
|
|
|
22
23
|
def call_tool(name, arguments={})
|
|
23
24
|
raise "Invalid tool: #{name}" if allowed_tools && !allowed_tools.include?(name)
|
|
24
|
-
result =
|
|
25
|
+
result = request_with_session 'tools/call', name: name, arguments: arguments
|
|
25
26
|
JSON.dump result['content'][0]
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
private
|
|
29
30
|
|
|
30
|
-
attr_reader :url, :allowed_tools, :logger
|
|
31
|
+
attr_reader :url, :allowed_tools, :logger, :session_id
|
|
32
|
+
|
|
33
|
+
def request_with_session(method, params={})
|
|
34
|
+
request_mcp method, params
|
|
35
|
+
rescue RuntimeError => e
|
|
36
|
+
raise unless e.message =~ /session/i && session_id.nil?
|
|
37
|
+
initialize_session
|
|
38
|
+
request_mcp method, params
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize_session
|
|
42
|
+
_result, response = request_mcp_raw 'initialize',
|
|
43
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
44
|
+
capabilities: {},
|
|
45
|
+
clientInfo: {name: 'rasti-ai', version: Rasti::AI::VERSION}
|
|
46
|
+
|
|
47
|
+
@session_id = response['mcp-session-id']
|
|
48
|
+
|
|
49
|
+
send_notification 'notifications/initialized' if session_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def send_notification(method)
|
|
53
|
+
uri = URI.parse url
|
|
54
|
+
http = Net::HTTP.new uri.host, uri.port
|
|
55
|
+
http.use_ssl = uri.scheme == 'https'
|
|
56
|
+
|
|
57
|
+
req = Net::HTTP::Post.new uri.path
|
|
58
|
+
req['Content-Type'] = 'application/json'
|
|
59
|
+
req['Accept'] = 'application/json, text/event-stream'
|
|
60
|
+
req['Mcp-Session-Id'] = session_id if session_id
|
|
61
|
+
req.body = JSON.dump(jsonrpc: JSON_RPC_VERSION, method: method)
|
|
62
|
+
|
|
63
|
+
http.request req
|
|
64
|
+
rescue => e
|
|
65
|
+
logger.warn(self.class) { "Notification failed: #{e.message}" }
|
|
66
|
+
end
|
|
31
67
|
|
|
32
68
|
def request_mcp(method, params={})
|
|
69
|
+
result, _response = request_mcp_raw method, params
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def request_mcp_raw(method, params={})
|
|
33
74
|
uri = URI.parse url
|
|
34
75
|
|
|
35
76
|
http = Net::HTTP.new uri.host, uri.port
|
|
@@ -38,9 +79,12 @@ module Rasti
|
|
|
38
79
|
request = Net::HTTP::Post.new uri.path
|
|
39
80
|
|
|
40
81
|
request['Content-Type'] = 'application/json'
|
|
82
|
+
request['Accept'] = 'application/json, text/event-stream'
|
|
83
|
+
request['Mcp-Session-Id'] = session_id if session_id
|
|
41
84
|
|
|
42
85
|
body = {
|
|
43
|
-
jsonrpc:
|
|
86
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
87
|
+
id: 1,
|
|
44
88
|
method: method,
|
|
45
89
|
params: params
|
|
46
90
|
}
|
|
@@ -48,18 +92,25 @@ module Rasti
|
|
|
48
92
|
|
|
49
93
|
logger.info(self.class) { "POST #{url} -> #{method}" }
|
|
50
94
|
logger.debug(self.class) { JSON.pretty_generate(params) }
|
|
51
|
-
|
|
95
|
+
|
|
52
96
|
response = http.request request
|
|
53
97
|
|
|
54
98
|
logger.info(self.class) { "Response #{response.code}" }
|
|
55
99
|
logger.debug(self.class) { response.body }
|
|
56
100
|
|
|
57
|
-
|
|
101
|
+
body_str = response.body
|
|
102
|
+
|
|
103
|
+
# Handle SSE format (text/event-stream)
|
|
104
|
+
if response['Content-Type']&.include?('text/event-stream')
|
|
105
|
+
body_str = body_str.scan(/^data:\s*(.+)$/).flatten.first || body_str
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
json = JSON.parse body_str
|
|
58
109
|
|
|
59
110
|
raise "MCP Error: #{json['error']['message']}" if json['error']
|
|
60
111
|
raise "MCP Error: invalid result" unless json['result']
|
|
61
112
|
|
|
62
|
-
json['result']
|
|
113
|
+
[json['result'], response]
|
|
63
114
|
end
|
|
64
115
|
|
|
65
116
|
end
|
|
@@ -2,6 +2,9 @@ module Rasti
|
|
|
2
2
|
module AI
|
|
3
3
|
module MCP
|
|
4
4
|
|
|
5
|
+
JSON_RPC_VERSION = '2.0'.freeze
|
|
6
|
+
PROTOCOL_VERSION = '2024-11-05'.freeze
|
|
7
|
+
|
|
5
8
|
# JSON-RPC oficiales
|
|
6
9
|
JSON_RPC_PARSE_ERROR = -32700
|
|
7
10
|
JSON_RPC_INVALID_REQUEST = -32600
|
|
@@ -26,4 +29,4 @@ module Rasti
|
|
|
26
29
|
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
|
-
end
|
|
32
|
+
end
|
data/lib/rasti/ai/mcp/server.rb
CHANGED
|
@@ -3,44 +3,22 @@ module Rasti
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Server
|
|
5
5
|
|
|
6
|
-
ToolSpecification = Rasti::Model[:tool, :serialization]
|
|
7
|
-
|
|
8
|
-
PROTOCOL_VERSION = '2024-11-05'.freeze
|
|
9
|
-
JSON_RPC_VERSION = '2.0'.freeze
|
|
10
|
-
|
|
11
6
|
extend ClassConfig
|
|
12
7
|
|
|
13
|
-
attr_config :server_name,
|
|
8
|
+
attr_config :server_name, 'MCP Server'
|
|
14
9
|
attr_config :server_version, '1.0.0'
|
|
15
|
-
attr_config :relative_path,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def register_tool(tool)
|
|
20
|
-
serialization = ToolSerializer.serialize tool.class
|
|
21
|
-
raise "Tool #{serialization[:name]} already exist" if tools.key? serialization[:name]
|
|
22
|
-
tools[serialization[:name]] = ToolSpecification.new tool: tool, serialization: serialization
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def clear_tools
|
|
26
|
-
tools.clear
|
|
27
|
-
end
|
|
10
|
+
attr_config :relative_path, '/mcp'
|
|
11
|
+
attr_config :tools_loader
|
|
12
|
+
attr_config :authenticator
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def call_tool(name, arguments={})
|
|
34
|
-
raise "Tool #{name} not found" unless tools.key? name
|
|
35
|
-
tools[name].tool.call arguments
|
|
14
|
+
class << self
|
|
15
|
+
def load_tools(&block)
|
|
16
|
+
self.tools_loader = block
|
|
36
17
|
end
|
|
37
18
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def tools
|
|
41
|
-
@tools ||= {}
|
|
19
|
+
def authenticate(&block)
|
|
20
|
+
self.authenticator = block
|
|
42
21
|
end
|
|
43
|
-
|
|
44
22
|
end
|
|
45
23
|
|
|
46
24
|
def initialize(app)
|
|
@@ -49,7 +27,7 @@ module Rasti
|
|
|
49
27
|
|
|
50
28
|
def call(env)
|
|
51
29
|
request = Rack::Request.new env
|
|
52
|
-
|
|
30
|
+
|
|
53
31
|
if request.post? && request.path == self.class.relative_path
|
|
54
32
|
handle_mcp_request request
|
|
55
33
|
else
|
|
@@ -62,31 +40,48 @@ module Rasti
|
|
|
62
40
|
attr_reader :app
|
|
63
41
|
|
|
64
42
|
def handle_mcp_request(request)
|
|
43
|
+
if !authorized? request
|
|
44
|
+
response = error_response nil, JSON_RPC_SERVER_UNAUTHORIZED, 'Unauthorized'
|
|
45
|
+
return [401, {'Content-Type' => 'application/json'}, [JSON.dump(response)]]
|
|
46
|
+
end
|
|
47
|
+
|
|
65
48
|
body = request.body.read
|
|
66
49
|
data = JSON.parse body
|
|
67
|
-
|
|
50
|
+
|
|
51
|
+
tools_registry = build_tools_registry request
|
|
52
|
+
|
|
68
53
|
response = case data['method']
|
|
69
54
|
when 'initialize'
|
|
70
55
|
handle_initialize data
|
|
71
56
|
when 'tools/list'
|
|
72
|
-
handle_tools_list data
|
|
57
|
+
handle_tools_list data, tools_registry
|
|
73
58
|
when 'tools/call'
|
|
74
|
-
handle_tool_call data
|
|
59
|
+
handle_tool_call data, tools_registry
|
|
75
60
|
else
|
|
76
61
|
error_response data['id'], JSON_RPC_METHOD_NOT_FOUND, 'Method not found'
|
|
77
62
|
end
|
|
78
|
-
|
|
63
|
+
|
|
79
64
|
[200, {'Content-Type' => 'application/json'}, [JSON.dump(response)]]
|
|
80
65
|
|
|
81
66
|
rescue JSON::ParserError => e
|
|
82
67
|
response = error_response nil, JSON_RPC_PARSE_ERROR, e.message
|
|
83
68
|
[400, {'Content-Type' => 'application/json'}, [JSON.dump(response)]]
|
|
84
|
-
|
|
69
|
+
|
|
85
70
|
rescue => e
|
|
86
71
|
response = error_response nil, JSON_RPC_INTERNAL_ERROR, e.message
|
|
87
72
|
[500, {'Content-Type' => 'application/json'}, [JSON.dump(response)]]
|
|
88
73
|
end
|
|
89
|
-
|
|
74
|
+
|
|
75
|
+
def authorized?(request)
|
|
76
|
+
!self.class.authenticator || self.class.authenticator.call(request)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_tools_registry(request)
|
|
80
|
+
tools_registry = ToolsRegistry.new
|
|
81
|
+
self.class.tools_loader.call tools_registry, request if self.class.tools_loader
|
|
82
|
+
tools_registry
|
|
83
|
+
end
|
|
84
|
+
|
|
90
85
|
def handle_initialize(data)
|
|
91
86
|
{
|
|
92
87
|
jsonrpc: JSON_RPC_VERSION,
|
|
@@ -106,23 +101,23 @@ module Rasti
|
|
|
106
101
|
}
|
|
107
102
|
}
|
|
108
103
|
end
|
|
109
|
-
|
|
110
|
-
def handle_tools_list(data)
|
|
104
|
+
|
|
105
|
+
def handle_tools_list(data, tools_registry)
|
|
111
106
|
{
|
|
112
107
|
jsonrpc: JSON_RPC_VERSION,
|
|
113
108
|
id: data['id'],
|
|
114
109
|
result: {
|
|
115
|
-
tools:
|
|
110
|
+
tools: tools_registry.serializations
|
|
116
111
|
}
|
|
117
112
|
}
|
|
118
113
|
end
|
|
119
|
-
|
|
120
|
-
def handle_tool_call(data)
|
|
114
|
+
|
|
115
|
+
def handle_tool_call(data, tools_registry)
|
|
121
116
|
tool_name = data['params']['name']
|
|
122
117
|
arguments = data['params']['arguments'] || {}
|
|
123
118
|
|
|
124
|
-
result =
|
|
125
|
-
|
|
119
|
+
result = tools_registry.call tool_name, arguments
|
|
120
|
+
|
|
126
121
|
{
|
|
127
122
|
jsonrpc: JSON_RPC_VERSION,
|
|
128
123
|
id: data['id'],
|
|
@@ -138,7 +133,7 @@ module Rasti
|
|
|
138
133
|
rescue => e
|
|
139
134
|
error_response data['id'], JSON_RPC_INTERNAL_ERROR, e.message
|
|
140
135
|
end
|
|
141
|
-
|
|
136
|
+
|
|
142
137
|
def error_response(id, code, message)
|
|
143
138
|
{
|
|
144
139
|
jsonrpc: JSON_RPC_VERSION,
|
|
@@ -153,4 +148,4 @@ module Rasti
|
|
|
153
148
|
end
|
|
154
149
|
end
|
|
155
150
|
end
|
|
156
|
-
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Rasti
|
|
2
|
+
module AI
|
|
3
|
+
module MCP
|
|
4
|
+
class ToolsRegistry
|
|
5
|
+
|
|
6
|
+
Entry = Rasti::Model[:serialization, :executor]
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@entries = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(tool: nil, name: nil, description: nil, form: nil, input_schema: nil, &block)
|
|
13
|
+
resolved_name = resolve_name tool, name
|
|
14
|
+
resolved_description = resolve_description tool, description
|
|
15
|
+
resolved_schema = resolve_schema tool, form, input_schema
|
|
16
|
+
resolved_executor = resolve_executor(tool, resolved_name, &block)
|
|
17
|
+
|
|
18
|
+
serialization = {name: resolved_name}
|
|
19
|
+
serialization[:description] = resolved_description if resolved_description
|
|
20
|
+
serialization[:inputSchema] = resolved_schema if resolved_schema
|
|
21
|
+
|
|
22
|
+
entries[resolved_name] = Entry.new serialization: serialization, executor: resolved_executor
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def serializations
|
|
26
|
+
entries.values.map(&:serialization)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(name, args={})
|
|
30
|
+
raise "Tool #{name} not found" unless entries.key? name
|
|
31
|
+
entries[name].executor.call args
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :entries
|
|
37
|
+
|
|
38
|
+
def resolve_name(tool, name)
|
|
39
|
+
return name if name
|
|
40
|
+
return ToolSerializer.serialize_name tool.class if tool
|
|
41
|
+
raise ArgumentError, 'name is required'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_description(tool, description)
|
|
45
|
+
return description if description
|
|
46
|
+
tool.class.description if tool && tool.class.respond_to?(:description)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve_schema(tool, form, input_schema)
|
|
50
|
+
return input_schema if input_schema
|
|
51
|
+
return ToolSerializer.serialize_form form if form
|
|
52
|
+
ToolSerializer.serialize_form tool.class.form if tool && tool.class.respond_to?(:form)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_executor(tool, name, &block)
|
|
56
|
+
return block if block
|
|
57
|
+
return tool.method :call if tool
|
|
58
|
+
raise ArgumentError, "executor required: provide a tool or a block for '#{name}'"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -33,10 +33,15 @@ module Rasti
|
|
|
33
33
|
messages
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
client.chat_completions messages:
|
|
37
|
-
model:
|
|
38
|
-
tools:
|
|
39
|
-
response_format:
|
|
36
|
+
client.chat_completions messages: msgs,
|
|
37
|
+
model: model,
|
|
38
|
+
tools: serialized_tools,
|
|
39
|
+
response_format: response_format,
|
|
40
|
+
reasoning_effort: thinking_config
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def thinking_config
|
|
44
|
+
thinking
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def parse_tool_calls(response)
|
|
@@ -3,7 +3,7 @@ module Rasti
|
|
|
3
3
|
module OpenAI
|
|
4
4
|
class Client < Rasti::AI::Client
|
|
5
5
|
|
|
6
|
-
def chat_completions(messages:, model:nil, tools:[], response_format:nil)
|
|
6
|
+
def chat_completions(messages:, model:nil, tools:[], response_format:nil, reasoning_effort:nil)
|
|
7
7
|
body = {
|
|
8
8
|
model: model || Rasti::AI.openai_default_model,
|
|
9
9
|
messages: messages,
|
|
@@ -11,7 +11,8 @@ module Rasti
|
|
|
11
11
|
tool_choice: tools.empty? ? 'none' : 'auto'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
body[:response_format]
|
|
14
|
+
body[:response_format] = response_format unless response_format.nil?
|
|
15
|
+
body[:reasoning_effort] = reasoning_effort if reasoning_effort
|
|
15
16
|
|
|
16
17
|
post '/chat/completions', body
|
|
17
18
|
end
|
|
@@ -9,50 +9,47 @@ module Rasti
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
serialization[:description] = normalize_description(tool_class.description) if tool_class.respond_to? :description
|
|
12
|
-
|
|
13
12
|
serialization[:inputSchema] = serialize_form(tool_class.form) if tool_class.respond_to? :form
|
|
14
13
|
|
|
15
14
|
serialization
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
rescue => ex
|
|
18
17
|
raise Errors::ToolSerializationError.new(tool_class), cause: ex
|
|
19
18
|
end
|
|
20
19
|
|
|
21
|
-
private
|
|
22
|
-
|
|
23
20
|
def serialize_name(tool_class)
|
|
24
21
|
Inflecto.underscore Inflecto.demodulize(tool_class.name)
|
|
25
22
|
end
|
|
26
23
|
|
|
27
24
|
def serialize_form(form_class)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
end
|
|
25
|
+
json_schema_from_model_schema form_class.to_schema
|
|
26
|
+
end
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
type: 'object',
|
|
34
|
-
properties: serialized_attributes
|
|
35
|
-
}
|
|
28
|
+
private
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
def json_schema_from_model_schema(schema)
|
|
31
|
+
properties = schema[:attributes].each_with_object({}) do |attribute, hash|
|
|
32
|
+
hash[attribute[:name]] = json_schema_for_attribute(attribute)
|
|
33
|
+
end
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
result = {type: 'object', properties: properties}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
required = schema[:attributes].select { |a| (a[:options] || {})[:required] }.map { |a| a[:name] }
|
|
38
|
+
result[:required] = required unless required.empty?
|
|
39
|
+
|
|
40
|
+
result
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def
|
|
43
|
+
def json_schema_for_attribute(attribute)
|
|
45
44
|
serialization = {}
|
|
46
|
-
|
|
47
|
-
if attribute.option(:description)
|
|
48
|
-
serialization[:description] = normalize_description attribute.option(:description)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
type_serialization = serialize_type attribute.type
|
|
52
|
-
serialization.merge! type_serialization
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
description = (attribute[:options] || {})[:description]
|
|
47
|
+
serialization[:description] = normalize_description(description) if description
|
|
48
|
+
|
|
49
|
+
serialization.merge! json_schema_for_type(attribute)
|
|
50
|
+
|
|
51
|
+
if attribute[:type] == :enum
|
|
52
|
+
values = attribute[:values].join(', ')
|
|
56
53
|
if serialization[:description]
|
|
57
54
|
serialization[:description] += " (#{values})"
|
|
58
55
|
else
|
|
@@ -63,50 +60,26 @@ module Rasti
|
|
|
63
60
|
serialization
|
|
64
61
|
end
|
|
65
62
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
elsif type.is_a? Types::Time
|
|
80
|
-
{
|
|
81
|
-
type: 'string',
|
|
82
|
-
format: 'date'
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
elsif type.is_a? Types::Enum
|
|
86
|
-
{
|
|
87
|
-
type: 'string',
|
|
88
|
-
enum: type.values
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
elsif type.is_a? Types::Array
|
|
92
|
-
{
|
|
93
|
-
type: 'array',
|
|
94
|
-
items: serialize_type(type.type)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
elsif type.is_a? Types::Model
|
|
98
|
-
serialize_form(type.model)
|
|
99
|
-
|
|
100
|
-
else
|
|
101
|
-
raise "Type not serializable #{type}"
|
|
63
|
+
def json_schema_for_type(type_hash)
|
|
64
|
+
case type_hash[:type]
|
|
65
|
+
when :string, :symbol then {type: 'string'}
|
|
66
|
+
when :integer then {type: 'integer'}
|
|
67
|
+
when :float then {type: 'number'}
|
|
68
|
+
when :boolean then {type: 'boolean'}
|
|
69
|
+
when :time then {type: 'string', format: 'date'}
|
|
70
|
+
when :enum then {type: 'string', enum: type_hash[:values]}
|
|
71
|
+
when :array then {type: 'array', items: json_schema_for_type(type_hash[:items])}
|
|
72
|
+
when :model then json_schema_from_model_schema(type_hash[:schema])
|
|
73
|
+
when :hash then {type: 'object'}
|
|
74
|
+
else {}
|
|
102
75
|
end
|
|
103
76
|
end
|
|
104
77
|
|
|
105
78
|
def normalize_description(description)
|
|
106
79
|
description.split("\n").map(&:strip).join(' ').strip
|
|
107
80
|
end
|
|
108
|
-
|
|
81
|
+
|
|
109
82
|
end
|
|
110
83
|
end
|
|
111
84
|
end
|
|
112
|
-
end
|
|
85
|
+
end
|
data/lib/rasti/ai/version.rb
CHANGED
data/lib/rasti/ai.rb
CHANGED
|
@@ -14,6 +14,13 @@ module Rasti
|
|
|
14
14
|
extend MultiRequire
|
|
15
15
|
extend ClassConfig
|
|
16
16
|
|
|
17
|
+
require_relative 'ai/errors'
|
|
18
|
+
require_relative 'ai/usage'
|
|
19
|
+
require_relative 'ai/assistant_state'
|
|
20
|
+
require_relative 'ai/tool'
|
|
21
|
+
require_relative 'ai/tool_serializer'
|
|
22
|
+
require_relative 'ai/client'
|
|
23
|
+
require_relative 'ai/assistant'
|
|
17
24
|
require_relative_pattern 'ai/**/*'
|
|
18
25
|
|
|
19
26
|
attr_config :logger, Logger.new(STDOUT)
|
|
@@ -28,6 +35,9 @@ module Rasti
|
|
|
28
35
|
attr_config :gemini_api_key, ENV['GEMINI_API_KEY']
|
|
29
36
|
attr_config :gemini_default_model, ENV['GEMINI_DEFAULT_MODEL']
|
|
30
37
|
|
|
38
|
+
attr_config :anthropic_api_key, ENV['ANTHROPIC_API_KEY']
|
|
39
|
+
attr_config :anthropic_default_model, ENV['ANTHROPIC_DEFAULT_MODEL']
|
|
40
|
+
|
|
31
41
|
attr_config :usage_tracker, nil
|
|
32
42
|
|
|
33
43
|
end
|
data/rasti-ai.gemspec
CHANGED
|
@@ -13,6 +13,8 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.homepage = 'https://github.com/gabynaiman/rasti-ai'
|
|
14
14
|
spec.license = 'MIT'
|
|
15
15
|
|
|
16
|
+
spec.required_ruby_version = '>= 2.3'
|
|
17
|
+
|
|
16
18
|
spec.files = `git ls-files -z`.split("\x0")
|
|
17
19
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
@@ -25,12 +27,13 @@ Gem::Specification.new do |spec|
|
|
|
25
27
|
spec.add_runtime_dependency 'class_config', '~> 0.0'
|
|
26
28
|
|
|
27
29
|
spec.add_development_dependency 'rake', '~> 12.0'
|
|
30
|
+
spec.add_development_dependency 'rack', '>= 1.3', '< 3'
|
|
28
31
|
spec.add_development_dependency 'rack-test', '~> 2.0'
|
|
29
32
|
spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
|
|
30
33
|
spec.add_development_dependency 'minitest-colorin', '~> 0.1'
|
|
31
34
|
spec.add_development_dependency 'minitest-line', '~> 0.6'
|
|
32
35
|
spec.add_development_dependency 'minitest-extended_assertions', '~> 1.0'
|
|
33
36
|
spec.add_development_dependency 'simplecov', '~> 0.12'
|
|
34
|
-
spec.add_development_dependency 'pry-nav', '
|
|
37
|
+
spec.add_development_dependency 'pry-nav', '>= 0.2'
|
|
35
38
|
spec.add_development_dependency 'webmock', '~> 3.0'
|
|
36
39
|
end
|