signalwire-agents 0.1.23__py3-none-any.whl → 0.1.25__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 (64) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +2 -1
  3. signalwire_agents/cli/config.py +61 -0
  4. signalwire_agents/cli/core/__init__.py +1 -0
  5. signalwire_agents/cli/core/agent_loader.py +254 -0
  6. signalwire_agents/cli/core/argparse_helpers.py +164 -0
  7. signalwire_agents/cli/core/dynamic_config.py +62 -0
  8. signalwire_agents/cli/execution/__init__.py +1 -0
  9. signalwire_agents/cli/execution/datamap_exec.py +437 -0
  10. signalwire_agents/cli/execution/webhook_exec.py +125 -0
  11. signalwire_agents/cli/output/__init__.py +1 -0
  12. signalwire_agents/cli/output/output_formatter.py +132 -0
  13. signalwire_agents/cli/output/swml_dump.py +177 -0
  14. signalwire_agents/cli/simulation/__init__.py +1 -0
  15. signalwire_agents/cli/simulation/data_generation.py +365 -0
  16. signalwire_agents/cli/simulation/data_overrides.py +187 -0
  17. signalwire_agents/cli/simulation/mock_env.py +271 -0
  18. signalwire_agents/cli/test_swaig.py +522 -2539
  19. signalwire_agents/cli/types.py +72 -0
  20. signalwire_agents/core/agent/__init__.py +1 -3
  21. signalwire_agents/core/agent/config/__init__.py +1 -3
  22. signalwire_agents/core/agent/prompt/manager.py +25 -7
  23. signalwire_agents/core/agent/tools/decorator.py +2 -0
  24. signalwire_agents/core/agent/tools/registry.py +8 -0
  25. signalwire_agents/core/agent_base.py +492 -3053
  26. signalwire_agents/core/function_result.py +31 -42
  27. signalwire_agents/core/mixins/__init__.py +28 -0
  28. signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
  29. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  30. signalwire_agents/core/mixins/prompt_mixin.py +345 -0
  31. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  32. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  33. signalwire_agents/core/mixins/state_mixin.py +219 -0
  34. signalwire_agents/core/mixins/tool_mixin.py +295 -0
  35. signalwire_agents/core/mixins/web_mixin.py +1130 -0
  36. signalwire_agents/core/skill_manager.py +3 -1
  37. signalwire_agents/core/swaig_function.py +10 -1
  38. signalwire_agents/core/swml_service.py +140 -58
  39. signalwire_agents/skills/README.md +452 -0
  40. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  41. signalwire_agents/skills/datasphere/README.md +210 -0
  42. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  43. signalwire_agents/skills/datetime/README.md +132 -0
  44. signalwire_agents/skills/joke/README.md +149 -0
  45. signalwire_agents/skills/math/README.md +161 -0
  46. signalwire_agents/skills/native_vector_search/skill.py +33 -13
  47. signalwire_agents/skills/play_background_file/README.md +218 -0
  48. signalwire_agents/skills/spider/README.md +236 -0
  49. signalwire_agents/skills/spider/__init__.py +4 -0
  50. signalwire_agents/skills/spider/skill.py +479 -0
  51. signalwire_agents/skills/swml_transfer/README.md +395 -0
  52. signalwire_agents/skills/swml_transfer/__init__.py +1 -0
  53. signalwire_agents/skills/swml_transfer/skill.py +257 -0
  54. signalwire_agents/skills/weather_api/README.md +178 -0
  55. signalwire_agents/skills/web_search/README.md +163 -0
  56. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  57. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/METADATA +47 -2
  58. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
  59. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/entry_points.txt +1 -1
  60. signalwire_agents/core/agent/config/ephemeral.py +0 -176
  61. signalwire_agents-0.1.23.data/data/schema.json +0 -5611
  62. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/WHEEL +0 -0
  63. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
  64. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/top_level.txt +0 -0
@@ -182,4 +182,6 @@ class SkillManager:
182
182
  if skill_identifier in self.loaded_skills:
183
183
  return self.loaded_skills[skill_identifier]
184
184
 
185
- return None
185
+ return None
186
+
187
+
@@ -31,6 +31,7 @@ class SWAIGFunction:
31
31
  secure: bool = False,
32
32
  fillers: Optional[Dict[str, List[str]]] = None,
33
33
  webhook_url: Optional[str] = None,
34
+ required: Optional[List[str]] = None,
34
35
  **extra_swaig_fields
35
36
  ):
