Class: SignalWire::AgentBase

Inherits:
SWML::Service show all
Defined in:
lib/signalwire/agent/agent_base.rb

Overview

Central agent class that composes SWML rendering, tool dispatch, prompt management, AI config, and HTTP serving.

AgentBase extends SWMLService with agent-specific capabilities:

- Prompt management (POM sections and raw text)
- Tool (SWAIG function) registration & dispatch
- AI configuration (hints, languages, pronunciations, params)
- Verb management (pre/post answer, post-AI)
- Context & step workflows
- Skill integration
- Dynamic configuration via per-request ephemeral copies

All configuration methods return self for method chaining.

Defined Under Namespace

Classes: AgentBodyLimitMiddleware, AgentSecurityHeadersMiddleware, AgentTimingSafeBasicAuth

Constant Summary collapse

MAX_BODY_SIZE =

Maximum request body size (1 MB)

1_048_576
SUPPORTED_INTERNAL_FILLER_NAMES =

The complete set of internal SWAIG function names that accept fillers, matching the SWAIGInternalFiller schema definition.

Any name outside this set is silently ignored by the runtime —set_internal_fillers and add_internal_filler warn if you pass an unknown name.

Notable absences: change_step, gather_submit, or arbitrary user-defined SWAIG function names are NOT supported.

%w[
  hangup
  check_time
  wait_for_user
  wait_seconds
  adjust_response_latency
  next_step
  change_context
  get_visual_input
  get_ideal_strategy
].freeze

Constants inherited from SWML::Service

SWML::Service::SWAIG_FN_NAME

Instance Attribute Summary collapse

Attributes inherited from SWML::Service

#config_file, #host, #name, #port, #route, #schema_path, #schema_validation

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from SWML::Service

#document, #execute_verb, #get_all_functions, #get_basic_auth_credentials_with_source, #get_full_url, #get_function, #has_function, #method_missing, #on_request, #on_swml_request, #register_routing_callback, #remove_function, #render, #render_main_swml, #render_pretty, #respond_to_missing?, #schema_utils, #stop, #swaig_pre_dispatch, #validate_basic_auth

Constructor Details

#initialize(name: 'agent', route: '/', host: '0.0.0.0', port: nil, basic_auth: nil, use_pom: true, token_expiry_secs: 3600, auto_answer: true, record_call: false, record_format: 'mp4', record_stereo: true, default_webhook_url: nil, agent_id: nil, native_functions: nil, schema_path: nil, suppress_logs: false, enable_post_prompt_override: false, check_for_input_override: false, config_file: nil, schema_validation: true, signing_key: nil, trust_proxy_for_signature: false) ⇒ AgentBase


Construction




59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/signalwire/agent/agent_base.rb', line 59

def initialize(name: 'agent', route: '/', host: '0.0.0.0', port: nil,
               basic_auth: nil,
               use_pom: true,
               token_expiry_secs: 3600,
               auto_answer: true, record_call: false,
               record_format: 'mp4', record_stereo: true,
               default_webhook_url: nil,
               agent_id: nil,
               native_functions: nil,
               schema_path: nil,
               suppress_logs: false,
               enable_post_prompt_override: false,
               check_for_input_override: false,
               config_file: nil,
               schema_validation: true,
               signing_key: nil,
               trust_proxy_for_signature: false)
  # Resolve auth before super so we can warn about auto-generated
  # passwords. Service's built-in auth fallback uses a fresh UUID per
  # process, which is fine, but we want the agent-specific warning.
  password_auto_generated = false
  resolved_auth = if basic_auth
                    basic_auth
                  elsif ENV['SWML_BASIC_AUTH_USER'] && ENV['SWML_BASIC_AUTH_PASSWORD']
                    [ENV['SWML_BASIC_AUTH_USER'], ENV['SWML_BASIC_AUTH_PASSWORD']]
                  else
                    password_auto_generated = true
                    [(ENV['SWML_BASIC_AUTH_USER'] || SecureRandom.uuid), SecureRandom.uuid]
                  end

  super(name: name, route: route, host: host, port: port, basic_auth: resolved_auth,
        schema_path: schema_path, config_file: config_file,
        schema_validation: schema_validation)
  @logger = Logging.logger("AgentBase[#{name}]")
  @suppress_logs = suppress_logs

  if password_auto_generated
    # Warn loudly so external callers (tests, RPC clients, MCP)
    # know why they are getting HTTP 401. This is the silent cause
    # of every external caller failing when .env wasn't loaded —
    # the password lives only in this process and changes on every
    # restart.
    @logger.warn(
      "basic_auth_password_autogenerated: username=#{@basic_auth[0].inspect}. " \
      "No SWML_BASIC_AUTH_PASSWORD found in environment and no basic_auth " \
      "passed to the agent constructor. The SDK generated a random " \
      "password that exists only in this process; external callers will " \
      "get HTTP 401 unless they read the value from this process's env. " \
      "To fix, set SWML_BASIC_AUTH_USER and SWML_BASIC_AUTH_PASSWORD in " \
      "your environment, or pass basic_auth: [user, pass] to " \
      "AgentBase.new."
    )
  end

  # --- call settings ------------------------------------------------
  @auto_answer   = auto_answer
  @record_call   = record_call
  @record_format = record_format
  @record_stereo = record_stereo

  # --- POM / prompt-mode flags --------------------------------------
  # Python parity: AgentBase constructor stores ``use_pom`` to
  # toggle POM-vs-raw prompt rendering. Ruby's POM is implicit (we
  # always have a @pom_sections array), but we honour the flag so
  # set_prompt_pom and friends can refuse when POM mode is off.
  @use_pom = use_pom

  # --- agent identity / config --------------------------------------
  # Python parity: ``agent_id`` (optional explicit UUID) and
  # ``default_webhook_url`` (used when SWAIG functions don't carry
  # an explicit URL). ``native_functions`` lists native SWAIG
  # callables that should appear in the SWAIG block alongside
  # user tools.
  @agent_id            = agent_id || SecureRandom.uuid
  @default_webhook_url = default_webhook_url
  @native_functions    = native_functions || []

  # --- override / validation flags ----------------------------------
  # Python parity: ``enable_post_prompt_override`` and
  # ``check_for_input_override`` are wired through the FastAPI
  # endpoint dispatcher. Ruby stashes them so subclass-controlled
  # routes can opt into the same behaviour.
  @enable_post_prompt_override = enable_post_prompt_override
  @check_for_input_override    = check_for_input_override

  # --- session manager ----------------------------------------------
  @session_manager = Security::SessionManager.new(token_expiry_secs: token_expiry_secs)

  # --- webhook signature validation (porting-sdk/webhooks.md) -------
  # Resolution order: explicit constructor arg → SIGNALWIRE_SIGNING_KEY env.
  # When set, _build_rack_app mounts WebhookMiddleware on the signed
  # routes (POST /, /swaig, /post_prompt). When unset, the SDK logs a
  # warning so production users notice unsigned traffic is being
  # accepted.
  @signing_key = signing_key || ENV['SIGNALWIRE_SIGNING_KEY']
  @trust_proxy_for_signature = trust_proxy_for_signature
  if @signing_key && !@signing_key.empty?
    @logger.info('webhook_signature_validation_enabled') unless @suppress_logs
  else
    unless @suppress_logs
      @logger.warn(
        '[signalwire] webhook signature validation is disabled — ' \
        'set signing_key or SIGNALWIRE_SIGNING_KEY to enable'
      )
    end
  end

  # --- prompt state -------------------------------------------------
  @prompt_text      = nil    # raw text mode
  @prompt_pom       = nil    # direct POM array
  @pom_sections     = []     # built via prompt_add_section
  @post_prompt_text = nil

  # --- tools --------------------------------------------------------
  # @tools and @swaig_functions are now initialised by Service (parent).
  # AgentBase's enhanced define_tool overrides Service's plain version.

  # --- AI config ----------------------------------------------------
  @hints               = []
  @languages           = []
  @pronounce           = []
  @params              = {}
  @global_data         = {}
  @function_includes   = []
  @internal_fillers    = {}
  @prompt_llm_params   = {}
  @post_prompt_llm_params = {}

  # --- debug --------------------------------------------------------
  @debug_events_enabled = false
  @debug_events_level   = 1
  @debug_event_callback = nil

  # --- verbs --------------------------------------------------------
  @pre_answer_verbs  = []    # [[verb_name, config], ...]
  @answer_config     = {}
  @post_answer_verbs = []
  @post_ai_verbs     = []

  # --- contexts -----------------------------------------------------
  @context_builder   = nil

  # --- skills -------------------------------------------------------
  # Python parity: ``SkillManager(agent)`` keeps a back-pointer
  # to the owning agent so loaded skills can attach SWAIG tools
  # and prompt sections directly through the manager.
  @skill_manager     = Skills::SkillManager.new(self)
  @loaded_skills     = {}    # skill_name => SkillBase

  # --- web ----------------------------------------------------------
  @dynamic_config_callback = nil
  @proxy_url_base          = ENV['SWML_PROXY_URL_BASE']
  @web_hook_url_override   = nil
  @post_prompt_url_override = nil
  @swaig_query_params      = {}
  @debug_routes_enabled    = false
  @summary_callback        = nil

  # --- SIP ----------------------------------------------------------
  @sip_routing_enabled = false
  @sip_auto_map        = false
  @sip_path            = '/sip'
  @sip_usernames       = []

  # --- MCP ----------------------------------------------------------
  @mcp_servers         = []     # external MCP server configs
  @mcp_server_enabled  = false  # expose /mcp endpoint

  @logger.info "Agent '#{@name}' initialised (route=#{@route}, port=#{@port})"
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class SignalWire::SWML::Service

