signalwire-agents 0.1.10__py3-none-any.whl → 0.1.12__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 (46) hide show
  1. signalwire_agents/__init__.py +43 -4
  2. signalwire_agents/agent_server.py +268 -15
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/build_search.py +457 -0
  5. signalwire_agents/cli/test_swaig.py +2609 -0
  6. signalwire_agents/core/agent_base.py +691 -82
  7. signalwire_agents/core/contexts.py +289 -0
  8. signalwire_agents/core/data_map.py +499 -0
  9. signalwire_agents/core/function_result.py +57 -10
  10. signalwire_agents/core/logging_config.py +232 -0
  11. signalwire_agents/core/skill_base.py +27 -37
  12. signalwire_agents/core/skill_manager.py +89 -23
  13. signalwire_agents/core/swaig_function.py +13 -1
  14. signalwire_agents/core/swml_handler.py +37 -13
  15. signalwire_agents/core/swml_service.py +37 -28
  16. signalwire_agents/search/__init__.py +131 -0
  17. signalwire_agents/search/document_processor.py +764 -0
  18. signalwire_agents/search/index_builder.py +534 -0
  19. signalwire_agents/search/query_processor.py +371 -0
  20. signalwire_agents/search/search_engine.py +383 -0
  21. signalwire_agents/search/search_service.py +251 -0
  22. signalwire_agents/skills/datasphere/__init__.py +12 -0
  23. signalwire_agents/skills/datasphere/skill.py +229 -0
  24. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  25. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  26. signalwire_agents/skills/datetime/skill.py +9 -5
  27. signalwire_agents/skills/joke/__init__.py +1 -0
  28. signalwire_agents/skills/joke/skill.py +88 -0
  29. signalwire_agents/skills/math/skill.py +9 -6
  30. signalwire_agents/skills/native_vector_search/__init__.py +1 -0
  31. signalwire_agents/skills/native_vector_search/skill.py +352 -0
  32. signalwire_agents/skills/registry.py +10 -4
  33. signalwire_agents/skills/web_search/skill.py +57 -21
  34. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  35. signalwire_agents/skills/wikipedia/skill.py +180 -0
  36. signalwire_agents/utils/__init__.py +14 -0
  37. signalwire_agents/utils/schema_utils.py +111 -44
  38. signalwire_agents-0.1.12.dist-info/METADATA +863 -0
  39. signalwire_agents-0.1.12.dist-info/RECORD +67 -0
  40. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
  41. signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
  42. signalwire_agents-0.1.10.dist-info/METADATA +0 -319
  43. signalwire_agents-0.1.10.dist-info/RECORD +0 -44
  44. {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
  45. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
  46. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/top_level.txt +0 -0
@@ -14,16 +14,55 @@ 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.10"
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.12"
18
22
 
19
23
  # Import core classes for easier access
20
- from signalwire_agents.core.agent_base import AgentBase
24
+ from .core.agent_base import AgentBase
25
+ from .core.contexts import ContextBuilder, Context, Step, create_simple_context
26
+ from .core.data_map import DataMap, create_simple_api_tool, create_expression_tool
27
+ from .core.state import StateManager, FileStateManager
21
28
  from signalwire_agents.agent_server import AgentServer
22
29
  from signalwire_agents.core.swml_service import SWMLService
23
30
  from signalwire_agents.core.swml_builder import SWMLBuilder
24
- from signalwire_agents.core.state import StateManager, FileStateManager
31
+ from signalwire_agents.core.function_result import SwaigFunctionResult
32
+ from signalwire_agents.core.swaig_function import SWAIGFunction
25
33
 
26
34
  # Import skills to trigger discovery
27
35
  import signalwire_agents.skills
28
36
 
29
- __all__ = ["AgentBase", "AgentServer", "SWMLService", "SWMLBuilder", "StateManager", "FileStateManager"]
37
+ # Import convenience functions from the CLI (if available)
38
+ try:
39
+ from signalwire_agents.cli.helpers import start_agent, run_agent, list_skills
40
+ except ImportError:
41
+ # CLI helpers not available, define minimal versions
42
+ def start_agent(*args, **kwargs):
43
+ raise NotImplementedError("CLI helpers not available")
44
+ def run_agent(*args, **kwargs):
45
+ raise NotImplementedError("CLI helpers not available")
46
+ def list_skills(*args, **kwargs):
47
+ raise NotImplementedError("CLI helpers not available")
48
+
49
+ __all__ = [
50
+ "AgentBase",
51
+ "AgentServer",
52
+ "SWMLService",
53
+ "SWMLBuilder",
54
+ "StateManager",
55
+ "FileStateManager",
56
+ "SwaigFunctionResult",
57
+ "SWAIGFunction",
58
+ "DataMap",
59
+ "create_simple_api_tool",
60
+ "create_expression_tool",
61
+ "ContextBuilder",
62
+ "Context",
63
+ "Step",
64
+ "create_simple_context",
65
+ "start_agent",
66
+ "run_agent",
67
+ "list_skills"
68
+ ]
@@ -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,25 +48,20 @@ 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(
67
61
  title="SignalWire AI Agents",
68
62
  description="Hosted SignalWire AI Agents",
69
- version="0.1.2"
63
+ version="0.1.2",
64
+ redirect_slashes=False
70
65
  )
71
66
 
72
67
  # Keep track of registered agents
@@ -105,7 +100,8 @@ class AgentServer:
105
100
  # Store the agent
106
101
  self.agents[route] = agent
107
102
 
108
- # Get the router and register it
103
+ # Get the router and register it using the standard approach
104
+ # The agent's router already handles both trailing slash versions properly
109
105
  router = agent.as_router()
110
106
  self.app.include_router(router, prefix=route)
111
107
 
@@ -302,14 +298,229 @@ class AgentServer:
302
298
 
303
299
  return self.agents.get(route)
304
300
 
305
- 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:
306
302
  """
307
- 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
308
309
 
309
310
  Args:
310
- host: Optional host to override the default
311
- 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
312
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"""
313
524
  if not self.agents:
314
525
  self.logger.warning("Starting server with no registered agents")
315
526
 
@@ -322,6 +533,48 @@ class AgentServer:
322
533
  "routes": list(self.agents.keys())
323
534
  }
