signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl

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 (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ Session Manager for MCP Gateway
13
+
14
+ Manages lifecycle of MCP server sessions tied to SignalWire call IDs.
15
+ Handles timeouts, cleanup, and resource limits.
16
+ """
17
+
18
+ import threading
19
+ import time
20
+ import logging
21
+ from typing import Dict, Optional, Any
22
+ from datetime import datetime, timedelta
23
+ from dataclasses import dataclass, field
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class Session:
30
+ """Represents an active MCP session"""
31
+ session_id: str
32
+ service_name: str
33
+ process: Any # MCPClient instance
34
+ created_at: datetime = field(default_factory=datetime.now)
35
+ last_accessed: datetime = field(default_factory=datetime.now)
36
+ timeout: int = 300 # seconds
37
+ metadata: Dict[str, Any] = field(default_factory=dict)
38
+
39
+ @property
40
+ def is_expired(self) -> bool:
41
+ """Check if session has expired based on timeout"""
42
+ return datetime.now() > self.last_accessed + timedelta(seconds=self.timeout)
43
+
44
+ @property
45
+ def is_alive(self) -> bool:
46
+ """Check if the underlying MCP client is still running"""
47
+ return self.process and self.process.process and self.process.process.poll() is None
48
+
49
+ def touch(self):
50
+ """Update last accessed time"""
51
+ self.last_accessed = datetime.now()
52
+
53
+
54
+ class SessionManager:
55
+ """Manages MCP server sessions with automatic cleanup"""
56
+
57
+ def __init__(self, config: Dict[str, Any]):
58
+ self.config = config
59
+ self.sessions: Dict[str, Session] = {}
60
+ self.lock = threading.RLock()
61
+ self.cleanup_interval = config.get('session', {}).get('cleanup_interval', 60)
62
+ self.max_sessions_per_service = config.get('session', {}).get('max_sessions_per_service', 100)
63
+ self.default_timeout = config.get('session', {}).get('default_timeout', 300)
64
+ self._shutdown = threading.Event()
65
+
66
+ # Start cleanup thread
67
+ self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
68
+ self.cleanup_thread.start()
69
+
70
+ logger.info(f"SessionManager initialized with cleanup_interval={self.cleanup_interval}s")
71
+
72
+ def create_session(self, session_id: str, service_name: str, process: Any,
73
+ timeout: Optional[int] = None, metadata: Optional[Dict[str, Any]] = None) -> Session:
74
+ """Create and register a new session"""
75
+ with self.lock:
76
+ # Check if session already exists
77
+ if session_id in self.sessions:
78
+ logger.warning(f"Session {session_id} already exists, closing old session")
79
+ self.close_session(session_id)
80
+
81
+ # Check service limits
82
+ service_count = sum(1 for s in self.sessions.values() if s.service_name == service_name)
83
+ if service_count >= self.max_sessions_per_service:
84
+ raise RuntimeError(f"Max sessions limit reached for service {service_name}")
85
+
86
+ # Create new session
87
+ session = Session(
88
+ session_id=session_id,
89
+ service_name=service_name,
90
+ process=process,
91
+ timeout=timeout or self.default_timeout,
92
+ metadata=metadata or {}
93
+ )
94
+
95
+ self.sessions[session_id] = session
96
+ logger.info(f"Created session {session_id} for service {service_name}")
97
+
98
+ return session
99
+
100
+ def get_session(self, session_id: str) -> Optional[Session]:
101
+ """Get an active session by ID"""
102
+ with self.lock:
103
+ session = self.sessions.get(session_id)
104
+
105
+ if session:
106
+ if not session.is_alive:
107
+ logger.warning(f"Session {session_id} process is dead, removing")
108
+ self.close_session(session_id)
109
+ return None
110
+
111
+ if session.is_expired:
112
+ logger.info(f"Session {session_id} has expired, removing")
113
+ self.close_session(session_id)
114
+ return None
115
+
116
+ # Update last accessed time
117
+ session.touch()
118
+
119
+ return session
120
+
121
+ def close_session(self, session_id: str) -> bool:
122
+ """Close and remove a session"""
123
+ with self.lock:
124
+ session = self.sessions.pop(session_id, None)
125
+
126
+ if not session:
127
+ logger.warning(f"Attempted to close non-existent session {session_id}")
128
+ return False
129
+
130
+ # Terminate the MCP client
131
+ if session.process:
132
+ try:
133
+ # Stop the MCP client
134
+ session.process.stop()
135
+ except Exception as e:
136
+ logger.error(f"Error stopping MCP client for session {session_id}: {e}")
137
+
138
+ logger.info(f"Closed session {session_id}")
139
+ return True
140
+
141
+ def list_sessions(self) -> Dict[str, Dict[str, Any]]:
142
+ """List all active sessions with their info"""
143
+ with self.lock:
144
+ result = {}
145
+
146
+ for session_id, session in list(self.sessions.items()):
147
+ # Check if still valid
148
+ if not session.is_alive or session.is_expired:
149
+ self.close_session(session_id)
150
+ continue
151
+
152
+ result[session_id] = {
153
+ 'service_name': session.service_name,
154
+ 'created_at': session.created_at.isoformat(),
155
+ 'last_accessed': session.last_accessed.isoformat(),
156
+ 'timeout': session.timeout,
157
+ 'metadata': session.metadata,
158
+ 'time_remaining': max(0, (session.last_accessed + timedelta(seconds=session.timeout) - datetime.now()).total_seconds())
159
+ }
160
+
161
+ return result
162
+
163
+ def get_service_session_count(self, service_name: str) -> int:
164
+ """Get number of active sessions for a service"""
165
+ with self.lock:
166
+ return sum(1 for s in self.sessions.values()
167
+ if s.service_name == service_name and s.is_alive and not s.is_expired)
168
+
169
+ def _cleanup_loop(self):
170
+ """Background thread that cleans up expired sessions"""
171
+ logger.info("Session cleanup thread started")
172
+
173
+ while not self._shutdown.is_set():
174
+ try:
175
+ # Wait with timeout so we can check shutdown flag
176
+ if self._shutdown.wait(timeout=self.cleanup_interval):
177
+ break
178
+
179
+ with self.lock:
180
+ expired_sessions = []
181
+
182
+ for session_id, session in self.sessions.items():
183
+ if session.is_expired or not session.is_alive:
184
+ expired_sessions.append(session_id)
185
+
186
+ for session_id in expired_sessions:
187
+ logger.info(f"Cleaning up expired session {session_id}")
188
+ self.close_session(session_id)
189
+
190
+ if expired_sessions:
191
+ logger.info(f"Cleaned up {len(expired_sessions)} expired sessions")
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error in cleanup thread: {e}")
195
+
196
+ logger.info("Session cleanup thread stopped")
197
+
198
+ def shutdown(self):
199
+ """Shutdown all sessions and cleanup"""
200
+ logger.info("Shutting down SessionManager")
201
+
202
+ # Signal cleanup thread to stop
203
+ self._shutdown.set()
204
+
205
+ with self.lock:
206
+ # Close all sessions
207
+ session_ids = list(self.sessions.keys())
208
+ logger.info(f"Closing {len(session_ids)} active sessions")
209
+ for session_id in session_ids:
210
+ self.close_session(session_id)
211
+
212
+ # Wait for cleanup thread to finish (with timeout)
213
+ if self.cleanup_thread.is_alive():
214
+ self.cleanup_thread.join(timeout=2.0)
215
+ if self.cleanup_thread.is_alive():
216
+ logger.warning("Cleanup thread did not stop gracefully")
217
+
218
+ logger.info("SessionManager shutdown complete")
@@ -52,7 +52,6 @@ class ConciergeAgent(AgentBase):
52
52
  welcome_message: Optional[str] = None,
53
53
  name: str = "concierge",
54
54
  route: str = "/concierge",
55
- enable_state_tracking: bool = True,
56
55
  **kwargs
57
56
  ):
58
57
  """
@@ -67,7 +66,6 @@ class ConciergeAgent(AgentBase):
67
66
  welcome_message: Optional custom welcome message
68
67
  name: Agent name for the route
69
68
  route: HTTP route for this agent
70
- enable_state_tracking: Whether to enable state tracking (default: True)
71
69
  **kwargs: Additional arguments for AgentBase
72
70
  """
73
71
  # Initialize the base agent
@@ -75,7 +73,6 @@ class ConciergeAgent(AgentBase):
75
73
  name=name,
76
74
  route=route,
77
75
  use_pom=True,
78
- enable_state_tracking=enable_state_tracking,
79
76
  **kwargs
80
77
  )
81
78
 
@@ -51,7 +51,6 @@ class FAQBotAgent(AgentBase):
51
51
  persona: Optional[str] = None,
52
52
  name: str = "faq_bot",
53
53
  route: str = "/faq",
54
- enable_state_tracking: bool = True, # Enable state tracking by default
55
54
  **kwargs
56
55
  ):
57
56
  """
@@ -66,7 +65,6 @@ class FAQBotAgent(AgentBase):
66
65
  persona: Optional custom personality description
67
66
  name: Agent name for the route
68
67
  route: HTTP route for this agent
69
- enable_state_tracking: Whether to enable state tracking (default: True)
70
68
  **kwargs: Additional arguments for AgentBase
71
69
  """
72
70
  # Initialize the base agent
@@ -74,7 +72,6 @@ class FAQBotAgent(AgentBase):
74
72
  name=name,
75
73
  route=route,
76
74
  use_pom=True,
77
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
78
75
  **kwargs
79
76
  )
80
77
 
@@ -45,7 +45,6 @@ class InfoGathererAgent(AgentBase):
45
45
  questions: Optional[List[Dict[str, str]]] = None,
46
46
  name: str = "info_gatherer",
47
47
  route: str = "/info_gatherer",
48
- enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
49
48
  **kwargs
50
49
  ):
51
50
  """
@@ -59,7 +58,6 @@ class InfoGathererAgent(AgentBase):
59
58
  - confirm: (Optional) If set to True, the agent will confirm the answer before submitting
60
59
  name: Agent name for the route
61
60
  route: HTTP route for this agent
62
- enable_state_tracking: Whether to enable state tracking (default: True)
63
61
  **kwargs: Additional arguments for AgentBase
64
62
  """
65
63
  # Initialize the base agent
@@ -67,7 +65,6 @@ class InfoGathererAgent(AgentBase):
67
65
  name=name,
68
66
  route=route,
69
67
  use_pom=True,
70
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
71
68
  **kwargs
72
69
  )
73
70
 
@@ -41,7 +41,6 @@ class ReceptionistAgent(AgentBase):
41
41
  route: str = "/receptionist",
42
42
  greeting: str = "Thank you for calling. How can I help you today?",
43
43
  voice: str = "rime.spore",
44
- enable_state_tracking: bool = True, # Enable state tracking by default
45
44
  **kwargs
46
45
  ):
47
46
  """
@@ -56,7 +55,6 @@ class ReceptionistAgent(AgentBase):
56
55
  route: HTTP route for this agent
57
56
  greeting: Initial greeting message
58
57
  voice: Voice ID to use
59
- enable_state_tracking: Whether to enable state tracking (default: True)
60
58
  **kwargs: Additional arguments for AgentBase
61
59
  """
62
60
  # Initialize the base agent
@@ -64,7 +62,6 @@ class ReceptionistAgent(AgentBase):
64
62
  name=name,
65
63
  route=route,
66
64
  use_pom=True,
67
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
68
65
  **kwargs
69
66
  )
70
67
 
@@ -62,7 +62,6 @@ class SurveyAgent(AgentBase):
62
62
  max_retries: int = 2,
63
63
  name: str = "survey",
64
64
  route: str = "/survey",
65
- enable_state_tracking: bool = True, # Enable state tracking by default
66
65
  **kwargs
67
66
  ):
68
67
  """
@@ -83,7 +82,6 @@ class SurveyAgent(AgentBase):
83
82
  max_retries: Maximum number of times to retry invalid answers
84
83
  name: Name for the agent (default: "survey")
85
84
  route: HTTP route for the agent (default: "/survey")
86
- enable_state_tracking: Whether to enable state tracking (default: True)
87
85
  **kwargs: Additional arguments for AgentBase
88
86
  """
89
87
  # Initialize the base agent
@@ -91,7 +89,6 @@ class SurveyAgent(AgentBase):
91
89
  name=name,
92
90
  route=route,
93
91
  use_pom=True,
94
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
95
92
  **kwargs
96
93
  )
97
94