Instance Attribute Details

#agent_idObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def agent_id
  @agent_id
end

#default_webhook_urlObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def default_webhook_url
  @default_webhook_url
end

#loggerObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def logger
  @logger
end

#native_functionsObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def native_functions
  @native_functions
end

#signing_keyObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def signing_key
  @signing_key
end

#skill_managerObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def skill_manager
  @skill_manager
end

#use_pomObject (readonly)

Python parity:

  • “logger“ — agent-specific structured logger (Python: “self.log“).

  • “skill_manager“ — owning SkillManager (Python’s “self.skill_manager“).

  • “agent_id“ — UUID identifier from constructor or auto-generated.

  • “default_webhook_url“ — base URL for SWAIG webhook fallbacks.

  • “native_functions“ — names of built-in SWAIG functions to advertise.

  • “use_pom“ — whether prompt-object-model rendering is enabled.



49
50
51
# File 'lib/signalwire/agent/agent_base.rb', line 49

def use_pom
  @use_pom
end

Class Method Details

.extract_sip_username(sip_uri) ⇒ String?

Extract a SIP username from a SIP URI string.

Parses URIs of the form “sip:user@domain” and returns the user part. Handles optional “sip:” or “sips:” scheme prefixes.

Parameters:

  • sip_uri (String)

    a SIP URI, e.g. “sip:alice@example.com”

Returns:

  • (String, nil)

    the username, or nil if the URI cannot be parsed



1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
# File 'lib/signalwire/agent/agent_base.rb', line 1226

def self.extract_sip_username(sip_uri)
  return nil if sip_uri.nil? || sip_uri.empty?

  # Strip optional sip:/sips: scheme
  uri = sip_uri.to_s.strip
  uri = uri.sub(%r{\Asips?:}, '')

  # Extract user part before @
  if uri.include?('@')
    user = uri.split('@', 2).first
    user && !user.empty? ? user : nil
  else
    nil
  end
end

.extract_sip_username_from_request(request_data) ⇒ String?

Extract the SIP username from request body data.

Looks for SIP URI in common request body fields (e.g., “to”, “from”, “sip_uri”, “call.to”, “call.from”).

Parameters:

  • request_data (Hash)

    the parsed request body

Returns:

  • (String, nil)

    the extracted SIP username, or nil



1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
# File 'lib/signalwire/agent/agent_base.rb', line 1249

def self.extract_sip_username_from_request(request_data)
  return nil unless request_data.is_a?(Hash)

  # Check common SIP URI fields
  candidates = [
    request_data['to'],
    request_data['from'],
    request_data['sip_uri'],
    request_data.dig('call', 'to'),
    request_data.dig('call', 'from')
  ].compact

  candidates.each do |uri|
    username = extract_sip_username(uri.to_s)
    return username if username
  end

  nil
end

Instance Method Details

#_build_mcp_tool_listObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build MCP tool list from registered tools.



1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
# File 'lib/signalwire/agent/agent_base.rb', line 1299

def _build_mcp_tool_list
  tools = []
  @tools.each do |name, tool|
    t = {
      'name'        => name,
      'description' => tool[:definition]['description'] || name,
    }
    params = tool[:definition]['parameters']
    if params && !params.empty?
      t['inputSchema'] = params.key?('type') ? params : { 'type' => 'object', 'properties' => params }
    else
      t['inputSchema'] = { 'type' => 'object', 'properties' => {} }
    end
    tools << t
  end
  tools
end

#_detect_run_modeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



1466
1467
1468
1469
1470
# File 'lib/signalwire/agent/agent_base.rb', line 1466

def _detect_run_mode
  return 'lambda' if ENV['AWS_LAMBDA_FUNCTION_NAME'] && !ENV['AWS_LAMBDA_FUNCTION_NAME'].empty?
  return 'cgi'    if ENV['GATEWAY_INTERFACE']
  'server'
end

#_handle_debug_events(request_data, _env) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Handle debug events.



2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
# File 'lib/signalwire/agent/agent_base.rb', line 2024

def _handle_debug_events(request_data, _env)
  if @debug_event_callback && request_data
    begin
      event_type = request_data['event_type'] || 'unknown'
      @debug_event_callback.call(event_type, request_data)
    rescue => e
      @logger.error "Debug event callback error: #{e.message}"
    end
  end

  body = JSON.generate({ 'status' => 'ok' })
  [200, { 'content-type' => 'application/json' }, [body]]
end

#_handle_mcp_endpoint(request_data, _env) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Handle MCP JSON-RPC 2.0 endpoint.



2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
# File 'lib/signalwire/agent/agent_base.rb', line 2040

def _handle_mcp_endpoint(request_data, _env)
  unless @mcp_server_enabled
    body = JSON.generate({ 'error' => 'MCP server not enabled' })
    return [404, { 'content-type' => 'application/json' }, [body]]
  end

  unless request_data
    body = JSON.generate(_mcp_error(nil, -32700, 'Parse error'))
    return [400, { 'content-type' => 'application/json' }, [body]]
  end

  resp = _handle_mcp_request(request_data)
  body = JSON.generate(resp)
  [200, { 'content-type' => 'application/json' }, [body]]
end

#_handle_mcp_request(body) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Handle a single MCP JSON-RPC 2.0 request and return the response hash.



1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
# File 'lib/signalwire/agent/agent_base.rb', line 1319

def _handle_mcp_request(body)
  jsonrpc = body['jsonrpc']
  method  = body['method'] || ''
  req_id  = body['id']
  params  = body['params'] || {}

  unless jsonrpc == '2.0'
    return _mcp_error(req_id, -32600, 'Invalid JSON-RPC version')
  end

  case method
  when 'initialize'
    {
      'jsonrpc' => '2.0', 'id' => req_id,
      'result' => {
        'protocolVersion' => '2025-06-18',
        'capabilities'   => { 'tools' => {} },
        'serverInfo'     => { 'name' => @name, 'version' => '1.0.0' }
      }
    }
  when 'notifications/initialized'
    { 'jsonrpc' => '2.0', 'id' => req_id, 'result' => {} }
  when 'tools/list'
    {
      'jsonrpc' => '2.0', 'id' => req_id,
      'result' => { 'tools' => _build_mcp_tool_list }
    }
  when 'tools/call'
    tool_name = params['name'] || ''
    arguments = params['arguments'] || {}

    tool = @tools[tool_name]
    unless tool
      return _mcp_error(req_id, -32602, "Unknown tool: #{tool_name}")
    end

    begin
      raw_data = {
        'function' => tool_name,
        'argument' => { 'parsed' => [arguments] }
      }
      result = tool[:handler].call(arguments, raw_data)

      response_text = ''
      if result.respond_to?(:to_h)
        h = result.to_h
        response_text = h['response'] || ''
      elsif result.is_a?(Hash)
        response_text = result['response'] || result.to_s
      elsif result.is_a?(String)
        response_text = result
      end

      {
        'jsonrpc' => '2.0', 'id' => req_id,
        'result' => {
          'content' => [{ 'type' => 'text', 'text' => response_text }],
          'isError' => false
        }
      }
    rescue => e
      @logger.error "MCP tool call error: #{tool_name}: #{e.message}"
      {
        'jsonrpc' => '2.0', 'id' => req_id,
        'result' => {
          'content' => [{ 'type' => 'text', 'text' => "Error: #{e.message}" }],
          'isError' => true
        }
      }
    end
  when 'ping'
    { 'jsonrpc' => '2.0', 'id' => req_id, 'result' => {} }
  else
    _mcp_error(req_id, -32601, "Method not found: #{method}")
  end
