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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../skill_base'
4
+ require_relative '../skill_registry'
5
+
6
+ module SignalWireAgents
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
+ tool = {
48
+ 'function' => @tool_name,
49
+ 'description' => "Get trivia questions for #{@tool_name.tr('_', ' ')}",
50
+ 'parameters' => {
51
+ 'type' => 'object',
52
+ 'properties' => {
53
+ 'category' => { 'type' => 'string', 'description' => param_desc, 'enum' => @categories }
54
+ },
55
+ 'required' => ['category']
56
+ },
57
+ 'data_map' => {
58
+ 'webhooks' => [
59
+ {
60
+ 'url' => 'https://api.api-ninjas.com/v1/trivia?category=%{args.category}',
61
+ 'method' => 'GET',
62
+ 'headers' => { 'X-Api-Key' => @api_key },
63
+ 'output' => Swaig::FunctionResult.new(
64
+ '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.'
65
+ ).to_h
66
+ }
67
+ ],
68
+ 'error_keys' => ['error'],
69
+ 'output' => Swaig::FunctionResult.new(
70
+ 'Sorry, I cannot get trivia questions right now. Please try again later.'
71
+ ).to_h
72
+ }
73
+ }
74
+
75
+ [{ datamap: tool }]
76
+ end
77
+
78
+ def get_parameter_schema
79
+ {
80
+ 'api_key' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'API_NINJAS_KEY' },
81
+ 'categories' => { 'type' => 'array', 'default' => VALID_CATEGORIES.keys }
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ SignalWireAgents::Skills::SkillRegistry.register('api_ninjas_trivia') do |params|
90
+ SignalWireAgents::Skills::Builtin::ApiNinjasTriviaSkill.new(params)
91
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../skill_base'
4
+ require_relative '../skill_registry'
5
+
6
+ module SignalWireAgents
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
+ SignalWireAgents::Skills::SkillRegistry.register('claude_skills') do |params|
91
+ SignalWireAgents::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 SignalWireAgents
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
+ SignalWireAgents::Skills::SkillRegistry.register('custom_skills') do |params|
53
+ SignalWireAgents::Skills::Builtin::CustomSkillsSkill.new(params)
54
+ end
@@ -0,0 +1,141 @@
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 SignalWireAgents
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
+ @api_url = "https://#{@space_name}.signalwire.com/api/datasphere/documents/search"
36
+ true
37
+ end
38
+
39
+ def instance_key; "datasphere_#{@tool_name}"; end
40
+
41
+ def register_tools
42
+ [
43
+ {
44
+ name: @tool_name,
45
+ description: 'Search the knowledge base for information on any topic and return relevant results',
46
+ parameters: {
47
+ 'query' => { 'type' => 'string', 'description' => 'The search query' }
48
+ },
49
+ handler: method(:handle_search)
50
+ }
51
+ ]
52
+ end
53
+
54
+ def get_global_data
55
+ {
56
+ 'datasphere_enabled' => true,
57
+ 'document_id' => @document_id,
58
+ 'knowledge_provider' => 'SignalWire DataSphere'
59
+ }
60
+ end
61
+
62
+ def get_prompt_sections
63
+ [
64
+ {
65
+ 'title' => 'Knowledge Search Capability',
66
+ 'body' => "You can search a knowledge base for information using the #{@tool_name} tool.",
67
+ 'bullets' => [
68
+ "Use the #{@tool_name} tool when users ask for information that might be in the knowledge base",
69
+ 'Search for relevant information using clear, specific queries',
70
+ 'Summarize search results in a clear, helpful way',
71
+ 'If no results are found, suggest the user try rephrasing their question'
72
+ ]
73
+ }
74
+ ]
75
+ end
76
+
77
+ def get_parameter_schema
78
+ {
79
+ 'space_name' => { 'type' => 'string', 'required' => true },
80
+ 'project_id' => { 'type' => 'string', 'required' => true, 'env_var' => 'SIGNALWIRE_PROJECT_ID' },
81
+ 'token' => { 'type' => 'string', 'required' => true, 'hidden' => true, 'env_var' => 'SIGNALWIRE_TOKEN' },
82
+ 'document_id' => { 'type' => 'string', 'required' => true },
83
+ 'count' => { 'type' => 'integer', 'default' => 1, 'min' => 1, 'max' => 10 },
84
+ 'distance' => { 'type' => 'number', 'default' => 3.0 }
85
+ }
86
+ end
87
+
88
+ private
89
+
90
+ def handle_search(args, _raw_data)
91
+ query = (args['query'] || '').strip
92
+ if query.empty?
93
+ return Swaig::FunctionResult.new('Please provide a search query.')
94
+ end
95
+
96
+ payload = {
97
+ 'document_id' => @document_id,
98
+ 'query_string' => query,
99
+ 'distance' => @distance,
100
+ 'count' => @count
101
+ }
102
+ payload['tags'] = @tags if @tags
103
+
104
+ uri = URI(@api_url)
105
+ http = Net::HTTP.new(uri.host, uri.port)
106
+ http.use_ssl = true
107
+
108
+ req = Net::HTTP::Post.new(uri.path)
109
+ req['Content-Type'] = 'application/json'
110
+ req['Accept'] = 'application/json'
111
+ req.basic_auth(@project_id, @token)
112
+ req.body = payload.to_json
113
+
114
+ resp = http.request(req)
115
+ unless resp.is_a?(Net::HTTPSuccess)
116
+ return Swaig::FunctionResult.new('Sorry, there was an error accessing the knowledge base.')
117
+ end
118
+
119
+ data = JSON.parse(resp.body)
120
+ chunks = data['chunks'] || []
121
+ if chunks.empty?
122
+ return Swaig::FunctionResult.new(@no_results_msg)
123
+ end
124
+
125
+ formatted = chunks.each_with_index.map do |chunk, i|
126
+ text = chunk['text'] || chunk['content'] || chunk['chunk'] || chunk.to_json
127
+ "=== RESULT #{i + 1} ===\n#{text}\n#{'=' * 50}"
128
+ end.join("\n\n")
129
+
130
+ Swaig::FunctionResult.new("I found #{chunks.size} result(s) for '#{query}':\n\n#{formatted}")
131
+ rescue => e
132
+ Swaig::FunctionResult.new("Error searching knowledge base: #{e.message}")
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ SignalWireAgents::Skills::SkillRegistry.register('datasphere') do |params|
140
+ SignalWireAgents::Skills::Builtin::DatasphereSkill.new(params)
141
+ 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 SignalWireAgents
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
+ SignalWireAgents::Skills::SkillRegistry.register('datasphere_serverless') do |params|
106
+ SignalWireAgents::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 SignalWireAgents
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
+ SignalWireAgents::Skills::SkillRegistry.register('datetime') do |params|
96
+ SignalWireAgents::Skills::Builtin::DateTimeSkill.new(params)
97
+ end