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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +259 -0
  4. data/bin/swaig-test +872 -0
  5. data/lib/signalwire/agent/agent_base.rb +2134 -0
  6. data/lib/signalwire/contexts/context_builder.rb +861 -0
  7. data/lib/signalwire/core/logging_config.rb +54 -0
  8. data/lib/signalwire/datamap/data_map.rb +315 -0
  9. data/lib/signalwire/logging.rb +92 -0
  10. data/lib/signalwire/pom/prompt_object_model.rb +269 -0
  11. data/lib/signalwire/pom/section.rb +202 -0
  12. data/lib/signalwire/prefabs/concierge.rb +92 -0
  13. data/lib/signalwire/prefabs/faq_bot.rb +67 -0
  14. data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
  15. data/lib/signalwire/prefabs/receptionist.rb +74 -0
  16. data/lib/signalwire/prefabs/survey.rb +75 -0
  17. data/lib/signalwire/relay/action.rb +291 -0
  18. data/lib/signalwire/relay/call.rb +523 -0
  19. data/lib/signalwire/relay/client.rb +789 -0
  20. data/lib/signalwire/relay/constants.rb +124 -0
  21. data/lib/signalwire/relay/message.rb +137 -0
  22. data/lib/signalwire/relay/relay_event.rb +670 -0
  23. data/lib/signalwire/rest/http_client.rb +159 -0
  24. data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
  25. data/lib/signalwire/rest/namespaces/calling.rb +179 -0
  26. data/lib/signalwire/rest/namespaces/chat.rb +18 -0
  27. data/lib/signalwire/rest/namespaces/compat.rb +229 -0
  28. data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
  29. data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
  30. data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
  31. data/lib/signalwire/rest/namespaces/logs.rb +46 -0
  32. data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
  33. data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
  34. data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
  35. data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
  36. data/lib/signalwire/rest/namespaces/project.rb +33 -0
  37. data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
  38. data/lib/signalwire/rest/namespaces/queues.rb +28 -0
  39. data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
  40. data/lib/signalwire/rest/namespaces/registry.rb +67 -0
  41. data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
  42. data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
  43. data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
  44. data/lib/signalwire/rest/namespaces/video.rb +129 -0
  45. data/lib/signalwire/rest/pagination.rb +89 -0
  46. data/lib/signalwire/rest/phone_call_handler.rb +56 -0
  47. data/lib/signalwire/rest/rest_client.rb +114 -0
  48. data/lib/signalwire/runtime.rb +98 -0
  49. data/lib/signalwire/security/session_manager.rb +124 -0
  50. data/lib/signalwire/security/webhook_middleware.rb +191 -0
  51. data/lib/signalwire/security/webhook_validator.rb +327 -0
  52. data/lib/signalwire/server/agent_server.rb +413 -0
  53. data/lib/signalwire/serverless/lambda_handler.rb +251 -0
  54. data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
  55. data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
  56. data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
  57. data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
  58. data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
  59. data/lib/signalwire/skills/builtin/datetime.rb +97 -0
  60. data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
  61. data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
  62. data/lib/signalwire/skills/builtin/joke.rb +65 -0
  63. data/lib/signalwire/skills/builtin/math.rb +176 -0
  64. data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
  65. data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
  66. data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
  67. data/lib/signalwire/skills/builtin/spider.rb +169 -0
  68. data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
  69. data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
  70. data/lib/signalwire/skills/builtin/web_search.rb +141 -0
  71. data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
  72. data/lib/signalwire/skills/skill_base.rb +82 -0
  73. data/lib/signalwire/skills/skill_manager.rb +97 -0
  74. data/lib/signalwire/skills/skill_registry.rb +258 -0
  75. data/lib/signalwire/swaig/function_result.rb +777 -0
  76. data/lib/signalwire/swml/document.rb +84 -0
  77. data/lib/signalwire/swml/schema.json +12250 -0
  78. data/lib/signalwire/swml/schema.rb +81 -0
  79. data/lib/signalwire/swml/service.rb +650 -0
  80. data/lib/signalwire/utils/schema_utils.rb +298 -0
  81. data/lib/signalwire/utils/serverless.rb +19 -0
  82. data/lib/signalwire/utils/url_validator.rb +138 -0
  83. data/lib/signalwire/version.rb +5 -0
  84. data/lib/signalwire.rb +114 -0
  85. 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