end

#_handle_post_prompt(request_data, _env) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Handle post_prompt callback.



2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
# File 'lib/signalwire/agent/agent_base.rb', line 2004

def _handle_post_prompt(request_data, _env)
  if @summary_callback && request_data
    begin
      post_prompt_data = request_data['post_prompt_data']
      summary = nil
      if post_prompt_data.is_a?(Hash)
        summary = post_prompt_data['parsed'] || post_prompt_data['raw']
      end
      @summary_callback.call(summary, request_data)
    rescue => e
      @logger.error "Post-prompt callback error: #{e.message}"
    end
  end

  body = JSON.generate({ 'status' => 'ok' })
  [200, { 'content-type' => 'application/json' }, [body]]
end

#_mcp_error(req_id, code, message) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



1397
1398
1399
1400
1401
1402
1403
# File 'lib/signalwire/agent/agent_base.rb', line 1397

def _mcp_error(req_id, code, message)
  {
    'jsonrpc' => '2.0',
    'id'      => req_id,
    'error'   => { 'code' => code, 'message' => message }
  }
end

#_render_swml_internalObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
# File 'lib/signalwire/agent/agent_base.rb', line 1585

def _render_swml_internal
  sections_main = []

  # PHASE 1: Pre-answer verbs
  @pre_answer_verbs.each do |verb_name, config|
    sections_main << { verb_name => config }
  end

  # PHASE 2: Answer verb
  if @auto_answer
    answer_conf = @answer_config.empty? ? {} : @answer_config
    sections_main << { 'answer' => answer_conf }
  end

  # PHASE 3: Post-answer verbs
  if @record_call
    sections_main << {
      'record_call' => {
        'format' => @record_format,
        'stereo' => @record_stereo
      }
    }
  end
  @post_answer_verbs.each do |verb_name, config|
    sections_main << { verb_name => config }
  end

  # PHASE 4: AI verb
  ai_config = _build_ai_config
  sections_main << { 'ai' => ai_config }

  # PHASE 5: Post-AI verbs
  @post_ai_verbs.each do |verb_name, config|
    sections_main << { verb_name => config }
  end

  {
    'version'  => '1.0.0',
    'sections' => {
      'main' => sections_main
    }
  }
end

#_run_cgiObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
# File 'lib/signalwire/agent/agent_base.rb', line 1496

def _run_cgi
  require 'stringio'
  env = {
    'PATH_INFO'      => ENV['PATH_INFO'] || '/',
    'REQUEST_METHOD' => ENV['REQUEST_METHOD'] || 'GET',
    'QUERY_STRING'   => ENV['QUERY_STRING'] || '',
    'rack.input'     => StringIO.new(''),
    'rack.errors'    => $stderr
  }
  status, headers, body = rack_app.call(env)
  body_str = body.respond_to?(:join) ? body.join : body.to_s
  out = +"Status: #{status}\r\n"
  headers.each { |k, v| out << "#{k}: #{v}\r\n" }
  out << "\r\n"
  out << body_str
  out
end

#_run_lambda(event, _context) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
# File 'lib/signalwire/agent/agent_base.rb', line 1473

def _run_lambda(event, _context)
  require 'stringio'
  event ||= {}
  path = event['path'] || event['rawPath'] || '/'
  method = event['httpMethod'] || event.dig('requestContext', 'http', 'method') || 'GET'
  body = event['body'] || ''
  env = {
    'PATH_INFO'      => path,
    'REQUEST_METHOD' => method,
    'QUERY_STRING'   => '',
    'rack.input'     => StringIO.new(body),
    'rack.errors'    => $stderr
  }
  status, headers, response_body = rack_app.call(env)
  body_str = response_body.respond_to?(:join) ? response_body.join : response_body.to_s
  {
    'statusCode' => Integer(status),
    'headers'    => headers,
    'body'       => body_str
  }
end

#add_answer_verb(config) ⇒ Object



995
996
997
998
# File 'lib/signalwire/agent/agent_base.rb', line 995

def add_answer_verb(config)
  @answer_config = config
  self
end

#add_function_include(url, functions, meta_data: nil) ⇒ Object



959
960
961
962
963
964
# File 'lib/signalwire/agent/agent_base.rb', line 959

def add_function_include(url, functions, meta_data: nil)
  include = { 'url' => url, 'functions' => functions }
  include['meta_data'] =  if .is_a?(Hash)
  @function_includes << include
  self
end

#add_hint(hint) ⇒ Object

AI Config methods



655
656
657
658
# File 'lib/signalwire/agent/agent_base.rb', line 655

def add_hint(hint)
  @hints << hint if hint.is_a?(String) && !hint.empty?
  self
end

#add_hints(hints) ⇒ Object



660
661
662
663
664
665
# File 'lib/signalwire/agent/agent_base.rb', line 660

def add_hints(hints)
  if hints.is_a?(Array)
    hints.each { |h| add_hint(h) }
  end
  self
end

#add_internal_filler(func_name, lang_code, fillers) ⇒ Object

Add internal fillers for a single internal function and language.

See set_internal_fillers for the complete list of supported func_name values (SUPPORTED_INTERNAL_FILLER_NAMES) and what fillers do. Names outside the supported set log a warning and are stored but the runtime will not play them.



936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/signalwire/agent/agent_base.rb', line 936

def add_internal_filler(func_name, lang_code, fillers)
  if func_name && lang_code && fillers.is_a?(Array) && !fillers.empty?
    unless SUPPORTED_INTERNAL_FILLER_NAMES.include?(func_name.to_s)
      @logger.warn(
        "unknown_internal_filler_name: #{func_name.inspect}. " \
        "add_internal_filler received a function name the SWML " \
        "schema does not recognize. The entry will be stored but " \
        "the runtime will not play these fillers. Supported " \
        "names: #{SUPPORTED_INTERNAL_FILLER_NAMES.sort.inspect}."
      )
    end
    @internal_fillers[func_name] ||= {}
    @internal_fillers[func_name][lang_code] = fillers
  end
  self
end

#add_language(config) ⇒ Object #add_language(name, code, voice, speech_fillers: nil) ⇒ Object

Add a language configuration.

Python parity: “add_language(name, code, voice, speech_fillers=None, function_fillers=None, engine=None, model=None)“. Ruby supports both the Python-style positional shape AND the original “add_language(config)“ hash form.

Voice argument can be either a simple voice id (““en-US-Neural2-F”“) or a combined ““engine.voice:model”“ string (““elevenlabs.josh:eleven_turbo_v2_5”“); the combined form is parsed into “engine“/“voice“/“model“ keys when “engine“ and “model“ aren’t supplied explicitly.

Overloads:

  • #add_language(config) ⇒ Object

    Parameters:

    • config (Hash)

      preformed language config

  • #add_language(name, code, voice, speech_fillers: nil) ⇒ Object

    function_fillers: nil, engine: nil, model: nil, params: nil)

    Parameters:

    • name (String)

      language name (e.g. ““English”“)

    • code (String)

      BCP47 language code (e.g. ““en-US”“)

    • voice (String)

      voice id or “engine.voice:model“ string

    • speech_fillers (Array<String>, nil) (defaults to: nil)

      filler phrases for natural speech

    • function_fillers (Array<String>, nil)

      filler phrases during function calls

    • engine (String, nil)

      explicit engine override

    • model (String, nil)

      explicit model override

    • params (Hash, nil)

      optional per-language params (engine- specific tuning, voice settings, etc.). Emitted as the language object’s “params“ key in SWML; the key is only emitted when non-empty so existing entries stay byte-identical.

Raises:

  • (ArgumentError)


750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# File 'lib/signalwire/agent/agent_base.rb', line 750

