signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -24,51 +24,11 @@ import types
24
24
  from typing import Dict, List, Any, Optional, Union, Callable, Tuple, Type
25
25
  from urllib.parse import urlparse
26
26
 
27
- # Import and configure structlog
28
- try:
29
- import structlog
30
-
31
- # Only configure if not already configured
32
- if not hasattr(structlog, "_configured") or not structlog._configured:
33
- structlog.configure(
34
- processors=[
35
- structlog.stdlib.filter_by_level,
36
- structlog.stdlib.add_logger_name,
37
- structlog.stdlib.add_log_level,
38
- structlog.stdlib.PositionalArgumentsFormatter(),
39
- structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
40
- structlog.processors.StackInfoRenderer(),
41
- structlog.processors.format_exc_info,
42
- structlog.processors.UnicodeDecoder(),
43
- structlog.dev.ConsoleRenderer()
44
- ],
45
- context_class=dict,
46
- logger_factory=structlog.stdlib.LoggerFactory(),
47
- wrapper_class=structlog.stdlib.BoundLogger,
48
- cache_logger_on_first_use=True,
49
- )
50
-
51
- # Set up root logger with structlog
52
- logging.basicConfig(
53
- format="%(message)s",
54
- stream=sys.stdout,
55
- level=logging.INFO,
56
- )
57
-
58
- # Mark as configured to avoid duplicate configuration
59
- structlog._configured = True
60
-
61
- # Create the module logger
62
- logger = structlog.get_logger("swml_service")
63
-
64
- except ImportError:
65
- # Fallback to standard logging if structlog is not available
66
- logging.basicConfig(
67
- level=logging.INFO,
68
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
69
- stream=sys.stdout
70
- )
71
- logger = logging.getLogger("swml_service")
27
+ # Import centralized logging system
28
+ from signalwire_agents.core.logging_config import get_logger
29
+
30
+ # Create the module logger using centralized system
31
+ logger = get_logger("swml_service")
72
32
 
73
33
  try:
74
34
  import fastapi
@@ -82,6 +42,7 @@ except ImportError:
82
42
 
83
43
  from signalwire_agents.utils.schema_utils import SchemaUtils
84
44
  from signalwire_agents.core.swml_handler import VerbHandlerRegistry, SWMLVerbHandler
45
+ from signalwire_agents.core.security_config import SecurityConfig
85
46
 
86
47
 
87
48
  class SWMLService:
@@ -103,9 +64,10 @@ class SWMLService:
103
64
  name: str,
104
65
  route: str = "/",
105
66
  host: str = "0.0.0.0",
106
- port: int = 3000,
67
+ port: Optional[int] = None,
107
68
  basic_auth: Optional[Tuple[str, str]] = None,
108
- schema_path: Optional[str] = None
69
+ schema_path: Optional[str] = None,
70
+ config_file: Optional[str] = None
109
71
  ):
110
72
  """
111
73
  Initialize a new SWML service
@@ -117,40 +79,44 @@ class SWMLService:
117
79
  port: Port to bind the web server to
118
80
  basic_auth: Optional (username, password) tuple for basic auth
119
81
  schema_path: Optional path to the schema file
82
+ config_file: Optional path to configuration file
120
83
  """
121
84
  self.name = name
122
85
  self.route = route.rstrip("/") # Ensure no trailing slash
123
86
  self.host = host
124
- self.port = port
125
- self.ssl_enabled = False
126
- self.domain = None
87
+ # Use provided port, or PORT env var, or default to 3000
88
+ self.port = port if port is not None else int(os.environ.get("PORT", 3000))
89
+
90
+ # Initialize logger for this instance FIRST before using it
91
+ self.log = logger.bind(service=name)
92
+
93
+ # Load unified security configuration with optional config file
94
+ self.security = SecurityConfig(config_file=config_file, service_name=name)
95
+ self.security.log_config("SWMLService")
96
+
97
+ # For backward compatibility, expose SSL settings as instance attributes
98
+ self.ssl_enabled = self.security.ssl_enabled
99
+ self.domain = self.security.domain
100
+ self.ssl_cert_path = self.security.ssl_cert_path
101
+ self.ssl_key_path = self.security.ssl_key_path
127
102
 
128
103
  # Initialize proxy detection attributes
129
104
  self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
