signalwire_agents 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/README.md +79 -0
- data/bin/swaig-test +310 -0
- data/lib/signalwire_agents/agent/agent_base.rb +1171 -0
- data/lib/signalwire_agents/contexts/context_builder.rb +622 -0
- data/lib/signalwire_agents/datamap/data_map.rb +279 -0
- data/lib/signalwire_agents/logging.rb +92 -0
- data/lib/signalwire_agents/prefabs/concierge.rb +92 -0
- data/lib/signalwire_agents/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire_agents/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire_agents/prefabs/receptionist.rb +74 -0
- data/lib/signalwire_agents/prefabs/survey.rb +75 -0
- data/lib/signalwire_agents/relay/action.rb +291 -0
- data/lib/signalwire_agents/relay/call.rb +523 -0
- data/lib/signalwire_agents/relay/client.rb +671 -0
- data/lib/signalwire_agents/relay/constants.rb +124 -0
- data/lib/signalwire_agents/relay/message.rb +137 -0
- data/lib/signalwire_agents/relay/relay_event.rb +670 -0
- data/lib/signalwire_agents/rest/http_client.rb +147 -0
- data/lib/signalwire_agents/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire_agents/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire_agents/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire_agents/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire_agents/rest/namespaces/fabric.rb +175 -0
- data/lib/signalwire_agents/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire_agents/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire_agents/rest/namespaces/phone_numbers.rb +20 -0
- data/lib/signalwire_agents/rest/namespaces/project.rb +33 -0
- data/lib/signalwire_agents/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire_agents/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire_agents/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire_agents/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire_agents/rest/namespaces/video.rb +129 -0
- data/lib/signalwire_agents/rest/signalwire_client.rb +110 -0
- data/lib/signalwire_agents/security/session_manager.rb +124 -0
- data/lib/signalwire_agents/server/agent_server.rb +260 -0
- data/lib/signalwire_agents/skills/builtin/api_ninjas_trivia.rb +91 -0
- data/lib/signalwire_agents/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire_agents/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire_agents/skills/builtin/datasphere.rb +141 -0
- data/lib/signalwire_agents/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire_agents/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire_agents/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire_agents/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire_agents/skills/builtin/joke.rb +65 -0
- data/lib/signalwire_agents/skills/builtin/math.rb +176 -0
- data/lib/signalwire_agents/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire_agents/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire_agents/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire_agents/skills/builtin/spider.rb +142 -0
- data/lib/signalwire_agents/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire_agents/skills/builtin/weather_api.rb +85 -0
- data/lib/signalwire_agents/skills/builtin/web_search.rb +123 -0
- data/lib/signalwire_agents/skills/builtin/wikipedia_search.rb +109 -0
- data/lib/signalwire_agents/skills/skill_base.rb +58 -0
- data/lib/signalwire_agents/skills/skill_manager.rb +85 -0
- data/lib/signalwire_agents/skills/skill_registry.rb +76 -0
- data/lib/signalwire_agents/swaig/function_result.rb +777 -0
- data/lib/signalwire_agents/swml/document.rb +84 -0
- data/lib/signalwire_agents/swml/schema.json +12250 -0
- data/lib/signalwire_agents/swml/schema.rb +81 -0
- data/lib/signalwire_agents/swml/service.rb +304 -0
- data/lib/signalwire_agents/version.rb +5 -0
- data/lib/signalwire_agents.rb +19 -0
- metadata +212 -0
|
@@ -0,0 +1,279 @@
|
|
|
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 SignalWireAgents
|
|
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 function description / purpose.
|
|
40
|
+
def purpose(desc)
|
|
41
|
+
@purpose_text = desc
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Alias for +purpose+.
|
|
46
|
+
def description(desc)
|
|
47
|
+
purpose(desc)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add a typed parameter to the function signature.
|
|
51
|
+
#
|
|
52
|
+
# @param name [String]
|
|
53
|
+
# @param type [String] JSON-Schema type (string, number, boolean, array, object)
|
|
54
|
+
# @param desc [String] Human-readable description
|
|
55
|
+
# @param required [Boolean] whether the parameter is required
|
|
56
|
+
# @param enum [Array<String>, nil] optional list of allowed values
|
|
57
|
+
def parameter(name, type, desc, required: false, enum: nil)
|
|
58
|
+
param_def = { "type" => type, "description" => desc }
|
|
59
|
+
param_def["enum"] = enum if enum && !enum.empty?
|
|
60
|
+
@parameters[name] = param_def
|
|
61
|
+
@required_params << name if required && !@required_params.include?(name)
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Add an expression (pattern-matching rule).
|
|
66
|
+
#
|
|
67
|
+
# @param test_value [String] template string to test, e.g. "${args.command}"
|
|
68
|
+
# @param pattern [String, Regexp] regex pattern to match against
|
|
69
|
+
# @param output [Swaig::FunctionResult, Hash] result when pattern matches
|
|
70
|
+
# @param nomatch_output [Swaig::FunctionResult, Hash, nil] result when pattern does not match
|
|
71
|
+
def expression(test_value, pattern, output, nomatch_output: nil)
|
|
72
|
+
pattern_str = pattern.is_a?(Regexp) ? pattern.source : pattern.to_s
|
|
73
|
+
output_h = output.respond_to?(:to_h) ? output.to_h : output
|
|
74
|
+
|
|
75
|
+
expr_def = {
|
|
76
|
+
"string" => test_value,
|
|
77
|
+
"pattern" => pattern_str,
|
|
78
|
+
"output" => output_h
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if nomatch_output
|
|
82
|
+
nomatch_h = nomatch_output.respond_to?(:to_h) ? nomatch_output.to_h : nomatch_output
|
|
83
|
+
expr_def["nomatch-output"] = nomatch_h
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@expressions << expr_def
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Add a webhook (HTTP call) to the data_map pipeline.
|
|
91
|
+
#
|
|
92
|
+
# @param method [String] HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
93
|
+
# @param url [String] endpoint URL (may contain ${variable} substitutions)
|
|
94
|
+
# @param headers [Hash, nil] optional HTTP headers
|
|
95
|
+
# @param form_param [String, nil] send JSON body as a single form parameter
|
|
96
|
+
# @param input_args_as_params [Boolean] merge function arguments into params
|
|
97
|
+
# @param require_args [Array<String>, nil] only execute when these args are present
|
|
98
|
+
def webhook(method, url, headers: nil, form_param: nil, input_args_as_params: false, require_args: nil)
|
|
99
|
+
wh = {
|
|
100
|
+
"url" => url,
|
|
101
|
+
"method" => method.upcase
|
|
102
|
+
}
|
|
103
|
+
wh["headers"] = headers if headers
|
|
104
|
+
wh["form_param"] = form_param if form_param
|
|
105
|
+
wh["input_args_as_params"] = true if input_args_as_params
|
|
106
|
+
wh["require_args"] = require_args if require_args
|
|
107
|
+
@webhooks << wh
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Add expressions to run after the most-recently-added webhook completes.
|
|
112
|
+
def webhook_expressions(expressions)
|
|
113
|
+
raise ArgumentError, "Must add webhook before setting webhook expressions" if @webhooks.empty?
|
|
114
|
+
|
|
115
|
+
@webhooks.last["expressions"] = expressions
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Set the request body for the most-recently-added webhook (POST / PUT).
|
|
120
|
+
def body(data)
|
|
121
|
+
raise ArgumentError, "Must add webhook before setting body" if @webhooks.empty?
|
|
122
|
+
|
|
123
|
+
@webhooks.last["body"] = data
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Set request params for the most-recently-added webhook.
|
|
128
|
+
def params(data)
|
|
129
|
+
raise ArgumentError, "Must add webhook before setting params" if @webhooks.empty?
|
|
130
|
+
|
|
131
|
+
@webhooks.last["params"] = data
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Configure array processing on the most-recently-added webhook response.
|
|
136
|
+
#
|
|
137
|
+
# @param config [Hash] must include keys: input_key, output_key, append. Optional: max.
|
|
138
|
+
def foreach(config)
|
|
139
|
+
raise ArgumentError, "Must add webhook before setting foreach" if @webhooks.empty?
|
|
140
|
+
raise ArgumentError, "foreach config must be a Hash" unless config.is_a?(Hash)
|
|
141
|
+
|
|
142
|
+
required_keys = %w[input_key output_key append]
|
|
143
|
+
missing = required_keys - config.keys.map(&:to_s)
|
|
144
|
+
raise ArgumentError, "foreach config missing required keys: #{missing.inspect}" unless missing.empty?
|
|
145
|
+
|
|
146
|
+
@webhooks.last["foreach"] = config
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Set the output result for the most-recently-added webhook.
|
|
151
|
+
#
|
|
152
|
+
# @param result [Swaig::FunctionResult, Hash]
|
|
153
|
+
def output(result)
|
|
154
|
+
raise ArgumentError, "Must add webhook before setting output" if @webhooks.empty?
|
|
155
|
+
|
|
156
|
+
@webhooks.last["output"] = result.respond_to?(:to_h) ? result.to_h : result
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Set a fallback output used when all webhooks fail.
|
|
161
|
+
#
|
|
162
|
+
# @param result [Swaig::FunctionResult, Hash]
|
|
163
|
+
def fallback_output(result)
|
|
164
|
+
@fallback_output = result.respond_to?(:to_h) ? result.to_h : result
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Set error keys on the most-recently-added webhook, or at the top level
|
|
169
|
+
# if no webhook has been added yet.
|
|
170
|
+
def error_keys(keys)
|
|
171
|
+
if @webhooks.any?
|
|
172
|
+
@webhooks.last["error_keys"] = keys
|
|
173
|
+
else
|
|
174
|
+
@global_error_keys = keys
|
|
175
|
+
end
|
|
176
|
+
self
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Set top-level error keys (applies to all webhooks).
|
|
180
|
+
def global_error_keys(keys)
|
|
181
|
+
@global_error_keys = keys
|
|
182
|
+
self
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Serialize this DataMap into a complete SWAIG function definition Hash.
|
|
186
|
+
#
|
|
187
|
+
# @return [Hash] with keys: "function", "description", "parameters", "data_map"
|
|
188
|
+
def to_swaig_function
|
|
189
|
+
# Build parameter schema
|
|
190
|
+
if @parameters.any?
|
|
191
|
+
param_schema = {
|
|
192
|
+
"type" => "object",
|
|
193
|
+
"properties" => @parameters.dup
|
|
194
|
+
}
|
|
195
|
+
param_schema["required"] = @required_params.dup if @required_params.any?
|
|
196
|
+
else
|
|
197
|
+
param_schema = { "type" => "object", "properties" => {} }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Build data_map
|
|
201
|
+
data_map = {}
|
|
202
|
+
data_map["expressions"] = @expressions if @expressions.any?
|
|
203
|
+
data_map["webhooks"] = @webhooks if @webhooks.any?
|
|
204
|
+
data_map["output"] = @fallback_output if @fallback_output
|
|
205
|
+
data_map["error_keys"] = @global_error_keys if @global_error_keys.any?
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
"function" => @function_name,
|
|
209
|
+
"description" => @purpose_text.empty? ? "Execute #{@function_name}" : @purpose_text,
|
|
210
|
+
"parameters" => param_schema,
|
|
211
|
+
"data_map" => data_map
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# ----------------------------------------------------------------
|
|
216
|
+
# Class-level convenience constructors
|
|
217
|
+
# ----------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
# Build a simple API-calling tool in one shot.
|
|
220
|
+
#
|
|
221
|
+
# @param name [String]
|
|
222
|
+
# @param url [String]
|
|
223
|
+
# @param response_template [String]
|
|
224
|
+
# @param parameters [Hash, nil] name => { "type" => ..., "description" => ..., "required" => bool }
|
|
225
|
+
# @param method [String] HTTP method (default GET)
|
|
226
|
+
# @param headers [Hash, nil]
|
|
227
|
+
# @param body [Hash, nil]
|
|
228
|
+
# @param error_keys [Array<String>, nil]
|
|
229
|
+
# @return [DataMap]
|
|
230
|
+
def self.create_simple_api_tool(name:, url:, response_template:, parameters: nil,
|
|
231
|
+
method: 'GET', headers: nil, body: nil, error_keys: nil)
|
|
232
|
+
dm = new(name)
|
|
233
|
+
|
|
234
|
+
if parameters
|
|
235
|
+
parameters.each do |pname, pdef|
|
|
236
|
+
dm.parameter(
|
|
237
|
+
pname,
|
|
238
|
+
pdef.fetch("type", "string"),
|
|
239
|
+
pdef.fetch("description", "#{pname} parameter"),
|
|
240
|
+
required: pdef.fetch("required", false)
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
dm.webhook(method, url, headers: headers)
|
|
246
|
+
dm.body(body) if body
|
|
247
|
+
dm.error_keys(error_keys) if error_keys
|
|
248
|
+
dm.output(Swaig::FunctionResult.new(response_template))
|
|
249
|
+
dm
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Build an expression-only tool (no HTTP calls).
|
|
253
|
+
#
|
|
254
|
+
# @param name [String]
|
|
255
|
+
# @param patterns [Hash] test_value => [pattern, Swaig::FunctionResult]
|
|
256
|
+
# @param parameters [Hash, nil] same format as +create_simple_api_tool+
|
|
257
|
+
# @return [DataMap]
|
|
258
|
+
def self.create_expression_tool(name:, patterns:, parameters: nil)
|
|
259
|
+
dm = new(name)
|
|
260
|
+
|
|
261
|
+
if parameters
|
|
262
|
+
parameters.each do |pname, pdef|
|
|
263
|
+
dm.parameter(
|
|
264
|
+
pname,
|
|
265
|
+
pdef.fetch("type", "string"),
|
|
266
|
+
pdef.fetch("description", "#{pname} parameter"),
|
|
267
|
+
required: pdef.fetch("required", false)
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
patterns.each do |test_value, (pattern, result)|
|
|
273
|
+
dm.expression(test_value, pattern, result)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
dm
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWireAgents
|
|
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
|
|
@@ -0,0 +1,92 @@
|
|
|
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 SignalWireAgents
|
|
11
|
+
module Prefabs
|
|
12
|
+
# Prefab agent for providing virtual concierge services.
|
|
13
|
+
#
|
|
14
|
+
# agent = Concierge.new(
|
|
15
|
+
# venue_name: 'Grand Hotel',
|
|
16
|
+
# services: ['room service', 'spa bookings'],
|
|
17
|
+
# amenities: { 'pool' => { 'hours' => '7 AM - 10 PM', 'location' => '2nd Floor' } }
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
class Concierge
|
|
21
|
+
attr_reader :venue_name, :services, :amenities, :name, :route
|
|
22
|
+
|
|
23
|
+
def initialize(venue_name:, services:, amenities:, hours_of_operation: nil,
|
|
24
|
+
special_instructions: nil, welcome_message: nil,
|
|
25
|
+
name: 'concierge', route: '/concierge', **_opts)
|
|
26
|
+
@venue_name = venue_name
|
|
27
|
+
@services = services || []
|
|
28
|
+
@amenities = (amenities || {}).transform_keys(&:to_s)
|
|
29
|
+
@hours = hours_of_operation
|
|
30
|
+
@instructions = special_instructions || []
|
|
31
|
+
@welcome = welcome_message || "Welcome to #{venue_name}! How can I assist you today?"
|
|
32
|
+
@name = name
|
|
33
|
+
@route = route
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def tools
|
|
37
|
+
%w[get_amenity_info get_service_info]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def prompt_sections
|
|
41
|
+
amenity_bullets = @amenities.map { |k, v| "#{k}: #{v.is_a?(Hash) ? v.map { |a, b| "#{a}: #{b}" }.join(', ') : v}" }
|
|
42
|
+
service_bullets = @services.map { |s| s.to_s }
|
|
43
|
+
|
|
44
|
+
sections = [
|
|
45
|
+
{
|
|
46
|
+
'title' => "#{@venue_name} Concierge",
|
|
47
|
+
'body' => @welcome,
|
|
48
|
+
'bullets' => service_bullets + amenity_bullets
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if @hours
|
|
53
|
+
sections << {
|
|
54
|
+
'title' => 'Hours of Operation',
|
|
55
|
+
'body' => @hours.is_a?(Hash) ? @hours.map { |k, v| "#{k}: #{v}" }.join('; ') : @hours.to_s
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sections
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def global_data
|
|
63
|
+
{
|
|
64
|
+
'venue_name' => @venue_name,
|
|
65
|
+
'services' => @services,
|
|
66
|
+
'amenities' => @amenities
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_amenity_info(args, _raw_data)
|
|
71
|
+
amenity = (args['amenity'] || '').downcase
|
|
72
|
+
info = @amenities.find { |k, _v| k.downcase == amenity }&.last
|
|
73
|
+
if info
|
|
74
|
+
detail = info.is_a?(Hash) ? info.map { |k, v| "#{k}: #{v}" }.join(', ') : info.to_s
|
|
75
|
+
Swaig::FunctionResult.new("#{amenity.capitalize}: #{detail}")
|
|
76
|
+
else
|
|
77
|
+
Swaig::FunctionResult.new("I don't have information about '#{amenity}'. Available amenities: #{@amenities.keys.join(', ')}")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def handle_service_info(args, _raw_data)
|
|
82
|
+
service = (args['service'] || '').downcase
|
|
83
|
+
match = @services.find { |s| s.downcase.include?(service) }
|
|
84
|
+
if match
|
|
85
|
+
Swaig::FunctionResult.new("#{match} is available at #{@venue_name}.")
|
|
86
|
+
else
|
|
87
|
+
Swaig::FunctionResult.new("Available services: #{@services.join(', ')}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
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 SignalWireAgents
|
|
11
|
+
module Prefabs
|
|
12
|
+
# Prefab agent for answering frequently asked questions.
|
|
13
|
+
#
|
|
14
|
+
# agent = FaqBot.new(
|
|
15
|
+
# faqs: [
|
|
16
|
+
# { 'question' => 'What is SignalWire?', 'answer' => 'A cloud communications platform.' }
|
|
17
|
+
# ]
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
class FaqBot
|
|
21
|
+
attr_reader :faqs, :name, :route
|
|
22
|
+
|
|
23
|
+
def initialize(faqs:, suggest_related: true, persona: nil,
|
|
24
|
+
name: 'faq_bot', route: '/faq', **_opts)
|
|
25
|
+
raise ArgumentError, 'faqs must be a non-empty Array' unless faqs.is_a?(Array) && !faqs.empty?
|
|
26
|
+
|
|
27
|
+
@faqs = faqs.map { |f| f.transform_keys(&:to_s) }
|
|
28
|
+
@suggest_related = suggest_related
|
|
29
|
+
@persona = persona || 'You are a helpful FAQ bot that provides accurate answers to common questions.'
|
|
30
|
+
@name = name
|
|
31
|
+
@route = route
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tools
|
|
35
|
+
%w[search_faq]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def prompt_sections
|
|
39
|
+
bullets = @faqs.map { |f| "Q: #{f['question']}" }
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
'title' => 'FAQ Bot',
|
|
43
|
+
'body' => @persona,
|
|
44
|
+
'bullets' => bullets
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def global_data
|
|
50
|
+
{
|
|
51
|
+
'faqs' => @faqs,
|
|
52
|
+
'suggest_related' => @suggest_related
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_search(args, _raw_data)
|
|
57
|
+
query = (args['query'] || '').downcase
|
|
58
|
+
match = @faqs.find { |f| f['question'].downcase.include?(query) || query.include?(f['question'].downcase) }
|
|
59
|
+
if match
|
|
60
|
+
Swaig::FunctionResult.new(match['answer'])
|
|
61
|
+
else
|
|
62
|
+
Swaig::FunctionResult.new("I don't have a specific answer for that. Here are the topics I can help with: #{@faqs.map { |f| f['question'] }.join('; ')}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
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 SignalWireAgents
|
|
11
|
+
module Prefabs
|
|
12
|
+
# Prefab agent for collecting answers to a series of questions.
|
|
13
|
+
#
|
|
14
|
+
# agent = InfoGatherer.new(
|
|
15
|
+
# questions: [
|
|
16
|
+
# { 'key_name' => 'full_name', 'question_text' => 'What is your full name?' },
|
|
17
|
+
# { 'key_name' => 'email', 'question_text' => 'Email?', 'confirm' => true }
|
|
18
|
+
# ]
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
class InfoGatherer
|
|
22
|
+
attr_reader :questions, :name, :route
|
|
23
|
+
|
|
24
|
+
def initialize(questions:, name: 'info_gatherer', route: '/info_gatherer', **_opts)
|
|
25
|
+
raise ArgumentError, 'questions must be a non-empty Array' unless questions.is_a?(Array) && !questions.empty?
|
|
26
|
+
questions.each_with_index do |q, i|
|
|
27
|
+
raise ArgumentError, "Question #{i} missing key_name" unless q['key_name'] || q[:key_name]
|
|
28
|
+
raise ArgumentError, "Question #{i} missing question_text" unless q['question_text'] || q[:question_text]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@questions = questions.map { |q| q.transform_keys(&:to_s) }
|
|
32
|
+
@name = name
|
|
33
|
+
@route = route
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tool definitions this prefab provides.
|
|
37
|
+
def tools
|
|
38
|
+
%w[start_questions submit_answer]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build the prompt sections.
|
|
42
|
+
def prompt_sections
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
'title' => 'Info Gatherer',
|
|
46
|
+
'body' => 'You need to gather answers to a series of questions. ' \
|
|
47
|
+
'Call start_questions to get the first question, then submit_answer after each response.'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Global data for initial state.
|
|
53
|
+
def global_data
|
|
54
|
+
{
|
|
55
|
+
'info_gatherer' => {
|
|
56
|
+
'questions' => @questions,
|
|
57
|
+
'question_index' => 0,
|
|
58
|
+
'answers' => []
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Tool handler: start_questions
|
|
64
|
+
def handle_start(_args, _raw_data)
|
|
65
|
+
q = @questions.first
|
|
66
|
+
Swaig::FunctionResult.new(
|
|
67
|
+
"[Question 1 of #{@questions.size}]: \"#{q['question_text']}\""
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Tool handler: submit_answer
|
|
72
|
+
def handle_submit(args, _raw_data)
|
|
73
|
+
answer = args['answer'] || ''
|
|
74
|
+
# In a real implementation, state would be tracked via global_data.
|
|
75
|
+
Swaig::FunctionResult.new("Answer recorded: #{answer}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
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 SignalWireAgents
|
|
11
|
+
module Prefabs
|
|
12
|
+
# Prefab agent for greeting callers and transferring them to departments.
|
|
13
|
+
#
|
|
14
|
+
# agent = Receptionist.new(
|
|
15
|
+
# departments: [
|
|
16
|
+
# { 'name' => 'sales', 'description' => 'Product inquiries', 'number' => '+15551235555' },
|
|
17
|
+
# { 'name' => 'support', 'description' => 'Technical help', 'number' => '+15551236666' }
|
|
18
|
+
# ]
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
class Receptionist
|
|
22
|
+
attr_reader :departments, :name, :route, :greeting
|
|
23
|
+
|
|
24
|
+
def initialize(departments:, name: 'receptionist', route: '/receptionist',
|
|
25
|
+
greeting: 'Thank you for calling. How can I help you today?', **_opts)
|
|
26
|
+
raise ArgumentError, 'departments must be a non-empty Array' unless departments.is_a?(Array) && !departments.empty?
|
|
27
|
+
departments.each_with_index do |d, i|
|
|
28
|
+
d = d.transform_keys(&:to_s)
|
|
29
|
+
raise ArgumentError, "Department #{i} missing 'name'" unless d['name']
|
|
30
|
+
raise ArgumentError, "Department #{i} missing 'number'" unless d['number']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@departments = departments.map { |d| d.transform_keys(&:to_s) }
|
|
34
|
+
@greeting = greeting
|
|
35
|
+
@name = name
|
|
36
|
+
@route = route
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tools
|
|
40
|
+
%w[transfer_to_department collect_caller_info]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prompt_sections
|
|
44
|
+
bullets = @departments.map { |d| "#{d['name']}: #{d['description'] || d['name']} (#{d['number']})" }
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
'title' => 'Receptionist',
|
|
48
|
+
'body' => @greeting,
|
|
49
|
+
'bullets' => bullets
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def global_data
|
|
55
|
+
{
|
|
56
|
+
'departments' => @departments,
|
|
57
|
+
'caller_info' => {}
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_transfer(args, _raw_data)
|
|
62
|
+
dept_name = args['department']
|
|
63
|
+
dept = @departments.find { |d| d['name'] == dept_name }
|
|
64
|
+
if dept
|
|
65
|
+
result = Swaig::FunctionResult.new("Transferring you to #{dept_name} now.")
|
|
66
|
+
result.connect(dept['number'])
|
|
67
|
+
result
|
|
68
|
+
else
|
|
69
|
+
Swaig::FunctionResult.new("I couldn't find that department. Available departments: #{@departments.map { |d| d['name'] }.join(', ')}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|