def add_language(name_or_config, code = nil, voice = nil,
                 speech_fillers: nil, function_fillers: nil,
                 engine: nil, model: nil, params: nil)
  # Hash form (legacy / direct config)
  if name_or_config.is_a?(Hash) && code.nil? && voice.nil?
    @languages << name_or_config
    return self
  end

  raise ArgumentError, 'add_language: name, code, voice are required (or pass a Hash)' if code.nil? || voice.nil?

  lang = { 'name' => name_or_config, 'code' => code }

  if engine || model
    lang['voice']  = voice
    lang['engine'] = engine if engine
    lang['model']  = model  if model
  elsif voice.is_a?(String) && voice.include?('.') && voice.include?(':')
    # "engine.voice:model"
    engine_voice, model_part = voice.split(':', 2)
    engine_part, voice_part  = engine_voice.split('.', 2)
    lang['voice']  = voice_part
    lang['engine'] = engine_part
    lang['model']  = model_part
  else
    lang['voice'] = voice
  end

  if speech_fillers && function_fillers
    lang['speech_fillers']   = speech_fillers
    lang['function_fillers'] = function_fillers
  elsif speech_fillers || function_fillers
    lang['fillers'] = speech_fillers || function_fillers
  end

  # Per-language params (engine-specific tuning, voice settings,
  # etc.). Only emit the key when non-empty so we don't pollute
  # SWML with empty objects.
  lang['params'] = params if params.is_a?(Hash) && !params.empty?

  @languages << lang
  self
end

#add_mcp_server(url, headers: nil, resources: false, resource_vars: nil) ⇒ self

Add an external MCP server for tool discovery and invocation.

Parameters:

  • url (String)

    MCP server HTTP endpoint URL

  • headers (Hash, nil) (defaults to: nil)

    optional HTTP headers

  • resources (Boolean) (defaults to: false)

    whether to fetch resources into global_data

  • resource_vars (Hash, nil) (defaults to: nil)

    variables for URI template substitution

Returns:

  • (self)


1280
1281
1282
1283
1284
1285
1286
1287
# File 'lib/signalwire/agent/agent_base.rb', line 1280

def add_mcp_server(url, headers: nil, resources: false, resource_vars: nil)
  server = { 'url' => url }
  server['headers']       = headers       if headers && !headers.empty?
  server['resources']     = true          if resources
  server['resource_vars'] = resource_vars if resource_vars && !resource_vars.empty?
  @mcp_servers << server
  self
end

#add_pattern_hint(hint, pattern, replace, ignore_case: false) ⇒ Object #add_pattern_hint(pattern, hint:, language: 'en-US') ⇒ Object

Add a complex (pattern-matched) hint.

Python parity: “add_pattern_hint(hint, pattern, replace, ignore_case=False)“. Ruby supports both the Python-style positional form and the legacy keyword form (“add_pattern_hint(pattern, hint:, language:)“) for backward compat.

Overloads:

  • #add_pattern_hint(hint, pattern, replace, ignore_case: false) ⇒ Object

    Parameters:

    • hint (String)

      hint to match

    • pattern (String)

      regex pattern

    • replace (String)

      replacement text

    • ignore_case (Boolean) (defaults to: false)

      match without regard to case

  • #add_pattern_hint(pattern, hint:, language: 'en-US') ⇒ Object

    Legacy Ruby form — pattern + optional hint string and language.

Raises:

  • (ArgumentError)


682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# File 'lib/signalwire/agent/agent_base.rb', line 682

def add_pattern_hint(*args, hint: nil, pattern: nil, replace: nil,
                    ignore_case: false, language: 'en-US')
  # Three positional args = Python positional shape.
  if args.length == 3
    h_hint, h_pattern, h_replace = args
    @hints << {
      'hint'        => h_hint,
      'pattern'     => h_pattern,
      'replace'     => h_replace,
      'ignore_case' => ignore_case
    }
    return self
  end

  # Single positional ≡ legacy ``add_pattern_hint(pattern, hint:, language:)``.
  if args.length == 1 && pattern.nil? && replace.nil?
    legacy_pattern = args.first
    entry = { 'pattern' => legacy_pattern }
    entry['hint']     = hint     if hint
    entry['language'] = language if language
    @hints << entry
    return self
  end

  # Pure-keyword form (Python-named keywords)
  if pattern && hint && replace
    @hints << {
      'hint'        => hint,
      'pattern'     => pattern,
      'replace'     => replace,
      'ignore_case' => ignore_case
    }
    return self
  end

  raise ArgumentError, 'add_pattern_hint: pass either (hint, pattern, replace) or use legacy (pattern, hint:, language:) form'
end

#add_post_ai_verb(verb_name, config) ⇒ Object



1010
1011
1012
1013
# File 'lib/signalwire/agent/agent_base.rb', line 1010

def add_post_ai_verb(verb_name, config)
  @post_ai_verbs << [verb_name.to_s, config]
  self
end

#add_post_answer_verb(verb_name, config) ⇒ Object



1000
1001
1002
1003
# File 'lib/signalwire/agent/agent_base.rb', line 1000

def add_post_answer_verb(verb_name, config)
  @post_answer_verbs << [verb_name.to_s, config]
  self
end

#add_pre_answer_verb(verb_name, config) ⇒ Object

Verb management



985
986
987
988
# File 'lib/signalwire/agent/agent_base.rb', line 985

def add_pre_answer_verb(verb_name, config)
  @pre_answer_verbs << [verb_name.to_s, config]
  self
end

#add_pronunciation(phrase, pronunciation, language_code: 'en-US') ⇒ Object



835
836
837
838
839
840
# File 'lib/signalwire/agent/agent_base.rb', line 835

def add_pronunciation(phrase, pronunciation, language_code: 'en-US')
  rule = { 'replace' => phrase, 'with' => pronunciation }
  rule['ignore_case'] = false
  @pronounce << rule
  self
end

#add_skill(skill_name, params = {}) ⇒ Object

Load and register a skill by name.

Raises:

  • (ArgumentError)


1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
# File 'lib/signalwire/agent/agent_base.rb', line 1099

def add_skill(skill_name, params = {})
  # Ensure builtins are registered
  Skills::SkillRegistry.register_builtins!

  factory = Skills::SkillRegistry.get_factory(skill_name)
  raise ArgumentError, "Unknown skill: '#{skill_name}'" unless factory

  skill = factory.call(params)
  @skill_manager.load(skill.instance_key, skill)
  @loaded_skills[skill_name] = skill

  # Register tools from the skill
  tool_defs = skill.register_tools
  if tool_defs.is_a?(Array)
    tool_defs.each do |td|
      td_name    = td[:name]    || td['name']
      td_desc    = td[:description] || td['description']
      td_params  = td[:parameters]  || td['parameters'] || {}
      td_handler = td[:handler]     || td['handler']
      next unless td_name && td_handler

      define_tool(
        name: td_name,
        description: td_desc || '',
        parameters: td_params,
        &td_handler
      )
    end
  end

  # Merge hints
  skill_hints = skill.get_hints
  @hints.concat(skill_hints) if skill_hints.is_a?(Array) && !skill_hints.empty?

  # Merge global data
  skill_data = skill.get_global_data
  @global_data.merge!(skill_data) if skill_data.is_a?(Hash) && !skill_data.empty?

  # Merge prompt sections
  skill_sections = skill.get_prompt_sections
  if skill_sections.is_a?(Array) && !skill_sections.empty?
    @prompt_text = nil  # switch to POM mode
    @prompt_pom  = nil
    skill_sections.each do |sec|
      @pom_sections << sec
    end
  end

  self
end

#add_swaig_query_params(params) ⇒ Object



1188
1189
1190
1191
# File 'lib/signalwire/agent/agent_base.rb', line 1188

def add_swaig_query_params(params)
  @swaig_query_params.merge!(params) if params.is_a?(Hash)
  self
end

#clear_post_ai_verbsObject



1015
1016
1017
1018
# File 'lib/signalwire/agent/agent_base.rb', line 1015

def clear_post_ai_verbs
  @post_ai_verbs = []
  self
end

#clear_post_answer_verbsObject



1005
1006
1007
1008
# File 'lib/signalwire/agent/agent_base.rb', line 1005

def clear_post_answer_verbs
  @post_answer_verbs = []
  self
end

#clear_pre_answer_verbsObject



990
991
992
993
# File 'lib/signalwire/agent/agent_base.rb', line 990

def clear_pre_answer_verbs
  @pre_answer_verbs = []
  self
end

#clear_swaig_query_paramsObject



1193
1194
1195
1196
# File 'lib/signalwire/agent/agent_base.rb', line 1193

def clear_swaig_query_params
  @swaig_query_params = {}
  self
end

#create_tool_token(tool_name, call_id) ⇒ Object

Mint a per-call SWAIG-function token via the agent’s SessionManager.

Python parity: state_mixin.StateMixin#_create_tool_token —delegates to SessionManager#create_token and returns “” on any raised error (Python rescues all exceptions and returns “”).



