signalwire-agents 0.1.28__py3-none-any.whl → 0.1.30__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.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/config.py +9 -0
- signalwire_agents/cli/core/__init__.py +9 -0
- signalwire_agents/cli/core/agent_loader.py +9 -0
- signalwire_agents/cli/core/argparse_helpers.py +9 -0
- signalwire_agents/cli/core/dynamic_config.py +9 -0
- signalwire_agents/cli/execution/__init__.py +9 -0
- signalwire_agents/cli/execution/datamap_exec.py +9 -0
- signalwire_agents/cli/execution/webhook_exec.py +9 -0
- signalwire_agents/cli/output/__init__.py +9 -0
- signalwire_agents/cli/output/output_formatter.py +9 -0
- signalwire_agents/cli/output/swml_dump.py +9 -0
- signalwire_agents/cli/simulation/__init__.py +9 -0
- signalwire_agents/cli/simulation/data_generation.py +9 -0
- signalwire_agents/cli/simulation/data_overrides.py +9 -0
- signalwire_agents/cli/simulation/mock_env.py +9 -0
- signalwire_agents/cli/test_swaig.py +9 -0
- signalwire_agents/cli/types.py +9 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +84 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/swml_service.py +19 -25
- signalwire_agents/search/search_service.py +200 -11
- signalwire_agents/skills/__init__.py +9 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +9 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +9 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/play_background_file/__init__.py +9 -0
- signalwire_agents/skills/play_background_file/skill.py +9 -0
- signalwire_agents/skills/spider/__init__.py +9 -0
- signalwire_agents/skills/spider/skill.py +9 -0
- signalwire_agents/skills/swml_transfer/__init__.py +9 -0
- signalwire_agents/skills/weather_api/__init__.py +9 -0
- signalwire_agents/skills/weather_api/skill.py +9 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/wikipedia_search/__init__.py +9 -0
- signalwire_agents/skills/wikipedia_search/skill.py +9 -0
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/METADATA +1 -1
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/RECORD +55 -52
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/entry_points.txt +0 -0
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.28.dist-info → signalwire_agents-0.1.30.dist-info}/top_level.txt +0 -0
@@ -42,6 +42,7 @@ except ImportError:
|
|
42
42
|
|
43
43
|
from signalwire_agents.utils.schema_utils import SchemaUtils
|
44
44
|
from signalwire_agents.core.swml_handler import VerbHandlerRegistry, SWMLVerbHandler
|
45
|
+
from signalwire_agents.core.security_config import SecurityConfig
|
45
46
|
|
46
47
|
|
47
48
|
class SWMLService:
|
@@ -65,7 +66,8 @@ class SWMLService:
|
|
65
66
|
host: str = "0.0.0.0",
|
66
67
|
port: int = 3000,
|
67
68
|
basic_auth: Optional[Tuple[str, str]] = None,
|
68
|
-
schema_path: Optional[str] = None
|
69
|
+
schema_path: Optional[str] = None,
|
70
|
+
config_file: Optional[str] = None
|
69
71
|
):
|
70
72
|
"""
|
71
73
|
Initialize a new SWML service
|
@@ -77,22 +79,26 @@ class SWMLService:
|
|
77
79
|
port: Port to bind the web server to
|
78
80
|
basic_auth: Optional (username, password) tuple for basic auth
|
79
81
|
schema_path: Optional path to the schema file
|
82
|
+
config_file: Optional path to configuration file
|
80
83
|
"""
|
81
84
|
self.name = name
|
82
85
|
self.route = route.rstrip("/") # Ensure no trailing slash
|
83
86
|
self.host = host
|
84
87
|
self.port = port
|
85
88
|
|
86
|
-
# Initialize SSL configuration from environment variables
|
87
|
-
ssl_enabled_env = os.environ.get('SWML_SSL_ENABLED', '').lower()
|
88
|
-
self.ssl_enabled = ssl_enabled_env in ('true', '1', 'yes')
|
89
|
-
self.domain = os.environ.get('SWML_DOMAIN')
|
90
|
-
self.ssl_cert_path = os.environ.get('SWML_SSL_CERT_PATH')
|
91
|
-
self.ssl_key_path = os.environ.get('SWML_SSL_KEY_PATH')
|
92
|
-
|
93
89
|
# Initialize logger for this instance FIRST before using it
|
94
90
|
self.log = logger.bind(service=name)
|
95
91
|
|
92
|
+
# Load unified security configuration with optional config file
|
93
|
+
self.security = SecurityConfig(config_file=config_file, service_name=name)
|
94
|
+
self.security.log_config("SWMLService")
|
95
|
+
|
96
|
+
# For backward compatibility, expose SSL settings as instance attributes
|
97
|
+
self.ssl_enabled = self.security.ssl_enabled
|
98
|
+
self.domain = self.security.domain
|
99
|
+
self.ssl_cert_path = self.security.ssl_cert_path
|
100
|
+
self.ssl_key_path = self.security.ssl_key_path
|
101
|
+
|
96
102
|
# Initialize proxy detection attributes
|
97
103
|
self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
|
98
104
|
self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
|
@@ -108,18 +114,8 @@ class SWMLService:
|
|
108
114
|
# Use provided credentials
|
109
115
|
self._basic_auth = basic_auth
|
110
116
|
else:
|
111
|
-
#
|
112
|
-
|
113
|
-
env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
|
114
|
-
|
115
|
-
if env_user and env_pass:
|
116
|
-
# Use environment variables
|
117
|
-
self._basic_auth = (env_user, env_pass)
|
118
|
-
else:
|
119
|
-
# Generate random credentials as fallback
|
120
|
-
username = f"user_{secrets.token_hex(4)}"
|
121
|
-
password = secrets.token_urlsafe(16)
|
122
|
-
self._basic_auth = (username, password)
|
117
|
+
# Use unified security config for auth credentials
|
118
|
+
self._basic_auth = self.security.get_basic_auth()
|
123
119
|
|
124
120
|
# Find the schema file if not provided
|
125
121
|
if schema_path is None:
|
@@ -768,11 +764,9 @@ class SWMLService:
|
|
768
764
|
|
769
765
|
# Validate SSL configuration if enabled
|
770
766
|
if self.ssl_enabled:
|
771
|
-
|
772
|
-
|
773
|
-
self.
|
774
|
-
elif not ssl_key_path or not os.path.exists(ssl_key_path):
|
775
|
-
self.log.warning("ssl_key_not_found", path=ssl_key_path)
|
767
|
+
is_valid, error = self.security.validate_ssl_config()
|
768
|
+
if not is_valid:
|
769
|
+
self.log.warning("ssl_config_invalid", error=error)
|
776
770
|
self.ssl_enabled = False
|
777
771
|
elif not self.domain:
|
778
772
|
self.log.warning("ssl_domain_not_specified")
|
@@ -8,15 +8,23 @@ See LICENSE file in the project root for full license information.
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
import logging
|
11
|
-
from typing import Dict, Any, List, Optional
|
11
|
+
from typing import Dict, Any, List, Optional, Tuple
|
12
12
|
|
13
13
|
try:
|
14
|
-
from fastapi import FastAPI, HTTPException
|
14
|
+
from fastapi import FastAPI, HTTPException, Request, Response, Depends
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
16
|
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
15
17
|
from pydantic import BaseModel
|
16
18
|
except ImportError:
|
17
19
|
FastAPI = None
|
18
20
|
HTTPException = None
|
19
21
|
BaseModel = None
|
22
|
+
Request = None
|
23
|
+
Response = None
|
24
|
+
Depends = None
|
25
|
+
CORSMiddleware = None
|
26
|
+
HTTPBasic = None
|
27
|
+
HTTPBasicCredentials = None
|
20
28
|
|
21
29
|
try:
|
22
30
|
from sentence_transformers import SentenceTransformer
|
@@ -25,8 +33,11 @@ except ImportError:
|
|
25
33
|
|
26
34
|
from .query_processor import preprocess_query
|
27
35
|
from .search_engine import SearchEngine
|
36
|
+
from signalwire_agents.core.security_config import SecurityConfig
|
37
|
+
from signalwire_agents.core.config_loader import ConfigLoader
|
38
|
+
from signalwire_agents.core.logging_config import get_logger
|
28
39
|
|
29
|
-
logger =
|
40
|
+
logger = get_logger("search_service")
|
30
41
|
|
31
42
|
# Pydantic models for API
|
32
43
|
if BaseModel:
|
@@ -73,14 +84,30 @@ else:
|
|
73
84
|
class SearchService:
|
74
85
|
"""Local search service with HTTP API"""
|
75
86
|
|
76
|
-
def __init__(self, port: int = 8001, indexes: Dict[str, str] = None
|
87
|
+
def __init__(self, port: int = 8001, indexes: Dict[str, str] = None,
|
88
|
+
basic_auth: Optional[Tuple[str, str]] = None,
|
89
|
+
config_file: Optional[str] = None):
|
90
|
+
# Load configuration first
|
91
|
+
self._load_config(config_file)
|
92
|
+
|
93
|
+
# Override with constructor params if provided
|
77
94
|
self.port = port
|
78
|
-
|
95
|
+
if indexes is not None:
|
96
|
+
self.indexes = indexes
|
97
|
+
|
79
98
|
self.search_engines = {}
|
80
99
|
self.model = None
|
81
100
|
|
101
|
+
# Load security configuration with optional config file
|
102
|
+
self.security = SecurityConfig(config_file=config_file, service_name="search")
|
103
|
+
self.security.log_config("SearchService")
|
104
|
+
|
105
|
+
# Set up authentication
|
106
|
+
self._basic_auth = basic_auth or self.security.get_basic_auth()
|
107
|
+
|
82
108
|
if FastAPI:
|
83
109
|
self.app = FastAPI(title="SignalWire Local Search Service")
|
110
|
+
self._setup_security()
|
84
111
|
self._setup_routes()
|
85
112
|
else:
|
86
113
|
self.app = None
|
@@ -88,22 +115,131 @@ class SearchService:
|
|
88
115
|
|
89
116
|
self._load_resources()
|
90
117
|
|
118
|
+
def _load_config(self, config_file: Optional[str]):
|
119
|
+
"""Load configuration from file if available"""
|
120
|
+
# Initialize defaults
|
121
|
+
self.indexes = {}
|
122
|
+
|
123
|
+
# Find config file
|
124
|
+
if not config_file:
|
125
|
+
config_file = ConfigLoader.find_config_file("search")
|
126
|
+
|
127
|
+
if not config_file:
|
128
|
+
return
|
129
|
+
|
130
|
+
# Load config
|
131
|
+
config_loader = ConfigLoader([config_file])
|
132
|
+
if not config_loader.has_config():
|
133
|
+
return
|
134
|
+
|
135
|
+
logger.info("loading_config_from_file", file=config_file)
|
136
|
+
|
137
|
+
# Get service section
|
138
|
+
service_config = config_loader.get_section('service')
|
139
|
+
if service_config:
|
140
|
+
if 'port' in service_config:
|
141
|
+
self.port = int(service_config['port'])
|
142
|
+
|
143
|
+
if 'indexes' in service_config and isinstance(service_config['indexes'], dict):
|
144
|
+
self.indexes = service_config['indexes']
|
145
|
+
|
146
|
+
def _setup_security(self):
|
147
|
+
"""Setup security middleware and authentication"""
|
148
|
+
if not self.app:
|
149
|
+
return
|
150
|
+
|
151
|
+
# Add CORS middleware if FastAPI has it
|
152
|
+
if CORSMiddleware:
|
153
|
+
self.app.add_middleware(
|
154
|
+
CORSMiddleware,
|
155
|
+
**self.security.get_cors_config()
|
156
|
+
)
|
157
|
+
|
158
|
+
# Add security headers middleware
|
159
|
+
@self.app.middleware("http")
|
160
|
+
async def add_security_headers(request: Request, call_next):
|
161
|
+
response = await call_next(request)
|
162
|
+
|
163
|
+
# Add security headers
|
164
|
+
is_https = request.url.scheme == "https"
|
165
|
+
headers = self.security.get_security_headers(is_https)
|
166
|
+
for header, value in headers.items():
|
167
|
+
response.headers[header] = value
|
168
|
+
|
169
|
+
return response
|
170
|
+
|
171
|
+
# Add host validation middleware
|
172
|
+
@self.app.middleware("http")
|
173
|
+
async def validate_host(request: Request, call_next):
|
174
|
+
host = request.headers.get("host", "").split(":")[0]
|
175
|
+
if host and not self.security.should_allow_host(host):
|
176
|
+
return Response(content="Invalid host", status_code=400)
|
177
|
+
|
178
|
+
return await call_next(request)
|
179
|
+
|
180
|
+
def _get_current_username(self, credentials: HTTPBasicCredentials = None) -> str:
|
181
|
+
"""Validate basic auth credentials"""
|
182
|
+
if not credentials:
|
183
|
+
return None
|
184
|
+
|
185
|
+
correct_username, correct_password = self._basic_auth
|
186
|
+
|
187
|
+
# Compare credentials
|
188
|
+
import secrets
|
189
|
+
username_correct = secrets.compare_digest(credentials.username, correct_username)
|
190
|
+
password_correct = secrets.compare_digest(credentials.password, correct_password)
|
191
|
+
|
192
|
+
if not (username_correct and password_correct):
|
193
|
+
raise HTTPException(
|
194
|
+
status_code=401,
|
195
|
+
detail="Invalid authentication credentials",
|
196
|
+
headers={"WWW-Authenticate": "Basic"},
|
197
|
+
)
|
198
|
+
|
199
|
+
return credentials.username
|
200
|
+
|
91
201
|
def _setup_routes(self):
|
92
202
|
"""Setup FastAPI routes"""
|
93
203
|
if not self.app:
|
94
204
|
return
|
205
|
+
|
206
|
+
# Create security dependency if HTTPBasic is available
|
207
|
+
security = HTTPBasic() if HTTPBasic else None
|
208
|
+
|
209
|
+
# Create dependency for authenticated routes
|
210
|
+
def get_authenticated():
|
211
|
+
if security:
|
212
|
+
return security
|
213
|
+
return None
|
95
214
|
|
96
215
|
@self.app.post("/search", response_model=SearchResponse)
|
97
|
-
async def search(
|
216
|
+
async def search(
|
217
|
+
request: SearchRequest,
|
218
|
+
credentials: HTTPBasicCredentials = None if not security else Depends(security)
|
219
|
+
):
|
220
|
+
if security:
|
221
|
+
self._get_current_username(credentials)
|
98
222
|
return await self._handle_search(request)
|
99
223
|
|
100
224
|
@self.app.get("/health")
|
101
225
|
async def health():
|
102
|
-
return {
|
226
|
+
return {
|
227
|
+
"status": "healthy",
|
228
|
+
"indexes": list(self.indexes.keys()),
|
229
|
+
"ssl_enabled": self.security.ssl_enabled,
|
230
|
+
"auth_required": bool(security)
|
231
|
+
}
|
103
232
|
|
104
233
|
@self.app.post("/reload_index")
|
105
|
-
async def reload_index(
|
234
|
+
async def reload_index(
|
235
|
+
index_name: str,
|
236
|
+
index_path: str,
|
237
|
+
credentials: HTTPBasicCredentials = None if not security else Depends(security)
|
238
|
+
):
|
106
239
|
"""Reload or add new index"""
|
240
|
+
if security:
|
241
|
+
self._get_current_username(credentials)
|
242
|
+
|
107
243
|
self.indexes[index_name] = index_path
|
108
244
|
self.search_engines[index_name] = SearchEngine(index_path, self.model)
|
109
245
|
return {"status": "reloaded", "index": index_name}
|
@@ -235,14 +371,67 @@ class SearchService:
|
|
235
371
|
'query_analysis': response.query_analysis
|
236
372
|
}
|
237
373
|
|
238
|
-
def start(self
|
239
|
-
|
374
|
+
def start(self, host: str = "0.0.0.0", port: Optional[int] = None,
|
375
|
+
ssl_cert: Optional[str] = None, ssl_key: Optional[str] = None):
|
376
|
+
"""
|
377
|
+
Start the service with optional HTTPS support.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
host: Host to bind to (default: "0.0.0.0")
|
381
|
+
port: Port to bind to (default: self.port)
|
382
|
+
ssl_cert: Path to SSL certificate file (overrides environment)
|
383
|
+
ssl_key: Path to SSL key file (overrides environment)
|
384
|
+
"""
|
240
385
|
if not self.app:
|
241
386
|
raise RuntimeError("FastAPI not available. Cannot start HTTP service.")
|
242
387
|
|
388
|
+
port = port or self.port
|
389
|
+
|
390
|
+
# Get SSL configuration
|
391
|
+
ssl_kwargs = {}
|
392
|
+
if ssl_cert and ssl_key:
|
393
|
+
# Use provided SSL files
|
394
|
+
ssl_kwargs = {
|
395
|
+
'ssl_certfile': ssl_cert,
|
396
|
+
'ssl_keyfile': ssl_key
|
397
|
+
}
|
398
|
+
else:
|
399
|
+
# Use security config SSL settings
|
400
|
+
ssl_kwargs = self.security.get_ssl_context_kwargs()
|
401
|
+
|
402
|
+
# Build startup URL
|
403
|
+
scheme = "https" if ssl_kwargs else "http"
|
404
|
+
startup_url = f"{scheme}://{host}:{port}"
|
405
|
+
|
406
|
+
# Get auth credentials
|
407
|
+
username, password = self._basic_auth
|
408
|
+
|
409
|
+
# Log startup information
|
410
|
+
logger.info(
|
411
|
+
"starting_search_service",
|
412
|
+
url=startup_url,
|
413
|
+
ssl_enabled=bool(ssl_kwargs),
|
414
|
+
indexes=list(self.indexes.keys()),
|
415
|
+
username=username
|
416
|
+
)
|
417
|
+
|
418
|
+
# Print user-friendly startup message
|
419
|
+
print(f"\nSignalWire Search Service starting...")
|
420
|
+
print(f"URL: {startup_url}")
|
421
|
+
print(f"Indexes: {', '.join(self.indexes.keys()) if self.indexes else 'None'}")
|
422
|
+
print(f"Basic Auth: {username}:{password}")
|
423
|
+
if ssl_kwargs:
|
424
|
+
print(f"SSL: Enabled")
|
425
|
+
print("")
|
426
|
+
|
243
427
|
try:
|
244
428
|
import uvicorn
|
245
|
-
uvicorn.run(
|
429
|
+
uvicorn.run(
|
430
|
+
self.app,
|
431
|
+
host=host,
|
432
|
+
port=port,
|
433
|
+
**ssl_kwargs
|
434
|
+
)
|
246
435
|
except ImportError:
|
247
436
|
raise RuntimeError("uvicorn not available. Cannot start HTTP service.")
|
248
437
|
|
@@ -1,3 +1,12 @@
|
|
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
|
+
|
1
10
|
from .skill import ApiNinjasTriviaSkill
|
2
11
|
|
3
12
|
__all__ = ['ApiNinjasTriviaSkill']
|
@@ -1 +1,10 @@
|
|
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
|
+
|
1
10
|
"""DataSphere Serverless Skill for SignalWire Agents using DataMap"""
|
@@ -1,3 +1,12 @@
|
|
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
|
+
|
1
10
|
from .skill import PlayBackgroundFileSkill
|
2
11
|
|
3
12
|
__all__ = ['PlayBackgroundFileSkill']
|
@@ -1,3 +1,12 @@
|
|
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
|
+
|
1
10
|
"""Spider skill for web scraping."""
|
2
11
|
from .skill import SpiderSkill
|
3
12
|
|
@@ -1,3 +1,12 @@
|
|
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
|
+
|
1
10
|
"""Spider skill for fast web scraping with SignalWire AI Agents."""
|
2
11
|
import re
|
3
12
|
import logging
|
@@ -1,3 +1,12 @@
|
|
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
|
+
|
1
10
|
from .skill import WeatherApiSkill
|
2
11
|
|
3
12
|
__all__ = ['WeatherApiSkill']
|