36
37
  """
@@ -44,6 +45,7 @@ class SWAIGFunction:
44
45
  secure: Whether this function requires token validation
45
46
  fillers: Optional dictionary of filler phrases by language code
46
47
  webhook_url: Optional external webhook URL to use instead of local handling
48
+ required: Optional list of required parameter names
47
49
  **extra_swaig_fields: Additional SWAIG fields to include in function definition
48
50
  """
49
51
  self.name = name
@@ -53,6 +55,7 @@ class SWAIGFunction:
53
55
  self.secure = secure
54
56
  self.fillers = fillers
55
57
  self.webhook_url = webhook_url
58
+ self.required = required or []
56
59
  self.extra_swaig_fields = extra_swaig_fields
57
60
 
58
61
  # Mark as external if webhook_url is provided
@@ -73,11 +76,17 @@ class SWAIGFunction:
73
76
  return self.parameters
74
77
 
75
78
  # Otherwise, wrap the parameters in the expected structure
76
- return {
79
+ result = {
77
80
  "type": "object",
78
81
  "properties": self.parameters
79
82
  }
80
83
 
84
+ # Add required fields if specified
85
+ if self.required:
86
+ result["required"] = self.required
87
+
88
+ return result
89
+
81
90
  def __call__(self, *args, **kwargs):
82
91
  """
83
92
  Call the underlying handler function
@@ -90,13 +90,17 @@ class SWMLService:
90
90
  self.ssl_cert_path = os.environ.get('SWML_SSL_CERT_PATH')
91
91
  self.ssl_key_path = os.environ.get('SWML_SSL_KEY_PATH')
92
92
 
93
+ # Initialize logger for this instance FIRST before using it
94
+ self.log = logger.bind(service=name)
95
+
93
96
  # Initialize proxy detection attributes
94
97
  self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
98
+ self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
99
+ if self._proxy_url_base:
100
+ self.log.warning("SWML_PROXY_URL_BASE is set in environment - This overrides SSL configuration and port settings. Remove this variable to use automatic detection.",
101
+ proxy_url_base=self._proxy_url_base)
95
102
  self._proxy_detection_done = False
96
103
  self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
97
-
98
- # Initialize logger for this instance
99
- self.log = logger.bind(service=name)
100
104
  self.log.info("service_initializing", route=self.route, host=host, port=port)
101
105
 
102
106
  # Set basic auth credentials
@@ -652,10 +656,8 @@ class SWMLService:
652
656
  Returns:
653
657
  Response with SWML document or error
654
658
  """
655
- # Auto-detect proxy on first request if not explicitly configured
656
- if not self._proxy_detection_done and not self._proxy_url_base:
657
- self._detect_proxy_from_request(request)
658
- self._proxy_detection_done = True
659
+ # Always detect proxy from current request - allows mixing direct and proxied access
660
+ self._detect_proxy_from_request(request)
659
661
 
660
662
  # Check auth
661
663
  if not self._check_basic_auth(request):
@@ -785,8 +787,11 @@ class SWMLService:
785
787
  # This avoids the FastAPI error about prefixes ending with slashes
786
788
  normalized_route = "/" + self.route.strip("/")
787
789
 
788
- # Include router with the normalized prefix
789
- app.include_router(router, prefix=normalized_route)
790
+ # Include router with the normalized prefix (handle root route special case)
791
+ if normalized_route == "/":
792
+ app.include_router(router)
793
+ else:
794
+ app.include_router(router, prefix=normalized_route)
790
795
 
791
796
  # Add a catch-all route handler that will handle both /path and /path/ formats
792
797
  # This provides the same behavior without using a trailing slash in the prefix
@@ -843,34 +848,27 @@ class SWMLService:
843
848
  # Get the auth credentials
844
849
  username, password = self._basic_auth
845
850
 
846
- # Use correct protocol and host in displayed URL
847
- protocol = "https" if self.ssl_enabled else "http"
848
-
849
- # Determine display host - include port unless it's the standard port for the protocol
850
- if self.ssl_enabled and self.domain:
851
- # Use domain, but include port if it's not the standard HTTPS port (443)
852
- display_host = f"{self.domain}:{port}" if port != 443 else self.domain
853
- else:
854
- # Use host:port for HTTP or when no domain is specified
855
- display_host = f"{host}:{port}"
851
+ # Get the proper URL using unified URL building
852
+ startup_url = self._build_full_url(include_auth=False)
856
853
 
857
854
  self.log.info("starting_server",
858
- url=f"{protocol}://{display_host}{self.route}",
855
+ url=startup_url,
859
856
  ssl_enabled=self.ssl_enabled,
860
857
  username=username,
861
858
  password_length=len(password))
862
859
 
863
860
  # Print user-friendly startup message (keep for UX)
864
861
  print(f"Service '{self.name}' is available at:")
865
- print(f"URL: {protocol}://{display_host}{self.route}")
866
- print(f"URL with trailing slash: {protocol}://{display_host}{self.route}/")
862
+ print(f"URL: {startup_url}")
863
+ print(f"URL with trailing slash: {startup_url}/")
867
864
  print(f"Basic Auth: {username}:{password}")
868
865
 
869
866
  # Check if SIP routing is enabled and log additional info
870
867
  if self._routing_callbacks:
871
868
  print(f"Callback endpoints:")
872
869
  for path in self._routing_callbacks:
873
- print(f"{protocol}://{display_host}{self.route}{path}")
870
+ callback_url = self._build_full_url(endpoint=path.lstrip('/'), include_auth=False)
871
+ print(f" {callback_url}")
874
872
 
875
873
  # Start uvicorn with or without SSL
876
874
  if self.ssl_enabled and ssl_cert_path and ssl_key_path:
@@ -947,65 +945,145 @@ class SWMLService:
947
945
  return username, password
948
946
 
949
947
 
950
- def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
948
+ def _get_base_url(self, include_auth: bool = True) -> str:
951
949
  """
952
- Helper method to build webhook URLs consistently
950
+ Get the base URL for this service, using proxy info if available or falling back to configured values
951
+
952
+ This is the central method for URL building that handles both startup configuration
953
+ and per-request proxy detection.
953
954
 
954
955
  Args:
955
- endpoint: The endpoint path (e.g., "swaig", "post_prompt")
956
- query_params: Optional query parameters to append
956
+ include_auth: Whether to include authentication credentials in the URL
957
957
 
958
958
  Returns:
959
- Fully constructed webhook URL
959
+ Base URL string (protocol://[auth@]host[:port])
960
960
  """
961
- # Base URL construction
962
- if hasattr(self, '_proxy_url_base') and getattr(self, '_proxy_url_base', None):
963
- # For proxy URLs
961
+ # Debug logging to understand state
962
+ self.log.debug("_get_base_url called",
963
+ has_proxy_url_base=hasattr(self, '_proxy_url_base'),
964
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
965
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False),
966
+ env_var=os.environ.get('SWML_PROXY_URL_BASE'),
967
+ include_auth=include_auth,
968
+ caller=inspect.stack()[1].function if len(inspect.stack()) > 1 else "unknown")
969
+
970
+ # Check if we have proxy information from a request
971
+ if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
964
972
  base = self._proxy_url_base.rstrip('/')