587
588
589
590
591
# File 'lib/signalwire/agent/agent_base.rb', line 587

def create_tool_token(tool_name, call_id)
  @session_manager.create_token(tool_name, call_id)
rescue StandardError
  ''
end

#define_contexts(contexts = nil) ⇒ SignalWire::Contexts::ContextBuilder Also known as: contexts

Define / retrieve the ContextBuilder for this agent.

Python parity: “define_contexts(contexts)“ accepts either a “ContextBuilder“ (calls “.to_dict()“ to materialise) or a raw “dict“ and stores it on the agent. Ruby supports both forms PLUS the original lazy-getter idiom:

  1. **Lazy getter** (Ruby idiom) — “agent.define_contexts“ returns the existing builder, creating one if needed.

  2. **Override with builder** — “agent.define_contexts(other_cb)“ replaces the current builder with the supplied one (Python parity).

  3. **Override with hash** — “agent.define_contexts(…)“ builds a fresh builder using the provided contexts hash (Python parity for raw-dict input).

Parameters:

Returns:



1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
# File 'lib/signalwire/agent/agent_base.rb', line 1043

def define_contexts(contexts = nil)
  if contexts.is_a?(Contexts::ContextBuilder)
    @context_builder = contexts
    @context_builder.attach_agent(self) if @context_builder.respond_to?(:attach_agent)
    return @context_builder
  end

  if contexts.is_a?(Hash)
    cb = Contexts::ContextBuilder.new(self)
    contexts.each do |name, body|
      ctx = cb.add_context(name.to_s)
      steps = (body.is_a?(Hash) ? body['steps'] : nil) || []
      steps.each do |step_h|
        step_name = step_h['name'] || step_h[:name] || raise(ArgumentError, 'step missing name')
        step = ctx.add_step(step_name)
        step.set_text(step_h['text']) if step_h['text']
      end
    end
    @context_builder = cb
    return cb
  end

  unless contexts.nil?
    raise ArgumentError, 'contexts must be a ContextBuilder, Hash, or nil'
  end

  @context_builder ||= begin
    cb = Contexts::ContextBuilder.new(self)
    cb.attach_agent(self) if cb.respond_to?(:attach_agent)
    cb
  end
end

#define_tool(name:, description:, parameters: {}, handler: nil, secure: false, fillers: nil, wait_file: nil, wait_file_loops: nil, webhook_url: nil, required: nil, is_typed_handler: false, swaig_fields: nil) {|args, raw_data| ... } ⇒ Object

Register a SWAIG tool (function) that the AI can invoke during a call.

How this becomes a tool the model sees

A SWAIG function is *exactly the same concept* as a “tool” in native OpenAI / Anthropic tool calling. On every LLM turn, the SDK renders each registered SWAIG function into the OpenAI tool schema:

{
  "type": "function",
  "function": {
    "name":        "your_name_here",
    "description": "your description text",
    "parameters":  { ... your JSON schema ... }
  }
}

That schema is sent to the model as part of the same API call that produces the next assistant message. The model reads:

- the function +description+ to decide WHEN to call this tool
- each parameter +description+ (inside +parameters+) to decide
  HOW to fill in that argument from the user's utterance

This means *descriptions are prompt engineering*, not developer comments. A vague description is the #1 cause of “the model has the right tool but doesn’t call it” failures.

Bad vs good descriptions

BAD : description: "Lookup function"
GOOD: description: "Look up a customer's account details by " \
                   "account number. Use this BEFORE quoting "  \
                   "any account-specific info (balance, plan, " \
                   "status). Do not use for general product "  \
                   "questions."

BAD : parameters: { id: { type: 'string', description: 'the id' } }
GOOD: parameters: { account_number: { type: 'string',
          description: "The customer's 8-digit account " \
          "number, no dashes or spaces. Ask the user if they " \
          "don't provide it." } }

Tool count matters

LLM tool selection accuracy degrades past ~7-8 simultaneously-active tools per call. Use Contexts::Step#set_functions to partition tools across steps so only the relevant subset is active at any moment.

Define a SWAIG tool.

Python parity: “define_tool(name, description, parameters, handler, secure=True, fillers=None, wait_file=None, wait_file_loops=None, webhook_url=None, required=None, is_typed_handler=False, **swaig_fields)“.

Parameters:

  • name (String)

    function name (snake_case verb recommended)

  • description (String)

    LLM-facing description of when to call this tool

  • parameters (Hash) (defaults to: {})

    JSON-Schema properties with LLM-facing descriptions for each parameter

  • secure (Boolean) (defaults to: false)
  • fillers (Hash, nil) (defaults to: nil)

    language_code => [phrases]

  • swaig_fields (Hash, nil) (defaults to: nil)

    extra fields merged into definition

  • name (String)

    tool name

  • description (String)

    LLM-facing description

  • parameters (Hash) (defaults to: {})

    JSON-Schema parameters

  • handler (Proc, nil) (defaults to: nil)

    explicit handler (alternative to a block); kept for backward compat

  • secure (Boolean) (defaults to: false)

    require token validation. Ruby defaults to “false“ (kept for backward compat); Python defaults to “True“. Pass “secure: true“ to match Python.

  • fillers (Hash, nil) (defaults to: nil)

    language-keyed filler phrases

  • wait_file (String, nil) (defaults to: nil)

    URL of audio file to play while the tool runs server-side

  • wait_file_loops (Integer, nil) (defaults to: nil)

    loop count for “wait_file“

  • webhook_url (String, nil) (defaults to: nil)

    external endpoint to use instead of dispatching to the local handler

  • required (Array<String>, nil) (defaults to: nil)

    required parameter names

  • is_typed_handler (Boolean) (defaults to: false)

    handler accepts type-coerced keyword args (parity flag; Ruby uses dynamic typing so this is a no-op at runtime but is preserved for surface parity)

  • swaig_fields (Hash, nil) (defaults to: nil)

    additional fields merged into the SWAIG function definition

Yields:

  • (args, raw_data)

    the tool handler

  • (args, raw_data)

    tool handler body (alternative to passing “handler:“)



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/signalwire/agent/agent_base.rb', line 523

def define_tool(name:, description:, parameters: {}, handler: nil,
                secure: false, fillers: nil,
                wait_file: nil, wait_file_loops: nil,
                webhook_url: nil, required: nil,
                is_typed_handler: false,
                swaig_fields: nil, &block)
  # Block is canonical — falls back to explicit handler kwarg.
  effective_handler = block || handler

  # Normalise parameters into JSON-Schema form
  param_schema = _normalise_parameters(parameters)

  # If the caller supplied required: (Python parity), inject it
  # into the parameter schema so SWML rendering carries the list.
  if required.is_a?(Array) && !required.empty?
    if param_schema.is_a?(Hash) && param_schema['type'] == 'object'
      existing = param_schema['required'] || []
      param_schema['required'] = (existing + required).uniq
    end
  end

  tool_def = {
    'function'    => name,
    'description' => description,
    'parameters'  => param_schema
  }
  tool_def['fillers']          = fillers          if fillers && !fillers.empty?
  tool_def['wait_file']        = wait_file        if wait_file
  tool_def['wait_file_loops']  = wait_file_loops  if wait_file_loops
  tool_def['webhook_url']      = webhook_url      if webhook_url
  tool_def['is_typed_handler'] = true             if is_typed_handler

  # Merge extra swaig fields
  if swaig_fields.is_a?(Hash)
    swaig_fields.each { |k, v| tool_def[k.to_s] = v }
  end

  @tools[name] = {
    definition: tool_def,
    handler:    effective_handler,
    secure:     secure
  }
  self
end

#define_toolsObject

Return an array of all tool definitions (for SWML rendering).



577
578
579
580
# File 'lib/signalwire/agent/agent_base.rb', line 577

def define_tools
  defs = @tools.values.map { |t| t[:definition].dup }
  defs + @swaig_functions.values.map(&:dup)
end

#enable_debug_events(level = 1) ⇒ Object



953
954
955
956
957
# File 'lib/signalwire/agent/agent_base.rb', line 953

def enable_debug_events(level = 1)
  @debug_events_enabled = true
  @debug_events_level   = level
  self
end

#enable_debug_routesObject



1198
1199
1200
1201
# File 'lib/signalwire/agent/agent_base.rb', line 1198

def enable_debug_routes
  @debug_routes_enabled = true
  self
end

#enable_mcp_serverself

