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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +259 -0
- data/bin/swaig-test +872 -0
- data/lib/signalwire/agent/agent_base.rb +2134 -0
- data/lib/signalwire/contexts/context_builder.rb +861 -0
- data/lib/signalwire/core/logging_config.rb +54 -0
- data/lib/signalwire/datamap/data_map.rb +315 -0
- data/lib/signalwire/logging.rb +92 -0
- data/lib/signalwire/pom/prompt_object_model.rb +269 -0
- data/lib/signalwire/pom/section.rb +202 -0
- data/lib/signalwire/prefabs/concierge.rb +92 -0
- data/lib/signalwire/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire/prefabs/receptionist.rb +74 -0
- data/lib/signalwire/prefabs/survey.rb +75 -0
- data/lib/signalwire/relay/action.rb +291 -0
- data/lib/signalwire/relay/call.rb +523 -0
- data/lib/signalwire/relay/client.rb +789 -0
- data/lib/signalwire/relay/constants.rb +124 -0
- data/lib/signalwire/relay/message.rb +137 -0
- data/lib/signalwire/relay/relay_event.rb +670 -0
- data/lib/signalwire/rest/http_client.rb +159 -0
- data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
- data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
- data/lib/signalwire/rest/namespaces/project.rb +33 -0
- data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire/rest/namespaces/video.rb +129 -0
- data/lib/signalwire/rest/pagination.rb +89 -0
- data/lib/signalwire/rest/phone_call_handler.rb +56 -0
- data/lib/signalwire/rest/rest_client.rb +114 -0
- data/lib/signalwire/runtime.rb +98 -0
- data/lib/signalwire/security/session_manager.rb +124 -0
- data/lib/signalwire/security/webhook_middleware.rb +191 -0
- data/lib/signalwire/security/webhook_validator.rb +327 -0
- data/lib/signalwire/server/agent_server.rb +413 -0
- data/lib/signalwire/serverless/lambda_handler.rb +251 -0
- data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
- data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
- data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire/skills/builtin/joke.rb +65 -0
- data/lib/signalwire/skills/builtin/math.rb +176 -0
- data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire/skills/builtin/spider.rb +169 -0
- data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
- data/lib/signalwire/skills/builtin/web_search.rb +141 -0
- data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
- data/lib/signalwire/skills/skill_base.rb +82 -0
- data/lib/signalwire/skills/skill_manager.rb +97 -0
- data/lib/signalwire/skills/skill_registry.rb +258 -0
- data/lib/signalwire/swaig/function_result.rb +777 -0
- data/lib/signalwire/swml/document.rb +84 -0
- data/lib/signalwire/swml/schema.json +12250 -0
- data/lib/signalwire/swml/schema.rb +81 -0
- data/lib/signalwire/swml/service.rb +650 -0
- data/lib/signalwire/utils/schema_utils.rb +298 -0
- data/lib/signalwire/utils/serverless.rb +19 -0
- data/lib/signalwire/utils/url_validator.rb +138 -0
- data/lib/signalwire/version.rb +5 -0
- data/lib/signalwire.rb +114 -0
- metadata +225 -0
|
@@ -0,0 +1,777 @@
|
|
|
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 Swaig
|
|
12
|
+
# Response builder that tool handlers return.
|
|
13
|
+
# All mutating methods return +self+ for fluent chaining.
|
|
14
|
+
#
|
|
15
|
+
# result = FunctionResult.new("Found your order")
|
|
16
|
+
# .update_global_data("order_id" => "12345")
|
|
17
|
+
# .say("Let me look that up")
|
|
18
|
+
#
|
|
19
|
+
# The result object has three main components:
|
|
20
|
+
# 1. response - Text the AI should say back to the user
|
|
21
|
+
# 2. action - List of structured actions to execute
|
|
22
|
+
# 3. post_process - Whether to let AI take another turn before executing actions
|
|
23
|
+
#
|
|
24
|
+
class FunctionResult
|
|
25
|
+
attr_accessor :response, :action, :post_process
|
|
26
|
+
|
|
27
|
+
# @param response [String, nil] text the AI speaks back to the user
|
|
28
|
+
# @param post_process [Boolean] whether to let AI take another turn before executing actions
|
|
29
|
+
def initialize(response = nil, post_process: false)
|
|
30
|
+
@response = response || ""
|
|
31
|
+
@action = []
|
|
32
|
+
@post_process = post_process
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# Core mutators
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
# Set the natural-language response text.
|
|
40
|
+
# @return [self]
|
|
41
|
+
def set_response(text)
|
|
42
|
+
@response = text
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Enable / disable post-processing.
|
|
47
|
+
# @return [self]
|
|
48
|
+
def set_post_process(val)
|
|
49
|
+
@post_process = val
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Add a single structured action.
|
|
54
|
+
# @param name [String] action key
|
|
55
|
+
# @param data [Object] action value
|
|
56
|
+
# @return [self]
|
|
57
|
+
def add_action(name, data)
|
|
58
|
+
@action << { name => data }
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Add multiple structured actions at once.
|
|
63
|
+
# @param actions [Array<Hash>]
|
|
64
|
+
# @return [self]
|
|
65
|
+
def add_actions(actions)
|
|
66
|
+
@action.concat(actions)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ==================================================================
|
|
71
|
+
# Call Control
|
|
72
|
+
# ==================================================================
|
|
73
|
+
|
|
74
|
+
# Connect / transfer the call to another destination.
|
|
75
|
+
#
|
|
76
|
+
# @param destination [String] phone number, SIP address, etc.
|
|
77
|
+
# @param final [Boolean] permanent (+true+) or temporary (+false+) transfer
|
|
78
|
+
# @param from_addr [String, nil] optional caller-ID override
|
|
79
|
+
# @return [self]
|
|
80
|
+
def connect(destination, final: true, from_addr: nil)
|
|
81
|
+
connect_params = { "to" => destination }
|
|
82
|
+
connect_params["from"] = from_addr if from_addr
|
|
83
|
+
|
|
84
|
+
swml_action = {
|
|
85
|
+
"SWML" => {
|
|
86
|
+
"sections" => {
|
|
87
|
+
"main" => [{ "connect" => connect_params }]
|
|
88
|
+
},
|
|
89
|
+
"version" => "1.0.0"
|
|
90
|
+
},
|
|
91
|
+
"transfer" => final.to_s
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@action << swml_action
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Transfer via SWML with an AI response when transfer completes.
|
|
99
|
+
#
|
|
100
|
+
# @param dest [String] destination URL for the transfer
|
|
101
|
+
# @param ai_response [String] message AI says when transfer completes
|
|
102
|
+
# @param final [Boolean] permanent or temporary transfer
|
|
103
|
+
# @return [self]
|
|
104
|
+
def swml_transfer(dest, ai_response, final: true)
|
|
105
|
+
swml_action = {
|
|
106
|
+
"SWML" => {
|
|
107
|
+
"version" => "1.0.0",
|
|
108
|
+
"sections" => {
|
|
109
|
+
"main" => [
|
|
110
|
+
{ "set" => { "ai_response" => ai_response } },
|
|
111
|
+
{ "transfer" => { "dest" => dest } }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"transfer" => final.to_s
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@action << swml_action
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Terminate the call.
|
|
123
|
+
# @return [self]
|
|
124
|
+
def hangup
|
|
125
|
+
add_action("hangup", true)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Put the call on hold.
|
|
129
|
+
# @param timeout [Integer] seconds, clamped to 0..900
|
|
130
|
+
# @return [self]
|
|
131
|
+
def hold(timeout = 300)
|
|
132
|
+
timeout = [[timeout, 0].max, 900].min
|
|
133
|
+
add_action("hold", timeout)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Control how the agent waits for user input.
|
|
137
|
+
#
|
|
138
|
+
# @param enabled [Boolean, nil] enable/disable waiting
|
|
139
|
+
# @param timeout [Integer, nil] seconds to wait
|
|
140
|
+
# @param answer_first [Boolean] special "answer_first" mode
|
|
141
|
+
# @return [self]
|
|
142
|
+
def wait_for_user(enabled: nil, timeout: nil, answer_first: false)
|
|
143
|
+
wait_value = if answer_first
|
|
144
|
+
"answer_first"
|
|
145
|
+
elsif timeout
|
|
146
|
+
timeout
|
|
147
|
+
elsif !enabled.nil?
|
|
148
|
+
enabled
|
|
149
|
+
else
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
add_action("wait_for_user", wait_value)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Stop agent execution.
|
|
156
|
+
# @return [self]
|
|
157
|
+
def stop
|
|
158
|
+
add_action("stop", true)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# ==================================================================
|
|
162
|
+
# State & Data Management
|
|
163
|
+
# ==================================================================
|
|
164
|
+
|
|
165
|
+
# Update global agent data variables.
|
|
166
|
+
# @param data [Hash] key-value pairs to set/update
|
|
167
|
+
# @return [self]
|
|
168
|
+
def update_global_data(data)
|
|
169
|
+
add_action("set_global_data", data)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Remove global agent data variables.
|
|
173
|
+
# @param keys [String, Array<String>] key(s) to remove
|
|
174
|
+
# @return [self]
|
|
175
|
+
def remove_global_data(keys)
|
|
176
|
+
add_action("unset_global_data", keys)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Set metadata scoped to current function's meta_data_token.
|
|
180
|
+
# @param data [Hash]
|
|
181
|
+
# @return [self]
|
|
182
|
+
def set_metadata(data)
|
|
183
|
+
add_action("set_meta_data", data)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Remove metadata from current function's scope.
|
|
187
|
+
# @param keys [String, Array<String>]
|
|
188
|
+
# @return [self]
|
|
189
|
+
def remove_metadata(keys)
|
|
190
|
+
add_action("unset_meta_data", keys)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Send a user event through SWML.
|
|
194
|
+
# @param event_data [Hash] event payload
|
|
195
|
+
# @return [self]
|
|
196
|
+
def swml_user_event(event_data)
|
|
197
|
+
swml_action = {
|
|
198
|
+
"sections" => {
|
|
199
|
+
"main" => [{
|
|
200
|
+
"user_event" => { "event" => event_data }
|
|
201
|
+
}]
|
|
202
|
+
},
|
|
203
|
+
"version" => "1.0.0"
|
|
204
|
+
}
|
|
205
|
+
add_action("SWML", swml_action)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Change the conversation step.
|
|
209
|
+
# @param step_name [String]
|
|
210
|
+
# @return [self]
|
|
211
|
+
def swml_change_step(step_name)
|
|
212
|
+
add_action("change_step", step_name)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Change the conversation context.
|
|
216
|
+
# @param context_name [String]
|
|
217
|
+
# @return [self]
|
|
218
|
+
def swml_change_context(context_name)
|
|
219
|
+
add_action("change_context", context_name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Switch agent context/prompt during conversation.
|
|
223
|
+
#
|
|
224
|
+
# When only +system_prompt+ is provided and all flags are false, emits
|
|
225
|
+
# a simple string context switch. Otherwise emits the full object form.
|
|
226
|
+
#
|
|
227
|
+
# @param system_prompt [String, nil]
|
|
228
|
+
# @param user_prompt [String, nil]
|
|
229
|
+
# @param consolidate [Boolean]
|
|
230
|
+
# @param full_reset [Boolean]
|
|
231
|
+
# @param isolated [Boolean]
|
|
232
|
+
# @return [self]
|
|
233
|
+
def switch_context(system_prompt: nil, user_prompt: nil,
|
|
234
|
+
consolidate: false, full_reset: false, isolated: false)
|
|
235
|
+
if system_prompt && !user_prompt && !consolidate && !full_reset && !isolated
|
|
236
|
+
return add_action("context_switch", system_prompt)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context_data = {}
|
|
240
|
+
context_data["system_prompt"] = system_prompt if system_prompt
|
|
241
|
+
context_data["user_prompt"] = user_prompt if user_prompt
|
|
242
|
+
context_data["consolidate"] = true if consolidate
|
|
243
|
+
context_data["full_reset"] = true if full_reset
|
|
244
|
+
context_data["isolated"] = true if isolated
|
|
245
|
+
add_action("context_switch", context_data)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Replace the tool_call + result pair in conversation history.
|
|
249
|
+
#
|
|
250
|
+
# @param text [String, true] replacement text, or +true+ to remove entirely
|
|
251
|
+
# @return [self]
|
|
252
|
+
def replace_in_history(text = true)
|
|
253
|
+
add_action("replace_in_history", text)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ==================================================================
|
|
257
|
+
# Media Control
|
|
258
|
+
# ==================================================================
|
|
259
|
+
|
|
260
|
+
# Make the agent speak specific text.
|
|
261
|
+
# @param text [String]
|
|
262
|
+
# @return [self]
|
|
263
|
+
def say(text)
|
|
264
|
+
add_action("say", text)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Play audio/video file in the background.
|
|
268
|
+
#
|
|
269
|
+
# @param filename [String] audio/video filename or URL
|
|
270
|
+
# @param wait [Boolean] suppress attention-getting behaviour during playback
|
|
271
|
+
# @return [self]
|
|
272
|
+
def play_background_file(filename, wait: false)
|
|
273
|
+
if wait
|
|
274
|
+
add_action("playback_bg", { "file" => filename, "wait" => true })
|
|
275
|
+
else
|
|
276
|
+
add_action("playback_bg", filename)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Stop currently playing background file.
|
|
281
|
+
# @return [self]
|
|
282
|
+
def stop_background_file
|
|
283
|
+
add_action("stop_playback_bg", true)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Start background call recording via SWML.
|
|
287
|
+
#
|
|
288
|
+
# @param control_id [String, nil]
|
|
289
|
+
# @param stereo [Boolean]
|
|
290
|
+
# @param format [String] "wav" or "mp3"
|
|
291
|
+
# @param direction [String] "speak", "listen", or "both"
|
|
292
|
+
# @return [self]
|
|
293
|
+
def record_call(control_id: nil, stereo: false, format: "wav",
|
|
294
|
+
direction: "both", terminators: nil, beep: false,
|
|
295
|
+
input_sensitivity: 44.0, initial_timeout: nil,
|
|
296
|
+
end_silence_timeout: nil, max_length: nil, status_url: nil)
|
|
297
|
+
raise ArgumentError, "format must be 'wav' or 'mp3'" unless %w[wav mp3].include?(format)
|
|
298
|
+
raise ArgumentError, "direction must be 'speak', 'listen', or 'both'" unless %w[speak listen both].include?(direction)
|
|
299
|
+
|
|
300
|
+
record_params = {
|
|
301
|
+
"stereo" => stereo,
|
|
302
|
+
"format" => format,
|
|
303
|
+
"direction" => direction,
|
|
304
|
+
"beep" => beep,
|
|
305
|
+
"input_sensitivity" => input_sensitivity
|
|
306
|
+
}
|
|
307
|
+
record_params["control_id"] = control_id if control_id
|
|
308
|
+
record_params["terminators"] = terminators if terminators
|
|
309
|
+
record_params["initial_timeout"] = initial_timeout if initial_timeout
|
|
310
|
+
record_params["end_silence_timeout"] = end_silence_timeout if end_silence_timeout
|
|
311
|
+
record_params["max_length"] = max_length if max_length
|
|
312
|
+
record_params["status_url"] = status_url if status_url
|
|
313
|
+
|
|
314
|
+
swml_doc = {
|
|
315
|
+
"version" => "1.0.0",
|
|
316
|
+
"sections" => { "main" => [{ "record_call" => record_params }] }
|
|
317
|
+
}
|
|
318
|
+
execute_swml(swml_doc)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Stop an active background call recording.
|
|
322
|
+
# @param control_id [String, nil]
|
|
323
|
+
# @return [self]
|
|
324
|
+
def stop_record_call(control_id: nil)
|
|
325
|
+
stop_params = {}
|
|
326
|
+
stop_params["control_id"] = control_id if control_id
|
|
327
|
+
|
|
328
|
+
swml_doc = {
|
|
329
|
+
"version" => "1.0.0",
|
|
330
|
+
"sections" => { "main" => [{ "stop_record_call" => stop_params }] }
|
|
331
|
+
}
|
|
332
|
+
execute_swml(swml_doc)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# ==================================================================
|
|
336
|
+
# Speech & AI Configuration
|
|
337
|
+
# ==================================================================
|
|
338
|
+
|
|
339
|
+
# Add dynamic speech recognition hints.
|
|
340
|
+
# @param hints [Array<String, Hash>]
|
|
341
|
+
# @return [self]
|
|
342
|
+
def add_dynamic_hints(hints)
|
|
343
|
+
add_action("add_dynamic_hints", hints)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Clear all dynamic speech recognition hints.
|
|
347
|
+
# @return [self]
|
|
348
|
+
def clear_dynamic_hints
|
|
349
|
+
@action << { "clear_dynamic_hints" => {} }
|
|
350
|
+
self
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Adjust end-of-speech timeout.
|
|
354
|
+
# @param milliseconds [Integer]
|
|
355
|
+
# @return [self]
|
|
356
|
+
def set_end_of_speech_timeout(milliseconds)
|
|
357
|
+
add_action("end_of_speech_timeout", milliseconds)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Adjust speech event timeout.
|
|
361
|
+
# @param milliseconds [Integer]
|
|
362
|
+
# @return [self]
|
|
363
|
+
def set_speech_event_timeout(milliseconds)
|
|
364
|
+
add_action("speech_event_timeout", milliseconds)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Enable / disable specific SWAIG functions.
|
|
368
|
+
# @param toggles [Array<Hash>] each with "function" and "active" keys
|
|
369
|
+
# @return [self]
|
|
370
|
+
def toggle_functions(toggles)
|
|
371
|
+
add_action("toggle_functions", toggles)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Enable function calls on speaker timeout.
|
|
375
|
+
# @param enabled [Boolean]
|
|
376
|
+
# @return [self]
|
|
377
|
+
def enable_functions_on_timeout(enabled = true)
|
|
378
|
+
add_action("functions_on_speaker_timeout", enabled)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Send full data to LLM for this turn only.
|
|
382
|
+
# @param enabled [Boolean]
|
|
383
|
+
# @return [self]
|
|
384
|
+
def enable_extensive_data(enabled = true)
|
|
385
|
+
add_action("extensive_data", enabled)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Update agent runtime settings (temperature, top_p, etc.).
|
|
389
|
+
# @param settings [Hash]
|
|
390
|
+
# @return [self]
|
|
391
|
+
def update_settings(settings)
|
|
392
|
+
add_action("settings", settings)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# ==================================================================
|
|
396
|
+
# Advanced Features
|
|
397
|
+
# ==================================================================
|
|
398
|
+
|
|
399
|
+
# Execute SWML content with optional transfer.
|
|
400
|
+
#
|
|
401
|
+
# @param swml_content [Hash, String] SWML data structure or JSON string
|
|
402
|
+
# @param transfer [Boolean] whether call should exit agent after execution
|
|
403
|
+
# @return [self]
|
|
404
|
+
def execute_swml(swml_content, transfer: false)
|
|
405
|
+
swml_data = case swml_content
|
|
406
|
+
when String
|
|
407
|
+
begin
|
|
408
|
+
JSON.parse(swml_content)
|
|
409
|
+
rescue JSON::ParserError
|
|
410
|
+
{ "raw_swml" => swml_content }
|
|
411
|
+
end
|
|
412
|
+
when Hash
|
|
413
|
+
swml_content.dup
|
|
414
|
+
else
|
|
415
|
+
if swml_content.respond_to?(:to_h)
|
|
416
|
+
swml_content.to_h
|
|
417
|
+
else
|
|
418
|
+
raise TypeError, "swml_content must be a String, Hash, or respond to #to_h"
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
swml_data["transfer"] = "true" if transfer
|
|
423
|
+
add_action("SWML", swml_data)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Join an ad-hoc audio conference via SWML.
|
|
427
|
+
#
|
|
428
|
+
# @param name [String] conference name (required)
|
|
429
|
+
# @return [self]
|
|
430
|
+
def join_conference(name, muted: false, beep: "true",
|
|
431
|
+
start_on_enter: true, end_on_exit: false,
|
|
432
|
+
wait_url: nil, max_participants: 250,
|
|
433
|
+
record: "do-not-record", region: nil,
|
|
434
|
+
trim: "trim-silence", coach: nil,
|
|
435
|
+
status_callback_event: nil, status_callback: nil,
|
|
436
|
+
status_callback_method: "POST",
|
|
437
|
+
recording_status_callback: nil,
|
|
438
|
+
recording_status_callback_method: "POST",
|
|
439
|
+
recording_status_callback_event: "completed",
|
|
440
|
+
result: nil)
|
|
441
|
+
raise ArgumentError, "name cannot be empty" if name.to_s.strip.empty?
|
|
442
|
+
raise ArgumentError, "beep must be one of: true, false, onEnter, onExit" unless %w[true false onEnter onExit].include?(beep)
|
|
443
|
+
raise ArgumentError, "max_participants must be 1..250" unless max_participants.between?(1, 250)
|
|
444
|
+
raise ArgumentError, "record must be 'do-not-record' or 'record-from-start'" unless %w[do-not-record record-from-start].include?(record)
|
|
445
|
+
raise ArgumentError, "trim must be 'trim-silence' or 'do-not-trim'" unless %w[trim-silence do-not-trim].include?(trim)
|
|
446
|
+
|
|
447
|
+
all_defaults = !muted && beep == "true" && start_on_enter && !end_on_exit &&
|
|
448
|
+
wait_url.nil? && max_participants == 250 && record == "do-not-record" &&
|
|
449
|
+
region.nil? && trim == "trim-silence" && coach.nil? &&
|
|
450
|
+
status_callback_event.nil? && status_callback.nil? &&
|
|
451
|
+
status_callback_method == "POST" && recording_status_callback.nil? &&
|
|
452
|
+
recording_status_callback_method == "POST" &&
|
|
453
|
+
recording_status_callback_event == "completed" && result.nil?
|
|
454
|
+
|
|
455
|
+
if all_defaults
|
|
456
|
+
join_params = name
|
|
457
|
+
else
|
|
458
|
+
join_params = { "name" => name }
|
|
459
|
+
join_params["muted"] = muted if muted
|
|
460
|
+
join_params["beep"] = beep if beep != "true"
|
|
461
|
+
join_params["start_on_enter"] = start_on_enter unless start_on_enter
|
|
462
|
+
join_params["end_on_exit"] = end_on_exit if end_on_exit
|
|
463
|
+
join_params["wait_url"] = wait_url if wait_url
|
|
464
|
+
join_params["max_participants"] = max_participants if max_participants != 250
|
|
465
|
+
join_params["record"] = record if record != "do-not-record"
|
|
466
|
+
join_params["region"] = region if region
|
|
467
|
+
join_params["trim"] = trim if trim != "trim-silence"
|
|
468
|
+
join_params["coach"] = coach if coach
|
|
469
|
+
join_params["status_callback_event"] = status_callback_event if status_callback_event
|
|
470
|
+
join_params["status_callback"] = status_callback if status_callback
|
|
471
|
+
join_params["status_callback_method"] = status_callback_method if status_callback_method != "POST"
|
|
472
|
+
join_params["recording_status_callback"] = recording_status_callback if recording_status_callback
|
|
473
|
+
join_params["recording_status_callback_method"] = recording_status_callback_method if recording_status_callback_method != "POST"
|
|
474
|
+
join_params["recording_status_callback_event"] = recording_status_callback_event if recording_status_callback_event != "completed"
|
|
475
|
+
join_params["result"] = result if result
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
swml_doc = {
|
|
479
|
+
"version" => "1.0.0",
|
|
480
|
+
"sections" => { "main" => [{ "join_conference" => join_params }] }
|
|
481
|
+
}
|
|
482
|
+
execute_swml(swml_doc)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Join a RELAY room via SWML.
|
|
486
|
+
# @param name [String]
|
|
487
|
+
# @return [self]
|
|
488
|
+
def join_room(name)
|
|
489
|
+
swml_doc = {
|
|
490
|
+
"version" => "1.0.0",
|
|
491
|
+
"sections" => { "main" => [{ "join_room" => { "name" => name } }] }
|
|
492
|
+
}
|
|
493
|
+
execute_swml(swml_doc)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Send SIP REFER via SWML.
|
|
497
|
+
# @param to_uri [String]
|
|
498
|
+
# @return [self]
|
|
499
|
+
def sip_refer(to_uri)
|
|
500
|
+
swml_doc = {
|
|
501
|
+
"version" => "1.0.0",
|
|
502
|
+
"sections" => { "main" => [{ "sip_refer" => { "to_uri" => to_uri } }] }
|
|
503
|
+
}
|
|
504
|
+
execute_swml(swml_doc)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Start a background call tap via SWML.
|
|
508
|
+
#
|
|
509
|
+
# @param uri [String] destination URI (rtp://, ws://, wss://)
|
|
510
|
+
# @param control_id [String, nil]
|
|
511
|
+
# @param direction [String] "speak", "hear", or "both"
|
|
512
|
+
# @param codec [String] "PCMU" or "PCMA"
|
|
513
|
+
# @param rtp_ptime [Integer] packetization time in ms
|
|
514
|
+
# @param status_url [String, nil]
|
|
515
|
+
# @return [self]
|
|
516
|
+
def tap(uri, control_id: nil, direction: "both", codec: "PCMU",
|
|
517
|
+
rtp_ptime: 20, status_url: nil)
|
|
518
|
+
raise ArgumentError, "direction must be 'speak', 'hear', or 'both'" unless %w[speak hear both].include?(direction)
|
|
519
|
+
raise ArgumentError, "codec must be 'PCMU' or 'PCMA'" unless %w[PCMU PCMA].include?(codec)
|
|
520
|
+
raise ArgumentError, "rtp_ptime must be positive" unless rtp_ptime.positive?
|
|
521
|
+
|
|
522
|
+
tap_params = { "uri" => uri }
|
|
523
|
+
tap_params["control_id"] = control_id if control_id
|
|
524
|
+
tap_params["direction"] = direction if direction != "both"
|
|
525
|
+
tap_params["codec"] = codec if codec != "PCMU"
|
|
526
|
+
tap_params["rtp_ptime"] = rtp_ptime if rtp_ptime != 20
|
|
527
|
+
tap_params["status_url"] = status_url if status_url
|
|
528
|
+
|
|
529
|
+
swml_doc = {
|
|
530
|
+
"version" => "1.0.0",
|
|
531
|
+
"sections" => { "main" => [{ "tap" => tap_params }] }
|
|
532
|
+
}
|
|
533
|
+
execute_swml(swml_doc)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Stop an active tap stream via SWML.
|
|
537
|
+
# @param control_id [String, nil]
|
|
538
|
+
# @return [self]
|
|
539
|
+
def stop_tap(control_id: nil)
|
|
540
|
+
stop_params = {}
|
|
541
|
+
stop_params["control_id"] = control_id if control_id
|
|
542
|
+
|
|
543
|
+
swml_doc = {
|
|
544
|
+
"version" => "1.0.0",
|
|
545
|
+
"sections" => { "main" => [{ "stop_tap" => stop_params }] }
|
|
546
|
+
}
|
|
547
|
+
execute_swml(swml_doc)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Send an SMS message via SWML.
|
|
551
|
+
#
|
|
552
|
+
# @param to_number [String] E.164 phone number
|
|
553
|
+
# @param from_number [String] E.164 phone number
|
|
554
|
+
# @param body [String, nil]
|
|
555
|
+
# @param media [Array<String>, nil]
|
|
556
|
+
# @param tags [Array<String>, nil]
|
|
557
|
+
# @param region [String, nil]
|
|
558
|
+
# @return [self]
|
|
559
|
+
def send_sms(to_number:, from_number:, body: nil, media: nil,
|
|
560
|
+
tags: nil, region: nil)
|
|
561
|
+
body_empty = body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
562
|
+
media_empty = media.nil? || (media.respond_to?(:empty?) && media.empty?)
|
|
563
|
+
raise ArgumentError, "Either body or media must be provided" if body_empty && media_empty
|
|
564
|
+
|
|
565
|
+
sms_params = {
|
|
566
|
+
"to_number" => to_number,
|
|
567
|
+
"from_number" => from_number
|
|
568
|
+
}
|
|
569
|
+
sms_params["body"] = body if body && !(body.respond_to?(:empty?) && body.empty?)
|
|
570
|
+
sms_params["media"] = media if media && !(media.respond_to?(:empty?) && media.empty?)
|
|
571
|
+
sms_params["tags"] = tags if tags && !(tags.respond_to?(:empty?) && tags.empty?)
|
|
572
|
+
sms_params["region"] = region if region
|
|
573
|
+
|
|
574
|
+
swml_doc = {
|
|
575
|
+
"version" => "1.0.0",
|
|
576
|
+
"sections" => { "main" => [{ "send_sms" => sms_params }] }
|
|
577
|
+
}
|
|
578
|
+
execute_swml(swml_doc)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Process payment using SWML pay action.
|
|
582
|
+
#
|
|
583
|
+
# @param payment_connector_url [String] URL to make payment requests to
|
|
584
|
+
# @param input_method [String] "dtmf" or "voice"
|
|
585
|
+
# @return [self]
|
|
586
|
+
def pay(payment_connector_url:, input_method: "dtmf",
|
|
587
|
+
status_url: nil, payment_method: "credit-card",
|
|
588
|
+
timeout: 5, max_attempts: 1, security_code: true,
|
|
589
|
+
postal_code: true, min_postal_code_length: 0,
|
|
590
|
+
token_type: "reusable", charge_amount: nil,
|
|
591
|
+
currency: "usd", language: "en-US", voice: "woman",
|
|
592
|
+
description: nil, valid_card_types: "visa mastercard amex",
|
|
593
|
+
parameters: nil, prompts: nil,
|
|
594
|
+
ai_response: 'The payment status is ${pay_result}, do not mention anything else about collecting payment if successful.')
|
|
595
|
+
pay_params = {
|
|
596
|
+
"payment_connector_url" => payment_connector_url,
|
|
597
|
+
"input" => input_method,
|
|
598
|
+
"payment_method" => payment_method,
|
|
599
|
+
"timeout" => timeout.to_s,
|
|
600
|
+
"max_attempts" => max_attempts.to_s,
|
|
601
|
+
"security_code" => security_code.to_s,
|
|
602
|
+
"min_postal_code_length" => min_postal_code_length.to_s,
|
|
603
|
+
"token_type" => token_type,
|
|
604
|
+
"currency" => currency,
|
|
605
|
+
"language" => language,
|
|
606
|
+
"voice" => voice,
|
|
607
|
+
"valid_card_types" => valid_card_types
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
pay_params["postal_code"] = postal_code.is_a?(String) ? postal_code : postal_code.to_s
|
|
611
|
+
pay_params["status_url"] = status_url if status_url
|
|
612
|
+
pay_params["charge_amount"] = charge_amount if charge_amount
|
|
613
|
+
pay_params["description"] = description if description
|
|
614
|
+
pay_params["parameters"] = parameters if parameters
|
|
615
|
+
pay_params["prompts"] = prompts if prompts
|
|
616
|
+
|
|
617
|
+
swml_doc = {
|
|
618
|
+
"version" => "1.0.0",
|
|
619
|
+
"sections" => {
|
|
620
|
+
"main" => [
|
|
621
|
+
{ "set" => { "ai_response" => ai_response } },
|
|
622
|
+
{ "pay" => pay_params }
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
execute_swml(swml_doc)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# ==================================================================
|
|
630
|
+
# RPC Actions
|
|
631
|
+
# ==================================================================
|
|
632
|
+
|
|
633
|
+
# Execute a generic RPC method via SWML.
|
|
634
|
+
#
|
|
635
|
+
# @param method [String] RPC method name
|
|
636
|
+
# @param params [Hash, nil]
|
|
637
|
+
# @param call_id [String, nil]
|
|
638
|
+
# @param node_id [String, nil]
|
|
639
|
+
# @return [self]
|
|
640
|
+
def execute_rpc(method, params: nil, call_id: nil, node_id: nil)
|
|
641
|
+
rpc_params = { "method" => method }
|
|
642
|
+
rpc_params["call_id"] = call_id if call_id
|
|
643
|
+
rpc_params["node_id"] = node_id if node_id
|
|
644
|
+
rpc_params["params"] = params if params && !params.empty?
|
|
645
|
+
|
|
646
|
+
swml_doc = {
|
|
647
|
+
"version" => "1.0.0",
|
|
648
|
+
"sections" => { "main" => [{ "execute_rpc" => rpc_params }] }
|
|
649
|
+
}
|
|
650
|
+
execute_swml(swml_doc)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Dial out to a number via RPC.
|
|
654
|
+
#
|
|
655
|
+
# @param to_number [String] E.164 phone number
|
|
656
|
+
# @param from_number [String] E.164 caller ID
|
|
657
|
+
# @param dest_swml [String] SWML URL for the outbound leg
|
|
658
|
+
# @param device_type [String]
|
|
659
|
+
# @return [self]
|
|
660
|
+
def rpc_dial(to_number:, from_number:, dest_swml:, device_type: "phone")
|
|
661
|
+
execute_rpc(
|
|
662
|
+
"dial",
|
|
663
|
+
params: {
|
|
664
|
+
"devices" => {
|
|
665
|
+
"type" => device_type,
|
|
666
|
+
"params" => {
|
|
667
|
+
"to_number" => to_number,
|
|
668
|
+
"from_number" => from_number
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
"dest_swml" => dest_swml
|
|
672
|
+
}
|
|
673
|
+
)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Inject a message into an AI agent on another call.
|
|
677
|
+
#
|
|
678
|
+
# @param call_id [String]
|
|
679
|
+
# @param message_text [String]
|
|
680
|
+
# @param role [String]
|
|
681
|
+
# @return [self]
|
|
682
|
+
def rpc_ai_message(call_id, message_text, role: "system")
|
|
683
|
+
execute_rpc(
|
|
684
|
+
"ai_message",
|
|
685
|
+
call_id: call_id,
|
|
686
|
+
params: {
|
|
687
|
+
"role" => role,
|
|
688
|
+
"message_text" => message_text
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Unhold another call via RPC.
|
|
694
|
+
# @param call_id [String]
|
|
695
|
+
# @return [self]
|
|
696
|
+
def rpc_ai_unhold(call_id)
|
|
697
|
+
execute_rpc("ai_unhold", call_id: call_id, params: {})
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Queue simulated user input.
|
|
701
|
+
# @param text [String]
|
|
702
|
+
# @return [self]
|
|
703
|
+
def simulate_user_input(text)
|
|
704
|
+
add_action("user_input", text)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# ==================================================================
|
|
708
|
+
# Payment helpers (class methods)
|
|
709
|
+
# ==================================================================
|
|
710
|
+
|
|
711
|
+
# Create a payment prompt structure for use with +pay+.
|
|
712
|
+
#
|
|
713
|
+
# @param for_situation [String] e.g. "payment-card-number"
|
|
714
|
+
# @param actions [Array<Hash>] actions with "type" and "phrase" keys
|
|
715
|
+
# @param card_type [String, nil]
|
|
716
|
+
# @param error_type [String, nil]
|
|
717
|
+
# @return [Hash]
|
|
718
|
+
def self.create_payment_prompt(for_situation, actions, card_type: nil, error_type: nil)
|
|
719
|
+
prompt = {
|
|
720
|
+
"for" => for_situation,
|
|
721
|
+
"actions" => actions
|
|
722
|
+
}
|
|
723
|
+
prompt["card_type"] = card_type if card_type
|
|
724
|
+
prompt["error_type"] = error_type if error_type
|
|
725
|
+
prompt
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Create a payment action for use inside payment prompts.
|
|
729
|
+
#
|
|
730
|
+
# @param action_type [String] "Say" or "Play"
|
|
731
|
+
# @param phrase [String]
|
|
732
|
+
# @return [Hash]
|
|
733
|
+
def self.create_payment_action(action_type, phrase)
|
|
734
|
+
{ "type" => action_type, "phrase" => phrase }
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Create a payment parameter for use with +pay+.
|
|
738
|
+
#
|
|
739
|
+
# @param name [String]
|
|
740
|
+
# @param value [String]
|
|
741
|
+
# @return [Hash]
|
|
742
|
+
def self.create_payment_parameter(name, value)
|
|
743
|
+
{ "name" => name, "value" => value }
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
# ==================================================================
|
|
747
|
+
# Serialization
|
|
748
|
+
# ==================================================================
|
|
749
|
+
|
|
750
|
+
# Convert to the Hash structure expected by SWAIG.
|
|
751
|
+
#
|
|
752
|
+
# Rules:
|
|
753
|
+
# - +response+ always included (string)
|
|
754
|
+
# - +action+ only included if at least one action exists
|
|
755
|
+
# - +post_process+ only included if +true+ and actions exist
|
|
756
|
+
#
|
|
757
|
+
# @return [Hash]
|
|
758
|
+
def to_h
|
|
759
|
+
result = {}
|
|
760
|
+
|
|
761
|
+
result["response"] = @response if @response && !@response.empty?
|
|
762
|
+
result["action"] = @action if @action && !@action.empty?
|
|
763
|
+
result["post_process"] = true if @post_process && @action && !@action.empty?
|
|
764
|
+
|
|
765
|
+
# Ensure at least one of response or action is present
|
|
766
|
+
result["response"] = "Action completed." if result.empty?
|
|
767
|
+
|
|
768
|
+
result
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# @return [String] JSON representation
|
|
772
|
+
def to_json(*args)
|
|
773
|
+
to_h.to_json(*args)
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|