signalwire-agents 0.1.23__py3-none-any.whl → 0.1.24__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/agent_server.py +2 -1
- signalwire_agents/cli/config.py +61 -0
- signalwire_agents/cli/core/__init__.py +1 -0
- signalwire_agents/cli/core/agent_loader.py +254 -0
- signalwire_agents/cli/core/argparse_helpers.py +164 -0
- signalwire_agents/cli/core/dynamic_config.py +62 -0
- signalwire_agents/cli/execution/__init__.py +1 -0
- signalwire_agents/cli/execution/datamap_exec.py +437 -0
- signalwire_agents/cli/execution/webhook_exec.py +125 -0
- signalwire_agents/cli/output/__init__.py +1 -0
- signalwire_agents/cli/output/output_formatter.py +132 -0
- signalwire_agents/cli/output/swml_dump.py +177 -0
- signalwire_agents/cli/simulation/__init__.py +1 -0
- signalwire_agents/cli/simulation/data_generation.py +365 -0
- signalwire_agents/cli/simulation/data_overrides.py +187 -0
- signalwire_agents/cli/simulation/mock_env.py +271 -0
- signalwire_agents/cli/test_swaig.py +522 -2539
- signalwire_agents/cli/types.py +72 -0
- signalwire_agents/core/agent/__init__.py +1 -3
- signalwire_agents/core/agent/config/__init__.py +1 -3
- signalwire_agents/core/agent/prompt/manager.py +25 -7
- signalwire_agents/core/agent/tools/decorator.py +2 -0
- signalwire_agents/core/agent/tools/registry.py +8 -0
- signalwire_agents/core/agent_base.py +492 -3053
- signalwire_agents/core/function_result.py +31 -42
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
- signalwire_agents/core/mixins/auth_mixin.py +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +345 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +219 -0
- signalwire_agents/core/mixins/tool_mixin.py +295 -0
- signalwire_agents/core/mixins/web_mixin.py +1130 -0
- signalwire_agents/core/skill_manager.py +3 -1
- signalwire_agents/core/swaig_function.py +10 -1
- signalwire_agents/core/swml_service.py +140 -58
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/native_vector_search/skill.py +33 -13
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +4 -0
- signalwire_agents/skills/spider/skill.py +479 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +1 -0
- signalwire_agents/skills/swml_transfer/skill.py +257 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/METADATA +47 -2
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/RECORD +62 -22
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/entry_points.txt +1 -1
- signalwire_agents/core/agent/config/ephemeral.py +0 -176
- signalwire_agents-0.1.23.data/data/schema.json +0 -5611
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
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
|
-
#
|
656
|
-
|
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
|
-
|
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
|
-
#
|
847
|
-
|
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=
|
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: {
|
866
|
-
print(f"URL with trailing slash: {
|
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
|
-
|
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
|
948
|
+
def _get_base_url(self, include_auth: bool = True) -> str:
|
951
949
|
"""
|
952
|
-
|
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
|
-
|
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
|
-
|
959
|
+
Base URL string (protocol://[auth@]host[:port])
|
960
960
|
"""
|
961
|
-
#
|
962
|
-
|
963
|
-
|
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
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
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
|
-
#
|
972
|
-
|
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
|
-
#
|
975
|
-
if
|
976
|
-
|
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
|
-
|
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
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
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
|
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
|
-
|
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
|