973
+ self.log.debug("Using proxy URL base", proxy_url_base=base)
974
+
975
+ # Add auth credentials if requested
976
+ if include_auth:
977
+ username, password = self._basic_auth
978
+ url = urlparse(base)
979
+ base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
965
980
 
966
- # Always add auth credentials
967
- username, password = self._basic_auth
968
- url = urlparse(base)
969
- base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
981
+ return base
982
+
983
+ # No proxy, use configured values
984
+ # Determine protocol based on SSL settings
985
+ protocol = "https" if self.ssl_enabled else "http"
986
+
987
+ # Debug logging
988
+ self.log.debug("_get_base_url",
989
+ ssl_enabled=self.ssl_enabled,
990
+ domain=self.domain,
991
+ port=self.port,
992
+ protocol=protocol)
993
+
994
+ # Determine host part
995
+ if self.ssl_enabled and self.domain:
996
+ # Use domain for SSL
997
+ if protocol == "https" and self.port == 443:
998
+ host_part = self.domain # Don't include port for standard HTTPS
999
+ elif protocol == "http" and self.port == 80:
1000
+ host_part = self.domain # Don't include port for standard HTTP
1001
+ else:
1002
+ host_part = f"{self.domain}:{self.port}"
1003
+ self.log.debug("Using domain with port", domain=self.domain, port=self.port, host_part=host_part)
970
1004
  else:
971
- # Determine protocol based on SSL settings
972
- protocol = "https" if getattr(self, 'ssl_enabled', False) else "http"
1005
+ # Use configured host
1006
+ if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1007
+ host = "localhost"
1008
+ else:
1009
+ host = self.host
973
1010
 
