signalwire-agents 0.1.13__py3-none-any.whl → 1.0.7__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 (138) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +176 -23
  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/execution/__init__.py +10 -0
  13. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  14. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  15. signalwire_agents/cli/init_project.py +1225 -0
  16. signalwire_agents/cli/output/__init__.py +10 -0
  17. signalwire_agents/cli/output/output_formatter.py +255 -0
  18. signalwire_agents/cli/output/swml_dump.py +186 -0
  19. signalwire_agents/cli/simulation/__init__.py +10 -0
  20. signalwire_agents/cli/simulation/data_generation.py +374 -0
  21. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  22. signalwire_agents/cli/simulation/mock_env.py +282 -0
  23. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  24. signalwire_agents/cli/test_swaig.py +566 -2366
  25. signalwire_agents/cli/types.py +81 -0
  26. signalwire_agents/core/__init__.py +2 -2
  27. signalwire_agents/core/agent/__init__.py +12 -0
  28. signalwire_agents/core/agent/config/__init__.py +12 -0
  29. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  30. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  31. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  32. signalwire_agents/core/agent/prompt/manager.py +306 -0
  33. signalwire_agents/core/agent/routing/__init__.py +9 -0
  34. signalwire_agents/core/agent/security/__init__.py +9 -0
  35. signalwire_agents/core/agent/swml/__init__.py +9 -0
  36. signalwire_agents/core/agent/tools/__init__.py +15 -0
  37. signalwire_agents/core/agent/tools/decorator.py +97 -0
  38. signalwire_agents/core/agent/tools/registry.py +210 -0
  39. signalwire_agents/core/agent_base.py +825 -2916
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +418 -0
  43. signalwire_agents/core/data_map.py +3 -15
  44. signalwire_agents/core/function_result.py +116 -44
  45. signalwire_agents/core/logging_config.py +162 -18
  46. signalwire_agents/core/mixins/__init__.py +28 -0
  47. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  48. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  49. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  50. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  51. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  52. signalwire_agents/core/mixins/state_mixin.py +153 -0
  53. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  54. signalwire_agents/core/mixins/web_mixin.py +1134 -0
  55. signalwire_agents/core/security_config.py +333 -0
  56. signalwire_agents/core/skill_base.py +84 -1
  57. signalwire_agents/core/skill_manager.py +62 -20
  58. signalwire_agents/core/swaig_function.py +18 -5
  59. signalwire_agents/core/swml_builder.py +207 -11
  60. signalwire_agents/core/swml_handler.py +27 -21
  61. signalwire_agents/core/swml_renderer.py +123 -312
  62. signalwire_agents/core/swml_service.py +167 -200
  63. signalwire_agents/prefabs/concierge.py +0 -3
  64. signalwire_agents/prefabs/faq_bot.py +0 -3
  65. signalwire_agents/prefabs/info_gatherer.py +0 -3
  66. signalwire_agents/prefabs/receptionist.py +0 -3
  67. signalwire_agents/prefabs/survey.py +0 -3
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +7 -1
  70. signalwire_agents/search/document_processor.py +490 -31
  71. signalwire_agents/search/index_builder.py +307 -37
  72. signalwire_agents/search/migration.py +418 -0
  73. signalwire_agents/search/models.py +30 -0
  74. signalwire_agents/search/pgvector_backend.py +752 -0
  75. signalwire_agents/search/query_processor.py +162 -31
  76. signalwire_agents/search/search_engine.py +916 -35
  77. signalwire_agents/search/search_service.py +376 -53
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +10 -1
  80. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  81. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  82. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  83. signalwire_agents/skills/datasphere/README.md +210 -0
  84. signalwire_agents/skills/datasphere/skill.py +84 -3
  85. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  86. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  87. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  88. signalwire_agents/skills/datetime/README.md +132 -0
  89. signalwire_agents/skills/datetime/__init__.py +9 -0
  90. signalwire_agents/skills/datetime/skill.py +20 -7
  91. signalwire_agents/skills/joke/README.md +149 -0
  92. signalwire_agents/skills/joke/__init__.py +9 -0
  93. signalwire_agents/skills/joke/skill.py +21 -0
  94. signalwire_agents/skills/math/README.md +161 -0
  95. signalwire_agents/skills/math/__init__.py +9 -0
  96. signalwire_agents/skills/math/skill.py +18 -4
  97. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  98. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  99. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  100. signalwire_agents/skills/native_vector_search/README.md +210 -0
  101. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  102. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  103. signalwire_agents/skills/play_background_file/README.md +218 -0
  104. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  105. signalwire_agents/skills/play_background_file/skill.py +242 -0
  106. signalwire_agents/skills/registry.py +395 -40
  107. signalwire_agents/skills/spider/README.md +236 -0
  108. signalwire_agents/skills/spider/__init__.py +13 -0
  109. signalwire_agents/skills/spider/skill.py +598 -0
  110. signalwire_agents/skills/swml_transfer/README.md +395 -0
  111. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  112. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  113. signalwire_agents/skills/weather_api/README.md +178 -0
  114. signalwire_agents/skills/weather_api/__init__.py +12 -0
  115. signalwire_agents/skills/weather_api/skill.py +191 -0
  116. signalwire_agents/skills/web_search/README.md +163 -0
  117. signalwire_agents/skills/web_search/__init__.py +9 -0
  118. signalwire_agents/skills/web_search/skill.py +586 -112
  119. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  120. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  121. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  122. signalwire_agents/web/__init__.py +17 -0
  123. signalwire_agents/web/web_service.py +559 -0
  124. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  125. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  126. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  127. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/METADATA +344 -215
  128. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  129. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  130. signalwire_agents/core/state/file_state_manager.py +0 -219
  131. signalwire_agents/core/state/state_manager.py +0 -101
  132. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  133. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  134. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  135. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  136. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +0 -0
  137. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  138. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.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:
