signalwire-agents 0.1.35__tar.gz → 0.1.36__tar.gz

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 (138) hide show
  1. {signalwire_agents-0.1.35/signalwire_agents.egg-info → signalwire_agents-0.1.36}/PKG-INFO +1 -1
  2. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/pyproject.toml +1 -1
  3. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/__init__.py +66 -2
  4. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent_base.py +15 -0
  5. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/skill_base.py +64 -1
  6. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datetime/skill.py +16 -1
  7. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/math/skill.py +16 -1
  8. signalwire_agents-0.1.36/signalwire_agents/skills/registry.py +423 -0
  9. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/web_search/skill.py +60 -1
  10. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36/signalwire_agents.egg-info}/PKG-INFO +1 -1
  11. signalwire_agents-0.1.35/signalwire_agents/skills/registry.py +0 -120
  12. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/LICENSE +0 -0
  13. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/README.md +0 -0
  14. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/setup.cfg +0 -0
  15. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/setup.py +0 -0
  16. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/agent_server.py +0 -0
  17. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/__init__.py +0 -0
  18. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/build_search.py +0 -0
  19. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/config.py +0 -0
  20. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/core/__init__.py +0 -0
  21. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/core/agent_loader.py +0 -0
  22. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/core/argparse_helpers.py +0 -0
  23. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/core/dynamic_config.py +0 -0
  24. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/core/service_loader.py +0 -0
  25. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/execution/__init__.py +0 -0
  26. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/execution/datamap_exec.py +0 -0
  27. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/execution/webhook_exec.py +0 -0
  28. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/output/__init__.py +0 -0
  29. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/output/output_formatter.py +0 -0
  30. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/output/swml_dump.py +0 -0
  31. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/simulation/__init__.py +0 -0
  32. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/simulation/data_generation.py +0 -0
  33. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/simulation/data_overrides.py +0 -0
  34. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/simulation/mock_env.py +0 -0
  35. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/swaig_test_wrapper.py +0 -0
  36. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/test_swaig.py +0 -0
  37. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/cli/types.py +0 -0
  38. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/__init__.py +0 -0
  39. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/__init__.py +0 -0
  40. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/config/__init__.py +0 -0
  41. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/deployment/__init__.py +0 -0
  42. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/deployment/handlers/__init__.py +0 -0
  43. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/prompt/__init__.py +0 -0
  44. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/prompt/manager.py +0 -0
  45. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/routing/__init__.py +0 -0
  46. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/security/__init__.py +0 -0
  47. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/swml/__init__.py +0 -0
  48. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/tools/__init__.py +0 -0
  49. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/tools/decorator.py +0 -0
  50. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/agent/tools/registry.py +0 -0
  51. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/auth_handler.py +0 -0
  52. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/config_loader.py +0 -0
  53. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/contexts.py +0 -0
  54. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/data_map.py +0 -0
  55. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/function_result.py +0 -0
  56. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/logging_config.py +0 -0
  57. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/__init__.py +0 -0
  58. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/ai_config_mixin.py +0 -0
  59. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/auth_mixin.py +0 -0
  60. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/prompt_mixin.py +0 -0
  61. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/serverless_mixin.py +0 -0
  62. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/skill_mixin.py +0 -0
  63. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/state_mixin.py +0 -0
  64. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/tool_mixin.py +0 -0
  65. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/mixins/web_mixin.py +0 -0
  66. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/pom_builder.py +0 -0
  67. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/security/__init__.py +0 -0
  68. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/security/session_manager.py +0 -0
  69. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/security_config.py +0 -0
  70. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/skill_manager.py +0 -0
  71. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/swaig_function.py +0 -0
  72. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/swml_builder.py +0 -0
  73. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/swml_handler.py +0 -0
  74. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/swml_renderer.py +0 -0
  75. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/core/swml_service.py +0 -0
  76. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/__init__.py +0 -0
  77. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/concierge.py +0 -0
  78. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/faq_bot.py +0 -0
  79. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/info_gatherer.py +0 -0
  80. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/receptionist.py +0 -0
  81. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/prefabs/survey.py +0 -0
  82. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/schema.json +0 -0
  83. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/__init__.py +0 -0
  84. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/document_processor.py +0 -0
  85. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/index_builder.py +0 -0
  86. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/query_processor.py +0 -0
  87. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/search_engine.py +0 -0
  88. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/search/search_service.py +0 -0
  89. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/README.md +0 -0
  90. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/__init__.py +0 -0
  91. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/api_ninjas_trivia/README.md +0 -0
  92. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/api_ninjas_trivia/__init__.py +0 -0
  93. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/api_ninjas_trivia/skill.py +0 -0
  94. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere/README.md +0 -0
  95. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere/__init__.py +0 -0
  96. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere/skill.py +0 -0
  97. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere_serverless/README.md +0 -0
  98. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere_serverless/__init__.py +0 -0
  99. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datasphere_serverless/skill.py +0 -0
  100. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datetime/README.md +0 -0
  101. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/datetime/__init__.py +0 -0
  102. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/joke/README.md +0 -0
  103. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/joke/__init__.py +0 -0
  104. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/joke/skill.py +0 -0
  105. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/math/README.md +0 -0
  106. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/math/__init__.py +0 -0
  107. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/mcp_gateway/README.md +0 -0
  108. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/mcp_gateway/__init__.py +0 -0
  109. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/mcp_gateway/skill.py +0 -0
  110. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/native_vector_search/__init__.py +0 -0
  111. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/native_vector_search/skill.py +0 -0
  112. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/play_background_file/README.md +0 -0
  113. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/play_background_file/__init__.py +0 -0
  114. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/play_background_file/skill.py +0 -0
  115. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/spider/README.md +0 -0
  116. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/spider/__init__.py +0 -0
  117. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/spider/skill.py +0 -0
  118. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/swml_transfer/README.md +0 -0
  119. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/swml_transfer/__init__.py +0 -0
  120. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/swml_transfer/skill.py +0 -0
  121. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/weather_api/README.md +0 -0
  122. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/weather_api/__init__.py +0 -0
  123. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/weather_api/skill.py +0 -0
  124. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/web_search/README.md +0 -0
  125. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/web_search/__init__.py +0 -0
  126. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/wikipedia_search/README.md +0 -0
  127. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/wikipedia_search/__init__.py +0 -0
  128. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/skills/wikipedia_search/skill.py +0 -0
  129. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/utils/__init__.py +0 -0
  130. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/utils/pom_utils.py +0 -0
  131. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/utils/schema_utils.py +0 -0
  132. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/utils/token_generators.py +0 -0
  133. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents/utils/validators.py +0 -0
  134. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents.egg-info/SOURCES.txt +0 -0
  135. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents.egg-info/dependency_links.txt +0 -0
  136. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents.egg-info/entry_points.txt +0 -0
  137. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents.egg-info/requires.txt +0 -0
  138. {signalwire_agents-0.1.35 → signalwire_agents-0.1.36}/signalwire_agents.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.35
