signalwire-sdk 2.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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +259 -0
  4. data/bin/swaig-test +872 -0
  5. data/lib/signalwire/agent/agent_base.rb +2134 -0
  6. data/lib/signalwire/contexts/context_builder.rb +861 -0
  7. data/lib/signalwire/core/logging_config.rb +54 -0
  8. data/lib/signalwire/datamap/data_map.rb +315 -0
  9. data/lib/signalwire/logging.rb +92 -0
  10. data/lib/signalwire/pom/prompt_object_model.rb +269 -0
  11. data/lib/signalwire/pom/section.rb +202 -0
  12. data/lib/signalwire/prefabs/concierge.rb +92 -0
  13. data/lib/signalwire/prefabs/faq_bot.rb +67 -0
  14. data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
  15. data/lib/signalwire/prefabs/receptionist.rb +74 -0
  16. data/lib/signalwire/prefabs/survey.rb +75 -0
  17. data/lib/signalwire/relay/action.rb +291 -0
  18. data/lib/signalwire/relay/call.rb +523 -0
  19. data/lib/signalwire/relay/client.rb +789 -0
  20. data/lib/signalwire/relay/constants.rb +124 -0
  21. data/lib/signalwire/relay/message.rb +137 -0
  22. data/lib/signalwire/relay/relay_event.rb +670 -0
  23. data/lib/signalwire/rest/http_client.rb +159 -0
  24. data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
  25. data/lib/signalwire/rest/namespaces/calling.rb +179 -0
  26. data/lib/signalwire/rest/namespaces/chat.rb +18 -0
  27. data/lib/signalwire/rest/namespaces/compat.rb +229 -0
  28. data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
  29. data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
  30. data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
  31. data/lib/signalwire/rest/namespaces/logs.rb +46 -0
  32. data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
  33. data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
  34. data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
  35. data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
  36. data/lib/signalwire/rest/namespaces/project.rb +33 -0
  37. data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
  38. data/lib/signalwire/rest/namespaces/queues.rb +28 -0
  39. data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
  40. data/lib/signalwire/rest/namespaces/registry.rb +67 -0
  41. data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
  42. data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
  43. data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
  44. data/lib/signalwire/rest/namespaces/video.rb +129 -0
  45. data/lib/signalwire/rest/pagination.rb +89 -0
  46. data/lib/signalwire/rest/phone_call_handler.rb +56 -0
  47. data/lib/signalwire/rest/rest_client.rb +114 -0
  48. data/lib/signalwire/runtime.rb +98 -0
  49. data/lib/signalwire/security/session_manager.rb +124 -0
  50. data/lib/signalwire/security/webhook_middleware.rb +191 -0
  51. data/lib/signalwire/security/webhook_validator.rb +327 -0
  52. data/lib/signalwire/server/agent_server.rb +413 -0
  53. data/lib/signalwire/serverless/lambda_handler.rb +251 -0
  54. data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
  55. data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
  56. data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
  57. data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
  58. data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
  59. data/lib/signalwire/skills/builtin/datetime.rb +97 -0
  60. data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
  61. data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
  62. data/lib/signalwire/skills/builtin/joke.rb +65 -0
  63. data/lib/signalwire/skills/builtin/math.rb +176 -0
  64. data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
  65. data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
  66. data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
  67. data/lib/signalwire/skills/builtin/spider.rb +169 -0
  68. data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
  69. data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
  70. data/lib/signalwire/skills/builtin/web_search.rb +141 -0
  71. data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
  72. data/lib/signalwire/skills/skill_base.rb +82 -0
  73. data/lib/signalwire/skills/skill_manager.rb +97 -0
  74. data/lib/signalwire/skills/skill_registry.rb +258 -0
  75. data/lib/signalwire/swaig/function_result.rb +777 -0
  76. data/lib/signalwire/swml/document.rb +84 -0
  77. data/lib/signalwire/swml/schema.json +12250 -0
  78. data/lib/signalwire/swml/schema.rb +81 -0
  79. data/lib/signalwire/swml/service.rb +650 -0
  80. data/lib/signalwire/utils/schema_utils.rb +298 -0
  81. data/lib/signalwire/utils/serverless.rb +19 -0
  82. data/lib/signalwire/utils/url_validator.rb +138 -0
  83. data/lib/signalwire/version.rb +5 -0
  84. data/lib/signalwire.rb +114 -0
  85. metadata +225 -0
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 SignalWire
4
+ #
5
+ # Licensed under the MIT License.
6
+ # See LICENSE file in the project root for full license information.
7
+
8
+ require 'json'
9
+
10
+ module SignalWire
11
+ module Utils
12
+ # SchemaValidationError — Ruby port of
13
+ # signalwire.utils.schema_utils.SchemaValidationError.
14
+ #
15
+ # Raised when SWML schema validation of a verb config fails.
16
+ class SchemaValidationError < StandardError
17
+ attr_reader :verb_name, :errors
18
+
19
+ # Construct a SchemaValidationError. Mirrors Python's
20
+ # SchemaValidationError(verb_name, errors).
21
+ def initialize(verb_name, errors)
22
+ @verb_name = verb_name
23
+ @errors = errors || []
24
+ message = "Schema validation failed for '#{verb_name}': #{@errors.join('; ')}"
25
+ super(message)
26
+ end
27
+ end
28
+
29
+ # SchemaUtils — Ruby port of signalwire.utils.schema_utils.SchemaUtils.
30
+ #
31
+ # Loads the SWML JSON Schema, extracts verb metadata, and validates
32
+ # either a single verb config or a complete SWML document.
33
+ #
34
+ # Construction rules mirror Python:
35
+ # - Pass schema_path: nil to use the bundled schema.json.
36
+ # - schema_validation: false disables validation (validate_verb returns
37
+ # true for every call).
38
+ # - The env var SWML_SKIP_SCHEMA_VALIDATION=1/true/yes also disables
39
+ # validation regardless of the constructor argument.
40
+ #
41
+ # The Ruby port currently ships only the lightweight validator (verb
42
+ # existence + required-property check). Full JSON Schema validation
43
+ # can be wired in via the `json_schemer` gem by extending
44
+ # init_full_validator. The lightweight contract matches Python's
45
+ # _validate_verb_lightweight() exactly.
46
+ class SchemaUtils
47
+ # @return [Hash{String=>Object}] parsed JSON Schema document
48
+ attr_reader :schema
49
+
50
+ # @return [String, nil] resolved schema path (nil = embedded default)
51
+ attr_reader :schema_path
52
+
53
+ # Construct a SchemaUtils.
54
+ #
55
+ # @param schema_path [String, nil] path to a schema.json file; nil for
56
+ # the bundled copy at lib/signalwire/swml/schema.json.
57
+ # @param schema_validation [Boolean] enable/disable schema validation.
58
+ def initialize(schema_path = nil, schema_validation = true)
59
+ env_skip = env_boolish(ENV.fetch('SWML_SKIP_SCHEMA_VALIDATION', ''))
60
+ @schema_path = schema_path
61
+ @validation_enabled = schema_validation && !env_skip
62
+ @schema = load_schema
63
+ @verbs = {}
64
+ extract_verbs
65
+ init_full_validator if @validation_enabled && !@schema.empty?
66
+ end
67
+
68
+ # Whether full JSON Schema validation is wired up.
69
+ # Mirrors Python's full_validation_available property.
70
+ def full_validation_available?
71
+ !@full_validator.nil?
72
+ end
73
+
74
+ # Read and parse the JSON Schema. Mirrors Python's load_schema().
75
+ def load_schema
76
+ path = @schema_path || default_schema_path
77
+ return {} if path.nil? || !File.exist?(path)
78
+
79
+ raw = File.read(path, encoding: 'UTF-8')
80
+ JSON.parse(raw)
81
+ rescue JSON::ParserError, Errno::ENOENT
82
+ {}
83
+ end
84
+
85
+ # Sorted list of all known verb names.
86
+ # Mirrors Python's get_all_verb_names().
87
+ def get_all_verb_names
88
+ @verbs.keys.sort
89
+ end
90
+
91
+ # The properties[verb_name] block for a verb, or {} when unknown.
92
+ # Mirrors Python's get_verb_properties(verb_name).
93
+ def get_verb_properties(verb_name)
94
+ v = @verbs[verb_name]
95
+ return {} if v.nil?
96
+
97
+ outer_props = v['definition']['properties'] rescue nil
98
+ return {} unless outer_props.is_a?(Hash)
99
+
100
+ inner = outer_props[verb_name]
101
+ return {} unless inner.is_a?(Hash)
102
+
103
+ inner
104
+ end
105
+
106
+ # The required list for a verb, or [] when unknown / no required.
107
+ # Mirrors Python's get_verb_required_properties(verb_name).
108
+ def get_verb_required_properties(verb_name)
109
+ inner = get_verb_properties(verb_name)
110
+ req = inner['required']
111
+ return [] unless req.is_a?(Array)
112
+
113
+ req.select { |x| x.is_a?(String) }
114
+ end
115
+
116
+ # Parameter-definition block used by code-gen tooling.
117
+ # Mirrors Python's get_verb_parameters(verb_name).
118
+ def get_verb_parameters(verb_name)
119
+ inner = get_verb_properties(verb_name)
120
+ props = inner['properties']
121
+ return {} unless props.is_a?(Hash)
122
+
123
+ props
124
+ end
125
+
126
+ # Validate a verb config against the schema.
127
+ # Mirrors Python's validate_verb(verb_name, verb_config).
128
+ #
129
+ # @return [Array(Boolean, Array<String>)] (valid, errors) tuple.
130
+ def validate_verb(verb_name, verb_config)
131
+ return [true, []] unless @validation_enabled
132
+
133
+ unless @verbs.key?(verb_name)
134
+ return [false, ["Unknown verb: #{verb_name}"]]
135
+ end
136
+
137
+ if @full_validator
138
+ validate_verb_full(verb_name, verb_config)
139
+ else
140
+ validate_verb_lightweight(verb_name, verb_config)
141
+ end
142
+ end
143
+
144
+ # Validate a complete SWML document.
145
+ # Mirrors Python's validate_document(document). Returns
146
+ # (false, ['Schema validator not initialized']) when no full
147
+ # validator is wired in.
148
+ def validate_document(document)
149
+ if @full_validator.nil?
150
+ return [false, ['Schema validator not initialized']]
151
+ end
152
+
153
+ # Reserved for full-validator wiring.
154
+ [true, []]
155
+ end
156
+
157
+ # Generate a Python-style method signature string for a verb.
158
+ # Mirrors Python's generate_method_signature(verb_name).
159
+ def generate_method_signature(verb_name)
160
+ params = get_verb_parameters(verb_name)
161
+ required = get_verb_required_properties(verb_name).to_set
162
+ parts = ['self']
163
+ keys = params.keys.sort
164
+ keys.each do |name|
165
+ t = python_type_annotation(params[name])
166
+ parts << if required.include?(name)
167
+ "#{name}: #{t}"
168
+ else
169
+ "#{name}: Optional[#{t}] = None"
170
+ end
171
+ end
172
+ parts << '**kwargs'
173
+ doc = +"\"\"\"\n Add the #{verb_name} verb to the current document\n \n"
174
+ keys.each do |name|
175
+ desc = ''
176
+ d = params[name]
177
+ if d.is_a?(Hash) && d['description']
178
+ desc = d['description'].to_s.gsub("\n", ' ').strip
179
+ end
180
+ doc << " Args:\n #{name}: #{desc}\n"
181
+ end
182
+ doc << " \n Returns:\n True if the verb was added successfully, False otherwise\n \"\"\"\n"
183
+ "def #{verb_name}(#{parts.join(', ')}) -> bool:\n#{doc}"
184
+ end
185
+
186
+ # Generate a Python-style method body string for a verb.
187
+ # Mirrors Python's generate_method_body(verb_name).
188
+ def generate_method_body(verb_name)
189
+ params = get_verb_parameters(verb_name)
190
+ keys = params.keys.sort
191
+ lines = [
192
+ ' # Prepare the configuration',
193
+ ' config = {}'
194
+ ]
195
+ keys.each do |name|
196
+ lines << " if #{name} is not None:"
197
+ lines << " config['#{name}'] = #{name}"
198
+ end
199
+ lines << ' # Add any additional parameters from kwargs'
200
+ lines << ' for key, value in kwargs.items():'
201
+ lines << ' if value is not None:'
202
+ lines << ' config[key] = value'
203
+ lines << ''
204
+ lines << " # Add the #{verb_name} verb"
205
+ lines << " return self.add_verb('#{verb_name}', config)"
206
+ lines.join("\n")
207
+ end
208
+
209
+ private
210
+
211
+ def default_schema_path
212
+ # Bundled schema lives in lib/signalwire/swml/schema.json
213
+ File.expand_path('../swml/schema.json', __dir__)
214
+ end
215
+
216
+ def env_boolish(value)
217
+ %w[1 true yes].include?(value.to_s.strip.downcase)
218
+ end
219
+
220
+ def extract_verbs
221
+ defs = @schema['$defs']
222
+ return unless defs.is_a?(Hash)
223
+
224
+ swml_method = defs['SWMLMethod']
225
+ return unless swml_method.is_a?(Hash)
226
+
227
+ any_of = swml_method['anyOf']
228
+ return unless any_of.is_a?(Array)
229
+
230
+ any_of.each do |entry|
231
+ next unless entry.is_a?(Hash)
232
+
233
+ ref = entry['$ref']
234
+ next unless ref.is_a?(String)
235
+
236
+ prefix = '#/$defs/'
237
+ next unless ref.start_with?(prefix)
238
+
239
+ schema_name = ref[prefix.length..]
240
+ defn = defs[schema_name]
241
+ next unless defn.is_a?(Hash)
242
+
243
+ props = defn['properties']
244
+ next unless props.is_a?(Hash) && !props.empty?
245
+
246
+ actual_verb = props.keys.first
247
+ @verbs[actual_verb] = {
248
+ 'name' => actual_verb,
249
+ 'schema_name' => schema_name,
250
+ 'definition' => defn
251
+ }
252
+ end
253
+ end
254
+
255
+ def init_full_validator
256
+ # Reserved for full-validator wiring (json_schemer gem).
257
+ @full_validator = nil
258
+ end
259
+
260
+ def validate_verb_full(verb_name, verb_config)
261
+ # Reserved for full-validator wiring; falls back to lightweight check.
262
+ validate_verb_lightweight(verb_name, verb_config)
263
+ end
264
+
265
+ def validate_verb_lightweight(verb_name, verb_config)
266
+ errors = []
267
+ get_verb_required_properties(verb_name).each do |prop|
268
+ unless verb_config.key?(prop)
269
+ errors << "Missing required property '#{prop}' for verb '#{verb_name}'"
270
+ end
271
+ end
272
+ [errors.empty?, errors]
273
+ end
274
+
275
+ def python_type_annotation(defn)
276
+ return 'Any' unless defn.is_a?(Hash)
277
+
278
+ case defn['type']
279
+ when 'string' then 'str'
280
+ when 'integer' then 'int'
281
+ when 'number' then 'float'
282
+ when 'boolean' then 'bool'
283
+ when 'array'
284
+ item = 'Any'
285
+ if defn['items'].is_a?(Hash)
286
+ item = python_type_annotation(defn['items'])
287
+ end
288
+ "List[#{item}]"
289
+ when 'object' then 'Dict[str, Any]'
290
+ else
291
+ 'Any'
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ require 'set'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/logging_config'
4
+
5
+ # Cross-language SDK contract: SignalWire::Utils.is_serverless_mode
6
+ # mirrors signalwire.utils.is_serverless_mode in the Python reference.
7
+ # Returns true when running inside any short-lived / event-driven
8
+ # environment (i.e. not 'server').
9
+
10
+ module SignalWire
11
+ module Utils
12
+ module_function
13
+
14
+ # @return [Boolean] true unless the detected mode is 'server'.
15
+ def is_serverless_mode
16
+ SignalWire::Core::LoggingConfig.get_execution_mode != 'server'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 SignalWire
4
+ #
5
+ # Licensed under the MIT License.
6
+ # See LICENSE file in the project root for full license information.
7
+
8
+ require 'ipaddr'
9
+ require 'resolv'
10
+ require 'uri'
11
+
12
+ require_relative '../logging'
13
+
14
+ module SignalWire
15
+ module Utils
16
+ # SSRF-prevention guard for user-supplied URLs.
17
+ #
18
+ # Mirrors Python's signalwire.utils.url_validator.validate_url:
19
+ # rejects non-http(s) schemes, missing hostnames, and any URL whose
20
+ # hostname resolves to a private / loopback / link-local / cloud-
21
+ # metadata IP. When +allow_private+ is true, OR the
22
+ # +SWML_ALLOW_PRIVATE_URLS+ env var is set to "1", "true" or "yes"
23
+ # (case-insensitive), the IP-blocklist check is skipped.
24
+ #
25
+ # The method UrlValidator.validate_url projects onto the Python free
26
+ # function signalwire.utils.url_validator.validate_url via
27
+ # scripts/enumerate_signatures.py.
28
+ module UrlValidator
29
+ # Cross-port SSRF block list. Order matches the Python reference.
30
+ BLOCKED_NETWORKS = %w[
31
+ 10.0.0.0/8
32
+ 172.16.0.0/12
33
+ 192.168.0.0/16
34
+ 127.0.0.0/8
35
+ 169.254.0.0/16
36
+ 0.0.0.0/8
37
+ ::1/128
38
+ fc00::/7
39
+ fe80::/10
40
+ ].freeze
41
+
42
+ LOG = SignalWire::Logging.logger('signalwire.url_validator')
43
+
44
+ # Pluggable resolver hook. Tests inject a lambda to keep the suite
45
+ # hermetic; production calls Resolv.getaddresses. Underscore prefix
46
+ # keeps it out of the public surface inventory — the Python
47
+ # reference only exposes ``validate_url`` at this module level.
48
+ def self._resolver
49
+ @_resolver
50
+ end
51
+
52
+ def self._resolver=(value)
53
+ @_resolver = value
54
+ end
55
+
56
+ # Validate that a URL is safe to fetch.
57
+ #
58
+ # @param url [String] URL to validate
59
+ # @param allow_private [Boolean] when true, bypass the IP-blocklist check
60
+ # @return [Boolean] true if the URL is safe to fetch
61
+ def self.validate_url(url, allow_private = false)
62
+ parsed = URI.parse(url)
63
+
64
+ scheme = (parsed.scheme || '').downcase
65
+ unless %w[http https].include?(scheme)
66
+ LOG.warn("URL rejected: invalid scheme #{parsed.scheme}")
67
+ return false
68
+ end
69
+
70
+ hostname = parsed.host
71
+ if hostname.nil? || hostname.empty?
72
+ LOG.warn('URL rejected: no hostname')
73
+ return false
74
+ end
75
+
76
+ # URI keeps brackets in host for IPv6 literals; strip them.
77
+ if hostname.start_with?('[') && hostname.end_with?(']')
78
+ hostname = hostname[1..-2]
79
+ end
80
+
81
+ if allow_private || _env_allows_private?
82
+ return true
83
+ end
84
+
85
+ ips = _resolve(hostname)
86
+ if ips.nil? || ips.empty?
87
+ LOG.warn("URL rejected: could not resolve hostname #{hostname}")
88
+ return false
89
+ end
90
+
91
+ ips.each do |ip_str|
92
+ ip = begin
93
+ IPAddr.new(ip_str)
94
+ rescue IPAddr::InvalidAddressError
95
+ next
96
+ end
97
+ BLOCKED_NETWORKS.each do |cidr|
98
+ net = IPAddr.new(cidr)
99
+ if net.include?(ip)
100
+ LOG.warn("URL rejected: #{hostname} resolves to blocked IP #{ip_str} (in #{cidr})")
101
+ return false
102
+ end
103
+ end
104
+ end
105
+
106
+ true
107
+ rescue URI::InvalidURIError, IPAddr::InvalidAddressError => e
108
+ LOG.warn("URL validation error: #{e.message}")
109
+ false
110
+ end
111
+
112
+ def self._env_allows_private?
113
+ v = (ENV['SWML_ALLOW_PRIVATE_URLS'] || '').downcase
114
+ %w[1 true yes].include?(v)
115
+ end
116
+ private_class_method :_env_allows_private?
117
+
118
+ # @param hostname [String]
119
+ # @return [Array<String>, nil]
120
+ def self._resolve(hostname)
121
+ if _resolver
122
+ return _resolver.call(hostname)
123
+ end
124
+ # Literal IP shortcut (covers tests that pass IP-as-hostname URLs).
125
+ begin
126
+ IPAddr.new(hostname)
127
+ return [hostname]
128
+ rescue IPAddr::InvalidAddressError
129
+ # not a literal IP — fall through to DNS
130
+ end
131
+ Resolv.getaddresses(hostname)
132
+ rescue StandardError
133
+ nil
134
+ end
135
+ private_class_method :_resolve
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SignalWire
4
+ VERSION = '2.0.0'
5
+ end
data/lib/signalwire.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'signalwire/version'
4
+ require_relative 'signalwire/logging'
5
+ require_relative 'signalwire/core/logging_config'
6
+ require_relative 'signalwire/utils/serverless'
7
+ require_relative 'signalwire/utils/schema_utils'
8
+ require_relative 'signalwire/runtime'
9
+ require_relative 'signalwire/swml/document'
10
+ require_relative 'signalwire/swml/schema'
11
+ require_relative 'signalwire/swml/service'
12
+ require_relative 'signalwire/swaig/function_result'
13
+ require_relative 'signalwire/security/session_manager'
14
+ require_relative 'signalwire/contexts/context_builder'
15
+ require_relative 'signalwire/pom/prompt_object_model'
16
+ require_relative 'signalwire/datamap/data_map'
17
+ require_relative 'signalwire/skills/skill_base'
18
+ require_relative 'signalwire/skills/skill_manager'
19
+ require_relative 'signalwire/skills/skill_registry'
20
+ require_relative 'signalwire/agent/agent_base'
21
+ require_relative 'signalwire/serverless/lambda_handler'
22
+
23
+ module SignalWire
24
+ # Top-level convenience entry points — mirror Python's
25
+ # ``signalwire/__init__.py`` factory + skill registry helpers.
26
+
27
+ module_function
28
+
29
+ # Construct a {SignalWire::REST::RestClient} instance.
30
+ #
31
+ # Mirrors Python's top-level ``signalwire.RestClient(*args, **kwargs)``
32
+ # factory — a thin wrapper that lazy-imports
33
+ # ``signalwire.rest.RestClient`` and instantiates it. Supports both
34
+ # positional credentials (matching Go-style ``RestClient(project,
35
+ # token, host)``) and keyword credentials (Ruby-idiomatic).
36
+ #
37
+ # @param args [Array<String>] Positional credentials (compat shim).
38
+ # @param kwargs [Hash] Keyword credentials forwarded to the constructor.
39
+ # @return [SignalWire::REST::RestClient]
40
+ def RestClient(*args, **kwargs)
41
+ require_relative 'signalwire/rest/rest_client'
42
+ if args.length >= 3 && kwargs.empty?
43
+ REST::RestClient.new(project: args[0], token: args[1], host: args[2])
44
+ elsif !args.empty? && kwargs.empty?
45
+ raise ArgumentError, 'positional form requires (project, token, host)'
46
+ else
47
+ REST::RestClient.new(**kwargs)
48
+ end
49
+ end
50
+
51
+ # Register a custom skill class with the global skill registry.
52
+ #
53
+ # Mirrors Python's ``signalwire.register_skill(skill_class)`` — the
54
+ # Ruby singleton registry stores factories by name. The class is
55
+ # expected to expose a ``::skill_name`` (or ``SKILL_NAME`` constant)
56
+ # so we can derive the registration key.
57
+ #
58
+ # @param skill_class [Class] A subclass of {SignalWire::Skills::SkillBase}
59
+ # @return [void]
60
+ def register_skill(skill_class)
61
+ require_relative 'signalwire/skills/skill_registry'
62
+ name = if skill_class.respond_to?(:skill_name)
63
+ skill_class.skill_name
64
+ elsif skill_class.const_defined?(:SKILL_NAME)
65
+ skill_class.const_get(:SKILL_NAME)
66
+ else
67
+ raise ArgumentError,
68
+ "skill class #{skill_class} must define ::skill_name or SKILL_NAME"
69
+ end
70
+ Skills::SkillRegistry.register_skill(name, ->(params) { skill_class.new(params) })
71
+ end
72
+
73
+ # Add a directory to search for skills.
74
+ #
75
+ # Mirrors Python's ``signalwire.add_skill_directory(path)`` — delegates
76
+ # to the singleton {SignalWire::Skills::SkillRegistry} instance so
77
+ # third-party skill collections can be registered by path. Subsequent
78
+ # calls accumulate (de-duplicated) into a shared external paths list.
79
+ #
80
+ # @param path [String] absolute or relative path to a skill directory.
81
+ # @return [void]
82
+ # @raise [ArgumentError] when the path doesn't exist or isn't a directory.
83
+ def add_skill_directory(path)
84
+ require_relative 'signalwire/skills/skill_registry'
85
+ _signalwire_singleton_registry.add_skill_directory(path)
86
+ end
87
+
88
+ # Get complete schema for all available skills, including parameter metadata.
89
+ #
90
+ # Mirrors Python's ``signalwire.list_skills_with_params()``. Keys are
91
+ # skill names; values describe metadata + parameter schema. Useful for
92
+ # GUI configuration tools, API documentation, or programmatic skill
93
+ # discovery.
94
+ #
95
+ # @return [Hash{String => Hash}]
96
+ def list_skills_with_params
97
+ require_relative 'signalwire/skills/skill_registry'
98
+ if Skills::SkillRegistry.respond_to?(:get_all_skills_schema)
99
+ Skills::SkillRegistry.get_all_skills_schema
100
+ else
101
+ # Fallback: list_skills returns names; pair them with empty params.
102
+ Skills::SkillRegistry.list_skills.each_with_object({}) do |name, h|
103
+ h[name] = { 'name' => name, 'parameters' => {} }
104
+ end
105
+ end
106
+ end
107
+
108
+ # Singleton SkillRegistry instance used by the top-level helpers.
109
+ # Internal — exposed only so the helpers can share state across calls.
110
+ def _signalwire_singleton_registry
111
+ @_signalwire_singleton_registry ||= Skills::SkillRegistry.new
112
+ end
113
+ private_class_method :_signalwire_singleton_registry
114
+ end