105
+ self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
106
+ if self._proxy_url_base:
107
+ 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.",
108
+ proxy_url_base=self._proxy_url_base)
130
109
  self._proxy_detection_done = False
131
110
  self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
132
-
133
- # Initialize logger for this instance
134
- self.log = logger.bind(service=name)
135
- self.log.info("service_initializing", route=self.route, host=host, port=port)
111
+ self.log.info("service_initializing", route=self.route, host=self.host, port=self.port)
136
112
 
137
113
  # Set basic auth credentials
138
114
  if basic_auth is not None:
139
115
  # Use provided credentials
140
116
  self._basic_auth = basic_auth
141
117
  else:
142
- # Check environment variables first
143
- env_user = os.environ.get('SWML_BASIC_AUTH_USER')
144
- env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
145
-
146
- if env_user and env_pass:
147
- # Use environment variables
148
- self._basic_auth = (env_user, env_pass)
149
- else:
150
- # Generate random credentials as fallback
151
- username = f"user_{secrets.token_hex(4)}"
152
- password = secrets.token_urlsafe(16)
153
- self._basic_auth = (username, password)
118
+ # Use unified security config for auth credentials
119
+ self._basic_auth = self.security.get_basic_auth()
154
120
 
155
121
  # Find the schema file if not provided
156
122
  if schema_path is None:
@@ -687,10 +653,8 @@ class SWMLService:
687
653
  Returns:
688
654
  Response with SWML document or error
689
655
  """
690
- # Auto-detect proxy on first request if not explicitly configured
691
- if not self._proxy_detection_done and not self._proxy_url_base:
692
- self._detect_proxy_from_request(request)
693
- self._proxy_detection_done = True
656
+ # Always detect proxy from current request - allows mixing direct and proxied access
657
+ self._detect_proxy_from_request(request)
694
658
 
695
659
  # Check auth
696
660
  if not self._check_basic_auth(request):
@@ -789,21 +753,21 @@ class SWMLService:
789
753
  """
790
754
  import uvicorn
791
755
 
792
- # Store SSL configuration
793
- self.ssl_enabled = ssl_enabled if ssl_enabled is not None else False
794
- self.domain = domain
756
+ # Store SSL configuration (override environment if explicitly provided)
757
+ if ssl_enabled is not None:
758
+ self.ssl_enabled = ssl_enabled
759
+ if domain is not None:
760
+ self.domain = domain
795
761
 
796
- # Set SSL paths
797
- ssl_cert_path = ssl_cert
798
- ssl_key_path = ssl_key
762
+ # Set SSL paths (use provided paths or fall back to environment)
763
+ ssl_cert_path = ssl_cert or getattr(self, 'ssl_cert_path', None)
764
+ ssl_key_path = ssl_key or getattr(self, 'ssl_key_path', None)
799
765
 
800
766
  # Validate SSL configuration if enabled
801
767
  if self.ssl_enabled:
802
- if not ssl_cert_path or not os.path.exists(ssl_cert_path):
803
- self.log.warning("ssl_cert_not_found", path=ssl_cert_path)
804
- self.ssl_enabled = False
805
- elif not ssl_key_path or not os.path.exists(ssl_key_path):
806
- self.log.warning("ssl_key_not_found", path=ssl_key_path)
768
+ is_valid, error = self.security.validate_ssl_config()
769
+ if not is_valid:
770
+ self.log.warning("ssl_config_invalid", error=error)
807
771
  self.ssl_enabled = False
808
772
  elif not self.domain:
809
773
  self.log.warning("ssl_domain_not_specified")
@@ -818,8 +782,11 @@ class SWMLService:
818
782
  # This avoids the FastAPI error about prefixes ending with slashes
819
783
  normalized_route = "/" + self.route.strip("/")
820
784
 
821
- # Include router with the normalized prefix
822
- app.include_router(router, prefix=normalized_route)
785
+ # Include router with the normalized prefix (handle root route special case)
786
+ if normalized_route == "/":
787
+ app.include_router(router)
788
+ else:
789
+ app.include_router(router, prefix=normalized_route)
823
790
 
824
791
  # Add a catch-all route handler that will handle both /path and /path/ formats
825
792
  # This provides the same behavior without using a trailing slash in the prefix
@@ -876,27 +843,27 @@ class SWMLService:
876
843
  # Get the auth credentials
877
844
  username, password = self._basic_auth
878
845
 