3
+ Version: 0.1.36
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "signalwire_agents"
7
- version = "0.1.35"
7
+ version = "0.1.36"
8
8
  description = "SignalWire AI Agents SDK"
9
9
  authors = [
10
10
  {name = "SignalWire Team", email = "info@signalwire.com"}
@@ -18,7 +18,7 @@ A package for building AI agents using SignalWire's AI and SWML capabilities.
18
18
  from .core.logging_config import configure_logging
19
19
  configure_logging()
20
20
 
21
- __version__ = "0.1.35"
21
+ __version__ = "0.1.36"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -62,6 +62,67 @@ def list_skills(*args, **kwargs):
62
62
  except ImportError:
63
63
  raise NotImplementedError("CLI helpers not available")
64
64
 
65
+ def list_skills_with_params():
66
+ """
67
+ Get complete schema for all available skills including parameter metadata
68
+
69
+ This function returns a comprehensive schema for all available skills,
70
+ including their metadata and parameter definitions. This is useful for
71
+ GUI configuration tools, API documentation, or programmatic skill discovery.
72
+
73
+ Returns:
74
+ Dict[str, Dict[str, Any]]: Complete skill schema where keys are skill names
75
+
76
+ Example:
77
+ >>> schema = list_skills_with_params()
78
+ >>> print(schema['web_search']['parameters']['api_key'])
79
+ {
80
+ 'type': 'string',
81
+ 'description': 'Google Custom Search API key',
82
+ 'required': True,
83
+ 'hidden': True,
84
+ 'env_var': 'GOOGLE_SEARCH_API_KEY'
85
+ }
86
+ """
87
+ from signalwire_agents.skills.registry import skill_registry
88
+ return skill_registry.get_all_skills_schema()
89
+
90
+ def register_skill(skill_class):
91
+ """
92
+ Register a custom skill class
93
+
94
+ This allows third-party code to register skill classes directly without
95
+ requiring them to be in a specific directory structure.
96
+
97
+ Args:
98
+ skill_class: A class that inherits from SkillBase
99
+
100
+ Example:
101
+ >>> from my_custom_skills import MyWeatherSkill
102
+ >>> register_skill(MyWeatherSkill)
103
+ >>> # Now you can use it in agents:
104
+ >>> agent.add_skill('my_weather')
105
+ """
106
+ from signalwire_agents.skills.registry import skill_registry
107
+ return skill_registry.register_skill(skill_class)
108
+
109
+ def add_skill_directory(path):
110
+ """
111
+ Add a directory to search for skills
112
+
113
+ This allows third-party skill collections to be registered by path.
114
+ Skills in these directories should follow the same structure as built-in skills.
115
+
116
+ Args:
117
+ path: Path to directory containing skill subdirectories
118
+
119
+ Example:
120
+ >>> add_skill_directory('/opt/custom_skills')
121
+ >>> # Now agent.add_skill('my_custom_skill') will search in this directory
122
+ """
123
+ from signalwire_agents.skills.registry import skill_registry
124
+ return skill_registry.add_skill_directory(path)
125
+
65
126
  __all__ = [
66
127
  "AgentBase",
67
128
  "AgentServer",
@@ -78,5 +139,8 @@ __all__ = [
78
139
  "create_simple_context",
79
140
  "start_agent",
80
141
  "run_agent",
81
- "list_skills"
142
+ "list_skills",
143
+ "list_skills_with_params",
144
+ "register_skill",
145
+ "add_skill_directory"
82
146
  ]
@@ -692,6 +692,13 @@ class AgentBase(
692
692
  # Add answer verb with auto-answer enabled
693
693
  agent_to_use.add_verb("answer", {})
694
694
 
695
+ # Add recording if enabled
696
+ if agent_to_use._record_call:
697
+ agent_to_use.add_verb("record_call", {
698
+ "format": agent_to_use._record_format,
699
+ "stereo": agent_to_use._record_stereo
700
+ })
701
+
695
702
  # Use the AI verb handler to build and validate the AI verb config
696
703
  ai_config = {}
697
704
 
@@ -811,6 +818,14 @@ class AgentBase(
811
818
  # Clear and rebuild the document with the modified AI config
812
819
  agent_to_use.reset_document()
813
820
  agent_to_use.add_verb("answer", {})
821
+
822
+ # Add recording if enabled
823
+ if agent_to_use._record_call:
824
+ agent_to_use.add_verb("record_call", {
825
+ "format": agent_to_use._record_format,
826
+ "stereo": agent_to_use._record_stereo
827
+ })
828
+
814
829
  agent_to_use.add_verb("ai", ai_config)
815
830
 
816
831
  # Return the rendered document as a string
@@ -114,4 +114,67 @@ class SkillBase(ABC):
114
114
 
115
115
  # For multi-instance skills, create key from skill name + tool name
116
116
  tool_name = self.params.get('tool_name', self.SKILL_NAME)
117
- return f"{self.SKILL_NAME}_{tool_name}"
117
+ return f"{self.SKILL_NAME}_{tool_name}"
118
+
119
+ @classmethod
120
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
121
+ """
122
+ Get the parameter schema for this skill
123
+
124
+ This method returns metadata about all parameters the skill accepts,
125
+ including their types, descriptions, default values, and whether they
126
+ are required or should be hidden (e.g., API keys).
127
+
128
+ The base implementation provides common parameters available to all skills.
129
+ Subclasses should override this method and merge their specific parameters
130
+ with the base schema.
131
+
132
+ Returns:
133
+ Dict[str, Dict[str, Any]]: Parameter schema where keys are parameter names
134
+ and values are dictionaries containing:
135
+ - type: Parameter type ("string", "integer", "number", "boolean", "object", "array")
136
+ - description: Human-readable description
137
+ - default: Default value if not provided (optional)
138
+ - required: Whether the parameter is required (default: False)
139
+ - hidden: Whether to hide this field in UIs (for secrets/keys)
140
+ - env_var: Environment variable that can provide this value (optional)
141
+ - enum: List of allowed values (optional)
142
+ - min/max: Minimum/maximum values for numeric types (optional)
143
+
144
+ Example:
145
+ {
146
+ "tool_name": {
147
+ "type": "string",
148
+ "description": "Name for the tool when using multiple instances",
149
+ "default": "my_skill",
150
+ "required": False
151
+ },
152
+ "api_key": {
153
+ "type": "string",
154
+ "description": "API key for the service",
155
+ "required": True,
156
+ "hidden": True,
157
+ "env_var": "MY_API_KEY"
158
+ }
159
+ }
160
+ """
161
+ schema = {}
162
+
163
+ # Add swaig_fields parameter (available to all skills)
164
+ schema["swaig_fields"] = {
165
+ "type": "object",
166
+ "description": "Additional SWAIG function metadata to merge into tool definitions",
167
+ "default": {},
168
+ "required": False
169
+ }
170
+
171
+ # Add tool_name for multi-instance skills
172
+ if cls.SUPPORTS_MULTIPLE_INSTANCES:
173
+ schema["tool_name"] = {
174
+ "type": "string",
175
+ "description": "Custom name for this skill instance (for multiple instances)",
176
+ "default": cls.SKILL_NAME,
177
+ "required": False
178
+ }
179
+
180
+ return schema
@@ -110,4 +110,19 @@ class DateTimeSkill(SkillBase):
110
110
  "Both tools support different timezones"
111
111
  ]
112
112
  }
113
- ]
113
+ ]
114
+
115
+ @classmethod
116
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
117
+ """
118
+ Get the parameter schema for the datetime skill
119
+
120
+ The datetime skill has no custom parameters - it inherits only
121
+ the base parameters from SkillBase.
122
+ """
123
+ # Get base schema from parent
124
+ schema = super().get_parameter_schema()
125
+
126
+ # No additional parameters for datetime skill
127
+
128
+ return schema
@@ -88,4 +88,19 @@ class MathSkill(SkillBase):
88
88
  "Can handle parentheses for complex expressions"
