signalwire-sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +259 -0
- data/bin/swaig-test +872 -0
- data/lib/signalwire/agent/agent_base.rb +2134 -0
- data/lib/signalwire/contexts/context_builder.rb +861 -0
- data/lib/signalwire/core/logging_config.rb +54 -0
- data/lib/signalwire/datamap/data_map.rb +315 -0
- data/lib/signalwire/logging.rb +92 -0
- data/lib/signalwire/pom/prompt_object_model.rb +269 -0
- data/lib/signalwire/pom/section.rb +202 -0
- data/lib/signalwire/prefabs/concierge.rb +92 -0
- data/lib/signalwire/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire/prefabs/receptionist.rb +74 -0
- data/lib/signalwire/prefabs/survey.rb +75 -0
- data/lib/signalwire/relay/action.rb +291 -0
- data/lib/signalwire/relay/call.rb +523 -0
- data/lib/signalwire/relay/client.rb +789 -0
- data/lib/signalwire/relay/constants.rb +124 -0
- data/lib/signalwire/relay/message.rb +137 -0
- data/lib/signalwire/relay/relay_event.rb +670 -0
- data/lib/signalwire/rest/http_client.rb +159 -0
- data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
- data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
- data/lib/signalwire/rest/namespaces/project.rb +33 -0
- data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire/rest/namespaces/video.rb +129 -0
- data/lib/signalwire/rest/pagination.rb +89 -0
- data/lib/signalwire/rest/phone_call_handler.rb +56 -0
- data/lib/signalwire/rest/rest_client.rb +114 -0
- data/lib/signalwire/runtime.rb +98 -0
- data/lib/signalwire/security/session_manager.rb +124 -0
- data/lib/signalwire/security/webhook_middleware.rb +191 -0
- data/lib/signalwire/security/webhook_validator.rb +327 -0
- data/lib/signalwire/server/agent_server.rb +413 -0
- data/lib/signalwire/serverless/lambda_handler.rb +251 -0
- data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
- data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
- data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire/skills/builtin/joke.rb +65 -0
- data/lib/signalwire/skills/builtin/math.rb +176 -0
- data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire/skills/builtin/spider.rb +169 -0
- data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
- data/lib/signalwire/skills/builtin/web_search.rb +141 -0
- data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
- data/lib/signalwire/skills/skill_base.rb +82 -0
- data/lib/signalwire/skills/skill_manager.rb +97 -0
- data/lib/signalwire/skills/skill_registry.rb +258 -0
- data/lib/signalwire/swaig/function_result.rb +777 -0
- data/lib/signalwire/swml/document.rb +84 -0
- data/lib/signalwire/swml/schema.json +12250 -0
- data/lib/signalwire/swml/schema.rb +81 -0
- data/lib/signalwire/swml/service.rb +650 -0
- data/lib/signalwire/utils/schema_utils.rb +298 -0
- data/lib/signalwire/utils/serverless.rb +19 -0
- data/lib/signalwire/utils/url_validator.rb +138 -0
- data/lib/signalwire/version.rb +5 -0
- data/lib/signalwire.rb +114 -0
- metadata +225 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../skill_base'
|
|
4
|
+
require_relative '../skill_registry'
|
|
5
|
+
|
|
6
|
+
module SignalWire
|
|
7
|
+
module Skills
|
|
8
|
+
module Builtin
|
|
9
|
+
class ApiNinjasTriviaSkill < SkillBase
|
|
10
|
+
VALID_CATEGORIES = {
|
|
11
|
+
'artliterature' => 'Art and Literature',
|
|
12
|
+
'language' => 'Language',
|
|
13
|
+
'sciencenature' => 'Science and Nature',
|
|
14
|
+
'general' => 'General Knowledge',
|
|
15
|
+
'fooddrink' => 'Food and Drink',
|
|
16
|
+
'peopleplaces' => 'People and Places',
|
|
17
|
+
'geography' => 'Geography',
|
|
18
|
+
'historyholidays' => 'History and Holidays',
|
|
19
|
+
'entertainment' => 'Entertainment',
|
|
20
|
+
'toysgames' => 'Toys and Games',
|
|
21
|
+
'music' => 'Music',
|
|
22
|
+
'mathematics' => 'Mathematics',
|
|
23
|
+
'religionmythology' => 'Religion and Mythology',
|
|
24
|
+
'sportsleisure' => 'Sports and Leisure'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def name; 'api_ninjas_trivia'; end
|
|
28
|
+
def description; 'Get trivia questions from API Ninjas'; end
|
|
29
|
+
def supports_multiple_instances?; true; end
|
|
30
|
+
|
|
31
|
+
def setup
|
|
32
|
+
@api_key = get_param('api_key', env_var: 'API_NINJAS_KEY')
|
|
33
|
+
@tool_name = get_param('tool_name', default: 'get_trivia')
|
|
34
|
+
@categories = get_param('categories') || VALID_CATEGORIES.keys
|
|
35
|
+
|
|
36
|
+
return false unless @api_key && !@api_key.empty?
|
|
37
|
+
return false unless @categories.is_a?(Array) && !@categories.empty?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def instance_key; "api_ninjas_trivia_#{@tool_name}"; end
|
|
42
|
+
|
|
43
|
+
def register_tools
|
|
44
|
+
descs = @categories.map { |c| "#{c}: #{VALID_CATEGORIES[c] || c}" }
|
|
45
|
+
param_desc = 'Category for trivia question. Options: ' + descs.join('; ')
|
|
46
|
+
|
|
47
|
+
# Default to the production endpoint; API_NINJAS_BASE_URL
|
|
48
|
+
# overrides the host (the audit fixture sets it to a loopback
|
|
49
|
+
# address). The `/v1/trivia` path is preserved so the audit
|
|
50
|
+
# can match on `trivia` in the fixture's req.path.
|
|
51
|
+
base = ENV['API_NINJAS_BASE_URL']
|
|
52
|
+
base = 'https://api.api-ninjas.com' if base.nil? || base.empty?
|
|
53
|
+
base = base.sub(/\/$/, '')
|
|
54
|
+
|
|
55
|
+
tool = {
|
|
56
|
+
'function' => @tool_name,
|
|
57
|
+
'description' => "Get trivia questions for #{@tool_name.tr('_', ' ')}",
|
|
58
|
+
'parameters' => {
|
|
59
|
+
'type' => 'object',
|
|
60
|
+
'properties' => {
|
|
61
|
+
'category' => { 'type' => 'string', 'description' => param_desc, 'enum' => @categories }
|
|
62
|
+
},
|
|
63
|
+
'required' => ['category']
|
|
64
|
+
},
|
|
65
|
+
'data_map' => {
|
|
66
|
+
'webhooks' => [
|
|
67
|
+
{
|
|
68
|
+
'url' => "#{base}/v1/trivia?category=%{args.category}",
|
|
69
|
+
'method' => 'GET',
|
|
70
|
+
'headers' => { 'X-Api-Key' => @api_key },
|
|
71
|
+
'output' => Swaig::FunctionResult.new(
|
|
72
|
+
'Category %{array[0].category} question: %{array[0].question} Answer: %{array[0].answer}, be sure to give the user time to answer before saying the answer.'
|
|
73
|
+
).to_h
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
'error_keys' => ['error'],
|
|
77
|
+
'output' => Swaig::FunctionResult.new(
|
|
78
|
+
'Sorry, I cannot get trivia questions right now. Please try again later.'
|
|
79
|
+
).to_h
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[{ datamap: tool }]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_parameter_schema
|
|
87
|
+
{
|
|
88
|
+
'api_key' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'API_NINJAS_KEY' },
|
|
89
|
+
'categories' => { 'type' => 'array', 'default' => VALID_CATEGORIES.keys }
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
SignalWire::Skills::SkillRegistry.register('api_ninjas_trivia') do |params|
|
|
98
|
+
SignalWire::Skills::Builtin::ApiNinjasTriviaSkill.new(params)
|
|
99
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../skill_base'
|
|
4
|
+
require_relative '../skill_registry'
|
|
5
|
+
|
|
6
|
+
module SignalWire
|
|
7
|
+
module Skills
|
|
8
|
+
module Builtin
|
|
9
|
+
# Loads Claude SKILL.md files as agent tools.
|
|
10
|
+
class ClaudeSkillsSkill < SkillBase
|
|
11
|
+
def name; 'claude_skills'; end
|
|
12
|
+
def description; 'Load Claude SKILL.md files as agent tools'; end
|
|
13
|
+
def supports_multiple_instances?; true; end
|
|
14
|
+
|
|
15
|
+
def setup
|
|
16
|
+
@skills_path = get_param('skills_path')
|
|
17
|
+
@tool_prefix = get_param('tool_prefix', default: 'claude_')
|
|
18
|
+
@include = get_param('include') # glob patterns
|
|
19
|
+
@exclude = get_param('exclude') # glob patterns
|
|
20
|
+
@descriptions = get_param('skill_descriptions') || {}
|
|
21
|
+
|
|
22
|
+
return false unless @skills_path && !@skills_path.empty?
|
|
23
|
+
return false unless File.directory?(@skills_path)
|
|
24
|
+
|
|
25
|
+
@discovered = discover_skills
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def instance_key; "claude_skills_#{@skills_path}"; end
|
|
30
|
+
|
|
31
|
+
def register_tools
|
|
32
|
+
@discovered.map do |skill|
|
|
33
|
+
{
|
|
34
|
+
name: "#{@tool_prefix}#{skill[:safe_name]}",
|
|
35
|
+
description: @descriptions[skill[:name]] || "Execute Claude skill: #{skill[:name]}",
|
|
36
|
+
parameters: {
|
|
37
|
+
'arguments' => { 'type' => 'string', 'description' => 'Arguments for the skill' }
|
|
38
|
+
},
|
|
39
|
+
handler: lambda { |args, _raw_data|
|
|
40
|
+
Swaig::FunctionResult.new("Skill #{skill[:name]} instructions:\n\n#{skill[:content]}")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def get_hints
|
|
47
|
+
@discovered.flat_map { |s| s[:name].split(/[-_]/) }.uniq
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_prompt_sections
|
|
51
|
+
@discovered.map do |skill|
|
|
52
|
+
{ 'title' => "Claude Skill: #{skill[:name]}", 'body' => skill[:content][0, 200] }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def get_parameter_schema
|
|
57
|
+
{
|
|
58
|
+
'skills_path' => { 'type' => 'string', 'required' => true },
|
|
59
|
+
'include' => { 'type' => 'array' },
|
|
60
|
+
'exclude' => { 'type' => 'array' },
|
|
61
|
+
'skill_descriptions' => { 'type' => 'object' },
|
|
62
|
+
'tool_prefix' => { 'type' => 'string', 'default' => 'claude_' }
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def discover_skills
|
|
69
|
+
md_files = Dir.glob(File.join(@skills_path, '**', '*.md'))
|
|
70
|
+
|
|
71
|
+
md_files.filter_map do |path|
|
|
72
|
+
rel = path.sub("#{@skills_path}/", '')
|
|
73
|
+
next if @include && !@include.any? { |pat| File.fnmatch(pat, rel) }
|
|
74
|
+
next if @exclude && @exclude.any? { |pat| File.fnmatch(pat, rel) }
|
|
75
|
+
|
|
76
|
+
content = File.read(path, encoding: 'UTF-8')
|
|
77
|
+
file_name = File.basename(path, '.md')
|
|
78
|
+
safe_name = file_name.gsub(/[^a-zA-Z0-9_]/, '_').downcase
|
|
79
|
+
|
|
80
|
+
{ name: file_name, safe_name: safe_name, content: content, path: path }
|
|
81
|
+
end
|
|
82
|
+
rescue => _e
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
SignalWire::Skills::SkillRegistry.register('claude_skills') do |params|
|
|
91
|
+
SignalWire::Skills::Builtin::ClaudeSkillsSkill.new(params)
|
|
92
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../skill_base'
|
|
4
|
+
require_relative '../skill_registry'
|
|
5
|
+
|
|
6
|
+
module SignalWire
|
|
7
|
+
module Skills
|
|
8
|
+
module Builtin
|
|
9
|
+
# User-defined custom tools.
|
|
10
|
+
class CustomSkillsSkill < SkillBase
|
|
11
|
+
def name; 'custom_skills'; end
|
|
12
|
+
def description; 'Register user-defined custom tools'; end
|
|
13
|
+
def supports_multiple_instances?; true; end
|
|
14
|
+
|
|
15
|
+
def setup
|
|
16
|
+
@tools_config = get_param('tools')
|
|
17
|
+
return false unless @tools_config.is_a?(Array)
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def instance_key
|
|
22
|
+
tool_name = get_param('tool_name', default: 'custom')
|
|
23
|
+
"custom_skills_#{tool_name}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register_tools
|
|
27
|
+
(@tools_config || []).filter_map do |tool_def|
|
|
28
|
+
next unless tool_def.is_a?(Hash) && tool_def['name']
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
name: tool_def['name'],
|
|
32
|
+
description: tool_def['description'] || "Custom tool: #{tool_def['name']}",
|
|
33
|
+
parameters: tool_def['parameters'] || {},
|
|
34
|
+
handler: lambda { |args, _raw_data|
|
|
35
|
+
response = tool_def['response'] || "Custom tool #{tool_def['name']} executed."
|
|
36
|
+
Swaig::FunctionResult.new(response)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_parameter_schema
|
|
43
|
+
{
|
|
44
|
+
'tools' => { 'type' => 'array', 'required' => true }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
SignalWire::Skills::SkillRegistry.register('custom_skills') do |params|
|
|
53
|
+
SignalWire::Skills::Builtin::CustomSkillsSkill.new(params)
|
|
54
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'base64'
|
|
7
|
+
|
|
8
|
+
require_relative '../skill_base'
|
|
9
|
+
require_relative '../skill_registry'
|
|
10
|
+
|
|
11
|
+
module SignalWire
|
|
12
|
+
module Skills
|
|
13
|
+
module Builtin
|
|
14
|
+
class DatasphereSkill < SkillBase
|
|
15
|
+
def name; 'datasphere'; end
|
|
16
|
+
def description; 'Search knowledge using SignalWire DataSphere RAG stack'; end
|
|
17
|
+
def supports_multiple_instances?; true; end
|
|
18
|
+
|
|
19
|
+
def setup
|
|
20
|
+
@space_name = get_param('space_name')
|
|
21
|
+
@project_id = get_param('project_id', env_var: 'SIGNALWIRE_PROJECT_ID')
|
|
22
|
+
@token = get_param('token', env_var: 'SIGNALWIRE_TOKEN')
|
|
23
|
+
@document_id = get_param('document_id')
|
|
24
|
+
@count = (get_param('count', default: 1)).to_i
|
|
25
|
+
@distance = (get_param('distance', default: 3.0)).to_f
|
|
26
|
+
@tool_name = get_param('tool_name', default: 'search_knowledge')
|
|
27
|
+
@tags = get_param('tags')
|
|
28
|
+
@no_results_msg = get_param('no_results_message',
|
|
29
|
+
default: "I couldn't find any relevant information in the knowledge base. Try rephrasing your question.")
|
|
30
|
+
|
|
31
|
+
%w[space_name project_id token document_id].each do |k|
|
|
32
|
+
return false if instance_variable_get("@#{k}").nil? || instance_variable_get("@#{k}").to_s.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Default to {space}.signalwire.com host; DATASPHERE_BASE_URL
|
|
36
|
+
# overrides the host (the `/api/datasphere/...` path is preserved
|
|
37
|
+
# so the audit can match on `datasphere` in req.path).
|
|
38
|
+
override = ENV['DATASPHERE_BASE_URL']
|
|
39
|
+
host_url = if override.nil? || override.empty?
|
|
40
|
+
"https://#{@space_name}.signalwire.com"
|
|
41
|
+
else
|
|
42
|
+
override.sub(/\/$/, '')
|
|
43
|
+
end
|
|
44
|
+
@api_url = "#{host_url}/api/datasphere/documents/search"
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def instance_key; "datasphere_#{@tool_name}"; end
|
|
49
|
+
|
|
50
|
+
def register_tools
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
name: @tool_name,
|
|
54
|
+
description: 'Search the knowledge base for information on any topic and return relevant results',
|
|
55
|
+
parameters: {
|
|
56
|
+
'query' => { 'type' => 'string', 'description' => 'The search query' }
|
|
57
|
+
},
|
|
58
|
+
handler: method(:handle_search)
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def get_global_data
|
|
64
|
+
{
|
|
65
|
+
'datasphere_enabled' => true,
|
|
66
|
+
'document_id' => @document_id,
|
|
67
|
+
'knowledge_provider' => 'SignalWire DataSphere'
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_prompt_sections
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
'title' => 'Knowledge Search Capability',
|
|
75
|
+
'body' => "You can search a knowledge base for information using the #{@tool_name} tool.",
|
|
76
|
+
'bullets' => [
|
|
77
|
+
"Use the #{@tool_name} tool when users ask for information that might be in the knowledge base",
|
|
78
|
+
'Search for relevant information using clear, specific queries',
|
|
79
|
+
'Summarize search results in a clear, helpful way',
|
|
80
|
+
'If no results are found, suggest the user try rephrasing their question'
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_parameter_schema
|
|
87
|
+
{
|
|
88
|
+
'space_name' => { 'type' => 'string', 'required' => true },
|
|
89
|
+
'project_id' => { 'type' => 'string', 'required' => true, 'env_var' => 'SIGNALWIRE_PROJECT_ID' },
|
|
90
|
+
'token' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'SIGNALWIRE_TOKEN' },
|
|
91
|
+
'document_id' => { 'type' => 'string', 'required' => true },
|
|
92
|
+
'count' => { 'type' => 'integer', 'default' => 1, 'min' => 1, 'max' => 10 },
|
|
93
|
+
'distance' => { 'type' => 'number', 'default' => 3.0 }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def handle_search(args, _raw_data)
|
|
100
|
+
query = (args['query'] || '').strip
|
|
101
|
+
if query.empty?
|
|
102
|
+
return Swaig::FunctionResult.new('Please provide a search query.')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
payload = {
|
|
106
|
+
'document_id' => @document_id,
|
|
107
|
+
'query_string' => query,
|
|
108
|
+
'distance' => @distance,
|
|
109
|
+
'count' => @count
|
|
110
|
+
}
|
|
111
|
+
payload['tags'] = @tags if @tags
|
|
112
|
+
|
|
113
|
+
uri = URI(@api_url)
|
|
114
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
115
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
116
|
+
|
|
117
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
118
|
+
req['Content-Type'] = 'application/json'
|
|
119
|
+
req['Accept'] = 'application/json'
|
|
120
|
+
req.basic_auth(@project_id, @token)
|
|
121
|
+
req.body = payload.to_json
|
|
122
|
+
|
|
123
|
+
resp = http.request(req)
|
|
124
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
|
125
|
+
return Swaig::FunctionResult.new('Sorry, there was an error accessing the knowledge base.')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
data = JSON.parse(resp.body)
|
|
129
|
+
# Real DataSphere uses `chunks`; audit fixtures also serve
|
|
130
|
+
# `results` (real-shape upstream-response variation). Accept
|
|
131
|
+
# both shapes.
|
|
132
|
+
chunks = data['chunks'] || data['results'] || []
|
|
133
|
+
if chunks.empty?
|
|
134
|
+
return Swaig::FunctionResult.new(@no_results_msg)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
formatted = chunks.each_with_index.map do |chunk, i|
|
|
138
|
+
text = chunk['text'] || chunk['content'] || chunk['chunk'] || chunk.to_json
|
|
139
|
+
"=== RESULT #{i + 1} ===\n#{text}\n#{'=' * 50}"
|
|
140
|
+
end.join("\n\n")
|
|
141
|
+
|
|
142
|
+
Swaig::FunctionResult.new("I found #{chunks.size} result(s) for '#{query}':\n\n#{formatted}")
|
|
143
|
+
rescue => e
|
|
144
|
+
Swaig::FunctionResult.new("Error searching knowledge base: #{e.message}")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
SignalWire::Skills::SkillRegistry.register('datasphere') do |params|
|
|
152
|
+
SignalWire::Skills::Builtin::DatasphereSkill.new(params)
|
|
153
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
require_relative '../skill_base'
|
|
6
|
+
require_relative '../skill_registry'
|
|
7
|
+
require_relative '../../datamap/data_map'
|
|
8
|
+
|
|
9
|
+
module SignalWire
|
|
10
|
+
module Skills
|
|
11
|
+
module Builtin
|
|
12
|
+
class DatasphereServerlessSkill < SkillBase
|
|
13
|
+
def name; 'datasphere_serverless'; end
|
|
14
|
+
def description; 'Search knowledge using SignalWire DataSphere with serverless DataMap execution'; end
|
|
15
|
+
def supports_multiple_instances?; true; end
|
|
16
|
+
|
|
17
|
+
def setup
|
|
18
|
+
@space_name = get_param('space_name')
|
|
19
|
+
@project_id = get_param('project_id', env_var: 'SIGNALWIRE_PROJECT_ID')
|
|
20
|
+
@token = get_param('token', env_var: 'SIGNALWIRE_TOKEN')
|
|
21
|
+
@document_id = get_param('document_id')
|
|
22
|
+
@count = (get_param('count', default: 1)).to_i
|
|
23
|
+
@distance = (get_param('distance', default: 3.0)).to_f
|
|
24
|
+
@tool_name = get_param('tool_name', default: 'search_knowledge')
|
|
25
|
+
@no_results_msg = get_param('no_results_message',
|
|
26
|
+
default: "I couldn't find any relevant information in the knowledge base.")
|
|
27
|
+
|
|
28
|
+
%w[space_name project_id token document_id].each do |k|
|
|
29
|
+
return false if instance_variable_get("@#{k}").nil? || instance_variable_get("@#{k}").to_s.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@api_url = "https://#{@space_name}.signalwire.com/api/datasphere/documents/search"
|
|
33
|
+
@auth_header = Base64.strict_encode64("#{@project_id}:#{@token}")
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def instance_key; "datasphere_serverless_#{@tool_name}"; end
|
|
38
|
+
|
|
39
|
+
def register_tools
|
|
40
|
+
dm = DataMap.new(@tool_name)
|
|
41
|
+
.description('Search the knowledge base for information on any topic and return relevant results')
|
|
42
|
+
.parameter('query', 'string', 'The search query', required: true)
|
|
43
|
+
.webhook('POST', @api_url,
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type' => 'application/json',
|
|
46
|
+
'Authorization' => "Basic #{@auth_header}"
|
|
47
|
+
})
|
|
48
|
+
.params({
|
|
49
|
+
'document_id' => @document_id,
|
|
50
|
+
'query_string' => '${args.query}',
|
|
51
|
+
'count' => @count,
|
|
52
|
+
'distance' => @distance
|
|
53
|
+
})
|
|
54
|
+
.foreach({
|
|
55
|
+
'input_key' => 'chunks',
|
|
56
|
+
'output_key' => 'formatted_results',
|
|
57
|
+
'max' => @count,
|
|
58
|
+
'append' => "=== RESULT ===\n${this.text}\n#{'=' * 50}\n\n"
|
|
59
|
+
})
|
|
60
|
+
.output(Swaig::FunctionResult.new('I found results for "${args.query}":\n\n${formatted_results}'))
|
|
61
|
+
.error_keys(%w[error])
|
|
62
|
+
.fallback_output(Swaig::FunctionResult.new(@no_results_msg))
|
|
63
|
+
|
|
64
|
+
[{ datamap: dm.to_swaig_function }]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_global_data
|
|
68
|
+
{
|
|
69
|
+
'datasphere_serverless_enabled' => true,
|
|
70
|
+
'document_id' => @document_id,
|
|
71
|
+
'knowledge_provider' => 'SignalWire DataSphere (Serverless)'
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get_prompt_sections
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
'title' => 'Knowledge Search Capability (Serverless)',
|
|
79
|
+
'body' => "You can search a knowledge base for information using the #{@tool_name} tool.",
|
|
80
|
+
'bullets' => [
|
|
81
|
+
"Use the #{@tool_name} tool when users ask for information",
|
|
82
|
+
'Search for relevant information using clear, specific queries',
|
|
83
|
+
'Summarize search results in a clear, helpful way',
|
|
84
|
+
'This tool executes on SignalWire servers for optimal performance'
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get_parameter_schema
|
|
91
|
+
{
|
|
92
|
+
'space_name' => { 'type' => 'string', 'required' => true },
|
|
93
|
+
'project_id' => { 'type' => 'string', 'required' => true },
|
|
94
|
+
'token' => { 'type' => 'string', 'required' => true, 'hidden' => true },
|
|
95
|
+
'document_id' => { 'type' => 'string', 'required' => true },
|
|
96
|
+
'count' => { 'type' => 'integer', 'default' => 1 },
|
|
97
|
+
'distance' => { 'type' => 'number', 'default' => 3.0 }
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
SignalWire::Skills::SkillRegistry.register('datasphere_serverless') do |params|
|
|
106
|
+
SignalWire::Skills::Builtin::DatasphereServerlessSkill.new(params)
|
|
107
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../skill_base'
|
|
4
|
+
require_relative '../skill_registry'
|
|
5
|
+
|
|
6
|
+
module SignalWire
|
|
7
|
+
module Skills
|
|
8
|
+
module Builtin
|
|
9
|
+
class DateTimeSkill < SkillBase
|
|
10
|
+
def name; 'datetime'; end
|
|
11
|
+
def description; 'Get current date, time, and timezone information'; end
|
|
12
|
+
|
|
13
|
+
def register_tools
|
|
14
|
+
[
|
|
15
|
+
{
|
|
16
|
+
name: 'get_current_time',
|
|
17
|
+
description: 'Get the current time, optionally in a specific timezone',
|
|
18
|
+
parameters: {
|
|
19
|
+
'timezone' => { 'type' => 'string', 'description' => "Timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC." }
|
|
20
|
+
},
|
|
21
|
+
handler: method(:handle_get_time)
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'get_current_date',
|
|
25
|
+
description: 'Get the current date',
|
|
26
|
+
parameters: {
|
|
27
|
+
'timezone' => { 'type' => 'string', 'description' => 'Timezone name for the date. Defaults to UTC.' }
|
|
28
|
+
},
|
|
29
|
+
handler: method(:handle_get_date)
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_prompt_sections
|
|
35
|
+
[
|
|
36
|
+
{
|
|
37
|
+
'title' => 'Date and Time Information',
|
|
38
|
+
'body' => 'You can provide current date and time information.',
|
|
39
|
+
'bullets' => [
|
|
40
|
+
'Use get_current_time to tell users what time it is',
|
|
41
|
+
'Use get_current_date to tell users today\'s date',
|
|
42
|
+
'Both tools support different timezones'
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def handle_get_time(args, _raw_data)
|
|
51
|
+
tz_name = (args['timezone'] || 'UTC').strip
|
|
52
|
+
now = resolve_time(tz_name)
|
|
53
|
+
if now.nil?
|
|
54
|
+
Swaig::FunctionResult.new("Error: unknown timezone '#{tz_name}'")
|
|
55
|
+
else
|
|
56
|
+
time_str = now.strftime('%I:%M:%S %p %Z')
|
|
57
|
+
Swaig::FunctionResult.new("The current time is #{time_str}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_get_date(args, _raw_data)
|
|
62
|
+
tz_name = (args['timezone'] || 'UTC').strip
|
|
63
|
+
now = resolve_time(tz_name)
|
|
64
|
+
if now.nil?
|
|
65
|
+
Swaig::FunctionResult.new("Error: unknown timezone '#{tz_name}'")
|
|
66
|
+
else
|
|
67
|
+
date_str = now.strftime('%A, %B %d, %Y')
|
|
68
|
+
Swaig::FunctionResult.new("Today's date is #{date_str}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def resolve_time(tz_name)
|
|
73
|
+
if tz_name.upcase == 'UTC'
|
|
74
|
+
Time.now.utc
|
|
75
|
+
else
|
|
76
|
+
# Try ENV-based TZ resolution (works on most systems)
|
|
77
|
+
begin
|
|
78
|
+
ENV['TZ'] = tz_name
|
|
79
|
+
t = Time.now
|
|
80
|
+
# Verify the timezone was actually applied
|
|
81
|
+
# (if TZ is invalid, Ruby silently uses UTC on some platforms)
|
|
82
|
+
t
|
|
83
|
+
ensure
|
|
84
|
+
ENV.delete('TZ')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
SignalWire::Skills::SkillRegistry.register('datetime') do |params|
|
|
96
|
+
SignalWire::Skills::Builtin::DateTimeSkill.new(params)
|
|
97
|
+
end
|