879
- # Use correct protocol and host in displayed URL
880
- protocol = "https" if self.ssl_enabled else "http"
881
- display_host = self.domain if self.ssl_enabled and self.domain else f"{host}:{port}"
846
+ # Get the proper URL using unified URL building
847
+ startup_url = self._build_full_url(include_auth=False)
882
848
 
883
849
  self.log.info("starting_server",
884
- url=f"{protocol}://{display_host}{self.route}",
850
+ url=startup_url,
885
851
  ssl_enabled=self.ssl_enabled,
886
852
  username=username,
887
853
  password_length=len(password))
888
854
 
889
855
  # Print user-friendly startup message (keep for UX)
890
856
  print(f"Service '{self.name}' is available at:")
891
- print(f"URL: {protocol}://{display_host}{self.route}")
892
- print(f"URL with trailing slash: {protocol}://{display_host}{self.route}/")
857
+ print(f"URL: {startup_url}")
858
+ print(f"URL with trailing slash: {startup_url}/")
893
859
  print(f"Basic Auth: {username}:{password}")
894
860
 
895
861
  # Check if SIP routing is enabled and log additional info
896
862
  if self._routing_callbacks:
897
863
  print(f"Callback endpoints:")
898
864
  for path in self._routing_callbacks:
899
- print(f"{protocol}://{display_host}{self.route}{path}")
865
+ callback_url = self._build_full_url(endpoint=path.lstrip('/'), include_auth=False)
866
+ print(f" {callback_url}")
900
867
 
901
868
  # Start uvicorn with or without SSL
902
869
  if self.ssl_enabled and ssl_cert_path and ssl_key_path:
@@ -972,149 +939,146 @@ class SWMLService:
972
939
 
973
940
  return username, password
974
941
 
975
- # Keep the existing methods for backward compatibility
976
-
977
- def add_answer_verb(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> bool:
978
- """
979
- Add an answer verb to the current document
980
942
 
981
- Args:
982
- max_duration: Maximum duration in seconds
983
- codecs: Comma-separated list of codecs
984
-
985
- Returns:
986
- True if added successfully, False otherwise
943
+ def _get_base_url(self, include_auth: bool = True) -> str:
987
944
  """
988
- config = {}
989
- if max_duration is not None:
990
- config["max_duration"] = max_duration
991
- if codecs is not None:
992
- config["codecs"] = codecs
993
-
994
- return self.add_verb("answer", config)
995
-
996
- def add_hangup_verb(self, reason: Optional[str] = None) -> bool:
997
- """
998
- Add a hangup verb to the current document
945
+ Get the base URL for this service, using proxy info if available or falling back to configured values
946
+
947
+ This is the central method for URL building that handles both startup configuration
948
+ and per-request proxy detection.
999
949
 
1000
950
  Args:
1001
- reason: Hangup reason (hangup, busy, decline)
951
+ include_auth: Whether to include authentication credentials in the URL
1002
952
 
1003
953
  Returns:
1004
- True if added successfully, False otherwise
954
+ Base URL string (protocol://[auth@]host[:port])
1005
955
  """
1006
- config = {}
1007
- if reason is not None:
1008
- config["reason"] = reason
956
+ # Debug logging to understand state
957
+ self.log.debug("_get_base_url called",
958
+ has_proxy_url_base=hasattr(self, '_proxy_url_base'),
959
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
960
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False),
961
+ env_var=os.environ.get('SWML_PROXY_URL_BASE'),
962
+ include_auth=include_auth,
963
+ caller=inspect.stack()[1].function if len(inspect.stack()) > 1 else "unknown")
964
+
965
+ # Check if we have proxy information from a request
966
+ if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
967
+ base = self._proxy_url_base.rstrip('/')
968
+ self.log.debug("Using proxy URL base", proxy_url_base=base)
969
+
970
+ # Add auth credentials if requested
971
+ if include_auth:
972
+ username, password = self._basic_auth
973
+ url = urlparse(base)
974
+ base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
1009
975
 