@@ -105,7 +66,8 @@ class SWMLService:
105
66
  host: str = "0.0.0.0",
106
67
  port: int = 3000,
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,21 +79,34 @@ 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
87
  self.port = port
125
- self.ssl_enabled = False
126
- self.domain = None
88
+
89
+ # Initialize logger for this instance FIRST before using it
90
+ self.log = logger.bind(service=name)
91
+
92
+ # Load unified security configuration with optional config file
93
+ self.security = SecurityConfig(config_file=config_file, service_name=name)
94
+ self.security.log_config("SWMLService")
95
+
96
+ # For backward compatibility, expose SSL settings as instance attributes
97
+ self.ssl_enabled = self.security.ssl_enabled
98
+ self.domain = self.security.domain
99
+ self.ssl_cert_path = self.security.ssl_cert_path
100
+ self.ssl_key_path = self.security.ssl_key_path
127
101
 
128
102
  # Initialize proxy detection attributes
129
103
  self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
104
+ self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
105
+ if self._proxy_url_base:
106
+ 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.",
107
+ proxy_url_base=self._proxy_url_base)
130
108
  self._proxy_detection_done = False
131
109
  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
110
  self.log.info("service_initializing", route=self.route, host=host, port=port)
136
111
 
137
112
  # Set basic auth credentials
@@ -139,18 +114,8 @@ class SWMLService:
139
114
  # Use provided credentials
140
115
  self._basic_auth = basic_auth
141
116
  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)
117
+ # Use unified security config for auth credentials
118
+ self._basic_auth = self.security.get_basic_auth()
154
119
 
155
120
  # Find the schema file if not provided
156
121
  if schema_path is None:
@@ -687,10 +652,8 @@ class SWMLService:
687
652
  Returns:
688
653
  Response with SWML document or error
689
654
  """
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
655
+ # Always detect proxy from current request - allows mixing direct and proxied access
656
+ self._detect_proxy_from_request(request)
694
657
 
695
658
  # Check auth
696
659
  if not self._check_basic_auth(request):
@@ -789,21 +752,21 @@ class SWMLService:
789
752
  """
