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
@@ -14,7 +14,11 @@ SignalWire AI Agents SDK
14
14
  A package for building AI agents using SignalWire's AI and SWML capabilities.
15
15
  """
16
16
 
17
- __version__ = "0.1.11"
17
+ # Configure logging before any other imports to ensure early initialization
18
+ from .core.logging_config import configure_logging
19
+ configure_logging()
20
+
21
+ __version__ = "0.1.13"
18
22
 
19
23
  # Import core classes for easier access
20
24
  from .core.agent_base import AgentBase
@@ -11,7 +11,6 @@ See LICENSE file in the project root for full license information.
11
11
  AgentServer - Class for hosting multiple SignalWire AI Agents in a single server
12
12
  """
13
13
 
14
- import logging
15
14
  import re
16
15
  from typing import Dict, Any, Optional, List, Tuple, Callable
17
16
 
@@ -25,6 +24,7 @@ except ImportError:
25
24
 
26
25
  from signalwire_agents.core.agent_base import AgentBase
27
26
  from signalwire_agents.core.swml_service import SWMLService
27
+ from signalwire_agents.core.logging_config import get_logger, get_execution_mode
28
28
 
29
29
 
30
30
  class AgentServer:
@@ -48,19 +48,13 @@ class AgentServer:
48
48
  Args:
49
49
  host: Host to bind the server to
50
50
  port: Port to bind the server to
51
- log_level: Logging level (debug, info, warning, error)
51
+ log_level: Logging level (debug, info, warning, error, critical)
52
52
  """
53
53
  self.host = host
54
54
  self.port = port
55
55
  self.log_level = log_level.lower()
56
56
 
57
- # Set up logging
58
- numeric_level = getattr(logging, self.log_level.upper(), logging.INFO)
59
- logging.basicConfig(
60
- level=numeric_level,
61
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
62
- )
63
- self.logger = logging.getLogger("AgentServer")
57
+ self.logger = get_logger("AgentServer")
64
58
 
65
59
  # Create FastAPI app
66
60
  self.app = FastAPI(
@@ -304,14 +298,229 @@ class AgentServer:
304
298
 
305
299
  return self.agents.get(route)
306
300
 
307
- def run(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
301
+ def run(self, event=None, context=None, host: Optional[str] = None, port: Optional[int] = None) -> Any:
308
302
  """
309
- Start the server
303
+ Universal run method that automatically detects environment and handles accordingly
304
+
305
+ Detects execution mode and routes appropriately:
306
+ - Server mode: Starts uvicorn server with FastAPI
307
+ - CGI mode: Uses same routing logic but outputs CGI headers
308
+ - Lambda mode: Uses same routing logic but returns Lambda response
310
309
 
311
310
  Args:
