signalwire_agents 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +79 -0
- data/bin/swaig-test +310 -0
- data/lib/signalwire_agents/agent/agent_base.rb +1171 -0
- data/lib/signalwire_agents/contexts/context_builder.rb +622 -0
- data/lib/signalwire_agents/datamap/data_map.rb +279 -0
- data/lib/signalwire_agents/logging.rb +92 -0
- data/lib/signalwire_agents/prefabs/concierge.rb +92 -0
- data/lib/signalwire_agents/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire_agents/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire_agents/prefabs/receptionist.rb +74 -0
- data/lib/signalwire_agents/prefabs/survey.rb +75 -0
- data/lib/signalwire_agents/relay/action.rb +291 -0
- data/lib/signalwire_agents/relay/call.rb +523 -0
- data/lib/signalwire_agents/relay/client.rb +671 -0
- data/lib/signalwire_agents/relay/constants.rb +124 -0
- data/lib/signalwire_agents/relay/message.rb +137 -0
- data/lib/signalwire_agents/relay/relay_event.rb +670 -0
- data/lib/signalwire_agents/rest/http_client.rb +147 -0
- data/lib/signalwire_agents/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire_agents/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire_agents/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire_agents/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire_agents/rest/namespaces/fabric.rb +175 -0
- data/lib/signalwire_agents/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire_agents/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire_agents/rest/namespaces/phone_numbers.rb +20 -0
- data/lib/signalwire_agents/rest/namespaces/project.rb +33 -0
- data/lib/signalwire_agents/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire_agents/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire_agents/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire_agents/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire_agents/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire_agents/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire_agents/rest/namespaces/video.rb +129 -0
- data/lib/signalwire_agents/rest/signalwire_client.rb +110 -0
- data/lib/signalwire_agents/security/session_manager.rb +124 -0
- data/lib/signalwire_agents/server/agent_server.rb +260 -0
- data/lib/signalwire_agents/skills/builtin/api_ninjas_trivia.rb +91 -0
- data/lib/signalwire_agents/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire_agents/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire_agents/skills/builtin/datasphere.rb +141 -0
- data/lib/signalwire_agents/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire_agents/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire_agents/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire_agents/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire_agents/skills/builtin/joke.rb +65 -0
- data/lib/signalwire_agents/skills/builtin/math.rb +176 -0
- data/lib/signalwire_agents/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire_agents/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire_agents/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire_agents/skills/builtin/spider.rb +142 -0
- data/lib/signalwire_agents/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire_agents/skills/builtin/weather_api.rb +85 -0
- data/lib/signalwire_agents/skills/builtin/web_search.rb +123 -0
- data/lib/signalwire_agents/skills/builtin/wikipedia_search.rb +109 -0
- data/lib/signalwire_agents/skills/skill_base.rb +58 -0
- data/lib/signalwire_agents/skills/skill_manager.rb +85 -0
- data/lib/signalwire_agents/skills/skill_registry.rb +76 -0
- data/lib/signalwire_agents/swaig/function_result.rb +777 -0
- data/lib/signalwire_agents/swml/document.rb +84 -0
- data/lib/signalwire_agents/swml/schema.json +12250 -0
- data/lib/signalwire_agents/swml/schema.rb +81 -0
- data/lib/signalwire_agents/swml/service.rb +304 -0
- data/lib/signalwire_agents/version.rb +5 -0
- data/lib/signalwire_agents.rb +19 -0
- metadata +212 -0
|
@@ -0,0 +1,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
|