790
753
  import uvicorn
791
754
 
792
- # Store SSL configuration
793
- self.ssl_enabled = ssl_enabled if ssl_enabled is not None else False
794
- self.domain = domain
755
+ # Store SSL configuration (override environment if explicitly provided)
756
+ if ssl_enabled is not None:
757
+ self.ssl_enabled = ssl_enabled
758
+ if domain is not None:
759
+ self.domain = domain
795
760
 
796
- # Set SSL paths
797
- ssl_cert_path = ssl_cert
798
- ssl_key_path = ssl_key
761
+ # Set SSL paths (use provided paths or fall back to environment)
762
+ ssl_cert_path = ssl_cert or getattr(self, 'ssl_cert_path', None)
763
+ ssl_key_path = ssl_key or getattr(self, 'ssl_key_path', None)
799
764
 
800
765
  # Validate SSL configuration if enabled
801
766
  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)
767
+ is_valid, error = self.security.validate_ssl_config()
768
+ if not is_valid:
769
+ self.log.warning("ssl_config_invalid", error=error)
807
770
  self.ssl_enabled = False
808
771
  elif not self.domain:
809
772
  self.log.warning("ssl_domain_not_specified")
@@ -818,8 +781,11 @@ class SWMLService:
818
781
  # This avoids the FastAPI error about prefixes ending with slashes
819
782
  normalized_route = "/" + self.route.strip("/")
820
783
 
821
- # Include router with the normalized prefix
822
- app.include_router(router, prefix=normalized_route)
784
+ # Include router with the normalized prefix (handle root route special case)
785
+ if normalized_route == "/":
786
+ app.include_router(router)
787
+ else:
788
+ app.include_router(router, prefix=normalized_route)
823
789
 
824
790
  # Add a catch-all route handler that will handle both /path and /path/ formats
825
791
  # This provides the same behavior without using a trailing slash in the prefix
@@ -876,27 +842,27 @@ class SWMLService:
876
842
  # Get the auth credentials
877
843
  username, password = self._basic_auth
878
844
 
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}"
845
+ # Get the proper URL using unified URL building
846
+ startup_url = self._build_full_url(include_auth=False)
882
847
 
883
848
  self.log.info("starting_server",
884
- url=f"{protocol}://{display_host}{self.route}",
849
+ url=startup_url,
885
850
  ssl_enabled=self.ssl_enabled,
886
851
  username=username,
887
852
  password_length=len(password))
888
853
 
889
854
  # Print user-friendly startup message (keep for UX)
890
855
  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}/")
856
+ print(f"URL: {startup_url}")
857
+ print(f"URL with trailing slash: {startup_url}/")
893
858
  print(f"Basic Auth: {username}:{password}")
894
859
 
895
860
  # Check if SIP routing is enabled and log additional info
896
861
  if self._routing_callbacks:
897
862
  print(f"Callback endpoints:")
898
863
  for path in self._routing_callbacks:
899
- print(f"{protocol}://{display_host}{self.route}{path}")
864
+ callback_url = self._build_full_url(endpoint=path.lstrip('/'), include_auth=False)
865
+ print(f" {callback_url}")
900
866
 
901
867
  # Start uvicorn with or without SSL
902
868
  if self.ssl_enabled and ssl_cert_path and ssl_key_path:
@@ -972,149 +938,146 @@ class SWMLService:
972
938
 
973
939
  return username, password
974
940
 
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
941
 
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
942
+ def _get_base_url(self, include_auth: bool = True) -> str:
987
943
  """
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
944
+ Get the base URL for this service, using proxy info if available or falling back to configured values
945
+
946
+ This is the central method for URL building that handles both startup configuration
947
+ and per-request proxy detection.
999
948
 
1000
949
  Args:
1001
- reason: Hangup reason (hangup, busy, decline)
950
+ include_auth: Whether to include authentication credentials in the URL
1002
951
 
1003
952
  Returns:
1004
- True if added successfully, False otherwise
953
+ Base URL string (protocol://[auth@]host[:port])
1005
954
  """