312
- host: Optional host to override the default
313
- port: Optional port to override the default
311
+ event: Serverless event object (Lambda, Cloud Functions)
312
+ context: Serverless context object (Lambda, Cloud Functions)
313
+ host: Optional host to override the default (server mode only)
314
+ port: Optional port to override the default (server mode only)
315
+
316
+ Returns:
317
+ Response for serverless modes, None for server mode
314
318
  """
319
+ from signalwire_agents.core.logging_config import get_execution_mode
320
+ import os
321
+ import json
322
+
323
+ # Detect execution mode
324
+ mode = get_execution_mode()
325
+
326
+ if mode == 'cgi':
327
+ return self._handle_cgi_request()
328
+ elif mode == 'lambda':
329
+ return self._handle_lambda_request(event, context)
330
+ else:
331
+ # Server mode - use existing logic
332
+ return self._run_server(host, port)
333
+
334
+ def _handle_cgi_request(self) -> str:
335
+ """Handle CGI request using same routing logic as server"""
336
+ import os
337
+ import sys
338
+ import json
339
+
340
+ # Get PATH_INFO to determine routing
341
+ path_info = os.getenv('PATH_INFO', '').strip('/')
342
+
343
+ # Use same routing logic as the server
344
+ if not path_info:
345
+ # Root request - return basic info or 404
346
+ response = {"error": "No agent specified in path"}
347
+ return self._format_cgi_response(response, status="404 Not Found")
348
+
349
+ # Find matching agent using same logic as server
350
+ for route, agent in self.agents.items():
351
+ route_clean = route.lstrip("/")
352
+
353
+ if path_info == route_clean:
354
+ # Request to agent root - return SWML
355
+ try:
356
+ swml = agent._render_swml()
357
+ return self._format_cgi_response(swml, content_type="application/json")
358
+ except Exception as e:
359
+ error_response = {"error": f"Failed to generate SWML: {str(e)}"}
360
+ return self._format_cgi_response(error_response, status="500 Internal Server Error")
361
+
362
+ elif path_info.startswith(route_clean + "/"):
363
+ # Request to agent sub-path
364
+ relative_path = path_info[len(route_clean):].lstrip("/")
365
+
366
+ if relative_path == "swaig":
367
+ # SWAIG function call - parse stdin for POST data
368
+ try:
369
+ # Read POST data from stdin
370
+ content_length = os.getenv('CONTENT_LENGTH')
371
+ if content_length:
372
+ raw_data = sys.stdin.buffer.read(int(content_length))
373
+ try:
374
+ post_data = json.loads(raw_data.decode('utf-8'))
375
+ except:
376
+ post_data = {}
377
+ else:
378
+ post_data = {}
379
+
380
+ # Execute SWAIG function
381
+ result = agent._execute_swaig_function("", post_data, None, None)
382
+ return self._format_cgi_response(result, content_type="application/json")
383
+
384
+ except Exception as e:
385
+ error_response = {"error": f"SWAIG function failed: {str(e)}"}
386
+ return self._format_cgi_response(error_response, status="500 Internal Server Error")
387
+
388
+ elif relative_path.startswith("swaig/"):
389
+ # Direct function call like /matti/swaig/function_name
390
+ function_name = relative_path[6:] # Remove "swaig/"
391
+ try:
392
+ # Read POST data from stdin
393
+ content_length = os.getenv('CONTENT_LENGTH')
394
+ if content_length:
395
+ raw_data = sys.stdin.buffer.read(int(content_length))
396
+ try:
397
+ post_data = json.loads(raw_data.decode('utf-8'))
398
+ except:
399
+ post_data = {}
400
+ else:
401
+ post_data = {}
402
+
403
+ result = agent._execute_swaig_function(function_name, post_data, None, None)
404
+ return self._format_cgi_response(result, content_type="application/json")
405
+
406
+ except Exception as e:
407
+ error_response = {"error": f"Function call failed: {str(e)}"}
408
+ return self._format_cgi_response(error_response, status="500 Internal Server Error")
409
+
410
+ # No matching agent found
411
+ error_response = {"error": "Not Found"}
412
+ return self._format_cgi_response(error_response, status="404 Not Found")
413
+
414
+ def _handle_lambda_request(self, event, context) -> dict:
415
+ """Handle Lambda request using same routing logic as server"""
416
+ import json
417
+
418
+ # Extract path from Lambda event
419
+ path = ""
420
+ if event and 'pathParameters' in event and event['pathParameters']:
421
+ path = event['pathParameters'].get('proxy', '')
422
+ elif event and 'path' in event:
423
+ path = event['path']
424
+
425
+ path = path.strip('/')
426
+
427
+ # Use same routing logic as server
428
+ if not path:
429
+ return {
430
+ "statusCode": 404,
431
+ "headers": {"Content-Type": "application/json"},
432
+ "body": json.dumps({"error": "No agent specified in path"})
433
+ }
434
+
435
+ # Find matching agent
436
+ for route, agent in self.agents.items():
437
+ route_clean = route.lstrip("/")
438
+
439
+ if path == route_clean:
440
+ # Request to agent root - return SWML
441
+ try:
442
+ swml = agent._render_swml()
443
+ return {
444
+ "statusCode": 200,
445
+ "headers": {"Content-Type": "application/json"},
446
+ "body": json.dumps(swml) if isinstance(swml, dict) else swml
447
+ }
448
+ except Exception as e:
449
+ return {
450
+ "statusCode": 500,
451
+ "headers": {"Content-Type": "application/json"},
452
+ "body": json.dumps({"error": f"Failed to generate SWML: {str(e)}"})
453
+ }
454
+
455
+ elif path.startswith(route_clean + "/"):
456
+ # Request to agent sub-path
457
+ relative_path = path[len(route_clean):].lstrip("/")
458
+
459
+ if relative_path == "swaig" or relative_path.startswith("swaig/"):
460
+ # SWAIG function call
461
+ try:
462
+ # Parse function name and body from event
463
+ function_name = relative_path[6:] if relative_path.startswith("swaig/") else ""
464
+
465
+ # Get POST data from Lambda event body
466
+ post_data = {}
467
+ if event and 'body' in event and event['body']:
468
+ try:
469
+ post_data = json.loads(event['body'])
470
+ except:
471
+ pass
472
+
473
+ result = agent._execute_swaig_function(function_name, post_data, None, None)
474
+ return {
475
+ "statusCode": 200,
476
+ "headers": {"Content-Type": "application/json"},
477
+ "body": json.dumps(result) if isinstance(result, dict) else result
478
+ }
479
+
480
+ except Exception as e:
481
+ return {
482
+ "statusCode": 500,
483
+ "headers": {"Content-Type": "application/json"},
484
+ "body": json.dumps({"error": f"Function call failed: {str(e)}"})
485
+ }
486
+
487
+ # No matching agent found
488
+ return {
489
+ "statusCode": 404,
490
+ "headers": {"Content-Type": "application/json"},
491
+ "body": json.dumps({"error": "Not Found"})
492
+ }
493
+
494
+ def _format_cgi_response(self, data, content_type: str = "application/json", status: str = "200 OK") -> str:
495
+ """Format response for CGI output"""
496
+ import json
497
+ import sys
498
+
499
+ # Format the body
500
+ if isinstance(data, dict):
501
+ body = json.dumps(data)
502
+ else:
503
+ body = str(data)
504
+
505
+ # Build CGI response with headers
506
+ response_lines = [
507
+ f"Status: {status}",
508
+ f"Content-Type: {content_type}",
509
+ f"Content-Length: {len(body.encode('utf-8'))}",
510
+ "", # Empty line separates headers from body
511
+ body
512
+ ]
513
+
514
+ response = "\n".join(response_lines)
515
+
516
+ # Write directly to stdout and flush to ensure immediate output
517
+ sys.stdout.write(response)
518
+ sys.stdout.flush()
519
+
520
+ return response
521
+
522
+ def _run_server(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
523
+ """Original server mode logic"""
315
524
  if not self.agents:
316
525
  self.logger.warning("Starting server with no registered agents")
317
526