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,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
|