Expose this agent’s tools as an MCP server endpoint at /mcp.

Returns:

  • (self)


1292
1293
1294
1295
# File 'lib/signalwire/agent/agent_base.rb', line 1292

def enable_mcp_server
  @mcp_server_enabled = true
  self
end

#enable_sip_routing(auto_map: true, path: '/sip') ⇒ Object

SIP



1207
1208
1209
1210
1211
1212
# File 'lib/signalwire/agent/agent_base.rb', line 1207

def enable_sip_routing(auto_map: true, path: '/sip')
  @sip_routing_enabled = true
  @sip_auto_map        = auto_map
  @sip_path            = path
  self
end

#get_basic_auth_credentials(include_source: false) ⇒ Object

Get the configured basic-auth credentials.

Python parity: “get_basic_auth_credentials(include_source=False)“. When “include_source“ is true, returns a 3-tuple “[user, pass, source]“ (““environment”“ / ““auto-generated”“ / ““provided”“). Otherwise returns “[user, pass]“.



1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
# File 'lib/signalwire/agent/agent_base.rb', line 1635

def get_basic_auth_credentials(include_source: false)
  u, p = @basic_auth
  return [u, p] unless include_source

  env_user = ENV['SWML_BASIC_AUTH_USER']
  env_pass = ENV['SWML_BASIC_AUTH_PASSWORD']
  source =
    if env_user && !env_user.empty? && env_pass && !env_pass.empty? && u == env_user && p == env_pass
      'environment'
    elsif u&.start_with?('user_') && p && p.length > 20
      'auto-generated'
    else
      'provided'
    end
  [u, p, source]
end

#get_contextsObject

Returns the contexts dictionary as a serialised hash, or nil when no contexts have been defined yet.

Mirrors Python’s PromptManager#get_contexts which returns the contexts dict or None.



423
424
425
426
# File 'lib/signalwire/agent/agent_base.rb', line 423

def get_contexts
  return nil if @context_builder.nil?
  @context_builder.to_h
end

#get_language_params(code) ⇒ Hash?

Read the per-language “params“ hash for a previously-added language.

Parameters:

  • code (String)

    language code as previously passed to “add_language“.

Returns:

  • (Hash, nil)

    the params hash if set, “nil“ otherwise (including when the code is unknown).



823
824
825
826
827
828
# File 'lib/signalwire/agent/agent_base.rb', line 823

def get_language_params(code)
  @languages.each do |lang|
    return lang['params'] if lang.is_a?(Hash) && lang['code'] == code
  end
  nil
end

#get_post_promptObject

Returns the post-prompt text whatever set_post_prompt stored, or nil when no post-prompt has been set.

Mirrors Python’s PromptManager#get_post_prompt / PromptMixin#get_post_prompt — used by SWML rendering when a post-prompt is configured.



405
406
407
# File 'lib/signalwire/agent/agent_base.rb', line 405

def get_post_prompt
  @post_prompt_text
end

#get_promptObject

Return the current prompt: either a string (text mode) or an array (POM).



351
352
353
354
355
356
# File 'lib/signalwire/agent/agent_base.rb', line 351

def get_prompt
  return @prompt_text if @prompt_text
  return @prompt_pom  if @prompt_pom
  return @pom_sections.dup unless @pom_sections.empty?
  nil
end

#get_raw_promptObject

Returns the raw prompt text whatever set_prompt_text stored, or nil when no raw prompt has been set. Distinct from #get_prompt which may return the POM array when use_pom is true.

Mirrors Python’s PromptManager#get_raw_prompt.



414
415
416
# File 'lib/signalwire/agent/agent_base.rb', line 414

def get_raw_prompt
  @prompt_text
end

#handle_additional_route(sub_path, request_data, env) ⇒ Object



1984
1985
1986
1987
1988
1989
1990
# File 'lib/signalwire/agent/agent_base.rb', line 1984

def handle_additional_route(sub_path, request_data, env)
  case sub_path
  when '/post_prompt'  then _handle_post_prompt(request_data, env)
  when '/debug_events' then _handle_debug_events(request_data, env)
  when '/mcp'          then _handle_mcp_endpoint(request_data, env)
  end
end

#has_skill?(skill_name) ⇒ Boolean

Returns:

  • (Boolean)


1160
1161
1162
# File 'lib/signalwire/agent/agent_base.rb', line 1160

def has_skill?(skill_name)
  @loaded_skills.key?(skill_name)
end

#list_skillsObject



1156
1157
1158
# File 'lib/signalwire/agent/agent_base.rb', line 1156

def list_skills
  @loaded_skills.keys
end

#list_tool_namesObject

Return the names of all registered SWAIG tools in insertion order. Used by ContextBuilder#validate! to detect collisions with reserved native tool names.



1090
1091
1092
# File 'lib/signalwire/agent/agent_base.rb', line 1090

def list_tool_names
  (@tools.keys + @swaig_functions.keys).uniq
end

#manual_set_proxy_url(url) ⇒ Object



1173
1174
1175
1176
# File 'lib/signalwire/agent/agent_base.rb', line 1173

def manual_set_proxy_url(url)
  @proxy_url_base = url
  self
end

#on_debug_event(&block) ⇒ Object



1434
1435
1436
1437
# File 'lib/signalwire/agent/agent_base.rb', line 1434

def on_debug_event(&block)
  @debug_event_callback = block
  self
end

#on_function_call(name, args, raw_data) ⇒ Object

Dispatch a function call to the registered handler.



607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# File 'lib/signalwire/agent/agent_base.rb', line 607

def on_function_call(name, args, raw_data)
  tool = @tools[name]
  unless tool
    return { 'response' => "Function '#{name}' not found" }
  end

  # Validate secure token if needed
  if tool[:secure]
    call_id = raw_data && (raw_data['call_id'] || (raw_data['call'] && raw_data['call']['call_id']))
    token   = raw_data && raw_data['meta_data_token']
    if call_id && token
      unless @session_manager.validate_token(name, token, call_id)
        return { 'response' => 'Invalid or expired token' }
      end
    end
  end

  result = tool[:handler].call(args, raw_data)
  if result.is_a?(Hash)
    result
  elsif result.respond_to?(:to_h) && !result.nil?
    # FunctionResult-like object that responds to to_h.
    result.to_h
  else
    # Neither a Hash nor a FunctionResult-like object. Warn and
    # fall back to wrapping the stringified value, matching
    # Python's web_mixin / serverless_mixin / tool_mixin behavior.
    @logger.warn(
      "unexpected_function_result_type: function=#{name.inspect} " \
      "result_type=#{result.class.name.inspect}. SWAIG function " \
      "returned a value that is neither a FunctionResult (responds " \
      "to to_h) nor a Hash; falling back to wrapping the " \
      "stringified value. The AI will see the stringified value as " \
      "its tool response. Return a " \
      "SignalWire::SWAIG::FunctionResult object or a Hash with at " \
      "least a 'response' key."
    )
    { 'response' => result.to_s }
  end
rescue => e
  @logger.error "Tool '#{name}' error: #{e.message}"
  { 'response' => "Error executing '#{name}': #{e.message}" }
end

#on_summary(summary = nil, raw_data = nil) {|summary, raw_data| ... } ⇒ Object

Python parity: “on_summary(self, summary, raw_data=None)“ is a virtual hook called when a post-prompt summary is received. Ruby supports two equivalent shapes:

  1. Registration (Ruby idiom) — pass a block to install a callback. The block receives “(summary, raw_data)“ when a summary is delivered. “on_summary { |sum, raw| … }“

  2. Override (Python idiom) — subclass and override “on_summary(summary, raw_data = nil)“. Default implementation calls the registered block (if any) and otherwise no-ops.

Parameters:

  • summary (Hash, nil) (defaults to: nil)

    the post-prompt summary

  • raw_data (Hash, nil) (defaults to: nil)

    the complete raw POST data

Yields:

  • (summary, raw_data)

    optional callback registration



1424
1425
1426
1427
1428
1429
1430
1431
1432
# File 'lib/signalwire/agent/agent_base.rb', line 1424

def on_summary(summary = nil, raw_data = nil, &block)
  if block
    @summary_callback = block
    return self
  end

  @summary_callback&.call(summary, raw_data)
  nil
end

#pomObject

Read-only snapshot of the agent’s POM as a typed POM::PromptObjectModel instance.

Python parity: “agent.pom“ instance attribute (agent_base.py line 209) is a “PromptObjectModel“ instance. Returns “nil“ when raw-text prompt mode is in effect (“set_prompt_text“ was called) — mirrors Python’s “self.pom = None when use_pom=False“.

