Class: SignalWire::SWML::Service
- Inherits:
-
Object
- Object
- SignalWire::SWML::Service
- Defined in:
- lib/signalwire/swml/service.rb
Direct Known Subclasses
Defined Under Namespace
Classes: SecurityHeadersMiddleware, TimingSafeBasicAuth
Constant Summary collapse
- SWAIG_FN_NAME =
Maximum request body size enforced on /swaig and the main route (1 MB).
/\A[a-zA-Z_][a-zA-Z0-9_]*\z/.freeze
Instance Attribute Summary collapse
-
#config_file ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#host ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#name ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#port ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#route ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#schema_path ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
-
#schema_validation ⇒ Object
readonly
Python parity: - “name“, “route“, “host“, “port“ — surface from SWMLService.
Instance Method Summary collapse
-
#define_tool(name:, description:, parameters: {}, secure: false, &handler) ⇒ Object
Define a SWAIG function the AI can call.
-
#define_tools ⇒ Object
Return an array of all tool definitions (for SWML rendering).
-
#document ⇒ Object
Expose the underlying document (useful for tests and subclasses).
-
#execute_verb(verb_name, args = [], kwargs = {}) ⇒ Object
Execute a SWML verb, adding it to the current document.
-
#get_all_functions ⇒ Object
Snapshot of all registered SWAIG functions keyed by name.
-
#get_basic_auth_credentials(include_source: false) ⇒ Array(String, String)
Get the configured basic-auth credentials.
-
#get_basic_auth_credentials_with_source ⇒ Array(String, String, String)
Backwards-compat alias for the legacy 3-tuple-only form.
-
#get_full_url(include_auth: false) ⇒ Object
Build the full URL for this service.
-
#get_function(name) ⇒ Object
Get a registered SWAIG function by name, or nil when absent.
-
#handle_additional_route(_sub_path, _request_data, _env) ⇒ Array?
Extension point: register additional Rack routes after Service mounts /health, /ready, /swaig, and the main route.
-
#has_function(name) ⇒ Object
Whether a SWAIG function with the given name is registered.
-
#initialize(name:, route: '/', host: '0.0.0.0', port: nil, basic_auth: nil, schema_path: nil, config_file: nil, schema_validation: true) ⇒ Service
constructor
A new instance of Service.
-
#list_tool_names ⇒ Object
List registered SWAIG tool names in registration order.
-
#method_missing(method_name, *args, **kwargs) ⇒ Object
—————————————————————— Verb auto-vivification via method_missing ——————————————————————.
-
#on_function_call(name, args, raw_data) ⇒ Object
Dispatch a function call to the registered handler.
-
#on_request(request_data = nil, callback_path = nil, request: nil) ⇒ Object
Customization hook called when SWML is requested.
-
#on_swml_request(request_data = nil, callback_path = nil, request: nil) ⇒ Object
Customization point for subclasses to modify SWML based on request data.
-
#rack_app ⇒ Object
Returns a Rack-compatible application.
-
#register_routing_callback(path, &block) ⇒ Object
—————————————————————— Routing callbacks & request handling ——————————————————————.
-
#register_swaig_function(func_def) ⇒ Object
Register a raw SWAIG function definition (e.g. from DataMap#to_swaig_function).
-
#remove_function(name) ⇒ Object
Remove a registered SWAIG function.
-
#render ⇒ Object
—————————————————————— Render the current SWML document ——————————————————————.
-
#render_main_swml(_request_data = nil, request: nil) ⇒ Object
Extension point: handle GET /swaig (returns the SWML document by default).
- #render_pretty ⇒ Object
- #respond_to_missing?(method_name, include_private = false) ⇒ Boolean
-
#schema_utils ⇒ Object
SchemaUtils helper bound to this Service.
-
#serve(host: nil, port: nil, ssl_cert: nil, ssl_key: nil, ssl_enabled: nil, domain: nil) ⇒ Object
Start serving (blocking).
-
#stop ⇒ Object
Gracefully stop the server.
-
#swaig_pre_dispatch(_request_data, _func_name, _env) ⇒ Object
Extension point: invoked between argument parsing and function dispatch on POST /swaig.
-
#validate_basic_auth(username, password) ⇒ Object
Validate provided basic-auth credentials against the configured ones using a constant-time comparison.
Constructor Details
#initialize(name:, route: '/', host: '0.0.0.0', port: nil, basic_auth: nil, schema_path: nil, config_file: nil, schema_validation: true) ⇒ Service
Returns a new instance of Service.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/signalwire/swml/service.rb', line 34 def initialize(name:, route: '/', host: '0.0.0.0', port: nil, basic_auth: nil, schema_path: nil, config_file: nil, schema_validation: true) @name = name @route = route.chomp('/') @route = '/' if @route.empty? @host = host @port = port || Integer(ENV.fetch('PORT', 3000)) @log = Logging.logger("SWML::Service[#{name}]") @document = Document.new @routing_callbacks = {} @server = nil # Python parity: # - ``schema_path`` — explicit path to the SWML schema file. # When nil we fall back to the schema bundled with the gem # via SWML::Schema. # - ``config_file`` — TOML/YAML configuration override file # (Python's ``ConfigLoader``). Ruby v1 stashes the path; the # loader is wired by AgentBase only when needed. # - ``schema_validation`` — when true (default), out-bound SWML # is validated against the schema. ``SWML_SKIP_SCHEMA_VALIDATION=1`` # env var overrides to false (Python parity). @schema_path = schema_path @config_file = config_file @schema_validation = schema_validation && ENV['SWML_SKIP_SCHEMA_VALIDATION'] != '1' # SWAIG tool registry — lifted from AgentBase so any Service (sidecar, # non-agent verb host) can register and dispatch SWAIG functions. @tools = {} # name => { definition + handler } @swaig_functions = {} # name => raw hash (DataMap etc.) # --- auth -------------------------------------------------------- @basic_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 [SecureRandom.uuid, SecureRandom.uuid] end @log.info "Service '#{@name}' initialised (route=#{@route}, port=#{@port})" end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_name, *args, **kwargs) ⇒ Object
Verb auto-vivification via method_missing
198 199 200 201 202 203 204 205 206 |
# File 'lib/signalwire/swml/service.rb', line 198 def method_missing(method_name, *args, **kwargs) verb = method_name.to_s if SWML.schema.valid_verb?(verb) execute_verb(verb, args, kwargs) else super end end |
Instance Attribute Details
#config_file ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def config_file @config_file end |
#host ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def host @host end |
#name ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def name @name end |
#port ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def port @port end |
#route ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def route @route end |
#schema_path ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def schema_path @schema_path end |
#schema_validation ⇒ Object (readonly)
Python parity:
-
“name“, “route“, “host“, “port“ — surface from SWMLService.
-
“schema_path“ — path to the SWML schema file (or nil to use the gem-bundled default).
-
“config_file“ — optional TOML/YAML config file path.
-
“schema_validation“ — boolean flag mirroring Python’s “self._schema_validation“. “SWML_SKIP_SCHEMA_VALIDATION=1“ env var forces this to false.
23 24 25 |
# File 'lib/signalwire/swml/service.rb', line 23 def schema_validation @schema_validation end |
Instance Method Details
#define_tool(name:, description:, parameters: {}, secure: false, &handler) ⇒ Object
Define a SWAIG function the AI can call. Tool descriptions and parameter descriptions are LLM-facing prompt engineering — see PORTING_GUIDE for guidance.
84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/signalwire/swml/service.rb', line 84 def define_tool(name:, description:, parameters: {}, secure: false, &handler) @tools[name] = { definition: { 'function' => name, 'description' => description, 'parameters' => parameters, }, handler: handler, secure: secure, } self end |
#define_tools ⇒ Object
Return an array of all tool definitions (for SWML rendering).
106 107 108 109 |
# File 'lib/signalwire/swml/service.rb', line 106 def define_tools defs = @tools.values.map { |t| t[:definition].dup } defs + @swaig_functions.values.map(&:dup) end |
#document ⇒ Object
Expose the underlying document (useful for tests and subclasses).
352 353 354 |
# File 'lib/signalwire/swml/service.rb', line 352 def document @document end |
#execute_verb(verb_name, args = [], kwargs = {}) ⇒ Object
Execute a SWML verb, adding it to the current document.
For most verbs the config is a keyword-args Hash. The sleep verb is special: it also accepts a bare Integer.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/signalwire/swml/service.rb', line 216 def execute_verb(verb_name, args = [], kwargs = {}) verb_name = verb_name.to_s if verb_name == 'sleep' # Accept sleep(2000) or sleep(duration: 2000) value = if args.length == 1 && args.first.is_a?(Integer) args.first elsif kwargs.key?(:duration) kwargs[:duration] elsif !kwargs.empty? kwargs.values.first else raise ArgumentError, "sleep requires an integer duration" end @document.add_verb(verb_name, value) else config = kwargs.transform_keys(&:to_s).reject { |_, v| v.nil? } @document.add_verb(verb_name, config) end end |
#get_all_functions ⇒ Object
Snapshot of all registered SWAIG functions keyed by name. (Python parity: ToolRegistry#get_all_functions.)
145 146 147 148 149 150 |
# File 'lib/signalwire/swml/service.rb', line 145 def get_all_functions out = {} @tools.each { |k, v| out[k] = v } @swaig_functions.each { |k, v| out[k] = v } out end |
#get_basic_auth_credentials(include_source: false) ⇒ Array(String, String)
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]“ where “source“ is one of ““environment”“, ““auto-generated”“, or ““provided”“. Otherwise returns the 2-tuple “[user, pass]“.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/signalwire/swml/service.rb', line 251 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_basic_auth_credentials_with_source ⇒ Array(String, String, String)
Backwards-compat alias for the legacy 3-tuple-only form.
284 285 286 |
# File 'lib/signalwire/swml/service.rb', line 284 def get_basic_auth_credentials_with_source get_basic_auth_credentials(include_source: true) end |
#get_full_url(include_auth: false) ⇒ Object
Build the full URL for this service.
get_full_url # => "http://0.0.0.0:3000/"
get_full_url(include_auth: true) # => "http://user:pass@0.0.0.0:3000/"
292 293 294 295 296 297 |
# File 'lib/signalwire/swml/service.rb', line 292 def get_full_url(include_auth: false) scheme = 'http' auth = include_auth ? "#{@basic_auth[0]}:#{@basic_auth[1]}@" : '' path = @route == '/' ? '/' : @route "#{scheme}://#{auth}#{@host}:#{@port}#{path}" end |
#get_function(name) ⇒ Object
Get a registered SWAIG function by name, or nil when absent. (Python parity: ToolRegistry#get_function.)
139 140 141 |
# File 'lib/signalwire/swml/service.rb', line 139 def get_function(name) @tools[name] || @swaig_functions[name] end |
#handle_additional_route(_sub_path, _request_data, _env) ⇒ Array?
Extension point: register additional Rack routes after Service mounts /health, /ready, /swaig, and the main route. AgentBase uses this to add /post_prompt, /debug_events, /mcp.
190 191 192 |
# File 'lib/signalwire/swml/service.rb', line 190 def handle_additional_route(_sub_path, _request_data, _env) nil end |
#has_function(name) ⇒ Object
Whether a SWAIG function with the given name is registered. (Python parity: ToolRegistry#has_function.)
133 134 135 |
# File 'lib/signalwire/swml/service.rb', line 133 def has_function(name) @tools.key?(name) || @swaig_functions.key?(name) end |
#list_tool_names ⇒ Object
List registered SWAIG tool names in registration order.
127 128 129 |
# File 'lib/signalwire/swml/service.rb', line 127 def list_tool_names @tools.keys end |
#on_function_call(name, args, raw_data) ⇒ Object
Dispatch a function call to the registered handler. Default plain implementation — AgentBase overrides with token validation.
113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/signalwire/swml/service.rb', line 113 def on_function_call(name, args, raw_data) tool = @tools[name] return nil unless tool && tool[:handler] result = tool[:handler].call(args, raw_data) if result.is_a?(Hash) result elsif result.respond_to?(:to_h) && !result.nil? result.to_h else { 'response' => result.to_s } end end |
#on_request(request_data = nil, callback_path = nil, request: nil) ⇒ Object
Customization hook called when SWML is requested. Default delegates to #on_swml_request and returns its result. Subclasses typically override on_swml_request rather than this method.
Return nil to use the default SWML rendering, or a Hash of modifications to merge into the document.
Python parity: WebMixin#on_request(request_data, callback_path). The Python third request argument is FastAPI-specific and intentionally not mirrored. Python parity: “on_request(request_data, callback_path)“. The third Python parameter (“request“) — a FastAPI “Request“ —is propagated through Ruby as the optional “request:“ keyword so subclasses can read query/header info when a Rack-style request is available. Default: delegate to “on_swml_request“.
323 324 325 |
# File 'lib/signalwire/swml/service.rb', line 323 def on_request(request_data = nil, callback_path = nil, request: nil) on_swml_request(request_data, callback_path, request: request) end |
#on_swml_request(request_data = nil, callback_path = nil, request: nil) ⇒ Object
Customization point for subclasses to modify SWML based on request data. The default returns nil (no modification).
Python parity: “on_swml_request(request_data, callback_path, request)“. The “request:“ keyword carries the Rack request (or FastAPI “Request“ analogue) for subclasses that need query params or headers.
335 336 337 |
# File 'lib/signalwire/swml/service.rb', line 335 def on_swml_request(request_data = nil, callback_path = nil, request: nil) nil end |
#rack_app ⇒ Object
Returns a Rack-compatible application.
371 372 373 |
# File 'lib/signalwire/swml/service.rb', line 371 def rack_app @rack_app ||= build_rack_app end |
#register_routing_callback(path, &block) ⇒ Object
Routing callbacks & request handling
303 304 305 |
# File 'lib/signalwire/swml/service.rb', line 303 def register_routing_callback(path, &block) @routing_callbacks[path] = block end |
#register_swaig_function(func_def) ⇒ Object
Register a raw SWAIG function definition (e.g. from DataMap#to_swaig_function).
98 99 100 101 102 103 |
# File 'lib/signalwire/swml/service.rb', line 98 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_function(name) ⇒ Object
Remove a registered SWAIG function. Returns true on success, false when the function was not registered. (Python parity: ToolRegistry#remove_function.)
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/signalwire/swml/service.rb', line 155 def remove_function(name) if @tools.key?(name) @tools.delete(name) true elsif @swaig_functions.key?(name) @swaig_functions.delete(name) true else false end end |
#render ⇒ Object
Render the current SWML document
343 344 345 |
# File 'lib/signalwire/swml/service.rb', line 343 def render @document.render end |
#render_main_swml(_request_data = nil, request: nil) ⇒ Object
Extension point: handle GET /swaig (returns the SWML document by default). AgentBase overrides to render with prompts + dynamic config.
178 179 180 |
# File 'lib/signalwire/swml/service.rb', line 178 def render_main_swml(_request_data = nil, request: nil) @document.to_h end |
#render_pretty ⇒ Object
347 348 349 |
# File 'lib/signalwire/swml/service.rb', line 347 def render_pretty @document.render_pretty end |
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
208 209 210 |
# File 'lib/signalwire/swml/service.rb', line 208 def respond_to_missing?(method_name, include_private = false) SWML.schema.valid_verb?(method_name.to_s) || super end |
#schema_utils ⇒ Object
SchemaUtils helper bound to this Service. Mirrors Python’s self.schema_utils public instance attribute on SWMLService. Built lazily on first access.
359 360 361 362 363 364 |
# File 'lib/signalwire/swml/service.rb', line 359 def schema_utils @schema_utils ||= begin require_relative '../utils/schema_utils' ::SignalWire::Utils::SchemaUtils.new end end |
#serve(host: nil, port: nil, ssl_cert: nil, ssl_key: nil, ssl_enabled: nil, domain: nil) ⇒ Object
Start serving (blocking).
Python parity: “serve(host=None, port=None, ssl_cert=None, ssl_key=None, ssl_enabled=None, domain=None)“. When SSL parameters are supplied the server is started with HTTPS bindings; otherwise plain HTTP. “host“/“port“ overrides default to the constructor-provided values.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
# File 'lib/signalwire/swml/service.rb', line 390 def serve(host: nil, port: nil, ssl_cert: nil, ssl_key: nil, ssl_enabled: nil, domain: nil) require 'webrick' bind_host = host || @host bind_port = port || @port if !ssl_enabled.nil? @ssl_enabled = ssl_enabled end @domain = domain if domain @ssl_cert_path = ssl_cert if ssl_cert @ssl_key_path = ssl_key if ssl_key @log.info "Starting server on #{bind_host}:#{bind_port} ..." user, _pass = @basic_auth @log.info "Basic-auth credentials — user: #{user} password: [REDACTED]" webrick_opts = { Host: bind_host, Port: bind_port, Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN), AccessLog: [] } if @ssl_enabled && @ssl_cert_path && @ssl_key_path require 'webrick/https' require 'openssl' webrick_opts[:SSLEnable] = true webrick_opts[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read(@ssl_cert_path)) webrick_opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(File.read(@ssl_key_path)) end @server = ::WEBrick::HTTPServer.new(**webrick_opts) # 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') { stop } trap('TERM') { stop } @server.start end |
#stop ⇒ Object
Gracefully stop the server.
443 444 445 |
# File 'lib/signalwire/swml/service.rb', line 443 def stop @server&.shutdown end |
#swaig_pre_dispatch(_request_data, _func_name, _env) ⇒ Object
Extension point: invoked between argument parsing and function dispatch on POST /swaig. Returns [target, short_circuit]. If short_circuit is non-nil, it’s returned as the SWAIG response without calling on_function_call. AgentBase overrides to add session-token validation and ephemeral dynamic-config copies.
172 173 174 |
# File 'lib/signalwire/swml/service.rb', line 172 def swaig_pre_dispatch(_request_data, _func_name, _env) [self, nil] end |
#validate_basic_auth(username, password) ⇒ Object
Validate provided basic-auth credentials against the configured ones using a constant-time comparison. Python parity: AuthMixin#validate_basic_auth(username, password).
271 272 273 274 275 276 277 278 279 280 |
# File 'lib/signalwire/swml/service.rb', line 271 def validate_basic_auth(username, password) require 'openssl' u, p = @basic_auth return false if u.nil? || p.nil? OpenSSL.fixed_length_secure_compare(username, u) && OpenSSL.fixed_length_secure_compare(password, p) rescue ArgumentError # fixed_length_secure_compare raises on length mismatch false end |