1006
- config = {}
1007
- if reason is not None:
1008
- config["reason"] = reason
955
+ # Debug logging to understand state
956
+ self.log.debug("_get_base_url called",
957
+ has_proxy_url_base=hasattr(self, '_proxy_url_base'),
958
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
959
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False),
960
+ env_var=os.environ.get('SWML_PROXY_URL_BASE'),
961
+ include_auth=include_auth,
962
+ caller=inspect.stack()[1].function if len(inspect.stack()) > 1 else "unknown")
963
+
964
+ # Check if we have proxy information from a request
965
+ if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
966
+ base = self._proxy_url_base.rstrip('/')
967
+ self.log.debug("Using proxy URL base", proxy_url_base=base)
968
+
969
+ # Add auth credentials if requested
970
+ if include_auth:
971
+ username, password = self._basic_auth
972
+ url = urlparse(base)
973
+ base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
1009
974
 
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
975
+ return base
1021
976
 
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 = {}
977
+ # No proxy, use configured values
978
+ # Determine protocol based on SSL settings
979
+ protocol = "https" if self.ssl_enabled else "http"
1034
980
 
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
981
+ # Debug logging
982
+ self.log.debug("_get_base_url",
983
+ ssl_enabled=self.ssl_enabled,
984
+ domain=self.domain,
985
+ port=self.port,
986
+ protocol=protocol)
987
+
988
+ # Determine host part
989
+ if self.ssl_enabled and self.domain:
990
+ # Use domain for SSL
991
+ if protocol == "https" and self.port == 443:
992
+ host_part = self.domain # Don't include port for standard HTTPS
993
+ elif protocol == "http" and self.port == 80:
994
+ host_part = self.domain # Don't include port for standard HTTP
995
+ else:
996
+ host_part = f"{self.domain}:{self.port}"
997
+ self.log.debug("Using domain with port", domain=self.domain, port=self.port, host_part=host_part)
998
+ else:
999
+ # Use configured host
1000
+ if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1001
+ host = "localhost"
1002
+ else:
1003
+ host = self.host
1040
1004
 
1041
- # Handle post prompt
1042
- if post_prompt is not None:
1043
- config["post_prompt"] = post_prompt
1005
+ # Include port unless it's the standard port for the protocol
1006
+ if (protocol == "https" and self.port == 443) or (protocol == "http" and self.port == 80):
1007
+ host_part = host
1008
+ else:
1009
+ host_part = f"{host}:{self.port}"
1044
1010
 
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)
1011
+ # Build base URL
1012
+ if include_auth:
1013
+ username, password = self._basic_auth
1014
+ base = f"{protocol}://{username}:{password}@{host_part}"
1015
+ else:
1016
+ base = f"{protocol}://{host_part}"
1059
1017
 
1060
- def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
1018
+ return base
1019
+
1020
+ def _build_full_url(self, endpoint: str = "", include_auth: bool = True, query_params: Optional[Dict[str, str]] = None) -> str:
1061
1021
  """
1062
- Helper method to build webhook URLs consistently
1022
+ Build the full URL for this service or a specific endpoint
1023
+
1024
+ This is the internal implementation used by both get_full_url (for AgentBase compatibility)
1025
+ and _build_webhook_url.
1063
1026
 
1064
1027
  Args:
1065
- endpoint: The endpoint path (e.g., "swaig", "post_prompt")
1028
+ endpoint: Optional endpoint path (e.g., "swaig", "post_prompt")
1029
+ include_auth: Whether to include authentication credentials in the URL
1066
1030
  query_params: Optional query parameters to append
1067
1031
 
1068
1032
  Returns:
1069
- Fully constructed webhook URL
1033
+ Full URL string
1070
1034
  """
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()
1035
+ # Get base URL using central method
1036
+ base = self._get_base_url(include_auth=include_auth)
1037
+
1038
+ # Build path
1039
+ if endpoint:
1040
+ # Ensure endpoint doesn't start with slash
1041
+ endpoint = endpoint.lstrip('/')
1042
+ # Add trailing slash to endpoint to prevent redirects
1043
+ if not endpoint.endswith('/'):
1044
+ endpoint = f"{endpoint}/"
1045
+ path = f"{self.route}/{endpoint}"
1080
1046
  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}"