The returned PromptObjectModel is a fresh build of the agent’s current section state, so caller mutations do not leak into agent state. Use “agent.pom.to_h“ to retrieve the legacy array-of-hashes representation.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/signalwire/agent/agent_base.rb', line 370

def pom
  return nil if @prompt_text

  sections = @prompt_pom || @pom_sections
  pom = SignalWire::POM::PromptObjectModel.new
  sections.each do |sec|
    # Each section is a Hash with possibly String or Symbol keys.
    h = sec.transform_keys(&:to_s)
    kwargs = {
      body: h.fetch('body', ''),
      bullets: h['bullets'] || [],
      numbered: h['numbered'],
      numbered_bullets: h['numbered_bullets'] || h['numberedBullets'] || false
    }
    section = pom.add_section(h['title'], **kwargs)
    (h['subsections'] || []).each do |sub|
      sh = sub.transform_keys(&:to_s)
      section.add_subsection(
        sh['title'],
        body: sh.fetch('body', ''),
        bullets: sh['bullets'] || [],
        numbered: sh['numbered'] || false,
        numbered_bullets: sh['numbered_bullets'] || sh['numberedBullets'] || false
      )
    end
  end
  pom
end

#prompt_add_section(title, body = nil, bullets: nil, numbered: false, numbered_bullets: false, subsections: nil) ⇒ Object

Add a POM section.

Python parity: “prompt_add_section(title, body=“”, bullets=None, numbered=False, numbered_bullets=False, subsections=None)“.

Parameters:

  • title (String)

    section title

  • body (String, nil) (defaults to: nil)

    optional body text

  • bullets (Array<String>, nil) (defaults to: nil)

    optional bullet items

  • numbered (Boolean) (defaults to: false)

    render as a numbered top-level entry

  • numbered_bullets (Boolean) (defaults to: false)

    render bullets as numbered

  • subsections (Array<Hash>, nil) (defaults to: nil)

    optional pre-rendered subsection hashes (each “body:, bullets:“)



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/signalwire/agent/agent_base.rb', line 269

def prompt_add_section(title, body = nil, bullets: nil,
                       numbered: false, numbered_bullets: false,
                       subsections: nil)
  @prompt_text = nil
  @prompt_pom  = nil
  section = { 'title' => title }
  section['body']             = body              if body
  section['bullets']          = bullets           if bullets
  section['numbered']         = true              if numbered
  section['numbered_bullets'] = true              if numbered_bullets

  if subsections.is_a?(Array) && !subsections.empty?
    section['subsections'] = subsections.map do |sub|
      h = { 'title' => sub['title'] || sub[:title] }
      h['body']    = sub['body']    || sub[:body]    if (sub['body'] || sub[:body])
      h['bullets'] = sub['bullets'] || sub[:bullets] if (sub['bullets'] || sub[:bullets])
      h
    end
  end

  @pom_sections << section
  self
end

#prompt_add_subsection(parent_title, title, body = nil, bullets: nil) ⇒ Object

Add a subsection under a parent section.



333
334
335
336
337
338
339
340
341
342
343
# File 'lib/signalwire/agent/agent_base.rb', line 333

def prompt_add_subsection(parent_title, title, body = nil, bullets: nil)
  parent = @pom_sections.find { |s| s['title'] == parent_title }
  if parent
    parent['subsections'] ||= []
    sub = { 'title' => title }
    sub['body']    = body    if body
    sub['bullets'] = bullets if bullets
    parent['subsections'] << sub
  end
  self
end

#prompt_add_to_section(title, body_arg = nil, body: nil, bullet: nil, bullets: nil) ⇒ Object

Append content to an existing POM section, creating it if absent.

Python parity: “prompt_add_to_section(title, body=None, bullet=None, bullets=None)“. Supports appending body text, a single bullet, or a list of bullets.

**Backwards compat:** the original Ruby signature was “prompt_add_to_section(title, text)“. When called with two positional arguments the second becomes “body“; this preserves existing call sites while still supporting Python’s keyword form.

Parameters:

  • title (String)

    section title

  • body (String, nil) (defaults to: nil)

    body text to append

  • bullet (String, nil) (defaults to: nil)

    single bullet to append

  • bullets (Array<String>, nil) (defaults to: nil)

    bullets to append



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/signalwire/agent/agent_base.rb', line 309

def prompt_add_to_section(title, body_arg = nil, body: nil, bullet: nil, bullets: nil)
  effective_body = body || body_arg

  sec = @pom_sections.find { |s| s['title'] == title }
  unless sec
    sec = { 'title' => title }
    @pom_sections << sec
  end

  if effective_body
    sec['body'] = (sec['body'] || '') + effective_body.to_s
  end

  to_add = []
  to_add << bullet if bullet
  to_add.concat(bullets) if bullets.is_a?(Array)
  unless to_add.empty?
    sec['bullets'] = (sec['bullets'] || []) + to_add
  end

  self
end

#prompt_has_section?(title) ⇒ Boolean

Check whether a POM section with the given title exists.

Returns:

  • (Boolean)


346
347
348
# File 'lib/signalwire/agent/agent_base.rb', line 346

def prompt_has_section?(title)
  @pom_sections.any? { |s| s['title'] == title }
end

#rack_appObject Also known as: as_rack_app

Return a Rack-compatible application for mounting.



1550
1551
1552
# File 'lib/signalwire/agent/agent_base.rb', line 1550

def rack_app
  @rack_app ||= _build_rack_app
end

#register_sip_username(username) ⇒ Object



1214
1215
1216
1217
# File 'lib/signalwire/agent/agent_base.rb', line 1214

def register_sip_username(username)
  @sip_usernames << username
  self
end

#register_swaig_function(func_def) ⇒ Object

Register a raw SWAIG function definition (e.g. from DataMap#to_swaig_function).



569
570
571
572
573
574
# File 'lib/signalwire/agent/agent_base.rb', line 569

def register_swaig_function(func_def)
  fname = func_def['function'] || func_def[:function]
  return self unless fname
  @swaig_functions[fname] = func_def.transform_keys(&:to_s)
  self
end

#remove_skill(skill_name) ⇒ Object



1150
1151
1152
1153
1154
# File 'lib/signalwire/agent/agent_base.rb', line 1150

def remove_skill(skill_name)
  skill = @loaded_skills.delete(skill_name)
  @skill_manager.unload(skill.instance_key) if skill
  self
end

#render_swml(request_data = nil, request: nil) ⇒ Hash

Build the complete SWML document hash.

Parameters:

  • request_data (Hash, nil) (defaults to: nil)

    parsed request body

  • request (Rack::Request, nil) (defaults to: nil)

    the HTTP request

Returns:

  • (Hash)


1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
# File 'lib/signalwire/agent/agent_base.rb', line 1565

def render_swml(request_data = nil, request: nil)
  agent = self

  # Dynamic config: clone into ephemeral copy
  if @dynamic_config_callback
    agent = _create_ephemeral_copy
    begin
      query_params = request ? _parse_query_string(request) : {}
      body_params  = request_data || {}
      headers      = request ? _extract_headers(request) : {}
      @dynamic_config_callback.call(query_params, body_params, headers, agent)
    rescue => e
      @logger.error "Dynamic config error: #{e.message}"
    end
  end

  agent._render_swml_internal
end

#reset_contextsObject

Remove all contexts, returning the agent to a no-contexts state. This is a convenience wrapper around define_contexts.reset. Use it in a dynamic config callback when you need to rebuild contexts from scratch for a specific request.



1082
1083
1084
1085
# File 'lib/signalwire/agent/agent_base.rb', line 1082

def reset_contexts
  @context_builder&.reset
  self
end

#run(event: nil, context: nil, force_mode: nil, host: nil, port: nil) ⇒ Object

Universal run method — mirrors Python’s “WebMixin.run(event=None, context=None, force_mode=None, host=None, port=None)“.

Detects execution mode (server / lambda / cgi) and routes accordingly. “force_mode“ overrides auto-detection.

Parameters:

  • event (Object, nil) (defaults to: nil)

    serverless event

  • context (Object, nil) (defaults to: nil)

    serverless context

  • force_mode (String, nil) (defaults to: nil)

    one of ““server”“, ““lambda”“, ““cgi”“

  • host (String, nil) (defaults to: nil)

    override bind host (server mode)

  • port (Integer, nil) (defaults to: nil)

    override bind port (server mode)



1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
# File 'lib/signalwire/agent/agent_base.rb', line 1452

def run(event: nil, context: nil, force_mode: nil, host: nil, port: nil)
  mode = force_mode || _detect_run_mode

  case mode
  when 'lambda'
    _run_lambda(event, context)
  when 'cgi'
    _run_cgi
  else
    serve(host: host, port: port)
  end
end

#serve(host: nil, port: nil) ⇒ Object

Start the HTTP server (blocking).

Python parity: “serve(host=None, port=None)“. “host“ / “port“ overrides default to constructor-supplied values.



1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
# File 'lib/signalwire/agent/agent_base.rb', line 1518

def serve(host: nil, port: nil)
  require 'webrick'
  bind_host = host || @host
  bind_port = port || @port
  @logger.info "Starting server on #{bind_host}:#{bind_port} ..."
  user, _pass = @basic_auth
  @logger.info "Basic-auth credentials — user: #{user}  password: [REDACTED]"

  @server = ::WEBrick::HTTPServer.new(
    Host: bind_host,
    Port: bind_port,
    Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN),
    AccessLog: []
  )

  # Rack 3+ moved Handler to the rackup gem
  handler = begin
              require 'rackup/handler/webrick'
              Rackup::Handler::WEBrick
            rescue LoadError
              require 'rack/handler/webrick'
              Rack::Handler::WEBrick
            end
  @server.mount '/', handler, rack_app

  trap('INT')  { @server.shutdown }
  trap('TERM') { @server.shutdown }

  @server.start
