signalwire-agents 0.1.11__py3-none-any.whl → 0.1.13__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 (26) hide show
  1. signalwire_agents/__init__.py +5 -1
  2. signalwire_agents/agent_server.py +222 -13
  3. signalwire_agents/cli/build_search.py +457 -0
  4. signalwire_agents/cli/test_swaig.py +177 -113
  5. signalwire_agents/core/agent_base.py +1 -3
  6. signalwire_agents/core/logging_config.py +232 -0
  7. signalwire_agents/core/swaig_function.py +2 -3
  8. signalwire_agents/core/swml_renderer.py +43 -28
  9. signalwire_agents/search/__init__.py +131 -0
  10. signalwire_agents/search/document_processor.py +764 -0
  11. signalwire_agents/search/index_builder.py +534 -0
  12. signalwire_agents/search/query_processor.py +371 -0
  13. signalwire_agents/search/search_engine.py +383 -0
  14. signalwire_agents/search/search_service.py +251 -0
  15. signalwire_agents/skills/native_vector_search/__init__.py +1 -0
  16. signalwire_agents/skills/native_vector_search/skill.py +352 -0
  17. signalwire_agents/skills/registry.py +2 -15
  18. signalwire_agents/utils/__init__.py +13 -1
  19. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/METADATA +110 -3
  20. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/RECORD +25 -16
  21. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/entry_points.txt +1 -0
  22. signalwire_agents/utils/serverless.py +0 -38
  23. {signalwire_agents-0.1.11.data → signalwire_agents-0.1.13.data}/data/schema.json +0 -0
  24. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/WHEEL +0 -0
  25. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/licenses/LICENSE +0 -0
  26. {signalwire_agents-0.1.11.dist-info → signalwire_agents-0.1.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,232 @@
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
+ """
11
+ Central logging configuration for SignalWire Agents SDK
12
+
13
+ This module provides a single point of control for all logging across the SDK
14
+ and applications built with it. All components should use get_logger() instead
15
+ of direct logging module usage or print() statements.
16
+
17
+ The StructuredLoggerWrapper provides backward compatibility with existing
18
+ structured logging calls (e.g., log.info("message", key=value)) while using
19
+ standard Python logging underneath. This allows the entire codebase to work
20
+ without changes while providing centralized logging control.
21
+ """
22
+
23
+ import logging
24
+ import os
25
+ import sys
26
+ from typing import Optional, Any, Dict
27
+
28
+ # Global flag to ensure configuration only happens once
29
+ _logging_configured = False
30
+
31
+
32
+ class StructuredLoggerWrapper:
33
+ """
34
+ A wrapper that provides structured logging interface while using standard Python logging
35
+
36
+ This allows existing structured logging calls to work without changes while
37
+ giving us centralized control over logging behavior.
38
+ """
39
+
40
+ def __init__(self, logger: logging.Logger):
41
+ self._logger = logger
42
+
43
+ def _format_structured_message(self, message: str, **kwargs) -> str:
44
+ """Format a message with structured keyword arguments"""
45
+ if not kwargs:
46
+ return message
47
+
48
+ # Convert kwargs to readable string format
49
+ parts = []
50
+ for key, value in kwargs.items():
51
+ # Handle different value types appropriately
52
+ if isinstance(value, str):
53
+ parts.append(f"{key}={value}")
54
+ elif isinstance(value, (list, dict)):
55
+ parts.append(f"{key}={str(value)}")
56
+ else:
57
+ parts.append(f"{key}={value}")
58
+
59
+ if parts:
60
+ return f"{message} ({', '.join(parts)})"
61
+ else:
62
+ return message
63
+
64
+ def debug(self, message: str, **kwargs) -> None:
65
+ """Log debug message with optional structured data"""
66
+ formatted = self._format_structured_message(message, **kwargs)
67
+ self._logger.debug(formatted)
68
+
69
+ def info(self, message: str, **kwargs) -> None:
70
+ """Log info message with optional structured data"""
71
+ formatted = self._format_structured_message(message, **kwargs)
72
+ self._logger.info(formatted)
73
+
74
+ def warning(self, message: str, **kwargs) -> None:
75
+ """Log warning message with optional structured data"""
76
+ formatted = self._format_structured_message(message, **kwargs)
77
+ self._logger.warning(formatted)
78
+
79
+ def error(self, message: str, **kwargs) -> None:
80
+ """Log error message with optional structured data"""
81
+ formatted = self._format_structured_message(message, **kwargs)
82
+ self._logger.error(formatted)
83
+
84
+ def critical(self, message: str, **kwargs) -> None:
85
+ """Log critical message with optional structured data"""
86
+ formatted = self._format_structured_message(message, **kwargs)
87
+ self._logger.critical(formatted)
88
+
89
+ # Also support the 'warn' alias
90
+ warn = warning
91
+
92
+ # Support direct access to underlying logger attributes if needed
93
+ def __getattr__(self, name: str) -> Any:
94
+ """Delegate any unknown attributes to the underlying logger"""
95
+ return getattr(self._logger, name)
96
+
97
+
98
+ def get_execution_mode() -> str:
99
+ """
100
+ Determine the execution mode based on environment variables
101
+
102
+ Returns:
103
+ 'cgi' if running in CGI mode
104
+ 'lambda' if running in AWS Lambda
105
+ 'server' for normal server mode
106
+ """
107
+ if os.getenv('GATEWAY_INTERFACE'):
108
+ return 'cgi'
109
+ if os.getenv('AWS_LAMBDA_FUNCTION_NAME') or os.getenv('LAMBDA_TASK_ROOT'):
110
+ return 'lambda'
111
+ return 'server'
112
+
113
+
114
+ def configure_logging():
115
+ """
116
+ Configure logging system once, globally, based on environment variables
117
+
118
+ Environment Variables:
119
+ SIGNALWIRE_LOG_MODE: off, stderr, default, auto
120
+ SIGNALWIRE_LOG_LEVEL: debug, info, warning, error, critical
121
+ """
122
+ global _logging_configured
123
+
124
+ if _logging_configured:
125
+ return
126
+
127
+ # Get configuration from environment
128
+ log_mode = os.getenv('SIGNALWIRE_LOG_MODE', '').lower()
129
+ log_level = os.getenv('SIGNALWIRE_LOG_LEVEL', 'info').lower()
130
+
131
+ # Determine log mode if auto or not specified
132
+ if not log_mode or log_mode == 'auto':
133
+ execution_mode = get_execution_mode()
134
+ if execution_mode == 'cgi':
135
+ log_mode = 'off'
136
+ else:
137
+ log_mode = 'default'
138
+
139
+ # Configure based on mode
140
+ if log_mode == 'off':
141
+ _configure_off_mode()
142
+ elif log_mode == 'stderr':
143
+ _configure_stderr_mode(log_level)
144
+ else: # default mode
145
+ _configure_default_mode(log_level)
146
+
147
+ _logging_configured = True
148
+
149
+
150
+ def _configure_off_mode():
151
+ """Suppress all logging output"""
152
+ # Redirect to devnull
153
+ null_file = open(os.devnull, 'w')
154
+
155
+ # Clear existing handlers and configure to devnull
156
+ logging.getLogger().handlers.clear()
157
+ logging.basicConfig(
158
+ stream=null_file,
159
+ level=logging.CRITICAL,
160
+ format=''
161
+ )
162
+
163
+ # Set all known loggers to CRITICAL to prevent any output
164
+ logger_names = [
165
+ '', 'signalwire_agents', 'skill_registry', 'swml_service',
166
+ 'agent_base', 'AgentServer', 'uvicorn', 'fastapi'
167
+ ]
168
+ for name in logger_names:
169
+ logging.getLogger(name).setLevel(logging.CRITICAL)
170
+
171
+ # Configure structlog if available
172
+ try:
173
+ import structlog
174
+ structlog.configure(
175
+ processors=[],
176
+ wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL),
177
+ logger_factory=structlog.PrintLoggerFactory(file=null_file),
178
+ cache_logger_on_first_use=True,
179
+ )
180
+ except ImportError:
181
+ pass
182
+
183
+
184
+ def _configure_stderr_mode(log_level: str):
185
+ """Configure logging to stderr"""
186
+ # Clear existing handlers
187
+ logging.getLogger().handlers.clear()
188
+
189
+ # Convert log level
190
+ numeric_level = getattr(logging, log_level.upper(), logging.INFO)
191
+
192
+ # Configure to stderr
193
+ logging.basicConfig(
194
+ stream=sys.stderr,
195
+ level=numeric_level,
196
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
197
+ )
198
+
199
+
200
+ def _configure_default_mode(log_level: str):
201
+ """Configure standard logging behavior"""
202
+ # Convert log level
203
+ numeric_level = getattr(logging, log_level.upper(), logging.INFO)
204
+
205
+ # Configure standard logging
206
+ logging.basicConfig(
207
+ level=numeric_level,
208
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
209
+ )
210
+
211
+
212
+ def get_logger(name: str) -> StructuredLoggerWrapper:
213
+ """
214
+ Get a logger instance for the specified name with structured logging support
215
+
216
+ This is the single entry point for all logging in the SDK.
217
+ All modules should use this instead of direct logging module usage.
218
+
219
+ Args:
220
+ name: Logger name, typically __name__
221
+
222
+ Returns:
223
+ StructuredLoggerWrapper that supports both regular and structured logging
224
+ """
225
+ # Ensure logging is configured
226
+ configure_logging()
227
+
228
+ # Get the standard Python logger
229
+ python_logger = logging.getLogger(name)
230
+
231
+ # Wrap it with our structured logging interface
232
+ return StructuredLoggerWrapper(python_logger)
@@ -15,6 +15,8 @@ from typing import Dict, Any, Optional, Callable, List, Type, Union
15
15
  import inspect