1010
- return self.add_verb("hangup", config)
1011
-
1012
- def add_ai_verb(self,
1013
- prompt_text: Optional[str] = None,
1014
- prompt_pom: Optional[List[Dict[str, Any]]] = None,
1015
- post_prompt: Optional[str] = None,
1016
- post_prompt_url: Optional[str] = None,
1017
- swaig: Optional[Dict[str, Any]] = None,
1018
- **kwargs) -> bool:
1019
- """
1020
- Add an AI verb to the current document
976
+ return base
1021
977
 
1022
- Args:
1023
- prompt_text: Simple prompt text
1024
- prompt_pom: Prompt object model
1025
- post_prompt: Post-prompt text
1026
- post_prompt_url: Post-prompt URL
1027
- swaig: SWAIG configuration
1028
- **kwargs: Additional parameters
1029
-
1030
- Returns:
1031
- True if added successfully, False otherwise
1032
- """
1033
- config = {}
978
+ # No proxy, use configured values
979
+ # Determine protocol based on SSL settings
980
+ protocol = "https" if self.ssl_enabled else "http"
1034
981
 
1035
- # Handle prompt
1036
- if prompt_text is not None:
1037
- config["prompt"] = prompt_text
1038
- elif prompt_pom is not None:
1039
- config["prompt"] = prompt_pom
982
+ # Debug logging
983
+ self.log.debug("_get_base_url",
984
+ ssl_enabled=self.ssl_enabled,
985
+ domain=self.domain,
986
+ port=self.port,
987
+ protocol=protocol)
988
+
989
+ # Determine host part
990
+ if self.ssl_enabled and self.domain:
991
+ # Use domain for SSL
992
+ if protocol == "https" and self.port == 443:
993
+ host_part = self.domain # Don't include port for standard HTTPS
994
+ elif protocol == "http" and self.port == 80:
995
+ host_part = self.domain # Don't include port for standard HTTP
996
+ else:
997
+ host_part = f"{self.domain}:{self.port}"
998
+ self.log.debug("Using domain with port", domain=self.domain, port=self.port, host_part=host_part)
999
+ else:
1000
+ # Use configured host
1001
+ if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1002
+ host = "localhost"
1003
+ else:
1004
+ host = self.host
1040
1005
 
1041
- # Handle post prompt
1042
- if post_prompt is not None:
1043
- config["post_prompt"] = post_prompt
1006
+ # Include port unless it's the standard port for the protocol
1007
+ if (protocol == "https" and self.port == 443) or (protocol == "http" and self.port == 80):
1008
+ host_part = host
1009
+ else:
1010
+ host_part = f"{host}:{self.port}"
1044
1011
 
1045
- # Handle post prompt URL
1046
- if post_prompt_url is not None:
1047
- config["post_prompt_url"] = post_prompt_url
1048
-
1049
- # Handle SWAIG
1050
- if swaig is not None:
1051
- config["SWAIG"] = swaig
1052
-
1053
- # Handle additional parameters
1054
- for key, value in kwargs.items():
1055
- if value is not None:
1056
- config[key] = value
1057
-
1058
- return self.add_verb("ai", config)
1012
+ # Build base URL
1013
+ if include_auth:
1014
+ username, password = self._basic_auth
1015
+ base = f"{protocol}://{username}:{password}@{host_part}"
1016
+ else:
1017
+ base = f"{protocol}://{host_part}"
1059
1018
 
1060
- def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
1019
+ return base
1020
+
1021
+ def _build_full_url(self, endpoint: str = "", include_auth: bool = True, query_params: Optional[Dict[str, str]] = None) -> str:
1061
1022
  """
1062
- Helper method to build webhook URLs consistently
1023
+ Build the full URL for this service or a specific endpoint
1024
+
1025
+ This is the internal implementation used by both get_full_url (for AgentBase compatibility)
1026
+ and _build_webhook_url.
1063
1027
 
1064
1028
  Args:
1065
- endpoint: The endpoint path (e.g., "swaig", "post_prompt")
1029
+ endpoint: Optional endpoint path (e.g., "swaig", "post_prompt")
1030
+ include_auth: Whether to include authentication credentials in the URL
1066
1031
  query_params: Optional query parameters to append
1067
1032
 
1068
1033
  Returns:
1069
- Fully constructed webhook URL
1034
+ Full URL string
1070
1035
  """