end

#set_dynamic_config_callback(callable = nil, &block) ⇒ Object

Web / HTTP configuration



1168
1169
1170
1171
# File 'lib/signalwire/agent/agent_base.rb', line 1168

def set_dynamic_config_callback(callable = nil, &block)
  @dynamic_config_callback = callable || block
  self
end

#set_function_includes(includes) ⇒ Object



966
967
968
969
# File 'lib/signalwire/agent/agent_base.rb', line 966

def set_function_includes(includes)
  @function_includes = includes.dup if includes.is_a?(Array)
  self
end

#set_global_data(data) ⇒ Object



859
860
861
862
# File 'lib/signalwire/agent/agent_base.rb', line 859

def set_global_data(data)
  @global_data.merge!(data) if data.is_a?(Hash)
  self
end

#set_internal_fillers(fillers) ⇒ Object

Set internal fillers for native SWAIG functions.

Internal fillers are short phrases the AI agent speaks (via TTS) while an internal/native function is running, so the caller doesn’t hear dead air during transitions or background work.

Supported function names (match the SWAIGInternalFiller schema): hangup, check_time, wait_for_user, wait_seconds, adjust_response_latency, next_step, change_context, get_visual_input, get_ideal_strategy. See SUPPORTED_INTERNAL_FILLER_NAMES.

Notably NOT supported: change_step, gather_submit, or arbitrary user-defined SWAIG function names. The runtime only honors fillers for the names listed above; everything else is silently ignored at the SWML level. This method warns at registration time if you pass an unknown name so you catch the typo early.

Expected format: { function_name => { language_code => [phrases] } }



914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
# File 'lib/signalwire/agent/agent_base.rb', line 914

def set_internal_fillers(fillers)
  if fillers.is_a?(Hash)
    unknown = (fillers.keys.map(&:to_s) - SUPPORTED_INTERNAL_FILLER_NAMES).sort
    if unknown.any?
      @logger.warn(
        "unknown_internal_filler_names: #{unknown.inspect}. " \
        "set_internal_fillers received names that the SWML schema " \
        "does not recognize. Those entries will be ignored by the " \
        "runtime. Supported names: #{SUPPORTED_INTERNAL_FILLER_NAMES.sort.inspect}."
      )
    end
    @internal_fillers.merge!(fillers)
  end
  self
end

#set_language_params(code, params) ⇒ self

Set (or replace) the per-language “params“ dict on an already-added language. Useful when language entries are built up via add_language first and engine-specific tuning is added later (e.g. from a config loader). Returns self for chaining.

Parameters:

  • code (String)

    language code as previously passed to “add_language“ (e.g. ““en-US”“).

  • params (Hash)

    engine-specific params hash to attach. Empty hash removes the key.

Returns:

  • (self)

    No-op if the code isn’t found.



804
805
806
807
808
809
810
811
812
813
814
815
# File 'lib/signalwire/agent/agent_base.rb', line 804

def set_language_params(code, params)
  @languages.each do |lang|
    next unless lang.is_a?(Hash) && lang['code'] == code
    if params.is_a?(Hash) && !params.empty?
      lang['params'] = params
    else
      lang.delete('params')
    end
    break
  end
  self
end

#set_languages(languages) ⇒ Object



830
831
832
833
# File 'lib/signalwire/agent/agent_base.rb', line 830

def set_languages(languages)
  @languages = languages.dup if languages.is_a?(Array)
  self
end

#set_native_functions(names) ⇒ Object



868
869
870
871
# File 'lib/signalwire/agent/agent_base.rb', line 868

def set_native_functions(names)
  @native_functions = names.dup if names.is_a?(Array)
  self
end

#set_param(key, value) ⇒ Object



847
848
849
850
# File 'lib/signalwire/agent/agent_base.rb', line 847

def set_param(key, value)
  @params[key.to_s] = value
  self
end

#set_params(params) ⇒ Object



852
853
854
855
856
857
# File 'lib/signalwire/agent/agent_base.rb', line 852

def set_params(params)
  if params.is_a?(Hash)
    params.each { |k, v| @params[k.to_s] = v }
  end
  self
end

#set_post_prompt(text) ⇒ Object

Set post-prompt text.



243
244
245
246
# File 'lib/signalwire/agent/agent_base.rb', line 243

def set_post_prompt(text)
  @post_prompt_text = text
  self
end

#set_post_prompt_llm_params(**params) ⇒ Object



976
977
978
979
# File 'lib/signalwire/agent/agent_base.rb', line 976

def set_post_prompt_llm_params(**params)
  @post_prompt_llm_params.merge!(params.transform_keys(&:to_s))
  self
end

#set_post_prompt_url(url) ⇒ Object



1183
1184
1185
1186
# File 'lib/signalwire/agent/agent_base.rb', line 1183

def set_post_prompt_url(url)
  @post_prompt_url_override = url
  self
end

#set_prompt_llm_params(**params) ⇒ Object



971
972
973
974
# File 'lib/signalwire/agent/agent_base.rb', line 971

def set_prompt_llm_params(**params)
  @prompt_llm_params.merge!(params.transform_keys(&:to_s))
  self
end

#set_prompt_pom(pom) ⇒ Object

Set POM array directly.



249
250
251
252
253
254
# File 'lib/signalwire/agent/agent_base.rb', line 249

def set_prompt_pom(pom)
  @prompt_pom   = pom
  @prompt_text  = nil
  @pom_sections = []
  self
end

#set_prompt_text(text) ⇒ Object

Set prompt as raw text. Clears any POM state.



235
236
237
238
239
240
# File 'lib/signalwire/agent/agent_base.rb', line 235

def set_prompt_text(text)
  @prompt_text  = text
  @pom_sections = []
  @prompt_pom   = nil
  self
end

#set_pronunciations(pronunciations) ⇒ Object



842
843
844
845
# File 'lib/signalwire/agent/agent_base.rb', line 842

def set_pronunciations(pronunciations)
  @pronounce = pronunciations.dup if pronunciations.is_a?(Array)
  self
end

#set_web_hook_url(url) ⇒ Object



1178
1179
1180
1181
# File 'lib/signalwire/agent/agent_base.rb', line 1178

def set_web_hook_url(url)
  @web_hook_url_override = url
  self
end

#update_global_data(data) ⇒ Object



864
865
866
# File 'lib/signalwire/agent/agent_base.rb', line 864

def update_global_data(data)
  set_global_data(data)
end

#validate_tool_token(function_name, token, call_id) ⇒ Object

Validate a per-call SWAIG-function token. Returns false when the function is not registered, when the SessionManager rejects the token, or on any underlying exception.

Python parity: state_mixin.StateMixin#validate_tool_token —rejects unknown function names up-front and rescues exceptions.



599
600
601
602
603
604
# File 'lib/signalwire/agent/agent_base.rb', line 599

def validate_tool_token(function_name, token, call_id)
  return false unless has_function(function_name)
  @session_manager.validate_token(function_name, token, call_id)
rescue StandardError
  false
end