1047
+ # Just the route itself
1048
+ path = self.route if self.route != "/" else ""
1099
1049
 
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
1050
  # Construct full URL
1108
1051
  url = f"{base}{path}"
1109
1052
 
1110
- # Add query parameters if any (only if they have values)
1053
+ # Add query parameters if any
1111
1054
  if query_params:
1112
1055
  filtered_params = {k: v for k, v in query_params.items() if v}
1113
1056
  if filtered_params:
1114
1057
  params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
1115
1058
  url = f"{url}?{params}"
1059
+
1060
+ return url
1061
+
1062
+ def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
1063
+ """
1064
+ Helper method to build webhook URLs consistently
1065
+
1066
+ Args:
1067
+ endpoint: The endpoint path (e.g., "swaig", "post_prompt")
1068
+ query_params: Optional query parameters to append
1116
1069
 
1117
- return url
1070
+ Returns:
1071
+ Fully constructed webhook URL
1072
+ """
1073
+ self.log.debug("_build_webhook_url called",
1074
+ endpoint=endpoint,
1075
+ query_params=query_params,
1076
+ proxy_url_base=getattr(self, '_proxy_url_base', None),
1077
+ proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False))
1078
+
1079
+ # Use the central URL building method
1080
+ return self._build_full_url(endpoint=endpoint, include_auth=True, query_params=query_params)
1118
1081
 
1119
1082
  def _detect_proxy_from_request(self, request: Request) -> None:
1120
1083
  """
@@ -1124,6 +1087,10 @@ class SWMLService:
1124
1087
  Args:
1125
1088
  request: FastAPI Request object
1126
1089
  """
1090
+ # If SWML_PROXY_URL_BASE was already set (e.g., from environment), don't override it
1091
+ if self._proxy_url_base:
1092
+ return
1093
+
1127
1094
  # First check for standard X-Forwarded headers (used by most proxies including ngrok)
1128
1095
  forwarded_host = request.headers.get("X-Forwarded-Host")
1129
1096
  forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
@@ -1208,4 +1175,4 @@ class SWMLService:
1208
1175
  if proxy_url:
1209
1176
  self._proxy_url_base = proxy_url.rstrip('/')
1210
1177
  self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
1211
- self._proxy_detection_done = True
1178
+ self._proxy_detection_done = True
@@ -52,7 +52,6 @@ class ConciergeAgent(AgentBase):
52
52
  welcome_message: Optional[str] = None,
53
53
  name: str = "concierge",
54
54
  route: str = "/concierge",
55
- enable_state_tracking: bool = True,
56
55
  **kwargs
57
56
  ):
58
57
  """
@@ -67,7 +66,6 @@ class ConciergeAgent(AgentBase):
67
66
  welcome_message: Optional custom welcome message
68
67
  name: Agent name for the route
69
68
  route: HTTP route for this agent
70
- enable_state_tracking: Whether to enable state tracking (default: True)
71
69
  **kwargs: Additional arguments for AgentBase
72
70
  """
73
71
  # Initialize the base agent
@@ -75,7 +73,6 @@ class ConciergeAgent(AgentBase):
75
73
  name=name,
76
74
  route=route,
77
75
  use_pom=True,
78
- enable_state_tracking=enable_state_tracking,
79
76
  **kwargs
80
77
  )
81
78
 
@@ -51,7 +51,6 @@ class FAQBotAgent(AgentBase):
51
51
  persona: Optional[str] = None,
