signalwire-agents 0.1.43__py3-none-any.whl → 0.1.45__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.
@@ -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.43"
21
+ __version__ = "0.1.45"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -31,6 +31,9 @@ from signalwire_agents.core.function_result import SwaigFunctionResult
31
31
  from signalwire_agents.core.swaig_function import SWAIGFunction
32
32
  from signalwire_agents.agents.bedrock import BedrockAgent
33
33
 
34
+ # Import WebService for static file serving
35
+ from signalwire_agents.web import WebService
36
+
34
37
  # Lazy import skills to avoid slow startup for CLI tools
35
38
  # Skills are now loaded on-demand when requested
36
39
  def _get_skill_registry():
@@ -138,6 +141,7 @@ __all__ = [
138
141
  "Context",
139
142
  "Step",
140
143
  "create_simple_context",
144
+ "WebService",
141
145
  "start_agent",
142
146
  "run_agent",
143
147
  "list_skills",
@@ -562,6 +562,7 @@ class AgentBase(
562
562
  SWML document as a string
563
563
  """
564
564
  self.log.debug("_render_swml_called",
565
+ call_id=call_id,
565
566
  has_modifications=bool(modifications),
566
567
  use_ephemeral=bool(modifications and modifications.get("__use_ephemeral_agent")),
567
568
  has_dynamic_callback=bool(self._dynamic_config_callback))
@@ -618,6 +619,9 @@ class AgentBase(
618
619
  # Generate a call ID if needed
619
620
  if call_id is None:
620
621
  call_id = agent_to_use._session_manager.create_session()
622
+ self.log.debug("generated_call_id", call_id=call_id)
623
+ else:
624
+ self.log.debug("using_provided_call_id", call_id=call_id)
621
625
 
622
626
  # Start with any SWAIG query params that were set
623
627
  query_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
@@ -679,6 +683,7 @@ class AgentBase(
679
683
  token = None
680
684
  if func.secure and call_id:
681
685
  token = agent_to_use._create_tool_token(tool_name=name, call_id=call_id)
686
+ self.log.debug("created_token_for_function", function=name, call_id=call_id, token_prefix=token[:20] if token else None)
682
687
 
683
688
  # Prepare function entry
684
689
  function_entry = {
@@ -7,7 +7,13 @@ Licensed under the MIT License.
7
7
  See LICENSE file in the project root for full license information.
8
8
  """
9
9
 
10
- from typing import Optional, Union, List, Dict, Any
10
+ from typing import Optional, Union, List, Dict, Any, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from signalwire_agents.core.agent_base import AgentBase
14
+ from signalwire_agents.core.contexts import ContextBuilder
15
+ else:
16
+ from signalwire_agents.core.contexts import ContextBuilder
11
17
 
12
18
 
13
19
  class PromptMixin:
@@ -112,7 +118,7 @@ class PromptMixin:
112
118
  bullets=sub_bullets if sub_bullets else None
113
119
  )
114
120
 
115
- def define_contexts(self, contexts=None) -> Optional['ContextBuilder']:
121
+ def define_contexts(self, contexts=None) -> Union['AgentBase', 'ContextBuilder']:
116
122
  """
117
123
  Define contexts and steps for this agent (alternative to POM/prompt)
118
124
 
@@ -132,15 +138,22 @@ class PromptMixin:
132
138
  return self
133
139
  else:
134
140
  # Legacy behavior - return ContextBuilder
135
- # Import here to avoid circular imports
136
- from signalwire_agents.core.contexts import ContextBuilder
137
-
138
141
  if self._contexts_builder is None:
139
142
  self._contexts_builder = ContextBuilder(self)
140
143
  self._contexts_defined = True
141
144
 
142
145
  return self._contexts_builder
143
146
 
147
+ @property
148
+ def contexts(self) -> 'ContextBuilder':
149
+ """
150
+ Get the ContextBuilder for this agent
151
+
152
+ Returns:
153
+ ContextBuilder instance for defining contexts
154
+ """
155
+ return self.define_contexts()
156
+
144
157
  def _validate_prompt_mode_exclusivity(self):
145
158
  """
146
159
  Validate that POM sections and raw text are not mixed in the main prompt
@@ -494,6 +494,10 @@ class WebMixin:
494
494
 
495
495
  # Get call_id from body if present
496
496
  call_id = body.get("call_id")
497
+ if not call_id and "call" in body:
498
+ # Sometimes it might be nested under 'call'
499
+ call_id = body.get("call", {}).get("call_id")
500
+ req_log.debug("extracted_call_id_from_body", call_id=call_id, body_keys=list(body.keys()))
497
501
  else:
498
502
  # Get call_id from query params for GET
499
503
  call_id = request.query_params.get("call_id")
@@ -0,0 +1,17 @@
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 Web Service Module
11
+
12
+ This module provides static file serving capabilities for the SignalWire Agents SDK.
13
+ """
14
+
15
+ from .web_service import WebService
16
+
17
+ __all__ = ['WebService']
@@ -0,0 +1,559 @@
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 mimetypes
12
+ from pathlib import Path
13
+ from typing import Dict, Optional, Tuple, Any
14
+ import logging
15
+
16
+ try:
17
+ from fastapi import FastAPI, HTTPException, Request, Response, Depends
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
20
+ from fastapi.staticfiles import StaticFiles
21
+ from fastapi.responses import FileResponse, HTMLResponse
22
+ except ImportError:
23
+ FastAPI = None
24
+ HTTPException = None
25
+ Request = None
26
+ Response = None
27
+ Depends = None
28
+ CORSMiddleware = None
29
+ HTTPBasic = None
30
+ HTTPBasicCredentials = None
31
+ StaticFiles = None
32
+ FileResponse = None
33
+ HTMLResponse = None
34
+
35
+ from signalwire_agents.core.security_config import SecurityConfig
36
+ from signalwire_agents.core.config_loader import ConfigLoader
37
+ from signalwire_agents.core.logging_config import get_logger
38
+
39
+ logger = get_logger("web_service")
40
+
41
+ class WebService:
42
+ """Static file serving service with HTTP API"""
43
+
44
+ def __init__(self,
45
+ port: int = 8002,
46
+ directories: Dict[str, str] = None,
47
+ basic_auth: Optional[Tuple[str, str]] = None,
48
+ config_file: Optional[str] = None,
49
+ enable_directory_browsing: bool = False,
50
+ allowed_extensions: Optional[list] = None,
51
+ blocked_extensions: Optional[list] = None,
52
+ max_file_size: int = 100 * 1024 * 1024, # 100MB default
53
+ enable_cors: bool = True):
54
+ """
55
+ Initialize WebService
56
+
57
+ Args:
58
+ port: Port to bind to (default: 8002)
59
+ directories: Dict mapping URL paths to local directories
60
+ basic_auth: Optional tuple of (username, password)
61
+ config_file: Optional configuration file path
62
+ enable_directory_browsing: Allow directory listing
63
+ allowed_extensions: List of allowed file extensions (e.g., ['.html', '.css'])
64
+ blocked_extensions: List of blocked extensions (e.g., ['.env', '.git'])
65
+ max_file_size: Maximum file size in bytes to serve
66
+ enable_cors: Enable CORS support
67
+ """
68
+ # Load configuration first
69
+ self._load_config(config_file)
70
+
71
+ # Override with constructor params if provided
72
+ self.port = port
73
+ self.enable_directory_browsing = enable_directory_browsing
74
+ self.max_file_size = max_file_size
75
+ self.enable_cors = enable_cors
76
+
77
+ if directories is not None:
78
+ self.directories = directories
79
+
80
+ # Set up file extension filters
81
+ self.allowed_extensions = allowed_extensions
82
+ self.blocked_extensions = blocked_extensions or [
83
+ '.env', '.git', '.gitignore', '.key', '.pem', '.crt',
84
+ '.pyc', '__pycache__', '.DS_Store', '.swp'
85
+ ]
86
+
87
+ # Initialize mimetypes
88
+ mimetypes.init()
89
+ # Add custom MIME types if needed
90
+ mimetypes.add_type('application/javascript', '.js')
91
+ mimetypes.add_type('text/css', '.css')
92
+ mimetypes.add_type('application/json', '.json')
93
+
94
+ # Load security configuration
95
+ self.security = SecurityConfig(config_file=config_file, service_name="web")
96
+ self.security.log_config("WebService")
97
+
98
+ # Set up authentication
99
+ self._basic_auth = basic_auth or self.security.get_basic_auth()
100
+
101
+ if FastAPI:
102
+ self.app = FastAPI(
103
+ title="SignalWire Web Service",
104
+ description="Static file serving for SignalWire Agents"
105
+ )
106
+ self._setup_security()
107
+ self._setup_routes()
108
+ self._mount_directories()
109
+ else:
110
+ self.app = None
111
+ logger.warning("FastAPI not available. HTTP service will not be available.")
112
+
113
+ def _load_config(self, config_file: Optional[str]):
114
+ """Load configuration from file if available"""
115
+ # Initialize defaults
116
+ self.directories = {}
117
+ self.port = 8002
118
+
119
+ # Find config file
120
+ if not config_file:
121
+ config_file = ConfigLoader.find_config_file("web")
122
+
123
+ if not config_file:
124
+ return
125
+
126
+ # Load config
127
+ config_loader = ConfigLoader([config_file])
128
+ if not config_loader.has_config():
129
+ return
130
+
131
+ logger.info("loading_config_from_file", file=config_file)
132
+
133
+ # Get service section
134
+ service_config = config_loader.get_section('service')
135
+ if service_config:
136
+ if 'port' in service_config:
137
+ self.port = int(service_config['port'])
138
+
139
+ if 'directories' in service_config and isinstance(service_config['directories'], dict):
140
+ self.directories = service_config['directories']
141
+
142
+ if 'enable_directory_browsing' in service_config:
143
+ self.enable_directory_browsing = bool(service_config['enable_directory_browsing'])
144
+
145
+ if 'max_file_size' in service_config:
146
+ self.max_file_size = int(service_config['max_file_size'])
147
+
148
+ if 'allowed_extensions' in service_config:
149
+ self.allowed_extensions = service_config['allowed_extensions']
150
+
151
+ if 'blocked_extensions' in service_config:
152
+ self.blocked_extensions = service_config['blocked_extensions']
153
+
154
+ def _setup_security(self):
155
+ """Setup security middleware and authentication"""
156
+ if not self.app:
157
+ return
158
+
159
+ # Add CORS middleware if enabled
160
+ if self.enable_cors and CORSMiddleware:
161
+ self.app.add_middleware(
162
+ CORSMiddleware,
163
+ **self.security.get_cors_config()
164
+ )
165
+
166
+ # Add security headers middleware
167
+ @self.app.middleware("http")
168
+ async def add_security_headers(request: Request, call_next):
169
+ response = await call_next(request)
170
+
171
+ # Add security headers
172
+ is_https = request.url.scheme == "https"
173
+ headers = self.security.get_security_headers(is_https)
174
+ for header, value in headers.items():
175
+ response.headers[header] = value
176
+
177
+ # Add cache headers for static files
178
+ if request.url.path.startswith(tuple(self.directories.keys())):
179
+ # Cache static files for 1 hour
180
+ response.headers["Cache-Control"] = "public, max-age=3600"
181
+
182
+ return response
183
+
184
+ # Add host validation middleware
185
+ @self.app.middleware("http")
186
+ async def validate_host(request: Request, call_next):
187
+ host = request.headers.get("host", "").split(":")[0]
188
+ if host and not self.security.should_allow_host(host):
189
+ return Response(content="Invalid host", status_code=400)
190
+
191
+ return await call_next(request)
192
+
193
+ def _get_current_username(self, credentials: HTTPBasicCredentials = None) -> str:
194
+ """Validate basic auth credentials"""
195
+ if not credentials:
196
+ return None
197
+
198
+ correct_username, correct_password = self._basic_auth
199
+
200
+ # Compare credentials
201
+ import secrets
202
+ username_correct = secrets.compare_digest(credentials.username, correct_username)
203
+ password_correct = secrets.compare_digest(credentials.password, correct_password)
204
+
205
+ if not (username_correct and password_correct):
206
+ raise HTTPException(
207
+ status_code=401,
208
+ detail="Invalid authentication credentials",
209
+ headers={"WWW-Authenticate": "Basic"},
210
+ )
211
+
212
+ return credentials.username
213
+
214
+ def _is_file_allowed(self, file_path: Path) -> bool:
215
+ """Check if file is allowed to be served"""
216
+ # Check file size
217
+ if file_path.stat().st_size > self.max_file_size:
218
+ return False
219
+
220
+ # Check extension and name
221
+ ext = file_path.suffix.lower()
222
+ name = file_path.name
223
+
224
+ # Check blocked extensions and names
225
+ for blocked in self.blocked_extensions:
226
+ if blocked.startswith('.'):
227
+ # Check both as extension and as full name (for files like .env, .gitignore)
228
+ if ext == blocked or name == blocked:
229
+ return False
230
+ else:
231
+ if name == blocked or blocked in str(file_path):
232
+ return False
233
+
234
+ # If allowed_extensions is set, only allow those
235
+ if self.allowed_extensions:
236
+ return ext in self.allowed_extensions
237
+
238
+ return True
239
+
240
+ def _generate_directory_listing(self, directory: Path, url_path: str) -> str:
241
+ """Generate HTML directory listing"""
242
+ items = []
243
+
244
+ # Add parent directory link if not at root
245
+ if url_path != '/':
246
+ items.append('<li><a href="../">../</a></li>')
247
+
248
+ # List directories first
249
+ for item in sorted(directory.iterdir()):
250
+ if item.name.startswith('.'):
251
+ continue # Skip hidden files
252
+
253
+ if item.is_dir():
254
+ items.append(f'<li>📁 <a href="{item.name}/">{item.name}/</a></li>')
255
+
256
+ # Then list files
257
+ for item in sorted(directory.iterdir()):
258
+ if item.name.startswith('.'):
259
+ continue
260
+
261
+ if item.is_file() and self._is_file_allowed(item):
262
+ size = item.stat().st_size
263
+ if size < 1024:
264
+ size_str = f"{size} B"
265
+ elif size < 1024 * 1024:
266
+ size_str = f"{size / 1024:.1f} KB"
267
+ else:
268
+ size_str = f"{size / (1024 * 1024):.1f} MB"
269
+
270
+ items.append(f'<li>📄 <a href="{item.name}">{item.name}</a> ({size_str})</li>')
271
+
272
+ html = f"""
273
+ <!DOCTYPE html>
274
+ <html>
275
+ <head>
276
+ <title>Directory listing for {url_path}</title>
277
+ <style>
278
+ body {{ font-family: sans-serif; margin: 40px; }}
279
+ h1 {{ color: #333; }}
280
+ ul {{ list-style: none; padding: 0; }}
281
+ li {{ padding: 5px 0; }}
282
+ a {{ text-decoration: none; color: #0066cc; }}
283
+ a:hover {{ text-decoration: underline; }}
284
+ </style>
285
+ </head>
286
+ <body>
287
+ <h1>Directory listing for {url_path}</h1>
288
+ <ul>
289
+ {''.join(items)}
290
+ </ul>
291
+ </body>
292
+ </html>
293
+ """
294
+ return html
295
+
296
+ def _setup_routes(self):
297
+ """Setup FastAPI routes"""
298
+ if not self.app:
299
+ return
300
+
301
+ # Create security dependency if HTTPBasic is available
302
+ security = HTTPBasic() if HTTPBasic else None
303
+
304
+ @self.app.get("/health")
305
+ async def health():
306
+ return {
307
+ "status": "healthy",
308
+ "directories": list(self.directories.keys()),
309
+ "ssl_enabled": self.security.ssl_enabled,
310
+ "auth_required": bool(security),
311
+ "directory_browsing": self.enable_directory_browsing
312
+ }
313
+
314
+ @self.app.get("/")
315
+ async def root():
316
+ """Root endpoint showing available directories"""
317
+ html = """
318
+ <!DOCTYPE html>
319
+ <html>
320
+ <head>
321
+ <title>SignalWire Web Service</title>
322
+ <style>
323
+ body { font-family: sans-serif; margin: 40px; }
324
+ h1 { color: #333; }
325
+ ul { list-style: none; padding: 0; }
326
+ li { padding: 10px 0; }
327
+ a { text-decoration: none; color: #0066cc; font-size: 18px; }
328
+ a:hover { text-decoration: underline; }
329
+ .path { color: #666; font-size: 14px; }
330
+ </style>
331
+ </head>
332
+ <body>
333
+ <h1>SignalWire Web Service</h1>
334
+ <h2>Available Directories:</h2>
335
+ <ul>
336
+ """
337
+
338
+ for route, local_path in self.directories.items():
339
+ html += f'<li>📁 <a href="{route}">{route}</a> <span class="path">→ {local_path}</span></li>'
340
+
341
+ html += """
342
+ </ul>
343
+ </body>
344
+ </html>
345
+ """
346
+
347
+ if HTMLResponse:
348
+ return HTMLResponse(content=html)
349
+ else:
350
+ return {"directories": list(self.directories.keys())}
351
+
352
+ def _mount_directories(self):
353
+ """Mount static file directories"""
354
+ if not self.app or not StaticFiles:
355
+ return
356
+
357
+ # Create security dependency if HTTPBasic is available
358
+ security = HTTPBasic() if HTTPBasic else None
359
+
360
+ for route, directory in self.directories.items():
361
+ # Ensure directory exists
362
+ dir_path = Path(directory)
363
+ if not dir_path.exists():
364
+ logger.warning(f"Directory does not exist: {directory}")
365
+ continue
366
+
367
+ if not dir_path.is_dir():
368
+ logger.warning(f"Path is not a directory: {directory}")
369
+ continue
370
+
371
+ # Normalize route
372
+ if not route.startswith('/'):
373
+ route = '/' + route
374
+
375
+ logger.info(f"Mounting directory {directory} at route {route}")
376
+
377
+ # Create custom static file handler with our security checks
378
+ @self.app.get(f"{route}/{{file_path:path}}")
379
+ async def serve_file(
380
+ file_path: str,
381
+ request: Request,
382
+ credentials: HTTPBasicCredentials = None if not security else Depends(security),
383
+ route=route,
384
+ directory=directory
385
+ ):
386
+ """Serve files with security checks"""
387
+ if security:
388
+ self._get_current_username(credentials)
389
+
390
+ # Build full file path
391
+ full_path = Path(directory) / file_path
392
+
393
+ # Security: Prevent path traversal
394
+ try:
395
+ full_path = full_path.resolve()
396
+ dir_path = Path(directory).resolve()
397
+ if not str(full_path).startswith(str(dir_path)):
398
+ raise HTTPException(status_code=403, detail="Access denied")
399
+ except Exception:
400
+ raise HTTPException(status_code=403, detail="Invalid path")
401
+
402
+ # Check if path exists
403
+ if not full_path.exists():
404
+ raise HTTPException(status_code=404, detail="File not found")
405
+
406
+ # Handle directory requests
407
+ if full_path.is_dir():
408
+ if not self.enable_directory_browsing:
409
+ # Try to serve index.html if it exists
410
+ index_path = full_path / "index.html"
411
+ if index_path.exists() and self._is_file_allowed(index_path):
412
+ return FileResponse(index_path)
413
+ else:
414
+ raise HTTPException(status_code=403, detail="Directory browsing disabled")
415
+ else:
416
+ # Generate directory listing
417
+ html = self._generate_directory_listing(full_path, request.url.path)
418
+ if HTMLResponse:
419
+ return HTMLResponse(content=html)
420
+ else:
421
+ raise HTTPException(status_code=403, detail="Directory browsing not available")
422
+
423
+ # Check if file is allowed
424
+ if not self._is_file_allowed(full_path):
425
+ raise HTTPException(status_code=403, detail="File type not allowed")
426
+
427
+ # Serve the file
428
+ mime_type = mimetypes.guess_type(str(full_path))[0] or 'application/octet-stream'
429
+
430
+ if FileResponse:
431
+ return FileResponse(
432
+ full_path,
433
+ media_type=mime_type,
434
+ headers={
435
+ "Cache-Control": "public, max-age=3600",
436
+ "X-Content-Type-Options": "nosniff"
437
+ }
438
+ )
439
+ else:
440
+ # Fallback if FileResponse not available
441
+ with open(full_path, 'rb') as f:
442
+ content = f.read()
443
+ return Response(
444
+ content=content,
445
+ media_type=mime_type,
446
+ headers={
447
+ "Cache-Control": "public, max-age=3600",
448
+ "X-Content-Type-Options": "nosniff"
449
+ }
450
+ )
451
+
452
+ def add_directory(self, route: str, directory: str) -> None:
453
+ """
454
+ Add a new directory to serve
455
+
456
+ Args:
457
+ route: URL path to mount at (e.g., "/docs")
458
+ directory: Local directory path to serve
459
+ """
460
+ # Normalize route
461
+ if not route.startswith('/'):
462
+ route = '/' + route
463
+
464
+ # Verify directory exists
465
+ dir_path = Path(directory)
466
+ if not dir_path.exists():
467
+ raise ValueError(f"Directory does not exist: {directory}")
468
+
469
+ if not dir_path.is_dir():
470
+ raise ValueError(f"Path is not a directory: {directory}")
471
+
472
+ self.directories[route] = directory
473
+
474
+ # If app is already running, mount the new directory
475
+ if self.app:
476
+ self._mount_directories()
477
+
478
+ def remove_directory(self, route: str) -> None:
479
+ """
480
+ Remove a directory from being served
481
+
482
+ Args:
483
+ route: URL path to remove
484
+ """
485
+ # Normalize route
486
+ if not route.startswith('/'):
487
+ route = '/' + route
488
+
489
+ if route in self.directories:
490
+ del self.directories[route]
491
+
492
+ def start(self, host: str = "0.0.0.0", port: Optional[int] = None,
493
+ ssl_cert: Optional[str] = None, ssl_key: Optional[str] = None):
494
+ """
495
+ Start the service with optional HTTPS support
496
+
497
+ Args:
498
+ host: Host to bind to (default: "0.0.0.0")
499
+ port: Port to bind to (default: self.port)
500
+ ssl_cert: Path to SSL certificate file (overrides environment)
501
+ ssl_key: Path to SSL key file (overrides environment)
502
+ """
503
+ if not self.app:
504
+ raise RuntimeError("FastAPI not available. Cannot start HTTP service.")
505
+
506
+ port = port or self.port
507
+
508
+ # Get SSL configuration
509
+ ssl_kwargs = {}
510
+ if ssl_cert and ssl_key:
511
+ # Use provided SSL files
512
+ ssl_kwargs = {
513
+ 'ssl_certfile': ssl_cert,
514
+ 'ssl_keyfile': ssl_key
515
+ }
516
+ else:
517
+ # Use security config SSL settings
518
+ ssl_kwargs = self.security.get_ssl_context_kwargs()
519
+
520
+ # Build startup URL
521
+ scheme = "https" if ssl_kwargs else "http"
522
+ startup_url = f"{scheme}://{host}:{port}"
523
+
524
+ # Get auth credentials
525
+ username, password = self._basic_auth
526
+
527
+ # Log startup information
528
+ logger.info(
529
+ "starting_web_service",
530
+ url=startup_url,
531
+ ssl_enabled=bool(ssl_kwargs),
532
+ directories=list(self.directories.keys()),
533
+ username=username
534
+ )
535
+
536
+ # Print user-friendly startup message
537
+ print(f"\nSignalWire Web Service starting...")
538
+ print(f"URL: {startup_url}")
539
+ print(f"Directories: {', '.join(self.directories.keys()) if self.directories else 'None'}")
540
+ print(f"Basic Auth: {username}:{password}")
541
+ print(f"Directory Browsing: {'Enabled' if self.enable_directory_browsing else 'Disabled'}")
542
+ if ssl_kwargs:
543
+ print(f"SSL: Enabled")
544
+ print("")
545
+
546
+ try:
547
+ import uvicorn
548
+ uvicorn.run(
549
+ self.app,
550
+ host=host,
551
+ port=port,
552
+ **ssl_kwargs
553
+ )
554
+ except ImportError:
555
+ raise RuntimeError("uvicorn not available. Cannot start HTTP service.")
556
+
557
+ def stop(self):
558
+ """Stop the service (placeholder for cleanup)"""
559
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.43
3
+ Version: 0.1.45
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- signalwire_agents/__init__.py,sha256=JEGLCo_LLxcqdvVcd64XAfHEJ2pfoanrZ-AS_691Mqc,4923
1
+ signalwire_agents/__init__.py,sha256=0r2v5Nsh4xlPBc9pra7UTAgiTozjZ0tI5tSRUtG68_U,5031
2
2
  signalwire_agents/agent_server.py,sha256=x9HyWia8D3r6KMqY-Q4DtNVivfJWLTx8B-KzUI8okuA,26880
3
3
  signalwire_agents/schema.json,sha256=6-7ccbt39iM1CO36dOfvupRPfd0gnQ0XoAdyo-EFyjo,238042
4
4
  signalwire_agents/agents/bedrock.py,sha256=J582gooNtxtep4xdVOfyDzRtHp_XrurPMS93xf2Xod0,10836
@@ -24,7 +24,7 @@ signalwire_agents/cli/simulation/data_generation.py,sha256=pxa9aJ6XkI0O8yAIGvBTU
24
24
  signalwire_agents/cli/simulation/data_overrides.py,sha256=3_3pT6j-q2gRufPX2bZ1BrmY7u1IdloLooKAJil33vI,6319
25
25
  signalwire_agents/cli/simulation/mock_env.py,sha256=fvaR_xdLMm8AbpNUbTJOFG9THcti3Zds-0QNDbKMaYk,10249
26
26
  signalwire_agents/core/__init__.py,sha256=xjPq8DmUnWYUG28sd17n430VWPmMH9oZ9W14gYwG96g,806
27
- signalwire_agents/core/agent_base.py,sha256=Wy1xAyJgquI4Cabss1lddoNonJzOYeo3VtY9suEFDFA,48634
27
+ signalwire_agents/core/agent_base.py,sha256=SGaPKnjblJcjkz0bujiNezyelyJcaP72c-e33cAfhtA,48963
28
28
  signalwire_agents/core/auth_handler.py,sha256=jXrof9WZ1W9qqlQT9WElcmSRafL2kG7207x5SqWN9MU,8481
29
29
  signalwire_agents/core/config_loader.py,sha256=rStVRRUaeMGrMc44ocr0diMQQARZhbKqwMqQ6kqUNos,8722
30
30
  signalwire_agents/core/contexts.py,sha256=g9FgOGMfGCUWlm57YZcv7CvOf-Ub9FdKZIOMu14ADfE,24428
@@ -55,12 +55,12 @@ signalwire_agents/core/agent/tools/registry.py,sha256=CgsCUMWBV4Use1baNl2KLYS2ew
55
55
  signalwire_agents/core/mixins/__init__.py,sha256=NsFpfF7TDP_lNR0Riw4Nbvt4fDbv_A3OoVbBqRrtXQM,652
56
56
  signalwire_agents/core/mixins/ai_config_mixin.py,sha256=kT-xVVWMIE6RQe6qQFCRYxli345bxOs5uS1dCtMTeTc,18232
57
57
  signalwire_agents/core/mixins/auth_mixin.py,sha256=Y9kR423-76U_pKL7KXzseeXX2a-4WxNWyo3odS7TDQM,9879
58
- signalwire_agents/core/mixins/prompt_mixin.py,sha256=2cAJpp3Ka-fmgpAg201xeTy-xps99k0vlP7YyqZiCnw,13374
58
+ signalwire_agents/core/mixins/prompt_mixin.py,sha256=bEsuw9J2F_upFYI02KyC7o2eGZjwOKQ352rmJBZirAM,13729
59
59
  signalwire_agents/core/mixins/serverless_mixin.py,sha256=QIIbl_-16XFJi5aqrWpNzORbyCJQmhaplWXnW6U9i68,16137
60
60
  signalwire_agents/core/mixins/skill_mixin.py,sha256=Qz3RKPmq_iMY4NecyxOHk3dW3W-O4iEm2ahhMjAcqRs,1861
61
61
  signalwire_agents/core/mixins/state_mixin.py,sha256=q3achpyUYZKuJaqKf12O22FXpSsNNsMEonSvlpSHCkA,6594
62
62
  signalwire_agents/core/mixins/tool_mixin.py,sha256=6CaNdaspHcfte0qSB_bSN8PTsqxRZzL_AXYk8QoWyXE,8660
63
- signalwire_agents/core/mixins/web_mixin.py,sha256=BUzmTkPIHGfitzbn1p94SDSqp-7x_IesjliypagB4oI,50324
63
+ signalwire_agents/core/mixins/web_mixin.py,sha256=2Tj0mulcjiUBuqHnJ76UHc5C2L5xV91HKyxfG3lNj4k,50612
64
64
  signalwire_agents/core/security/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
65
65
  signalwire_agents/core/security/session_manager.py,sha256=s5hXYcFnrsYFoyo-zcN7EJy-wInZQI_cWTBHX9MxHR4,9164
66
66
  signalwire_agents/prefabs/__init__.py,sha256=MW11J63XH7KxF2MWguRsMFM9iqMWexaEO9ynDPL_PDM,715
@@ -126,9 +126,11 @@ signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_Pu
126
126
  signalwire_agents/utils/schema_utils.py,sha256=i4okv_O9bUApwT_jJf4Yoij3bLCrGrW3DC-vzSy2RuY,16392
127
127
  signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
128
128
  signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
129
- signalwire_agents-0.1.43.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
130
- signalwire_agents-0.1.43.dist-info/METADATA,sha256=kgPeaomBUFGZXUo9UcFBL9oRYI6taPxbbgJjgKIzxcI,41281
131
- signalwire_agents-0.1.43.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
132
- signalwire_agents-0.1.43.dist-info/entry_points.txt,sha256=ZDT65zfTO_YyDzi_hwQbCxIhrUfu_t8RpNXMMXlUPWI,144
133
- signalwire_agents-0.1.43.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
134
- signalwire_agents-0.1.43.dist-info/RECORD,,
129
+ signalwire_agents/web/__init__.py,sha256=XE_pSTY9Aalzr7J7wqFth1Zr3cccQHPPcF5HWNrOpz8,383
130
+ signalwire_agents/web/web_service.py,sha256=a2PSHJgX1tlZr0Iz1A1UouZjXEePJAZL632evvLVM38,21071
131
+ signalwire_agents-0.1.45.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
132
+ signalwire_agents-0.1.45.dist-info/METADATA,sha256=_UXOEqLIwgDb90G0K9fO95MKagAx-8VKp2UdLYX9_hs,41281
133
+ signalwire_agents-0.1.45.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
134
+ signalwire_agents-0.1.45.dist-info/entry_points.txt,sha256=ZDT65zfTO_YyDzi_hwQbCxIhrUfu_t8RpNXMMXlUPWI,144
135
+ signalwire_agents-0.1.45.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
136
+ signalwire_agents-0.1.45.dist-info/RECORD,,