89
89
  ]
90
90
  }
91
- ]
91
+ ]
92
+
93
+ @classmethod
94
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
95
+ """
96
+ Get the parameter schema for the math skill
97
+
98
+ The math skill has no custom parameters - it inherits only
99
+ the base parameters from SkillBase.
100
+ """
101
+ # Get base schema from parent
102
+ schema = super().get_parameter_schema()
103
+
104
+ # No additional parameters for math skill
105
+
106
+ return schema
@@ -0,0 +1,423 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ import os
11
+ import importlib
12
+ import importlib.util
13
+ import inspect
14
+ import sys
15
+ from typing import Dict, List, Type, Optional, Any
16
+ from pathlib import Path
17
+
18
+ from signalwire_agents.core.skill_base import SkillBase
19
+ from signalwire_agents.core.logging_config import get_logger
20
+
21
+ class SkillRegistry:
22
+ """Global registry for on-demand skill loading"""
23
+
24
+ def __init__(self):
25
+ self._skills: Dict[str, Type[SkillBase]] = {}
26
+ self._external_paths: List[Path] = [] # Additional paths to search for skills
27
+ self._entry_points_loaded = False
28
+ self.logger = get_logger("skill_registry")
29
+
30
+ def _load_skill_on_demand(self, skill_name: str) -> Optional[Type[SkillBase]]:
31
+ """Load a skill on-demand by name"""
32
+ if skill_name in self._skills:
33
+ return self._skills[skill_name]
34
+
35
+ # First, ensure entry points are loaded
36
+ self._load_entry_points()
37
+
38
+ # Check if skill was loaded from entry points
39
+ if skill_name in self._skills:
40
+ return self._skills[skill_name]
41
+
42
+ # Search in built-in skills directory
43
+ skills_dir = Path(__file__).parent
44
+ skill_class = self._load_skill_from_path(skill_name, skills_dir)
45
+ if skill_class:
46
+ return skill_class
47
+
48
+ # Search in external paths
49
+ for external_path in self._external_paths:
50
+ skill_class = self._load_skill_from_path(skill_name, external_path)
51
+ if skill_class:
52
+ return skill_class
53
+
54
+ # Search in environment variable paths
55
+ env_paths = os.environ.get('SIGNALWIRE_SKILL_PATHS', '').split(':')
56
+ for path_str in env_paths:
57
+ if path_str:
58
+ skill_class = self._load_skill_from_path(skill_name, Path(path_str))
59
+ if skill_class:
60
+ return skill_class
61
+
62
+ self.logger.debug(f"Skill '{skill_name}' not found in any registered paths")
63
+ return None
64
+
65
+ def _load_skill_from_path(self, skill_name: str, base_path: Path) -> Optional[Type[SkillBase]]:
66
+ """Try to load a skill from a specific base path"""
67
+ skill_dir = base_path / skill_name
68
+ skill_file = skill_dir / "skill.py"
69
+
70
+ if not skill_file.exists():
71
+ return None
72
+
73
+ try:
74
+ # Create unique module name to avoid conflicts
75
+ module_name = f"signalwire_agents_external.{base_path.name}.{skill_name}.skill"
76
+ spec = importlib.util.spec_from_file_location(module_name, skill_file)
77
+ module = importlib.util.module_from_spec(spec)
78
+
79
+ # Add to sys.modules to handle relative imports
80
+ sys.modules[module_name] = module
81
+ spec.loader.exec_module(module)
82
+
83
+ # Find SkillBase subclasses in the module
84
+ for name, obj in inspect.getmembers(module):
85
+ if (inspect.isclass(obj) and
86
+ issubclass(obj, SkillBase) and
87
+ obj != SkillBase and
88
+ hasattr(obj, 'SKILL_NAME') and
89
+ obj.SKILL_NAME == skill_name): # Match exact skill name
90
+
91
+ self.register_skill(obj)
92
+ return obj
93
+
94
+ self.logger.warning(f"No skill class found with name '{skill_name}' in {skill_file}")
95
+ return None
96
+
97
+ except Exception as e:
98
+ self.logger.error(f"Failed to load skill '{skill_name}' from {skill_file}: {e}")
99
+ return None
100
+
101
+ def discover_skills(self) -> None:
102
+ """Deprecated: Skills are now loaded on-demand"""
103
+ # Keep this method for backwards compatibility but make it a no-op
104
+ pass
105
+
106
+ def _load_skill_from_directory(self, skill_dir: Path) -> None:
107
+ """Deprecated: Skills are now loaded on-demand"""
108
+ # Keep this method for backwards compatibility but make it a no-op
109
+ pass
110
+
111
+ def register_skill(self, skill_class: Type[SkillBase]) -> None:
112
+ """
113
+ Register a skill class directly
114
+
115
+ This allows third-party code to register skill classes without
116
+ requiring them to be in a specific directory structure.
117
+
118
+ Args:
119
+ skill_class: A class that inherits from SkillBase
120
+
121
+ Example:
122
+ from my_custom_skills import MyWeatherSkill
123
+ skill_registry.register_skill(MyWeatherSkill)
124
+ """
125
+ if not issubclass(skill_class, SkillBase):
126
+ raise ValueError(f"{skill_class} must inherit from SkillBase")
127
+
128
+ if not hasattr(skill_class, 'SKILL_NAME') or skill_class.SKILL_NAME is None:
129
+ raise ValueError(f"{skill_class} must define SKILL_NAME")
130
+
131
+ if skill_class.SKILL_NAME in self._skills:
132
+ self.logger.warning(f"Skill '{skill_class.SKILL_NAME}' already registered")
133
+ return
134
+
135
+ self._skills[skill_class.SKILL_NAME] = skill_class
136
+ self.logger.debug(f"Registered skill '{skill_class.SKILL_NAME}'")
137
+
138
+ def get_skill_class(self, skill_name: str) -> Optional[Type[SkillBase]]:
139
+ """Get skill class by name, loading on-demand if needed"""
140
+ # First check if already loaded
141
+ if skill_name in self._skills:
142
+ return self._skills[skill_name]
143
+
144
+ # Try to load on-demand
145
+ return self._load_skill_on_demand(skill_name)
146
+
147
+ def list_skills(self) -> List[Dict[str, str]]:
148
+ """List all available skills by scanning directories (only when explicitly requested)"""
149
+ # Only scan when this method is explicitly called (e.g., for CLI tools)
150
+ skills_dir = Path(__file__).parent
151
+ available_skills = []
152
+
153
+ for item in skills_dir.iterdir():
154
+ if item.is_dir() and not item.name.startswith('__'):
155
+ skill_file = item / "skill.py"
156
+ if skill_file.exists():
157
+ # Try to load the skill to get its metadata
158
+ skill_class = self._load_skill_on_demand(item.name)
159
+ if skill_class:
160
+ available_skills.append({
161
+ "name": skill_class.SKILL_NAME,
162
+ "description": skill_class.SKILL_DESCRIPTION,
163
+ "version": skill_class.SKILL_VERSION,
164
+ "required_packages": skill_class.REQUIRED_PACKAGES,
165
+ "required_env_vars": skill_class.REQUIRED_ENV_VARS,
166
+ "supports_multiple_instances": skill_class.SUPPORTS_MULTIPLE_INSTANCES
167
+ })
168
+
169
+ return available_skills
170
+
171
+ def get_all_skills_schema(self) -> Dict[str, Dict[str, Any]]:
172
+ """
173
+ Get complete schema for all available skills including parameter metadata
174
+
175
+ This method scans all available skills and returns a comprehensive schema
176
+ that includes skill metadata and parameter definitions suitable for GUI
177
+ configuration or API documentation.
178
+
179
+ Returns:
180
+ Dict[str, Dict[str, Any]]: Complete skill schema where keys are skill names
181
+ and values contain:
182
+ - name: Skill name
183
+ - description: Skill description
184
+ - version: Skill version
185
+ - supports_multiple_instances: Whether multiple instances are allowed
186
+ - required_packages: List of required Python packages
187
+ - required_env_vars: List of required environment variables
188
+ - parameters: Parameter schema from get_parameter_schema()
189
+ - source: Where the skill was loaded from ('built-in', 'external', 'entry_point', 'registered')
190
+
191
+ Example:
192
+ {
193
+ "web_search": {
194
+ "name": "web_search",
195
+ "description": "Search the web for information",
196
+ "version": "1.0.0",
197
+ "supports_multiple_instances": True,
198
+ "required_packages": ["bs4", "requests"],
199
+ "required_env_vars": [],
200
+ "parameters": {
201
+ "api_key": {
202
+ "type": "string",
203
+ "description": "Google API key",
204
+ "required": True,
205
+ "hidden": True,
206
+ "env_var": "GOOGLE_SEARCH_API_KEY"
207
+ },
208
+ ...
209
+ },
210
+ "source": "built-in"
211
+ }
212
+ }
213
+ """
214
+ skills_schema = {}
215
+
216
+ # Load entry points first
217
+ self._load_entry_points()
218
+
219
+ # Helper function to add skill to schema
220
+ def add_skill_to_schema(skill_class, source):
221
+ try:
222
+ # Get parameter schema
223
+ try:
224
+ parameter_schema = skill_class.get_parameter_schema()
225
+ except AttributeError:
226
+ # Skill doesn't implement get_parameter_schema yet
227
+ parameter_schema = {}
228
+
229
+ skills_schema[skill_class.SKILL_NAME] = {
230
+ "name": skill_class.SKILL_NAME,
231
+ "description": skill_class.SKILL_DESCRIPTION,
232
+ "version": getattr(skill_class, 'SKILL_VERSION', '1.0.0'),
233
+ "supports_multiple_instances": getattr(skill_class, 'SUPPORTS_MULTIPLE_INSTANCES', False),
234
+ "required_packages": getattr(skill_class, 'REQUIRED_PACKAGES', []),
235
+ "required_env_vars": getattr(skill_class, 'REQUIRED_ENV_VARS', []),
236
+ "parameters": parameter_schema,
237
+ "source": source
238
+ }
239
+ except Exception as e:
240
+ self.logger.error(f"Failed to get schema for skill '{skill_class.SKILL_NAME}': {e}")
241
+
242
+ # Add already registered skills first (includes entry points)
243
+ for skill_name, skill_class in self._skills.items():
244
+ add_skill_to_schema(skill_class, 'registered')
245
+
246
+ # Scan built-in skills directory
247
+ skills_dir = Path(__file__).parent
248
+ for item in skills_dir.iterdir():
249
+ if item.is_dir() and not item.name.startswith('__'):
250
+ skill_file = item / "skill.py"
251
+ if skill_file.exists() and item.name not in skills_schema:
252
+ try:
253
+ skill_class = self._load_skill_on_demand(item.name)
254
+ if skill_class:
255
+ add_skill_to_schema(skill_class, 'built-in')
256
+ except Exception as e:
257
+ self.logger.error(f"Failed to load skill '{item.name}': {e}")
258
+
259
+ # Scan external directories
260
+ for external_path in self._external_paths:
261
+ if external_path.exists():
262
+ for item in external_path.iterdir():
263
+ if item.is_dir() and not item.name.startswith('__'):
264
+ skill_file = item / "skill.py"
265
+ if skill_file.exists() and item.name not in skills_schema:
266
+ try:
267
+ skill_class = self._load_skill_on_demand(item.name)
268
+ if skill_class:
269
+ add_skill_to_schema(skill_class, 'external')
270
+ except Exception as e:
271
+ self.logger.error(f"Failed to load skill '{item.name}': {e}")
272
+
273
+ # Scan environment variable paths
274
+ env_paths = os.environ.get('SIGNALWIRE_SKILL_PATHS', '').split(':')
275
+ for path_str in env_paths:
276
+ if path_str:
277
+ env_path = Path(path_str)
278
+ if env_path.exists():
279
+ for item in env_path.iterdir():
280
+ if item.is_dir() and not item.name.startswith('__'):
281
+ skill_file = item / "skill.py"
282
+ if skill_file.exists() and item.name not in skills_schema:
283
+ try:
284
+ skill_class = self._load_skill_on_demand(item.name)
285
+ if skill_class:
286
+ add_skill_to_schema(skill_class, 'external')
287
+ except Exception as e:
288
+ self.logger.error(f"Failed to load skill '{item.name}': {e}")
289
+
290
+ return skills_schema
291
+
292
+ def add_skill_directory(self, path: str) -> None:
293
+ """
294
+ Add a directory to search for skills
295
+
296
+ This allows third-party skill collections to be registered by path.
297
+ Skills in these directories should follow the same structure as built-in skills:
298
+ - Each skill in its own subdirectory
299
+ - skill.py file containing the skill class
300
+
301
+ Args:
302
+ path: Path to directory containing skill subdirectories
303
+
304
+ Example:
305
+ skill_registry.add_skill_directory('/opt/custom_skills')
306
+ # Now agent.add_skill('my_custom_skill') will search in this directory
307
+ """
308
+ skill_path = Path(path)
309
+ if not skill_path.exists():
310
+ raise ValueError(f"Skill directory does not exist: {path}")
311
+ if not skill_path.is_dir():
312
+ raise ValueError(f"Path is not a directory: {path}")
313
+
314
+ if skill_path not in self._external_paths:
315
+ self._external_paths.append(skill_path)
316
+ self.logger.info(f"Added external skill directory: {path}")
317
+
318
+ def _load_entry_points(self) -> None:
319
+ """
320
+ Load skills from Python entry points
321
+
322
+ This allows installed packages to register skills via setup.py:
323
+
324
+ entry_points={
325
+ 'signalwire_agents.skills': [
326
+ 'weather = my_package.skills:WeatherSkill',
327
+ 'stock = my_package.skills:StockSkill',
328
+ ]
329
+ }
330
+ """
331
+ if self._entry_points_loaded:
332
+ return
333
+
334
+ self._entry_points_loaded = True
335
+
336
+ try:
337
+ import pkg_resources
338
+
339
+ for entry_point in pkg_resources.iter_entry_points('signalwire_agents.skills'):
340
+ try:
341
+ skill_class = entry_point.load()
342
+ if issubclass(skill_class, SkillBase):
343
+ self.register_skill(skill_class)
344
+ self.logger.info(f"Loaded skill '{skill_class.SKILL_NAME}' from entry point '{entry_point.name}'")
345
+ else:
346
+ self.logger.warning(f"Entry point '{entry_point.name}' does not provide a SkillBase subclass")
347
+ except Exception as e:
348
+ self.logger.error(f"Failed to load skill from entry point '{entry_point.name}': {e}")
349
+
350
+ except ImportError:
351
+ # pkg_resources not available, try importlib.metadata (Python 3.8+)
352
+ try:
353
+ from importlib import metadata
354
+
355
+ entry_points = metadata.entry_points()
356
+ if hasattr(entry_points, 'select'):
357
+ # Python 3.10+
358
+ skill_entries = entry_points.select(group='signalwire_agents.skills')
359
+ else:
360
+ # Python 3.8-3.9
361
+ skill_entries = entry_points.get('signalwire_agents.skills', [])
362
+
363
+ for entry_point in skill_entries:
364
+ try:
365
+ skill_class = entry_point.load()
366
+ if issubclass(skill_class, SkillBase):
367
+ self.register_skill(skill_class)
368
+ self.logger.info(f"Loaded skill '{skill_class.SKILL_NAME}' from entry point '{entry_point.name}'")
369
+ else:
370
+ self.logger.warning(f"Entry point '{entry_point.name}' does not provide a SkillBase subclass")
371
+ except Exception as e:
372
+ self.logger.error(f"Failed to load skill from entry point '{entry_point.name}': {e}")
373
+
374
+ except ImportError:
375
+ # Neither pkg_resources nor importlib.metadata available
376
+ self.logger.debug("Entry point loading not available - install setuptools or use Python 3.8+")
377
+
378
+ def list_all_skill_sources(self) -> Dict[str, List[str]]:
379
+ """
380
+ List all skill sources and the skills available from each
381
+
382
+ Returns a dictionary mapping source types to lists of skill names:
383
+ {
384
+ 'built-in': ['datetime', 'math', ...],
385
+ 'external_paths': ['custom_skill1', ...],
386
+ 'entry_points': ['weather', ...],
387
+ 'registered': ['my_skill', ...]
388
+ }
389
+ """
390
+ sources = {
391
+ 'built-in': [],
392
+ 'external_paths': [],
393
+ 'entry_points': [],
394
+ 'registered': []
395
+ }
396
+
397
+ # Built-in skills
398
+ skills_dir = Path(__file__).parent
399
+ for item in skills_dir.iterdir():
400
+ if item.is_dir() and not item.name.startswith('__'):
401
+ skill_file = item / "skill.py"
402
+ if skill_file.exists():
403
+ sources['built-in'].append(item.name)
404
+
405
+ # External path skills
406
+ for external_path in self._external_paths:
407
+ if external_path.exists():
408
+ for item in external_path.iterdir():
409
+ if item.is_dir() and not item.name.startswith('__'):
410
+ skill_file = item / "skill.py"
411
+ if skill_file.exists():
412
+ sources['external_paths'].append(item.name)
413
+
414
+ # Already registered skills
415
+ for skill_name in self._skills:
416
+ # Determine source of registered skill
417
+ if skill_name not in sources['built-in']:
418
+ sources['registered'].append(skill_name)
419
+
420
+ return sources
421
+
422
+ # Global registry instance
423
+ skill_registry = SkillRegistry()