signalwire_agents 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +79 -0
  3. data/bin/swaig-test +310 -0
  4. data/lib/signalwire_agents/agent/agent_base.rb +1171 -0
  5. data/lib/signalwire_agents/contexts/context_builder.rb +622 -0
  6. data/lib/signalwire_agents/datamap/data_map.rb +279 -0
  7. data/lib/signalwire_agents/logging.rb +92 -0
  8. data/lib/signalwire_agents/prefabs/concierge.rb +92 -0
  9. data/lib/signalwire_agents/prefabs/faq_bot.rb +67 -0
  10. data/lib/signalwire_agents/prefabs/info_gatherer.rb +79 -0
  11. data/lib/signalwire_agents/prefabs/receptionist.rb +74 -0
  12. data/lib/signalwire_agents/prefabs/survey.rb +75 -0
  13. data/lib/signalwire_agents/relay/action.rb +291 -0
  14. data/lib/signalwire_agents/relay/call.rb +523 -0
  15. data/lib/signalwire_agents/relay/client.rb +671 -0
  16. data/lib/signalwire_agents/relay/constants.rb +124 -0
  17. data/lib/signalwire_agents/relay/message.rb +137 -0
  18. data/lib/signalwire_agents/relay/relay_event.rb +670 -0
  19. data/lib/signalwire_agents/rest/http_client.rb +147 -0
  20. data/lib/signalwire_agents/rest/namespaces/addresses.rb +19 -0
  21. data/lib/signalwire_agents/rest/namespaces/calling.rb +179 -0
  22. data/lib/signalwire_agents/rest/namespaces/chat.rb +18 -0
  23. data/lib/signalwire_agents/rest/namespaces/compat.rb +229 -0
  24. data/lib/signalwire_agents/rest/namespaces/datasphere.rb +39 -0
  25. data/lib/signalwire_agents/rest/namespaces/fabric.rb +175 -0
  26. data/lib/signalwire_agents/rest/namespaces/imported_numbers.rb +18 -0
  27. data/lib/signalwire_agents/rest/namespaces/logs.rb +46 -0
  28. data/lib/signalwire_agents/rest/namespaces/lookup.rb +18 -0
  29. data/lib/signalwire_agents/rest/namespaces/mfa.rb +26 -0
  30. data/lib/signalwire_agents/rest/namespaces/number_groups.rb +32 -0
  31. data/lib/signalwire_agents/rest/namespaces/phone_numbers.rb +20 -0
  32. data/lib/signalwire_agents/rest/namespaces/project.rb +33 -0
  33. data/lib/signalwire_agents/rest/namespaces/pubsub.rb +18 -0
  34. data/lib/signalwire_agents/rest/namespaces/queues.rb +28 -0
  35. data/lib/signalwire_agents/rest/namespaces/recordings.rb +18 -0
  36. data/lib/signalwire_agents/rest/namespaces/registry.rb +67 -0
  37. data/lib/signalwire_agents/rest/namespaces/short_codes.rb +26 -0
  38. data/lib/signalwire_agents/rest/namespaces/sip_profile.rb +22 -0
  39. data/lib/signalwire_agents/rest/namespaces/verified_callers.rb +24 -0
  40. data/lib/signalwire_agents/rest/namespaces/video.rb +129 -0
  41. data/lib/signalwire_agents/rest/signalwire_client.rb +110 -0
  42. data/lib/signalwire_agents/security/session_manager.rb +124 -0
  43. data/lib/signalwire_agents/server/agent_server.rb +260 -0
  44. data/lib/signalwire_agents/skills/builtin/api_ninjas_trivia.rb +91 -0
  45. data/lib/signalwire_agents/skills/builtin/claude_skills.rb +92 -0
  46. data/lib/signalwire_agents/skills/builtin/custom_skills.rb +54 -0
  47. data/lib/signalwire_agents/skills/builtin/datasphere.rb +141 -0
  48. data/lib/signalwire_agents/skills/builtin/datasphere_serverless.rb +107 -0
  49. data/lib/signalwire_agents/skills/builtin/datetime.rb +97 -0
  50. data/lib/signalwire_agents/skills/builtin/google_maps.rb +168 -0
  51. data/lib/signalwire_agents/skills/builtin/info_gatherer.rb +189 -0
  52. data/lib/signalwire_agents/skills/builtin/joke.rb +65 -0
  53. data/lib/signalwire_agents/skills/builtin/math.rb +176 -0
  54. data/lib/signalwire_agents/skills/builtin/mcp_gateway.rb +121 -0
  55. data/lib/signalwire_agents/skills/builtin/native_vector_search.rb +116 -0
  56. data/lib/signalwire_agents/skills/builtin/play_background_file.rb +86 -0
  57. data/lib/signalwire_agents/skills/builtin/spider.rb +142 -0
  58. data/lib/signalwire_agents/skills/builtin/swml_transfer.rb +118 -0
  59. data/lib/signalwire_agents/skills/builtin/weather_api.rb +85 -0
  60. data/lib/signalwire_agents/skills/builtin/web_search.rb +123 -0
  61. data/lib/signalwire_agents/skills/builtin/wikipedia_search.rb +109 -0
  62. data/lib/signalwire_agents/skills/skill_base.rb +58 -0
  63. data/lib/signalwire_agents/skills/skill_manager.rb +85 -0
  64. data/lib/signalwire_agents/skills/skill_registry.rb +76 -0
  65. data/lib/signalwire_agents/swaig/function_result.rb +777 -0
  66. data/lib/signalwire_agents/swml/document.rb +84 -0
  67. data/lib/signalwire_agents/swml/schema.json +12250 -0
  68. data/lib/signalwire_agents/swml/schema.rb +81 -0
  69. data/lib/signalwire_agents/swml/service.rb +304 -0
  70. data/lib/signalwire_agents/version.rb +5 -0
  71. data/lib/signalwire_agents.rb +19 -0
  72. metadata +212 -0
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ require_relative '../skill_base'
8
+ require_relative '../skill_registry'
9
+
10
+ module SignalWireAgents
11
+ module Skills
12
+ module Builtin
13
+ class WebSearchSkill < SkillBase
14
+ def name; 'web_search'; end
15
+ def description; 'Search the web for information using Google Custom Search API'; end
16
+ def version; '2.0.0'; end
17
+ def supports_multiple_instances?; true; end
18
+
19
+ def setup
20
+ @api_key = get_param('api_key', env_var: 'GOOGLE_SEARCH_API_KEY')
21
+ @search_engine_id = get_param('search_engine_id', env_var: 'GOOGLE_SEARCH_ENGINE_ID')
22
+ @num_results = (get_param('num_results', default: 3)).to_i
23
+ @tool_name = get_param('tool_name', default: 'web_search')
24
+ @no_results_msg = get_param('no_results_message',
25
+ default: "I couldn't find quality results for that query. Try rephrasing your search.")
26
+
27
+ return false unless @api_key && !@api_key.empty?
28
+ return false unless @search_engine_id && !@search_engine_id.empty?
29
+ true
30
+ end
31
+
32
+ def instance_key; "web_search_#{@tool_name}"; end
33
+
34
+ def register_tools
35
+ [
36
+ {
37
+ name: @tool_name,
38
+ description: 'Search the web for high-quality information, automatically filtering low-quality results',
39
+ parameters: {
40
+ 'query' => { 'type' => 'string', 'description' => 'The search query - what you want to find information about' }
41
+ },
42
+ handler: method(:handle_search)
43
+ }
44
+ ]
45
+ end
46
+
47
+ def get_global_data
48
+ { 'web_search_enabled' => true, 'search_provider' => 'Google Custom Search', 'quality_filtering' => true }
49
+ end
50
+
51
+ def get_prompt_sections
52
+ [
53
+ {
54
+ 'title' => 'Web Search Capability (Quality Enhanced)',
55
+ 'body' => "You can search the internet for high-quality information using the #{@tool_name} tool.",
56
+ 'bullets' => [
57
+ "Use the #{@tool_name} tool when users ask for information you need to look up",
58
+ 'The search automatically filters out low-quality results like empty pages',
59
+ 'Results are ranked by content quality, relevance, and domain reputation',
60
+ 'Summarize the high-quality results in a clear, helpful way'
61
+ ]
62
+ }
63
+ ]
64
+ end
65
+
66
+ def get_parameter_schema
67
+ {
68
+ 'api_key' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'GOOGLE_SEARCH_API_KEY' },
69
+ 'search_engine_id' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'GOOGLE_SEARCH_ENGINE_ID' },
70
+ 'num_results' => { 'type' => 'integer', 'default' => 3, 'min' => 1, 'max' => 10 },
71
+ 'no_results_message' => { 'type' => 'string' }
72
+ }
73
+ end
74
+
75
+ private
76
+
77
+ def handle_search(args, _raw_data)
78
+ query = (args['query'] || '').strip
79
+ if query.empty?
80
+ return Swaig::FunctionResult.new('Please provide a search query. What would you like me to search for?')
81
+ end
82
+
83
+ begin
84
+ results = google_search(query, @num_results)
85
+ if results.empty?
86
+ return Swaig::FunctionResult.new(@no_results_msg)
87
+ end
88
+
89
+ formatted = results.map.with_index(1) do |r, i|
90
+ "=== RESULT #{i} ===\nTitle: #{r['title']}\nURL: #{r['url']}\nSnippet: #{r['snippet']}\n#{'=' * 50}"
91
+ end.join("\n\n")
92
+
93
+ Swaig::FunctionResult.new("Web search results for '#{query}':\n\n#{formatted}")
94
+ rescue => e
95
+ Swaig::FunctionResult.new("Sorry, I encountered an error while searching: #{e.message}")
96
+ end
97
+ end
98
+
99
+ def google_search(query, num)
100
+ uri = URI('https://www.googleapis.com/customsearch/v1')
101
+ uri.query = URI.encode_www_form(
102
+ key: @api_key,
103
+ cx: @search_engine_id,
104
+ q: query,
105
+ num: [num, 10].min
106
+ )
107
+
108
+ response = Net::HTTP.get_response(uri)
109
+ return [] unless response.is_a?(Net::HTTPSuccess)
110
+
111
+ data = JSON.parse(response.body)
112
+ (data['items'] || []).first(num).map do |item|
113
+ { 'title' => item['title'] || '', 'url' => item['link'] || '', 'snippet' => item['snippet'] || '' }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ SignalWireAgents::Skills::SkillRegistry.register('web_search') do |params|
122
+ SignalWireAgents::Skills::Builtin::WebSearchSkill.new(params)
123
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ require_relative '../skill_base'
8
+ require_relative '../skill_registry'
9
+
10
+ module SignalWireAgents
11
+ module Skills
12
+ module Builtin
13
+ class WikipediaSearchSkill < SkillBase
14
+ def name; 'wikipedia_search'; end
15
+ def description; 'Search Wikipedia for information about a topic and get article summaries'; end
16
+
17
+ def setup
18
+ @num_results = [1, (get_param('num_results', default: 1)).to_i].max
19
+ @no_results_msg = get_param('no_results_message',
20
+ default: "I couldn't find any Wikipedia articles for that query. Try rephrasing your search or using different keywords.")
21
+ true
22
+ end
23
+
24
+ def register_tools
25
+ [
26
+ {
27
+ name: 'search_wiki',
28
+ description: 'Search Wikipedia for information about a topic and get article summaries',
29
+ parameters: {
30
+ 'query' => { 'type' => 'string', 'description' => 'The search term or topic to look up on Wikipedia' }
31
+ },
32
+ handler: method(:handle_search)
33
+ }
34
+ ]
35
+ end
36
+
37
+ def get_prompt_sections
38
+ [
39
+ {
40
+ 'title' => 'Wikipedia Search',
41
+ 'body' => "You can search Wikipedia for factual information using search_wiki. This will return up to #{@num_results || 1} Wikipedia article summaries.",
42
+ 'bullets' => [
43
+ 'Use search_wiki for factual, encyclopedic information',
44
+ 'Great for answering questions about people, places, concepts, and history',
45
+ 'Returns reliable, well-sourced information from Wikipedia articles'
46
+ ]
47
+ }
48
+ ]
49
+ end
50
+
51
+ def get_parameter_schema
52
+ {
53
+ 'num_results' => { 'type' => 'integer', 'default' => 1, 'min' => 1, 'max' => 5 },
54
+ 'no_results_message' => { 'type' => 'string' }
55
+ }
56
+ end
57
+
58
+ private
59
+
60
+ def handle_search(args, _raw_data)
61
+ query = (args['query'] || '').strip
62
+ if query.empty?
63
+ return Swaig::FunctionResult.new('Please provide a search query for Wikipedia.')
64
+ end
65
+
66
+ begin
67
+ result = search_wiki(query)
68
+ Swaig::FunctionResult.new(result)
69
+ rescue => e
70
+ Swaig::FunctionResult.new("Error searching Wikipedia: #{e.message}")
71
+ end
72
+ end
73
+
74
+ def search_wiki(query)
75
+ # Step 1: Search
76
+ search_uri = URI("https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=#{URI.encode_www_form_component(query)}&srlimit=#{@num_results}")
77
+ search_resp = Net::HTTP.get_response(search_uri)
78
+ return @no_results_msg unless search_resp.is_a?(Net::HTTPSuccess)
79
+
80
+ search_data = JSON.parse(search_resp.body)
81
+ results = search_data.dig('query', 'search') || []
82
+ return @no_results_msg if results.empty?
83
+
84
+ # Step 2: Get extracts
85
+ articles = results.first(@num_results).filter_map do |r|
86
+ title = r['title']
87
+ extract_uri = URI("https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&titles=#{URI.encode_www_form_component(title)}")
88
+ extract_resp = Net::HTTP.get_response(extract_uri)
89
+ next unless extract_resp.is_a?(Net::HTTPSuccess)
90
+
91
+ pages = JSON.parse(extract_resp.body).dig('query', 'pages') || {}
92
+ page = pages.values.first
93
+ extract = page&.dig('extract')&.strip
94
+ next if extract.nil? || extract.empty?
95
+
96
+ "**#{title}**\n\n#{extract}"
97
+ end
98
+
99
+ return @no_results_msg if articles.empty?
100
+ articles.join("\n\n#{'=' * 50}\n\n")
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ SignalWireAgents::Skills::SkillRegistry.register('wikipedia_search') do |params|
108
+ SignalWireAgents::Skills::Builtin::WikipediaSearchSkill.new(params)
109
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 SignalWire
4
+ #
5
+ # Licensed under the MIT License.
6
+ # See LICENSE file in the project root for full license information.
7
+
8
+ require_relative '../swaig/function_result'
9
+
10
+ module SignalWireAgents
11
+ module Skills
12
+ # Base class for all skills. Subclasses override the metadata methods
13
+ # and +register_tools+ to supply tool hashes.
14
+ class SkillBase
15
+ attr_reader :params
16
+
17
+ def name; raise NotImplementedError, "#{self.class}#name"; end
18
+ def description; raise NotImplementedError, "#{self.class}#description"; end
19
+ def version; '1.0.0'; end
20
+ def required_env_vars; []; end
21
+ def supports_multiple_instances?; false; end
22
+
23
+ def initialize(params = {})
24
+ @params = (params || {}).transform_keys(&:to_s)
25
+ end
26
+
27
+ # Called once after construction. Return +true+ if the skill is ready.
28
+ def setup; true; end
29
+
30
+ # Return an Array of tool definition hashes. Each hash should have:
31
+ # :name, :description, :parameters, :handler (lambda/proc)
32
+ def register_tools; []; end
33
+
34
+ # Speech recognition hints.
35
+ def get_hints; []; end
36
+
37
+ # Global data to merge into the agent.
38
+ def get_global_data; {}; end
39
+
40
+ # Prompt sections to add to the agent.
41
+ def get_prompt_sections; []; end
42
+
43
+ # Called when the skill is unloaded.
44
+ def cleanup; end
45
+
46
+ # Unique key for tracking this skill instance.
47
+ def instance_key; name; end
48
+
49
+ # Parameter schema for GUI / validation.
50
+ def get_parameter_schema; {}; end
51
+
52
+ # Helper to get a param with env-var fallback.
53
+ def get_param(key, env_var: nil, default: nil)
54
+ @params[key.to_s] || @params[key.to_sym.to_s] || (env_var && ENV[env_var]) || default
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 SignalWire
4
+ #
5
+ # Licensed under the MIT License.
6
+ # See LICENSE file in the project root for full license information.
7
+
8
+ require 'thread'
9
+ require_relative 'skill_base'
10
+
11
+ module SignalWireAgents
12
+ module Skills
13
+ # Thread-safe lifecycle manager for loaded skill instances.
14
+ #
15
+ # manager = SkillManager.new
16
+ # manager.load('datetime', DateTimeSkill.new)
17
+ # manager.get('datetime') #=> <DateTimeSkill>
18
+ # manager.unload('datetime')
19
+ #
20
+ class SkillManager
21
+ def initialize
22
+ @skills = {} # instance_key => SkillBase instance
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ # Load a skill instance. Calls +setup+ on the skill; raises if it fails.
27
+ # @param key [String] the instance key
28
+ # @param skill [SkillBase] the skill instance
29
+ # @return [SkillBase] the loaded skill
30
+ def load(key, skill)
31
+ @mutex.synchronize do
32
+ raise ArgumentError, "Skill already loaded: #{key}" if @skills.key?(key)
33
+
34
+ unless skill.setup
35
+ raise "Skill setup failed for '#{key}'"
36
+ end
37
+
38
+ @skills[key] = skill
39
+ end
40
+ skill
41
+ end
42
+
43
+ # Unload a skill by instance key. Calls +cleanup+ on it.
44
+ # @param key [String]
45
+ # @return [SkillBase, nil] the removed skill, or nil
46
+ def unload(key)
47
+ @mutex.synchronize do
48
+ skill = @skills.delete(key)
49
+ skill&.cleanup
50
+ skill
51
+ end
52
+ end
53
+
54
+ # Retrieve a loaded skill.
55
+ # @param key [String]
56
+ # @return [SkillBase, nil]
57
+ def get(key)
58
+ @mutex.synchronize { @skills[key] }
59
+ end
60
+
61
+ # @return [Boolean]
62
+ def loaded?(key)
63
+ @mutex.synchronize { @skills.key?(key) }
64
+ end
65
+
66
+ # @return [Array<String>]
67
+ def loaded_keys
68
+ @mutex.synchronize { @skills.keys.dup }
69
+ end
70
+
71
+ # @return [Integer]
72
+ def size
73
+ @mutex.synchronize { @skills.size }
74
+ end
75
+
76
+ # Unload all skills.
77
+ def clear
78
+ @mutex.synchronize do
79
+ @skills.each_value(&:cleanup)
80
+ @skills.clear
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 SignalWire
4
+ #
5
+ # Licensed under the MIT License.
6
+ # See LICENSE file in the project root for full license information.
7
+
8
+ require 'thread'
9
+
10
+ module SignalWireAgents
11
+ module Skills
12
+ # Global registry mapping skill names to factory lambdas.
13
+ #
14
+ # SkillRegistry.register('datetime') { |params| DateTimeSkill.new(params) }
15
+ # factory = SkillRegistry.get_factory('datetime')
16
+ # skill = factory.call({ 'timezone' => 'UTC' })
17
+ #
18
+ class SkillRegistry
19
+ @factories = {} # skill_name => lambda { |params| SkillBase }
20
+ @mutex = Mutex.new
21
+
22
+ class << self
23
+ # Register a skill factory.
24
+ # @param skill_name [String]
25
+ # @yield [params] block that receives params hash and returns a SkillBase
26
+ def register(skill_name, &block)
27
+ @mutex.synchronize do
28
+ @factories[skill_name.to_s] = block
29
+ end
30
+ end
31
+
32
+ # Register with an explicit lambda / proc instead of a block.
33
+ # @param skill_name [String]
34
+ # @param factory [Proc]
35
+ def register_skill(skill_name, factory)
36
+ @mutex.synchronize do
37
+ @factories[skill_name.to_s] = factory
38
+ end
39
+ end
40
+
41
+ # Get the factory for a skill.
42
+ # @param skill_name [String]
43
+ # @return [Proc, nil]
44
+ def get_factory(skill_name)
45
+ @mutex.synchronize { @factories[skill_name.to_s] }
46
+ end
47
+
48
+ # List all registered skill names.
49
+ # @return [Array<String>]
50
+ def list_skills
51
+ @mutex.synchronize { @factories.keys.dup }
52
+ end
53
+
54
+ # Check if a skill is registered.
55
+ # @param skill_name [String]
56
+ # @return [Boolean]
57
+ def registered?(skill_name)
58
+ @mutex.synchronize { @factories.key?(skill_name.to_s) }
59
+ end
60
+
61
+ # Clear all registrations (primarily for testing).
62
+ def reset!
63
+ @mutex.synchronize { @factories.clear }
64
+ end
65
+
66
+ # Register all built-in skills. Called at load time.
67
+ def register_builtins!
68
+ # Each builtin file calls SkillRegistry.register on require.
69
+ # We just need to require them all.
70
+ builtin_dir = File.join(__dir__, 'builtin')
71
+ Dir[File.join(builtin_dir, '*.rb')].sort.each { |f| require f }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end