974
- # Use domain if available and SSL is enabled
975
- if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
976
- # Use domain, but include port if it's not the standard HTTPS port (443)
977
- host_part = f"{self.domain}:{self.port}" if self.port != 443 else self.domain
1011
+ # Include port unless it's the standard port for the protocol
1012
+ if (protocol == "https" and self.port == 443) or (protocol == "http" and self.port == 80):
1013
+ host_part = host
978
1014
  else:
979
- # For local URLs
980
- if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
981
- host = "localhost"
982
- else:
983
- host = self.host
984
-
985
1015
  host_part = f"{host}:{self.port}"
986
-
987
- # Always include auth credentials
1016
+
1017
+ # Build base URL
1018
+ if include_auth:
988
1019
  username, password = self._basic_auth
989
1020
  base = f"{protocol}://{username}:{password}@{host_part}"
1021
+ else:
1022
+ base = f"{protocol}://{host_part}"
990
1023
 
991
- # Ensure the endpoint has a trailing slash to prevent redirects
992
- if endpoint and not endpoint.endswith('/'):
993
- endpoint = f"{endpoint}/"
994
-
995
- # Simple path - use the route directly with the endpoint
996
- path = f"{self.route}/{endpoint}"
1024
+ return base
1025
+
1026
+ def _build_full_url(self, endpoint: str = "", include_auth: bool = True, query_params: Optional[Dict[str, str]] = None) -> str:
1027
+ """
1028
+ Build the full URL for this service or a specific endpoint
1029
+
1030
+ This is the internal implementation used by both get_full_url (for AgentBase compatibility)
1031
+ and _build_webhook_url.
1032
+
1033
+ Args:
1034
+ endpoint: Optional endpoint path (e.g., "swaig", "post_prompt")
1035
+ include_auth: Whether to include authentication credentials in the URL
1036
+ query_params: Optional query parameters to append
997
1037
 
1038
+ Returns:
1039
+ Full URL string
1040
+ """
1041
+ # Get base URL using central method
1042
+ base = self._get_base_url(include_auth=include_auth)
1043
+
1044
+ # Build path
1045
+ if endpoint:
1046
+ # Ensure endpoint doesn't start with slash
1047
+ endpoint = endpoint.lstrip('/')
1048
+ # Add trailing slash to endpoint to prevent redirects
1049
+ if not endpoint.endswith('/'):
1050
+ endpoint = f"{endpoint}/"
1051
+ path = f"{self.route}/{endpoint}"
1052
+ else:
1053
+ # Just the route itself
1054
+ path = self.route if self.route != "/" else ""
1055
+
998
1056
  # Construct full URL
999
1057
  url = f"{base}{path}"
1000
1058
 
1001
- # Add query parameters if any (only if they have values)
1059
+ # Add query parameters if any
1002
1060
  if query_params:
1003
1061
  filtered_params = {k: v for k, v in query_params.items() if v}
1004
1062
  if filtered_params:
1005
1063
  params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
1006
1064
  url = f"{url}?{params}"
1065
+
1066
+ return url
1067
+
1068
+ def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
1069
+ """
1070
+ Helper method to build webhook URLs consistently
1071
+
1072
+ Args:
1073
+ endpoint: The endpoint path (e.g., "swaig", "post_prompt")
1074
+ query_params: Optional query parameters to append
1007
1075
 
1008
- return url
1076
+ Returns:
1077
+ Fully constructed webhook URL
1078
+ """
1079
+ self.log.debug("_build_webhook_url called",
1080
+ endpoint=endpoint,
1081
+ query_params=query_params,
1082
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
1083
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False))
1084
+
1085
+ # Use the central URL building method
1086
+ return self._build_full_url(endpoint=endpoint, include_auth=True, query_params=query_params)
1009
1087
 
1010
1088
  def _detect_proxy_from_request(self, request: Request) -> None:
1011
1089
  """
@@ -1015,6 +1093,10 @@ class SWMLService:
1015
1093
  Args:
1016
1094
  request: FastAPI Request object
1017
1095
  """
1096
+ # If SWML_PROXY_URL_BASE was already set (e.g., from environment), don't override it
1097
+ if self._proxy_url_base:
1098
+ return
1099
+
1018
1100
  # First check for standard X-Forwarded headers (used by most proxies including ngrok)
1019
1101
  forwarded_host = request.headers.get("X-Forwarded-Host")
1020
1102
  forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
@@ -1099,4 +1181,4 @@ class SWMLService:
1099
1181
  if proxy_url:
1100
1182
  self._proxy_url_base = proxy_url.rstrip('/')
1101
1183
  self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
1102
- self._proxy_detection_done = True
1184
+ self._proxy_detection_done = True