1071
- # Base URL construction
1072
- if hasattr(self, '_proxy_url_base') and getattr(self, '_proxy_url_base', None):
1073
- # For proxy URLs
1074
- base = self._proxy_url_base.rstrip('/')
1075
-
1076
- # Always add auth credentials
1077
- username, password = self._basic_auth
1078
- url = urlparse(base)
1079
- base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
1036
+ # Get base URL using central method
1037
+ base = self._get_base_url(include_auth=include_auth)
1038
+
1039
+ # Build path
1040
+ if endpoint:
1041
+ # Ensure endpoint doesn't start with slash
1042
+ endpoint = endpoint.lstrip('/')
1043
+ # Add trailing slash to endpoint to prevent redirects
1044
+ if not endpoint.endswith('/'):
1045
+ endpoint = f"{endpoint}/"
1046
+ path = f"{self.route}/{endpoint}"
1080
1047
  else:
1081
- # Determine protocol based on SSL settings
1082
- protocol = "https" if getattr(self, 'ssl_enabled', False) else "http"
1083
-
1084
- # Use domain if available and SSL is enabled
1085
- if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
1086
- host_part = self.domain
1087
- else:
1088
- # For local URLs
1089
- if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1090
- host = "localhost"
1091
- else:
1092
- host = self.host
1093
-
1094
- host_part = f"{host}:{self.port}"
1095
-
1096
- # Always include auth credentials
1097
- username, password = self._basic_auth
1098
- base = f"{protocol}://{username}:{password}@{host_part}"
1048
+ # Just the route itself
1049
+ path = self.route if self.route != "/" else ""
1099
1050
 
1100
- # Ensure the endpoint has a trailing slash to prevent redirects
1101
- if endpoint and not endpoint.endswith('/'):
1102
- endpoint = f"{endpoint}/"
1103
-
1104
- # Simple path - use the route directly with the endpoint
1105
- path = f"{self.route}/{endpoint}"
1106
-
1107
1051
  # Construct full URL
1108
1052
  url = f"{base}{path}"
1109
1053
 
1110
- # Add query parameters if any (only if they have values)
1054
+ # Add query parameters if any
1111
1055
  if query_params:
1112
1056
  filtered_params = {k: v for k, v in query_params.items() if v}
1113
1057
  if filtered_params:
1114
1058
  params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
1115
1059
  url = f"{url}?{params}"
1060
+
1061
+ return url
1062
+
1063
+ def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
1064
+ """
1065
+ Helper method to build webhook URLs consistently
1066
+
1067
+ Args:
1068
+ endpoint: The endpoint path (e.g., "swaig", "post_prompt")
1069
+ query_params: Optional query parameters to append
1116
1070
 
1117
- return url
1071
+ Returns:
1072
+ Fully constructed webhook URL
1073
+ """
1074
+ self.log.debug("_build_webhook_url called",
1075
+ endpoint=endpoint,
1076
+ query_params=query_params,
1077
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
1078
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False))
1079
+
1080
+ # Use the central URL building method
1081
+ return self._build_full_url(endpoint=endpoint, include_auth=True, query_params=query_params)
1118
1082
 
1119
1083
  def _detect_proxy_from_request(self, request: Request) -> None:
1120
1084
  """
@@ -1124,6 +1088,10 @@ class SWMLService:
1124
1088
  Args:
1125
1089
  request: FastAPI Request object
1126
1090
  """
1091
+ # If SWML_PROXY_URL_BASE was already set (e.g., from environment), don't override it
1092
+ if self._proxy_url_base:
1093
+ return
1094
+
1127
1095
  # First check for standard X-Forwarded headers (used by most proxies including ngrok)
1128
1096
  forwarded_host = request.headers.get("X-Forwarded-Host")
1129
1097
  forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
@@ -1208,4 +1176,4 @@ class SWMLService:
1208
1176
  if proxy_url:
1209
1177
  self._proxy_url_base = proxy_url.rstrip('/')
1210
1178
  self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
1211
- self._proxy_detection_done = True
1179
+ self._proxy_detection_done = True
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ MCP-SWAIG Gateway Package
13
+
14
+ HTTP/HTTPS server that bridges MCP servers with SignalWire SWAIG functions.
15
+ """
16
+
17
+ from .gateway_service import MCPGateway, main
18
+ from .session_manager import Session, SessionManager
19
+ from .mcp_manager import MCPService, MCPClient, MCPManager
20
+
21
+ __all__ = [
22
+ 'MCPGateway',
23
+ 'main',
24
+ 'Session',
25
+ 'SessionManager',
26
+ 'MCPService',
27
+ 'MCPClient',
28
+ 'MCPManager',
29
+ ]