16
16
  import logging
17
17
 
18
+ # Import here to avoid circular imports
19
+ from signalwire_agents.core.function_result import SwaigFunctionResult
18
20
 
19
21
  class SWAIGFunction:
20
22
  """
@@ -101,9 +103,6 @@ class SWAIGFunction:
101
103
  # Call the handler with both args and raw_data
102
104
  result = self.handler(args, raw_data)
103
105
 
104
- # Import here to avoid circular imports
105
- from signalwire_agents.core.function_result import SwaigFunctionResult
106
-
107
106
  # Handle different result types - everything must end up as a SwaigFunctionResult
108
107
  if isinstance(result, SwaigFunctionResult):
109
108
  # Already a SwaigFunctionResult - just convert to dict
@@ -269,6 +269,7 @@ class SwmlRenderer:
269
269
 
270
270
  # Return in requested format
271
271
  if format.lower() == "yaml":
272
+ import yaml
272
273
  return yaml.dump(swml, sort_keys=False)
273
274
  else:
274
275
  return json.dumps(swml, indent=2)
@@ -305,17 +306,24 @@ class SwmlRenderer:
305
306
  # Add any actions
306
307
  if actions:
307
308
  for action in actions:
308
- if action["type"] == "play":
309
- service.add_verb("play", {
310
- "url": action["url"]
311
- })
312
- elif action["type"] == "transfer":
313
- service.add_verb("connect", [
314
- {"to": action["dest"]}
315
- ])
316
- elif action["type"] == "hang_up":
317
- service.add_verb("hangup", {})
318
- # Additional action types could be added here
309
+ # Support both type-based actions and direct SWML verbs
310
+ if "type" in action:
311
+ # Type-based action format
312
+ if action["type"] == "play":
313
+ service.add_verb("play", {
314
+ "url": action["url"]
315
+ })
316
+ elif action["type"] == "transfer":
317
+ service.add_verb("connect", [
318
+ {"to": action["dest"]}
319
+ ])
320
+ elif action["type"] == "hang_up":
321
+ service.add_verb("hangup", {})
322
+ # Additional action types could be added here
323
+ else:
324
+ # Direct SWML verb format
325
+ for verb_name, verb_config in action.items():
326
+ service.add_verb(verb_name, verb_config)
319
327
 
320
328
  # Return in requested format
321
329
  if format.lower() == "yaml":
@@ -343,26 +351,33 @@ class SwmlRenderer:
343
351
  # Add any actions
344
352
  if actions:
345
353
  for action in actions:
346
- if action["type"] == "play":
347
- swml["sections"]["main"].append({
348
- "play": {
349
- "url": action["url"]
350
- }
351
- })
352
- elif action["type"] == "transfer":
353
- swml["sections"]["main"].append({
354
- "connect": [
355
- {"to": action["dest"]}
356
- ]
357
- })
358
- elif action["type"] == "hang_up":
359
- swml["sections"]["main"].append({
360
- "hangup": {}
361
- })
362
- # Additional action types could be added here
354
+ # Support both type-based actions and direct SWML verbs
355
+ if "type" in action:
356
+ # Type-based action format
357
+ if action["type"] == "play":
358
+ swml["sections"]["main"].append({
359
+ "play": {
360
+ "url": action["url"]
361
+ }
362
+ })
363
+ elif action["type"] == "transfer":
364
+ swml["sections"]["main"].append({
365
+ "connect": [
366
+ {"to": action["dest"]}
367
+ ]
368
+ })
369
+ elif action["type"] == "hang_up":
370
+ swml["sections"]["main"].append({
371
+ "hangup": {}
372
+ })
373
+ # Additional action types could be added here
374
+ else:
375
+ # Direct SWML verb format - add the action as-is
376
+ swml["sections"]["main"].append(action)
363
377
 
364
378
  # Return in requested format
365
379
  if format.lower() == "yaml":
380
+ import yaml
366
381
  return yaml.dump(swml, sort_keys=False)
367
382
  else:
368
383
  return json.dumps(swml)
@@ -0,0 +1,131 @@
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
+ """SignalWire Agents Local Search Module
11
+
12
+ This module provides local search capabilities for the SignalWire Agents SDK.
13
+ It requires additional dependencies that can be installed with:
14
+
15
+ pip install signalwire-agents[search] # Basic search
16
+ pip install signalwire-agents[search-full] # + Document processing
17
+ pip install signalwire-agents[search-nlp] # + Advanced NLP
18
+ pip install signalwire-agents[search-all] # All features
19
+ """
20
+
21
+ import warnings
22
+
23
+ # Check for core search dependencies
24
+ _SEARCH_AVAILABLE = True
25
+ _MISSING_DEPS = []
26
+
27
+ try:
28
+ import numpy
29
+ except ImportError:
30
+ _SEARCH_AVAILABLE = False
31
+ _MISSING_DEPS.append('numpy')
32
+
33
+ try:
34
+ import sklearn
35
+ except ImportError:
36
+ _SEARCH_AVAILABLE = False
37
+ _MISSING_DEPS.append('scikit-learn')
38
+
39
+ try:
40
+ import sentence_transformers
41
+ except ImportError:
42
+ _SEARCH_AVAILABLE = False
43
+ _MISSING_DEPS.append('sentence-transformers')
44
+
45
+ try:
46
+ import nltk
47
+ except ImportError:
48
+ _SEARCH_AVAILABLE = False
49
+ _MISSING_DEPS.append('nltk')
50
+
51
+ def _check_search_dependencies():
52
+ """Check if search dependencies are available and provide helpful error message"""
53
+ if not _SEARCH_AVAILABLE:
54
+ missing = ', '.join(_MISSING_DEPS)
55
+ raise ImportError(
56
+ f"Search functionality requires additional dependencies: {missing}\n"
57
+ f"Install with: pip install signalwire-agents[search]\n"
58
+ f"For full features: pip install signalwire-agents[search-all]"
59
+ )
60
+
61
+ # Conditional imports based on available dependencies
62
+ __all__ = []
63
+
64
+ if _SEARCH_AVAILABLE:
65
+ try:
66
+ from .query_processor import preprocess_query, preprocess_document_content
67
+ from .document_processor import DocumentProcessor
68
+ from .index_builder import IndexBuilder
69
+ from .search_engine import SearchEngine
70
+ from .search_service import SearchService
71
+
72
+ __all__ = [
73
+ 'preprocess_query',
74
+ 'preprocess_document_content',
75
+ 'DocumentProcessor',
76
+ 'IndexBuilder',
77
+ 'SearchEngine',
78
+ 'SearchService'
79
+ ]
80
+ except ImportError as e:
81
+ # Some search components failed to import
82
+ warnings.warn(
83
+ f"Some search components failed to import: {e}\n"
84
+ f"For full search functionality, install: pip install signalwire-agents[search-all]",
85
+ ImportWarning
86
+ )
87
+
88
+ # Try to import what we can
89
+ try:
90
+ from .query_processor import preprocess_query, preprocess_document_content
91
+ __all__.extend(['preprocess_query', 'preprocess_document_content'])
92
+ except ImportError:
93
+ pass
94
+
95
+ try:
96
+ from .document_processor import DocumentProcessor
97
+ __all__.append('DocumentProcessor')
98
+ except ImportError:
99
+ pass
100
+ else:
101
+ # Provide stub functions that give helpful error messages
102
+ def preprocess_query(*args, **kwargs):
103
+ _check_search_dependencies()
104
+
105
+ def preprocess_document_content(*args, **kwargs):
106
+ _check_search_dependencies()
107
+
108
+ class DocumentProcessor:
109
+ def __init__(self, *args, **kwargs):
110
+ _check_search_dependencies()
111
+
112
+ class IndexBuilder:
113
+ def __init__(self, *args, **kwargs):
114
+ _check_search_dependencies()
115
+
116
+ class SearchEngine:
117
+ def __init__(self, *args, **kwargs):
118
+ _check_search_dependencies()
119
+
120
+ class SearchService:
121
+ def __init__(self, *args, **kwargs):
122
+ _check_search_dependencies()
123
+
124
+ __all__ = [
125
+ 'preprocess_query',
126
+ 'preprocess_document_content',
127
+ 'DocumentProcessor',
128
+ 'IndexBuilder',
129
+ 'SearchEngine',
130
+ 'SearchService'
131
+ ]