ruby-utcp 1.1.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 +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +22 -0
- data/Makefile +226 -0
- data/README.md +331 -0
- data/examples/basic.rb +33 -0
- data/examples/cli.rb +32 -0
- data/examples/generated/__init__.py +1 -0
- data/examples/generated/utcp_pb2.py +46 -0
- data/examples/generated/utcp_pb2_grpc.py +183 -0
- data/examples/graphql.rb +15 -0
- data/examples/grpc.rb +42 -0
- data/examples/grpc_python.py +52 -0
- data/examples/http.rb +17 -0
- data/examples/mcp.rb +28 -0
- data/examples/servers/graphql_server.rb +39 -0
- data/examples/servers/grpc_server.py +97 -0
- data/examples/servers/grpc_server.rb +62 -0
- data/examples/servers/http_helpers.rb +34 -0
- data/examples/servers/http_server.rb +28 -0
- data/examples/servers/mcp_stdio_server.rb +43 -0
- data/examples/servers/requirements-grpc.txt +2 -0
- data/examples/servers/sse_server.rb +36 -0
- data/examples/servers/streamable_http_server.rb +39 -0
- data/examples/servers/tcp_server.rb +58 -0
- data/examples/servers/udp_server.rb +33 -0
- data/examples/servers/webrtc_server.rb +78 -0
- data/examples/servers/websocket_server.rb +92 -0
- data/examples/sse.rb +16 -0
- data/examples/streamable_http.rb +17 -0
- data/examples/tcp.rb +20 -0
- data/examples/text.rb +23 -0
- data/examples/udp.rb +18 -0
- data/examples/webrtc.rb +19 -0
- data/examples/websocket.rb +17 -0
- data/lib/ruby-utcp.rb +4 -0
- data/lib/utcp/client.rb +217 -0
- data/lib/utcp/config.rb +79 -0
- data/lib/utcp/errors.rb +48 -0
- data/lib/utcp/migration.rb +88 -0
- data/lib/utcp/models.rb +794 -0
- data/lib/utcp/openapi_converter.rb +179 -0
- data/lib/utcp/protocols/base.rb +97 -0
- data/lib/utcp/protocols/cli.rb +186 -0
- data/lib/utcp/protocols/file.rb +52 -0
- data/lib/utcp/protocols/graphql.rb +277 -0
- data/lib/utcp/protocols/grpc.rb +207 -0
- data/lib/utcp/protocols/http.rb +340 -0
- data/lib/utcp/protocols/http_stream_support.rb +122 -0
- data/lib/utcp/protocols/mcp.rb +339 -0
- data/lib/utcp/protocols/socket_support.rb +51 -0
- data/lib/utcp/protocols/sse.rb +107 -0
- data/lib/utcp/protocols/streamable_http.rb +78 -0
- data/lib/utcp/protocols/tcp.rb +143 -0
- data/lib/utcp/protocols/text.rb +44 -0
- data/lib/utcp/protocols/udp.rb +61 -0
- data/lib/utcp/protocols/webrtc.rb +217 -0
- data/lib/utcp/protocols/websocket.rb +350 -0
- data/lib/utcp/registry.rb +67 -0
- data/lib/utcp/repository.rb +137 -0
- data/lib/utcp/serializer.rb +71 -0
- data/lib/utcp/utils.rb +118 -0
- data/lib/utcp/variables.rb +170 -0
- data/lib/utcp/version.rb +6 -0
- data/lib/utcp.rb +70 -0
- data/proto/utcp.proto +31 -0
- metadata +148 -0
data/lib/utcp/client.rb
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module UTCP
|
|
6
|
+
class Client
|
|
7
|
+
attr_reader :config, :root_dir, :variable_substitutor, :logger, :registration_results
|
|
8
|
+
|
|
9
|
+
def self.create(root_dir: nil, config: nil, logger: nil)
|
|
10
|
+
client = new(root_dir: root_dir, config: config, logger: logger)
|
|
11
|
+
client.register_configured_manuals
|
|
12
|
+
client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(root_dir: nil, config: nil, logger: nil)
|
|
16
|
+
@root_dir = File.expand_path(root_dir || Dir.pwd)
|
|
17
|
+
@logger = logger || Logger.new($stderr, level: Logger::WARN)
|
|
18
|
+
@config = ClientConfig.from(config, root_dir: @root_dir)
|
|
19
|
+
@variable_substitutor = VariableSubstitutor.new
|
|
20
|
+
@registration_results = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def register_configured_manuals
|
|
24
|
+
@registration_results = register_manuals(config.manual_call_templates)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def register_manual(manual_call_template)
|
|
29
|
+
template = copy_template(CallTemplate.from_h(manual_call_template))
|
|
30
|
+
template.name = sanitize_name(template.name)
|
|
31
|
+
if config.tool_repository.get_manual(template.name)
|
|
32
|
+
raise ManualAlreadyRegisteredError,
|
|
33
|
+
"Manual #{template.name.inspect} is already registered; deregister it or use another name"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
template = substitute_template(template, template.name)
|
|
37
|
+
protocol = fetch_protocol(template.call_template_type)
|
|
38
|
+
result = protocol.register_manual(self, template)
|
|
39
|
+
return result unless result.success?
|
|
40
|
+
|
|
41
|
+
allowed = template.allowed_protocols
|
|
42
|
+
filtered = result.manual.tools.each_with_object([]) do |tool, tools|
|
|
43
|
+
type = tool.tool_call_template&.call_template_type || template.call_template_type
|
|
44
|
+
if allowed.include?(type)
|
|
45
|
+
tool.name = "#{template.name}.#{tool.name}" unless tool.name.start_with?("#{template.name}.")
|
|
46
|
+
tools << tool
|
|
47
|
+
else
|
|
48
|
+
logger.warn(
|
|
49
|
+
"Tool #{tool.name.inspect} uses protocol #{type.inspect}, which is not allowed " \
|
|
50
|
+
"by manual #{template.name.inspect}; allowed protocols: #{allowed.inspect}"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
result.manual.tools = filtered
|
|
55
|
+
result.manual_call_template = template
|
|
56
|
+
config.tool_repository.save_manual(template, result.manual)
|
|
57
|
+
result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register_manuals(manual_call_templates)
|
|
61
|
+
Array(manual_call_templates).map do |template|
|
|
62
|
+
register_manual(template)
|
|
63
|
+
rescue VariableNotFoundError
|
|
64
|
+
raise
|
|
65
|
+
rescue StandardError => error
|
|
66
|
+
parsed = CallTemplate.from_h(template)
|
|
67
|
+
logger.warn("Unable to register manual #{parsed.name.inspect}: #{error.message}")
|
|
68
|
+
RegisterManualResult.new(
|
|
69
|
+
manual_call_template: parsed,
|
|
70
|
+
manual: Manual.new(tools: [], manual_version: "0.0.0"),
|
|
71
|
+
success: false,
|
|
72
|
+
errors: [error.message]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deregister_manual(manual_name)
|
|
78
|
+
template = config.tool_repository.get_manual_call_template(manual_name)
|
|
79
|
+
return false unless template
|
|
80
|
+
|
|
81
|
+
fetch_protocol(template.call_template_type).deregister_manual(self, template)
|
|
82
|
+
config.tool_repository.remove_manual(manual_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def call_tool(tool_name, tool_args = {})
|
|
86
|
+
tool = config.tool_repository.get_tool(tool_name)
|
|
87
|
+
raise ToolNotFoundError, "Tool not found: #{tool_name}" unless tool
|
|
88
|
+
|
|
89
|
+
manual_name = tool_name.to_s.split(".", 2).first
|
|
90
|
+
template = substitute_template(tool.tool_call_template, manual_name)
|
|
91
|
+
enforce_allowed_protocol!(manual_name, tool_name, template.call_template_type)
|
|
92
|
+
result = fetch_protocol(template.call_template_type).call_tool(self, tool_name, tool_args, template)
|
|
93
|
+
apply_post_processing(result, tool, template)
|
|
94
|
+
rescue Error
|
|
95
|
+
raise
|
|
96
|
+
rescue StandardError => error
|
|
97
|
+
raise ToolCallError.new("Tool #{tool_name.inspect} failed: #{error.message}", tool_name: tool_name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def call_tool_streaming(tool_name, tool_args = {})
|
|
101
|
+
return enum_for(__method__, tool_name, tool_args) unless block_given?
|
|
102
|
+
|
|
103
|
+
tool = config.tool_repository.get_tool(tool_name)
|
|
104
|
+
raise ToolNotFoundError, "Tool not found: #{tool_name}" unless tool
|
|
105
|
+
|
|
106
|
+
manual_name = tool_name.to_s.split(".", 2).first
|
|
107
|
+
template = substitute_template(tool.tool_call_template, manual_name)
|
|
108
|
+
enforce_allowed_protocol!(manual_name, tool_name, template.call_template_type)
|
|
109
|
+
protocol = fetch_protocol(template.call_template_type)
|
|
110
|
+
protocol.call_tool_streaming(self, tool_name, tool_args, template) do |item|
|
|
111
|
+
yield apply_post_processing(item, tool, template)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def search_tools(query, limit: 10, any_of_tags_required: nil)
|
|
116
|
+
config.tool_search_strategy.search_tools(
|
|
117
|
+
tool_repository: config.tool_repository,
|
|
118
|
+
query: query,
|
|
119
|
+
limit: limit,
|
|
120
|
+
any_of_tags_required: any_of_tags_required
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def list_tools
|
|
125
|
+
config.tool_repository.get_tools
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_required_variables_for_manual_and_tools(manual_call_template)
|
|
129
|
+
template = copy_template(CallTemplate.from_h(manual_call_template))
|
|
130
|
+
template.name = sanitize_name(template.name)
|
|
131
|
+
required = variable_substitutor.find_required_variables(template.to_h, template.name)
|
|
132
|
+
return required unless required.empty?
|
|
133
|
+
|
|
134
|
+
substituted = substitute_template(template, template.name)
|
|
135
|
+
result = fetch_protocol(substituted.call_template_type).register_manual(self, substituted)
|
|
136
|
+
return [] unless result.success?
|
|
137
|
+
|
|
138
|
+
result.manual.tools.flat_map do |tool|
|
|
139
|
+
variable_substitutor.find_required_variables(tool.tool_call_template.to_h, template.name)
|
|
140
|
+
end.uniq
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def get_required_variables_for_registered_tool(tool_name)
|
|
144
|
+
tool = config.tool_repository.get_tool(tool_name)
|
|
145
|
+
raise ToolNotFoundError, "Tool not found: #{tool_name}" unless tool
|
|
146
|
+
|
|
147
|
+
manual_name = tool_name.to_s.split(".", 2).first
|
|
148
|
+
variable_substitutor.find_required_variables(tool.tool_call_template.to_h, manual_name)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def manual(name)
|
|
152
|
+
config.tool_repository.get_manual(name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def manuals
|
|
156
|
+
config.tool_repository.get_manuals
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def close
|
|
160
|
+
config.tool_repository.get_manual_call_templates.each do |template|
|
|
161
|
+
deregister_manual(template.name)
|
|
162
|
+
rescue StandardError => error
|
|
163
|
+
logger.warn("Unable to deregister manual #{template.name.inspect}: #{error.message}")
|
|
164
|
+
end
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def fetch_protocol(type)
|
|
171
|
+
UTCP.protocol(type) || raise(
|
|
172
|
+
ProtocolNotFoundError,
|
|
173
|
+
"No communication protocol registered for #{type.inspect}; available: #{UTCP.protocol_types.join(', ')}"
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def sanitize_name(name)
|
|
178
|
+
sanitized = name.to_s.gsub(/[^[:alnum:]_]/, "_")
|
|
179
|
+
raise ValidationError, "manual name cannot be empty" if sanitized.empty?
|
|
180
|
+
|
|
181
|
+
sanitized
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def substitute_template(template, namespace)
|
|
185
|
+
substituted = variable_substitutor.substitute(template.to_h, config, namespace)
|
|
186
|
+
CallTemplate.from_h(substituted)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def copy_template(template)
|
|
190
|
+
CallTemplate.from_h(template.to_h)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def enforce_allowed_protocol!(manual_name, tool_name, type)
|
|
194
|
+
manual_template = config.tool_repository.get_manual_call_template(manual_name)
|
|
195
|
+
return unless manual_template
|
|
196
|
+
return if manual_template.allowed_protocols.include?(type)
|
|
197
|
+
|
|
198
|
+
raise ProtocolNotAllowedError,
|
|
199
|
+
"Tool #{tool_name.inspect} uses protocol #{type.inspect}, which is not allowed by " \
|
|
200
|
+
"manual #{manual_name.inspect}; allowed protocols: #{manual_template.allowed_protocols.inspect}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def apply_post_processing(result, tool, template)
|
|
204
|
+
config.post_processing.reduce(result) do |value, processor|
|
|
205
|
+
if processor.respond_to?(:post_process)
|
|
206
|
+
processor.post_process(self, tool, template, value)
|
|
207
|
+
elsif processor.respond_to?(:call)
|
|
208
|
+
processor.call(value)
|
|
209
|
+
else
|
|
210
|
+
raise ValidationError, "post processor must respond to call or post_process"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
UtcpClient = Client
|
|
216
|
+
end
|
|
217
|
+
|
data/lib/utcp/config.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class ClientConfig
|
|
5
|
+
include ModelSerialization
|
|
6
|
+
attr_accessor :variables, :load_variables_from, :tool_repository,
|
|
7
|
+
:tool_search_strategy, :post_processing, :manual_call_templates
|
|
8
|
+
|
|
9
|
+
def self.from(value = nil, root_dir: Dir.pwd)
|
|
10
|
+
return new(root_dir: root_dir) if value.nil?
|
|
11
|
+
return value if value.is_a?(self)
|
|
12
|
+
|
|
13
|
+
data = if value.respond_to?(:to_path) || value.is_a?(String)
|
|
14
|
+
Utils.load_document(File.expand_path(value.to_s, root_dir))
|
|
15
|
+
else
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
new(root_dir: root_dir, **Utils.symbolize_keys(Utils.stringify_keys(Utils.hash!(data, "client config"))))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_h(value, root_dir: Dir.pwd)
|
|
22
|
+
from(value, root_dir: root_dir)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(variables: nil, load_variables_from: nil, tool_repository: nil,
|
|
26
|
+
tool_search_strategy: nil, post_processing: nil,
|
|
27
|
+
manual_call_templates: nil, root_dir: Dir.pwd, **_extra)
|
|
28
|
+
@variables = Utils.stringify_keys(variables || {})
|
|
29
|
+
@load_variables_from = Array(load_variables_from).map do |loader|
|
|
30
|
+
VariableLoader.from_h(loader, root_dir: root_dir)
|
|
31
|
+
end
|
|
32
|
+
@tool_repository = build_repository(tool_repository)
|
|
33
|
+
@tool_search_strategy = build_search_strategy(tool_search_strategy)
|
|
34
|
+
@post_processing = Array(post_processing)
|
|
35
|
+
@manual_call_templates = Array(manual_call_templates).map { |template| CallTemplate.from_h(template) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
"variables" => Utils.deep_copy(variables),
|
|
41
|
+
"load_variables_from" => load_variables_from.map { |loader| loader.respond_to?(:to_h) ? loader.to_h : loader },
|
|
42
|
+
"tool_repository" => tool_repository.respond_to?(:to_h) ? tool_repository.to_h : tool_repository,
|
|
43
|
+
"tool_search_strategy" => tool_search_strategy.respond_to?(:to_h) ? tool_search_strategy.to_h : tool_search_strategy,
|
|
44
|
+
"post_processing" => post_processing.map { |processor| processor.respond_to?(:to_h) ? processor.to_h : processor },
|
|
45
|
+
"manual_call_templates" => manual_call_templates.map(&:to_h)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_repository(value)
|
|
52
|
+
return value if value && value.respond_to?(:save_manual)
|
|
53
|
+
|
|
54
|
+
data = Utils.stringify_keys(value || { "tool_repository_type" => "in_memory" })
|
|
55
|
+
case data["tool_repository_type"]
|
|
56
|
+
when nil, "in_memory"
|
|
57
|
+
InMemoryToolRepository.new
|
|
58
|
+
else
|
|
59
|
+
raise ValidationError, "Unsupported tool_repository_type: #{data['tool_repository_type']}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_search_strategy(value)
|
|
64
|
+
return value if value && value.respond_to?(:search_tools)
|
|
65
|
+
|
|
66
|
+
data = Utils.stringify_keys(value || { "tool_search_strategy_type" => "tag_and_description_word_match" })
|
|
67
|
+
case data["tool_search_strategy_type"]
|
|
68
|
+
when nil, "tag_and_description_word_match"
|
|
69
|
+
TagSearchStrategy.new(
|
|
70
|
+
description_weight: data.fetch("description_weight", 1),
|
|
71
|
+
tag_weight: data.fetch("tag_weight", 3)
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
raise ValidationError, "Unsupported tool_search_strategy_type: #{data['tool_search_strategy_type']}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
UtcpClientConfig = ClientConfig
|
|
79
|
+
end
|
data/lib/utcp/errors.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UTCP
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
UtcpError = Error
|
|
6
|
+
UTCPError = Error
|
|
7
|
+
|
|
8
|
+
class ValidationError < Error
|
|
9
|
+
attr_reader :path
|
|
10
|
+
|
|
11
|
+
def initialize(message, path: nil)
|
|
12
|
+
@path = path
|
|
13
|
+
super(path ? "#{path}: #{message}" : message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class SerializerValidationError < ValidationError; end
|
|
18
|
+
UtcpSerializerValidationError = SerializerValidationError
|
|
19
|
+
class VariableNotFoundError < Error
|
|
20
|
+
attr_reader :variable_name
|
|
21
|
+
|
|
22
|
+
def initialize(variable_name)
|
|
23
|
+
@variable_name = variable_name
|
|
24
|
+
super("Required variable not found: #{variable_name}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
UtcpVariableNotFound = VariableNotFoundError
|
|
28
|
+
UtcpVariableNotFoundException = VariableNotFoundError
|
|
29
|
+
|
|
30
|
+
class ToolNotFoundError < Error; end
|
|
31
|
+
class ManualAlreadyRegisteredError < Error; end
|
|
32
|
+
class ProtocolNotFoundError < Error; end
|
|
33
|
+
class ProtocolNotAllowedError < Error; end
|
|
34
|
+
class MissingDependencyError < Error; end
|
|
35
|
+
class AuthenticationError < Error; end
|
|
36
|
+
class ToolCallError < Error
|
|
37
|
+
attr_reader :tool_name, :status, :response_body
|
|
38
|
+
|
|
39
|
+
def initialize(message, tool_name: nil, status: nil, response_body: nil)
|
|
40
|
+
@tool_name = tool_name
|
|
41
|
+
@status = status
|
|
42
|
+
@response_body = response_body
|
|
43
|
+
super(message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
class SecurityError < Error; end
|
|
47
|
+
class TimeoutError < ToolCallError; end
|
|
48
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module UTCP
|
|
6
|
+
module Migration
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def config_v0_1_to_v1_1(config)
|
|
10
|
+
data = Utils.stringify_keys(Utils.deep_copy(Utils.hash!(config, "v0.1 config")))
|
|
11
|
+
providers = Array(data.delete("providers"))
|
|
12
|
+
existing = Array(data["manual_call_templates"])
|
|
13
|
+
data["manual_call_templates"] = existing + providers.map { |provider| migrate_provider(provider) }
|
|
14
|
+
data["variables"] ||= {}
|
|
15
|
+
data
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def manual_v0_1_to_v1_1(manual)
|
|
19
|
+
data = Utils.stringify_keys(Utils.deep_copy(Utils.hash!(manual, "v0.1 manual")))
|
|
20
|
+
provider_info = Utils.stringify_keys(data.delete("provider_info") || {})
|
|
21
|
+
info = Utils.stringify_keys(data["info"] || {})
|
|
22
|
+
info["title"] ||= provider_info["name"] || "UTCP Manual"
|
|
23
|
+
info["version"] ||= provider_info["version"] || "1.0.0"
|
|
24
|
+
info["description"] ||= provider_info["description"] if provider_info["description"]
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"manual_version" => "1.0.0",
|
|
28
|
+
"utcp_version" => VERSION,
|
|
29
|
+
"info" => info,
|
|
30
|
+
"tools" => Array(data["tools"]).map { |tool| migrate_tool(tool) }
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def v0_1_manual?(value)
|
|
35
|
+
return false unless value.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
data = Utils.stringify_keys(value)
|
|
38
|
+
data.key?("provider_info") || Array(data["tools"]).any? do |tool|
|
|
39
|
+
tool.is_a?(Hash) && (tool.key?("provider") || tool.key?("parameters") || tool.key?(:provider))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def migrate_provider(provider)
|
|
44
|
+
source = Utils.stringify_keys(Utils.deep_copy(Utils.hash!(provider, "provider")))
|
|
45
|
+
type = source.delete("provider_type") || source.delete("type") || source["call_template_type"]
|
|
46
|
+
raise ValidationError, "v0.1 provider is missing provider_type" if type.nil? || type.to_s.empty?
|
|
47
|
+
|
|
48
|
+
source["call_template_type"] = type.to_s
|
|
49
|
+
source["http_method"] = source.delete("method") if source["method"] && !source["http_method"]
|
|
50
|
+
source["working_dir"] = source.delete("cwd") if source["cwd"] && !source["working_dir"]
|
|
51
|
+
source["working_dir"] = source.delete("working_directory") if source["working_directory"] && !source["working_dir"]
|
|
52
|
+
|
|
53
|
+
if type.to_s == "cli" && source["command"]
|
|
54
|
+
command = source.delete("command").to_s
|
|
55
|
+
arguments = Array(source.delete("args")).map { |argument| migrate_cli_argument(argument) }
|
|
56
|
+
source["commands"] ||= [{ "command" => ([command] + arguments).join(" "), "append_to_final_output" => true }]
|
|
57
|
+
end
|
|
58
|
+
if type.to_s == "http" && source.key?("body") && !source.key?("body_field")
|
|
59
|
+
source.delete("body")
|
|
60
|
+
source["body_field"] = "body"
|
|
61
|
+
end
|
|
62
|
+
source
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def migrate_tool(tool)
|
|
66
|
+
source = Utils.stringify_keys(Utils.deep_copy(Utils.hash!(tool, "tool")))
|
|
67
|
+
source["inputs"] ||= source.delete("parameters") || {}
|
|
68
|
+
source["outputs"] ||= {}
|
|
69
|
+
source["tags"] ||= []
|
|
70
|
+
provider = source.delete("provider") || source.delete("tool_provider")
|
|
71
|
+
source["tool_call_template"] ||= migrate_provider(provider) if provider
|
|
72
|
+
source
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def migrate_cli_argument(argument)
|
|
76
|
+
converted = argument.to_s.gsub(/\$\{([A-Za-z0-9_]+)\}|\$([A-Za-z0-9_]+)/) do
|
|
77
|
+
"UTCP_ARG_#{Regexp.last_match(1) || Regexp.last_match(2)}_UTCP_END"
|
|
78
|
+
end
|
|
79
|
+
converted.include?("UTCP_ARG_") ? converted : Shellwords.escape(converted)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class << self
|
|
83
|
+
alias migrate_config config_v0_1_to_v1_1
|
|
84
|
+
alias migrate_manual manual_v0_1_to_v1_1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|