324
535
 
536
+ # Add catch-all route handler to handle both trailing slash and non-trailing slash versions
537
+ @self.app.get("/{full_path:path}")
538
+ @self.app.post("/{full_path:path}")
539
+ async def handle_all_routes(request: Request, full_path: str):
540
+ """Handle requests that don't match registered routes (e.g. /matti instead of /matti/)"""
541
+ # Check if this path maps to one of our registered agents
542
+ for route, agent in self.agents.items():
543
+ # Check for exact match with registered route
544
+ if full_path == route.lstrip("/"):
545
+ # This is a request to an agent's root without trailing slash
546
+ return await agent._handle_root_request(request)
547
+ elif full_path.startswith(route.lstrip("/") + "/"):
548
+ # This is a request to an agent's sub-path
549
+ relative_path = full_path[len(route.lstrip("/")):]
550
+ relative_path = relative_path.lstrip("/")
551
+
552
+ # Route to appropriate handler based on path
553
+ if not relative_path or relative_path == "/":
554
+ return await agent._handle_root_request(request)
555
+
556
+ clean_path = relative_path.rstrip("/")
557
+ if clean_path == "debug":
558
+ return await agent._handle_debug_request(request)
559
+ elif clean_path == "swaig":
560
+ from fastapi import Response
561
+ return await agent._handle_swaig_request(request, Response())
562
+ elif clean_path == "post_prompt":
563
+ return await agent._handle_post_prompt_request(request)
564
+ elif clean_path == "check_for_input":
565
+ return await agent._handle_check_for_input_request(request)
566
+
567
+ # Check for custom routing callbacks
568
+ if hasattr(agent, '_routing_callbacks'):
569
+ for callback_path, callback_fn in agent._routing_callbacks.items():
570
+ cb_path_clean = callback_path.strip("/")
571
+ if clean_path == cb_path_clean:
572
+ request.state.callback_path = callback_path
573
+ return await agent._handle_root_request(request)
574
+
575
+ # No matching agent found
576
+ return {"error": "Not Found"}
577
+
325
578
  # Print server info
326
579
  host = host or self.host
327
580
  port = port or self.port
@@ -0,0 +1,9 @@
1
+ """
2
+ SignalWire Agents CLI Tools
3
+
4
+ This package contains command-line tools for working with SignalWire AI Agents.
5
+ """
6
+
7
+ from .test_swaig import main as test_swaig_main
8
+
9
+ __all__ = ['test_swaig_main']