52
52
  name: str = "faq_bot",
53
53
  route: str = "/faq",
54
- enable_state_tracking: bool = True, # Enable state tracking by default
55
54
  **kwargs
56
55
  ):
57
56
  """
@@ -66,7 +65,6 @@ class FAQBotAgent(AgentBase):
66
65
  persona: Optional custom personality description
67
66
  name: Agent name for the route
68
67
  route: HTTP route for this agent
69
- enable_state_tracking: Whether to enable state tracking (default: True)
70
68
  **kwargs: Additional arguments for AgentBase
71
69
  """
72
70
  # Initialize the base agent
@@ -74,7 +72,6 @@ class FAQBotAgent(AgentBase):
74
72
  name=name,
75
73
  route=route,
76
74
  use_pom=True,
77
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
78
75
  **kwargs
79
76
  )
80
77
 
@@ -45,7 +45,6 @@ class InfoGathererAgent(AgentBase):
45
45
  questions: Optional[List[Dict[str, str]]] = None,
46
46
  name: str = "info_gatherer",
47
47
  route: str = "/info_gatherer",
48
- enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
49
48
  **kwargs
50
49
  ):
51
50
  """
@@ -59,7 +58,6 @@ class InfoGathererAgent(AgentBase):
59
58
  - confirm: (Optional) If set to True, the agent will confirm the answer before submitting
60
59
  name: Agent name for the route
61
60
  route: HTTP route for this agent
62
- enable_state_tracking: Whether to enable state tracking (default: True)
63
61
  **kwargs: Additional arguments for AgentBase
64
62
  """
65
63
  # Initialize the base agent
@@ -67,7 +65,6 @@ class InfoGathererAgent(AgentBase):
67
65
  name=name,
68
66
  route=route,
69
67
  use_pom=True,
70
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
71
68
  **kwargs
72
69
  )
73
70
 
@@ -41,7 +41,6 @@ class ReceptionistAgent(AgentBase):
41
41
  route: str = "/receptionist",
42
42
  greeting: str = "Thank you for calling. How can I help you today?",
43
43
  voice: str = "rime.spore",
44
- enable_state_tracking: bool = True, # Enable state tracking by default
45
44
  **kwargs
46
45
  ):
47
46
  """
@@ -56,7 +55,6 @@ class ReceptionistAgent(AgentBase):
56
55
  route: HTTP route for this agent
57
56
  greeting: Initial greeting message
58
57
  voice: Voice ID to use
59
- enable_state_tracking: Whether to enable state tracking (default: True)
60
58
  **kwargs: Additional arguments for AgentBase
61
59
  """
62
60
  # Initialize the base agent
@@ -64,7 +62,6 @@ class ReceptionistAgent(AgentBase):
64
62
  name=name,
65
63
  route=route,
66
64
  use_pom=True,
67
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
68
65
  **kwargs
69
66
  )
70
67
 
@@ -62,7 +62,6 @@ class SurveyAgent(AgentBase):
62
62
  max_retries: int = 2,
63
63
  name: str = "survey",
64
64
  route: str = "/survey",
65
- enable_state_tracking: bool = True, # Enable state tracking by default
66
65
  **kwargs
67
66
  ):
68
67
  """
@@ -83,7 +82,6 @@ class SurveyAgent(AgentBase):
83
82
  max_retries: Maximum number of times to retry invalid answers
84
83
  name: Name for the agent (default: "survey")
85
84
  route: HTTP route for the agent (default: "/survey")
86
- enable_state_tracking: Whether to enable state tracking (default: True)
87
85
  **kwargs: Additional arguments for AgentBase
88
86
  """
89
87
  # Initialize the base agent
@@ -91,7 +89,6 @@ class SurveyAgent(AgentBase):
91
89
  name=name,
92
90
  route=route,
93
91
  use_pom=True,
94
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
95
92
  **kwargs
96
93
  )
97
94