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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cross-language SDK contract for serverless / deployment-mode detection.
4
+ #
5
+ # Mirrors signalwire.core.logging_config.get_execution_mode in the Python
6
+ # reference. Order of precedence (FIRST match wins):
7
+ #
8
+ # 1. GATEWAY_INTERFACE -> 'cgi'
9
+ # 2. AWS_LAMBDA_FUNCTION_NAME or LAMBDA_TASK_ROOT -> 'lambda'
10
+ # 3. FUNCTION_TARGET, K_SERVICE, or GOOGLE_CLOUD_PROJECT -> 'google_cloud_function'
11
+ # 4. AZURE_FUNCTIONS_ENVIRONMENT, FUNCTIONS_WORKER_RUNTIME, or
12
+ # AzureWebJobsStorage -> 'azure_function'
13
+ # 5. otherwise -> 'server'
14
+ #
15
+ # The companion helper SignalWire::Utils.is_serverless_mode lives in
16
+ # lib/signalwire/utils/serverless.rb.
17
+
18
+ module SignalWire
19
+ module Core
20
+ module LoggingConfig
21
+ module_function
22
+
23
+ # Detect the SDK's deployment environment based on well-known
24
+ # environment variables.
25
+ #
26
+ # @return [String] one of 'cgi', 'lambda', 'google_cloud_function',
27
+ # 'azure_function', or 'server'.
28
+ def get_execution_mode
29
+ return 'cgi' if env_set?('GATEWAY_INTERFACE')
30
+ return 'lambda' if env_set?('AWS_LAMBDA_FUNCTION_NAME') || env_set?('LAMBDA_TASK_ROOT')
31
+
32
+ if env_set?('FUNCTION_TARGET') ||
33
+ env_set?('K_SERVICE') ||
34
+ env_set?('GOOGLE_CLOUD_PROJECT')
35
+ return 'google_cloud_function'
36
+ end
37
+
38
+ if env_set?('AZURE_FUNCTIONS_ENVIRONMENT') ||
39
+ env_set?('FUNCTIONS_WORKER_RUNTIME') ||
40
+ env_set?('AzureWebJobsStorage')
41
+ return 'azure_function'
42
+ end
43
+
44
+ 'server'
45
+ end
46
+
47
+ def env_set?(name)
48
+ v = ENV[name]
49
+ !v.nil? && !v.empty?
50
+ end
51
+ private_class_method :env_set?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,315 @@
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_relative '../swaig/function_result'
9
+
10
+ module SignalWire
11
+ # Fluent builder for server-side DataMap tools.
12
+ #
13
+ # DataMap tools execute on SignalWire servers without requiring webhook
14
+ # endpoints. This class provides a chainable API for building data_map
15
+ # configurations that become SWAIG function definitions.
16
+ #
17
+ # All mutator methods return +self+ so calls can be chained:
18
+ #
19
+ # dm = DataMap.new('get_weather')
20
+ # .purpose('Get current weather')
21
+ # .parameter('location', 'string', 'City name', required: true)
22
+ # .webhook('GET', 'https://api.weather.com/v1/current?q=${location}')
23
+ # .output(Swaig::FunctionResult.new('Weather: ${response.current.temp_f}F'))
24
+ #
25
+ class DataMap
26
+ attr_reader :function_name
27
+
28
+ def initialize(function_name)
29
+ @function_name = function_name
30
+ @purpose_text = ''
31
+ @parameters = {} # name => { "type" => ..., "description" => ... }
32
+ @required_params = []
33
+ @expressions = []
34
+ @webhooks = []
35
+ @fallback_output = nil
36
+ @global_error_keys = []
37
+ end
38
+
39
+ # Set the LLM-facing tool description (a.k.a. "purpose"). *PROMPT
40
+ # ENGINEERING*, not developer documentation.
41
+ #
42
+ # The description string is rendered into the OpenAI tool schema
43
+ # +description+ field on every LLM turn. The model reads it to
44
+ # decide WHEN to call this tool. A vague +purpose+ is the #1 cause
45
+ # of "the model has the right tool but doesn't call it" failures
46
+ # with data-map tools.
47
+ #
48
+ # == Bad vs good
49
+ #
50
+ # BAD : .purpose("weather api")
51
+ # GOOD: .purpose("Get the current weather conditions and " \
52
+ # "forecast for a specific city. Use this " \
53
+ # "whenever the user asks about weather, " \
54
+ # "temperature, rain, or similar conditions in a " \
55
+ # "named location.")
56
+ def purpose(desc)
57
+ @purpose_text = desc
58
+ self
59
+ end
60
+
61
+ # Alias for +purpose+. Sets the LLM-facing tool description. This
62
+ # string is read by the model to decide WHEN to call this tool.
63
+ # See +purpose+ for bad-vs-good examples.
64
+ def description(desc)
65
+ purpose(desc)
66
+ end
67
+
68
+ # Add a typed parameter to the function signature — the +desc+ is
69
+ # LLM-FACING.
70
+ #
71
+ # Each parameter description is rendered into the OpenAI tool
72
+ # schema under +parameters.properties.<name>.description+ and sent
73
+ # to the model. The model uses it to decide HOW to fill in the
74
+ # argument from user speech. It is prompt engineering, not
75
+ # developer FYI.
76
+ #
77
+ # == Bad vs good
78
+ #
79
+ # BAD : .parameter("city", "string", "the city")
80
+ # GOOD: .parameter("city", "string",
81
+ # "The name of the city to get weather for, e.g. " \
82
+ # "'San Francisco'. Ask the user if they did not " \
83
+ # "provide one. Include the state or country if the " \
84
+ # "city name is ambiguous.")
85
+ #
86
+ # @param name [String]
87
+ # @param type [String] JSON-Schema type (string, number, boolean, array, object)
88
+ # @param desc [String] LLM-facing prompt-engineering description
89
+ # telling the model how to extract this value from the user's
90
+ # utterance
91
+ # @param required [Boolean] whether the parameter is required
92
+ # @param enum [Array<String>, nil] optional list of allowed values
93
+ def parameter(name, type, desc, required: false, enum: nil)
94
+ param_def = { "type" => type, "description" => desc }
95
+ param_def["enum"] = enum if enum && !enum.empty?
96
+ @parameters[name] = param_def
97
+ @required_params << name if required && !@required_params.include?(name)
98
+ self
99
+ end
100
+
101
+ # Add an expression (pattern-matching rule).
102
+ #
103
+ # @param test_value [String] template string to test, e.g. "${args.command}"
104
+ # @param pattern [String, Regexp] regex pattern to match against
105
+ # @param output [Swaig::FunctionResult, Hash] result when pattern matches
106
+ # @param nomatch_output [Swaig::FunctionResult, Hash, nil] result when pattern does not match
107
+ def expression(test_value, pattern, output, nomatch_output: nil)
108
+ pattern_str = pattern.is_a?(Regexp) ? pattern.source : pattern.to_s
109
+ output_h = output.respond_to?(:to_h) ? output.to_h : output
110
+
111
+ expr_def = {
112
+ "string" => test_value,
113
+ "pattern" => pattern_str,
114
+ "output" => output_h
115
+ }
116
+
117
+ if nomatch_output
118
+ nomatch_h = nomatch_output.respond_to?(:to_h) ? nomatch_output.to_h : nomatch_output
119
+ expr_def["nomatch-output"] = nomatch_h
120
+ end
121
+
122
+ @expressions << expr_def
123
+ self
124
+ end
125
+
126
+ # Add a webhook (HTTP call) to the data_map pipeline.
127
+ #
128
+ # @param method [String] HTTP method (GET, POST, PUT, DELETE, etc.)
129
+ # @param url [String] endpoint URL (may contain ${variable} substitutions)
130
+ # @param headers [Hash, nil] optional HTTP headers
131
+ # @param form_param [String, nil] send JSON body as a single form parameter
132
+ # @param input_args_as_params [Boolean] merge function arguments into params
133
+ # @param require_args [Array<String>, nil] only execute when these args are present
134
+ def webhook(method, url, headers: nil, form_param: nil, input_args_as_params: false, require_args: nil)
135
+ wh = {
136
+ "url" => url,
137
+ "method" => method.upcase
138
+ }
139
+ wh["headers"] = headers if headers
140
+ wh["form_param"] = form_param if form_param
141
+ wh["input_args_as_params"] = true if input_args_as_params
142
+ wh["require_args"] = require_args if require_args
143
+ @webhooks << wh
144
+ self
145
+ end
146
+
147
+ # Add expressions to run after the most-recently-added webhook completes.
148
+ def webhook_expressions(expressions)
149
+ raise ArgumentError, "Must add webhook before setting webhook expressions" if @webhooks.empty?
150
+
151
+ @webhooks.last["expressions"] = expressions
152
+ self
153
+ end
154
+
155
+ # Set the request body for the most-recently-added webhook (POST / PUT).
156
+ def body(data)
157
+ raise ArgumentError, "Must add webhook before setting body" if @webhooks.empty?
158
+
159
+ @webhooks.last["body"] = data
160
+ self
161
+ end
162
+
163
+ # Set request params for the most-recently-added webhook.
164
+ def params(data)
165
+ raise ArgumentError, "Must add webhook before setting params" if @webhooks.empty?
166
+
167
+ @webhooks.last["params"] = data
168
+ self
169
+ end
170
+
171
+ # Configure array processing on the most-recently-added webhook response.
172
+ #
173
+ # @param config [Hash] must include keys: input_key, output_key, append. Optional: max.
174
+ def foreach(config)
175
+ raise ArgumentError, "Must add webhook before setting foreach" if @webhooks.empty?
176
+ raise ArgumentError, "foreach config must be a Hash" unless config.is_a?(Hash)
177
+
178
+ required_keys = %w[input_key output_key append]
179
+ missing = required_keys - config.keys.map(&:to_s)
180
+ raise ArgumentError, "foreach config missing required keys: #{missing.inspect}" unless missing.empty?
181
+
182
+ @webhooks.last["foreach"] = config
183
+ self
184
+ end
185
+
186
+ # Set the output result for the most-recently-added webhook.
187
+ #
188
+ # @param result [Swaig::FunctionResult, Hash]
189
+ def output(result)
190
+ raise ArgumentError, "Must add webhook before setting output" if @webhooks.empty?
191
+
192
+ @webhooks.last["output"] = result.respond_to?(:to_h) ? result.to_h : result
193
+ self
194
+ end
195
+
196
+ # Set a fallback output used when all webhooks fail.
197
+ #
198
+ # @param result [Swaig::FunctionResult, Hash]
199
+ def fallback_output(result)
200
+ @fallback_output = result.respond_to?(:to_h) ? result.to_h : result
201
+ self
202
+ end
203
+
204
+ # Set error keys on the most-recently-added webhook, or at the top level
205
+ # if no webhook has been added yet.
206
+ def error_keys(keys)
207
+ if @webhooks.any?
208
+ @webhooks.last["error_keys"] = keys
209
+ else
210
+ @global_error_keys = keys
211
+ end
212
+ self
213
+ end
214
+
215
+ # Set top-level error keys (applies to all webhooks).
216
+ def global_error_keys(keys)
217
+ @global_error_keys = keys
218
+ self
219
+ end
220
+
221
+ # Serialize this DataMap into a complete SWAIG function definition Hash.
222
+ #
223
+ # @return [Hash] with keys: "function", "description", "parameters", "data_map"
224
+ def to_swaig_function
225
+ # Build parameter schema
226
+ if @parameters.any?
227
+ param_schema = {
228
+ "type" => "object",
229
+ "properties" => @parameters.dup
230
+ }
231
+ param_schema["required"] = @required_params.dup if @required_params.any?
232
+ else
233
+ param_schema = { "type" => "object", "properties" => {} }
234
+ end
235
+
236
+ # Build data_map
237
+ data_map = {}
238
+ data_map["expressions"] = @expressions if @expressions.any?
239
+ data_map["webhooks"] = @webhooks if @webhooks.any?
240
+ data_map["output"] = @fallback_output if @fallback_output
241
+ data_map["error_keys"] = @global_error_keys if @global_error_keys.any?
242
+
243
+ {
244
+ "function" => @function_name,
245
+ "description" => @purpose_text.empty? ? "Execute #{@function_name}" : @purpose_text,
246
+ "parameters" => param_schema,
247
+ "data_map" => data_map
248
+ }
249
+ end
250
+
251
+ # ----------------------------------------------------------------
252
+ # Class-level convenience constructors
253
+ # ----------------------------------------------------------------
254
+
255
+ # Build a simple API-calling tool in one shot.
256
+ #
257
+ # @param name [String]
258
+ # @param url [String]
259
+ # @param response_template [String]
260
+ # @param parameters [Hash, nil] name => { "type" => ..., "description" => ..., "required" => bool }
261
+ # @param method [String] HTTP method (default GET)
262
+ # @param headers [Hash, nil]
263
+ # @param body [Hash, nil]
264
+ # @param error_keys [Array<String>, nil]
265
+ # @return [DataMap]
266
+ def self.create_simple_api_tool(name:, url:, response_template:, parameters: nil,
267
+ method: 'GET', headers: nil, body: nil, error_keys: nil)
268
+ dm = new(name)
269
+
270
+ if parameters
271
+ parameters.each do |pname, pdef|
272
+ dm.parameter(
273
+ pname,
274
+ pdef.fetch("type", "string"),
275
+ pdef.fetch("description", "#{pname} parameter"),
276
+ required: pdef.fetch("required", false)
277
+ )
278
+ end
279
+ end
280
+
281
+ dm.webhook(method, url, headers: headers)
282
+ dm.body(body) if body
283
+ dm.error_keys(error_keys) if error_keys
284
+ dm.output(Swaig::FunctionResult.new(response_template))
285
+ dm
286
+ end
287
+
288
+ # Build an expression-only tool (no HTTP calls).
289
+ #
290
+ # @param name [String]
291
+ # @param patterns [Hash] test_value => [pattern, Swaig::FunctionResult]
292
+ # @param parameters [Hash, nil] same format as +create_simple_api_tool+
293
+ # @return [DataMap]
294
+ def self.create_expression_tool(name:, patterns:, parameters: nil)
295
+ dm = new(name)
296
+
297
+ if parameters
298
+ parameters.each do |pname, pdef|
299
+ dm.parameter(
300
+ pname,
301
+ pdef.fetch("type", "string"),
302
+ pdef.fetch("description", "#{pname} parameter"),
303
+ required: pdef.fetch("required", false)
304
+ )
305
+ end
306
+ end
307
+
308
+ patterns.each do |test_value, (pattern, result)|
309
+ dm.expression(test_value, pattern, result)
310
+ end
311
+
312
+ dm
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SignalWire
4
+ module Logging
5
+ LEVELS = { debug: 0, info: 1, warn: 2, error: 3, off: 4 }.freeze
6
+
7
+ # Returns the current global log level, derived from:
8
+ # 1. SIGNALWIRE_LOG_MODE=off -> :off (suppresses everything)
9
+ # 2. SIGNALWIRE_LOG_LEVEL env -> the named level
10
+ # 3. Default -> :info
11
+ def self.global_level
12
+ @global_level || resolve_level_from_env
13
+ end
14
+
15
+ def self.global_level=(level)
16
+ level = level.to_sym if level.is_a?(String)
17
+ raise ArgumentError, "Unknown log level: #{level}" unless LEVELS.key?(level)
18
+
19
+ @global_level = level
20
+ end
21
+
22
+ def self.reset!
23
+ @global_level = nil
24
+ end
25
+
26
+ def self.suppressed?
27
+ global_level == :off
28
+ end
29
+
30
+ # Convenience factory
31
+ def self.logger(name)
32
+ Logger.new(name)
33
+ end
34
+
35
+ # -------------------------------------------------------------------
36
+ class Logger
37
+ attr_reader :name
38
+
39
+ def initialize(name)
40
+ @name = name
41
+ @output = $stderr
42
+ end
43
+
44
+ def debug(msg)
45
+ log(:debug, msg)
46
+ end
47
+
48
+ def info(msg)
49
+ log(:info, msg)
50
+ end
51
+
52
+ def warn(msg)
53
+ log(:warn, msg)
54
+ end
55
+
56
+ def error(msg)
57
+ log(:error, msg)
58
+ end
59
+
60
+ private
61
+
62
+ def log(level, msg)
63
+ return if Logging.suppressed?
64
+ return if LEVELS[level] < LEVELS[Logging.global_level]
65
+
66
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
67
+ @output.puts "[#{timestamp}] #{level.upcase} [#{@name}] #{msg}"
68
+ end
69
+ end
70
+
71
+ # -------------------------------------------------------------------
72
+ # Private helpers
73
+ # -------------------------------------------------------------------
74
+ private_class_method def self.resolve_level_from_env
75
+ if ENV['SIGNALWIRE_LOG_MODE']&.downcase == 'off'
76
+ @global_level = :off
77
+ return :off
78
+ end
79
+
80
+ raw = ENV['SIGNALWIRE_LOG_LEVEL']
81
+ if raw
82
+ sym = raw.downcase.to_sym
83
+ if LEVELS.key?(sym)
84
+ @global_level = sym
85
+ return sym
86
+ end
87
+ end
88
+
89
+ :info # default — not cached so env changes take effect
90
+ end
91
+ end
92
+ end