signalwire-agents 0.1.20__py3-none-any.whl → 0.1.23__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 (37) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +50 -11
  3. signalwire_agents/core/__init__.py +2 -2
  4. signalwire_agents/core/agent/__init__.py +14 -0
  5. signalwire_agents/core/agent/config/__init__.py +14 -0
  6. signalwire_agents/core/agent/config/ephemeral.py +176 -0
  7. signalwire_agents/core/agent/deployment/__init__.py +0 -0
  8. signalwire_agents/core/agent/deployment/handlers/__init__.py +0 -0
  9. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  10. signalwire_agents/core/agent/prompt/manager.py +288 -0
  11. signalwire_agents/core/agent/routing/__init__.py +0 -0
  12. signalwire_agents/core/agent/security/__init__.py +0 -0
  13. signalwire_agents/core/agent/swml/__init__.py +0 -0
  14. signalwire_agents/core/agent/tools/__init__.py +15 -0
  15. signalwire_agents/core/agent/tools/decorator.py +95 -0
  16. signalwire_agents/core/agent/tools/registry.py +192 -0
  17. signalwire_agents/core/agent_base.py +131 -413
  18. signalwire_agents/core/data_map.py +3 -15
  19. signalwire_agents/core/skill_manager.py +0 -17
  20. signalwire_agents/core/swaig_function.py +0 -2
  21. signalwire_agents/core/swml_builder.py +207 -11
  22. signalwire_agents/core/swml_renderer.py +123 -312
  23. signalwire_agents/core/swml_service.py +25 -94
  24. signalwire_agents/search/index_builder.py +1 -1
  25. signalwire_agents/skills/api_ninjas_trivia/__init__.py +3 -0
  26. signalwire_agents/skills/api_ninjas_trivia/skill.py +192 -0
  27. signalwire_agents/skills/play_background_file/__init__.py +3 -0
  28. signalwire_agents/skills/play_background_file/skill.py +197 -0
  29. signalwire_agents/skills/weather_api/__init__.py +3 -0
  30. signalwire_agents/skills/weather_api/skill.py +154 -0
  31. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/METADATA +5 -8
  32. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/RECORD +37 -18
  33. {signalwire_agents-0.1.20.data → signalwire_agents-0.1.23.data}/data/schema.json +0 -0
  34. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/WHEEL +0 -0
  35. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/entry_points.txt +0 -0
  36. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/licenses/LICENSE +0 -0
  37. {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/top_level.txt +0 -0
@@ -58,171 +58,15 @@ from signalwire_agents.core.skill_manager import SkillManager
58
58
  from signalwire_agents.utils.schema_utils import SchemaUtils
59
59
  from signalwire_agents.core.logging_config import get_logger, get_execution_mode
60
60
 
61
+ # Import refactored components
62
+ from signalwire_agents.core.agent.config.ephemeral import EphemeralAgentConfig
63
+ from signalwire_agents.core.agent.prompt.manager import PromptManager
64
+ from signalwire_agents.core.agent.tools.registry import ToolRegistry
65
+ from signalwire_agents.core.agent.tools.decorator import ToolDecorator
66
+
61
67
  # Create a logger using centralized system
62
68
  logger = get_logger("agent_base")
63
69
 
64
- class EphemeralAgentConfig:
65
- """
66
- An ephemeral configurator object that mimics AgentBase's configuration interface.
67
-
68
- This allows dynamic configuration callbacks to use the same familiar methods
69
- they would use during agent initialization, but for per-request configuration.
70
- """
71
-
72
- def __init__(self):
73
- # Initialize all configuration containers
74
- self._hints = []
75
- self._languages = []
76
- self._pronounce = []
77
- self._params = {}
78
- self._global_data = {}
79
- self._prompt_sections = []
80
- self._raw_prompt = None
81
- self._post_prompt = None
82
- self._function_includes = []
83
- self._native_functions = []
84
-
85
- # Mirror all the AgentBase configuration methods
86
-
87
- def add_hint(self, hint: str) -> 'EphemeralAgentConfig':
88
- """Add a simple string hint"""
89
- if isinstance(hint, str) and hint:
90
- self._hints.append(hint)
91
- return self
92
-
93
- def add_hints(self, hints: List[str]) -> 'EphemeralAgentConfig':
94
- """Add multiple string hints"""
95
- if hints and isinstance(hints, list):
96
- for hint in hints:
97
- if isinstance(hint, str) and hint:
98
- self._hints.append(hint)
99
- return self
100
-
101
- def add_language(self, name: str, code: str, voice: str, **kwargs) -> 'EphemeralAgentConfig':
102
- """Add a language configuration"""
103
- language = {
104
- "name": name,
105
- "code": code,
106
- "voice": voice
107
- }
108
-
109
- # Handle additional parameters
110
- for key, value in kwargs.items():
111
- if key in ["engine", "model", "speech_fillers", "function_fillers", "fillers"]:
112
- language[key] = value
113
-
114
- self._languages.append(language)
115
- return self
116
-
117
- def add_pronunciation(self, replace: str, with_text: str, ignore_case: bool = False) -> 'EphemeralAgentConfig':
118
- """Add a pronunciation rule"""
119
- if replace and with_text:
120
- rule = {"replace": replace, "with": with_text}
121
- if ignore_case:
122
- rule["ignore_case"] = True
123
- self._pronounce.append(rule)
124
- return self
125
-
126
- def set_param(self, key: str, value: Any) -> 'EphemeralAgentConfig':
127
- """Set a single AI parameter"""
128
- if key:
129
- self._params[key] = value
130
- return self
131
-
132
- def set_params(self, params: Dict[str, Any]) -> 'EphemeralAgentConfig':
133
- """Set multiple AI parameters"""
134
- if params and isinstance(params, dict):
135
- self._params.update(params)
136
- return self
137
-
138
- def set_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
139
- """Set global data"""
140
- if data and isinstance(data, dict):
141
- self._global_data = data
142
- return self
143
-
144
- def update_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
145
- """Update global data"""
146
- if data and isinstance(data, dict):
147
- self._global_data.update(data)
148
- return self
149
-
150
- def set_prompt_text(self, text: str) -> 'EphemeralAgentConfig':
151
- """Set raw prompt text"""
152
- self._raw_prompt = text
153
- return self
154
-
155
- def set_post_prompt(self, text: str) -> 'EphemeralAgentConfig':
156
- """Set post-prompt text"""
157
- self._post_prompt = text
158
- return self
159
-
160
- def prompt_add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None, **kwargs) -> 'EphemeralAgentConfig':
161
- """Add a prompt section"""
162
- section = {
163
- "title": title,
164
- "body": body
165
- }
166
- if bullets:
167
- section["bullets"] = bullets
168
-
169
- # Handle additional parameters
170
- for key, value in kwargs.items():
171
- if key in ["numbered", "numbered_bullets", "subsections"]:
172
- section[key] = value
173
-
174
- self._prompt_sections.append(section)
175
- return self
176
-
177
- def set_native_functions(self, function_names: List[str]) -> 'EphemeralAgentConfig':
178
- """Set native functions"""
179
- if function_names and isinstance(function_names, list):
180
- self._native_functions = [name for name in function_names if isinstance(name, str)]
181
- return self
182
-
183
- def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'EphemeralAgentConfig':
184
- """Add a function include"""
185
- if url and functions and isinstance(functions, list):
186
- include = {"url": url, "functions": functions}
187
- if meta_data and isinstance(meta_data, dict):
188
- include["meta_data"] = meta_data
189
- self._function_includes.append(include)
190
- return self
191
-
192
- def extract_config(self) -> Dict[str, Any]:
193
- """
194
- Extract the configuration as a dictionary for applying to the real agent.
195
-
196
- Returns:
197
- Dictionary containing all the configuration changes
198
- """
199
- config = {}
200
-
201
- if self._hints:
202
- config["hints"] = self._hints
203
- if self._languages:
204
- config["languages"] = self._languages
205
- if self._pronounce:
206
- config["pronounce"] = self._pronounce
207
- if self._params:
208
- config["params"] = self._params
209
- if self._global_data:
210
- config["global_data"] = self._global_data
211
- if self._function_includes:
212
- config["function_includes"] = self._function_includes
213
- if self._native_functions:
214
- config["native_functions"] = self._native_functions
215
-
216
- # Handle prompt sections - these should be applied to the agent's POM, not as raw config
217
- # The calling code should use these to build the prompt properly
218
- if self._prompt_sections:
219
- config["_ephemeral_prompt_sections"] = self._prompt_sections
220
- if self._raw_prompt:
221
- config["_ephemeral_raw_prompt"] = self._raw_prompt
222
- if self._post_prompt:
223
- config["_ephemeral_post_prompt"] = self._post_prompt
224
-
225
- return config
226
70
 
227
71
  class AgentBase(SWMLService):
228
72
  """
@@ -328,8 +172,6 @@ class AgentBase(SWMLService):
328
172
 
329
173
  # Initialize prompt handling
330
174
  self._use_pom = use_pom
331
- self._raw_prompt = None
332
- self._post_prompt = None
333
175
 
334
176
  # Initialize POM if needed
335
177
  if self._use_pom:
@@ -345,7 +187,6 @@ class AgentBase(SWMLService):
345
187
  self.pom = None
346
188
 
347
189
  # Initialize tool registry (separate from SWMLService verb registry)
348
- self._swaig_functions = {}
349
190
 
350
191
  # Initialize session manager
351
192
  self._session_manager = SessionManager(token_expiry_secs=token_expiry_secs)
@@ -364,6 +205,10 @@ class AgentBase(SWMLService):
364
205
  self._record_format = record_format
365
206
  self._record_stereo = record_stereo
366
207
 
208
+ # Initialize refactored managers early
209
+ self._prompt_manager = PromptManager(self)
210
+ self._tool_registry = ToolRegistry(self)
211
+
367
212
  # Process declarative PROMPT_SECTIONS if defined in subclass
368
213
  self._process_prompt_sections()
369
214
 
@@ -371,7 +216,7 @@ class AgentBase(SWMLService):
371
216
  self._state_manager = state_manager or FileStateManager()
372
217
 
373
218
  # Process class-decorated tools (using @AgentBase.tool)
374
- self._register_class_decorated_tools()
219
+ self._tool_registry.register_class_decorated_tools()
375
220
 
376
221
  # Add native_functions parameter
377
222
  self.native_functions = native_functions or []
@@ -504,25 +349,34 @@ class AgentBase(SWMLService):
504
349
  # Prompt Building Methods
505
350
  # ----------------------------------------------------------------------
506
351
 
507
- def define_contexts(self) -> 'ContextBuilder':
352
+ def define_contexts(self, contexts=None) -> Optional['ContextBuilder']:
508
353
  """
509
354
  Define contexts and steps for this agent (alternative to POM/prompt)
510
355
 
356
+ Args:
357
+ contexts: Optional context configuration (dict or ContextBuilder)
358
+
511
359
  Returns:
512
- ContextBuilder for method chaining
360
+ ContextBuilder for method chaining if no contexts provided
513
361
 
514
362
  Note:
515
363
  Contexts can coexist with traditional prompts. The restriction is only
516
364
  that you can't mix POM sections with raw text in the main prompt.
517
365
  """
518
- # Import here to avoid circular imports
519
- from signalwire_agents.core.contexts import ContextBuilder
520
-
521
- if self._contexts_builder is None:
522
- self._contexts_builder = ContextBuilder(self)
523
- self._contexts_defined = True
524
-
525
- return self._contexts_builder
366
+ if contexts is not None:
367
+ # New behavior - set contexts
368
+ self._prompt_manager.define_contexts(contexts)
369
+ return self
370
+ else:
371
+ # Legacy behavior - return ContextBuilder
372
+ # Import here to avoid circular imports
373
+ from signalwire_agents.core.contexts import ContextBuilder
374
+
375
+ if self._contexts_builder is None:
376
+ self._contexts_builder = ContextBuilder(self)
377
+ self._contexts_defined = True
378
+
379
+ return self._contexts_builder
526
380
 
527
381
  def _validate_prompt_mode_exclusivity(self):
528
382
  """
@@ -530,12 +384,8 @@ class AgentBase(SWMLService):
530
384
 
531
385
  Note: This does NOT prevent contexts from being used alongside traditional prompts
532
386
  """
533
- # Only check for mixing POM sections with raw text in the main prompt
534
- if self._raw_prompt and (self.pom and hasattr(self.pom, 'sections') and self.pom.sections):
535
- raise ValueError(
536
- "Cannot mix raw text prompt with POM sections in the main prompt. "
537
- "Use either set_prompt_text() OR prompt_add_section() methods, not both."
538
- )
387
+ # Delegate to prompt manager
388
+ self._prompt_manager._validate_prompt_mode_exclusivity()
539
389
 
540
390
  def set_prompt_text(self, text: str) -> 'AgentBase':
541
391
  """
@@ -547,8 +397,7 @@ class AgentBase(SWMLService):
547
397
  Returns:
548
398
  Self for method chaining
549
399
  """
550
- self._validate_prompt_mode_exclusivity()
551
- self._raw_prompt = text
400
+ self._prompt_manager.set_prompt_text(text)
552
401
  return self
553
402
 
554
403
  def set_post_prompt(self, text: str) -> 'AgentBase':
@@ -561,7 +410,7 @@ class AgentBase(SWMLService):
561
410
  Returns:
562
411
  Self for method chaining
563
412
  """
564
- self._post_prompt = text
413
+ self._prompt_manager.set_post_prompt(text)
565
414
  return self
566
415
 
567
416
  def set_prompt_pom(self, pom: List[Dict[str, Any]]) -> 'AgentBase':
@@ -574,10 +423,7 @@ class AgentBase(SWMLService):
574
423
  Returns:
575
424
  Self for method chaining
576
425
  """
577
- if self._use_pom:
578
- self.pom = pom
579
- else:
580
- raise ValueError("use_pom must be True to use set_prompt_pom")
426
+ self._prompt_manager.set_prompt_pom(pom)
581
427
  return self
582
428
 
583
429
  def prompt_add_section(
@@ -603,38 +449,14 @@ class AgentBase(SWMLService):
603
449
  Returns:
604
450
  Self for method chaining
605
451
  """
606
- self._validate_prompt_mode_exclusivity()
607
- if self._use_pom and self.pom:
608
- # Create parameters for add_section based on what's supported
609
- kwargs = {}
610
-
611
- # Start with basic parameters
612
- kwargs['title'] = title
613
- kwargs['body'] = body
614
- if bullets:
615
- kwargs['bullets'] = bullets
616
-
617
- # Add optional parameters if they look supported
618
- if hasattr(self.pom, 'add_section'):
619
- sig = inspect.signature(self.pom.add_section)
620
- if 'numbered' in sig.parameters:
621
- kwargs['numbered'] = numbered
622
- if 'numberedBullets' in sig.parameters:
623
- kwargs['numberedBullets'] = numbered_bullets
624
-
625
- # Create the section
626
- section = self.pom.add_section(**kwargs)
627
-
628
- # Now add subsections if provided, by calling add_subsection on the section
629
- if subsections:
630
- for subsection in subsections:
631
- if 'title' in subsection:
632
- section.add_subsection(
633
- title=subsection.get('title'),
634
- body=subsection.get('body', ''),
635
- bullets=subsection.get('bullets', [])
636
- )
637
-
452
+ self._prompt_manager.prompt_add_section(
453
+ title=title,
454
+ body=body,
455
+ bullets=bullets,
456
+ numbered=numbered,
457
+ numbered_bullets=numbered_bullets,
458
+ subsections=subsections
459
+ )
638
460
  return self
639
461
 
640
462
  def prompt_add_to_section(
@@ -656,13 +478,12 @@ class AgentBase(SWMLService):
656
478
  Returns:
657
479
  Self for method chaining
658
480
  """
659
- if self._use_pom and self.pom:
660
- self.pom.add_to_section(
661
- title=title,
662
- body=body,
663
- bullet=bullet,
664
- bullets=bullets
665
- )
481
+ self._prompt_manager.prompt_add_to_section(
482
+ title=title,
483
+ body=body,
484
+ bullet=bullet,
485
+ bullets=bullets
486
+ )
666
487
  return self
667
488
 
668
489
  def prompt_add_subsection(
@@ -684,30 +505,26 @@ class AgentBase(SWMLService):
684
505
  Returns:
685
506
  Self for method chaining
686
507
  """
687
- if self._use_pom and self.pom:
688
- # First find or create the parent section
689
- parent_section = None
690
-
691
- # Try to find the parent section by title
692
- if hasattr(self.pom, 'sections'):
693
- for section in self.pom.sections:
694
- if hasattr(section, 'title') and section.title == parent_title:
695
- parent_section = section
696
- break
697
-
698
- # If parent section not found, create it
699
- if not parent_section:
700
- parent_section = self.pom.add_section(title=parent_title)
701
-
702
- # Now call add_subsection on the parent section object, not on POM
703
- parent_section.add_subsection(
704
- title=title,
705
- body=body,
706
- bullets=bullets or []
707
- )
708
-
508
+ self._prompt_manager.prompt_add_subsection(
509
+ parent_title=parent_title,
510
+ title=title,
511
+ body=body,
512
+ bullets=bullets
513
+ )
709
514
  return self
710
515
 
516
+ def prompt_has_section(self, title: str) -> bool:
517
+ """
518
+ Check if a section exists in the prompt
519
+
520
+ Args:
521
+ title: Section title to check
522
+
523
+ Returns:
524
+ True if section exists, False otherwise
525
+ """
526
+ return self._prompt_manager.prompt_has_section(title)
527
+
711
528
  # ----------------------------------------------------------------------
712
529
  # Tool/Function Management
713
530
  # ----------------------------------------------------------------------
@@ -739,10 +556,7 @@ class AgentBase(SWMLService):
739
556
  Returns:
740
557
  Self for method chaining
741
558
  """
742
- if name in self._swaig_functions:
743
- raise ValueError(f"Tool with name '{name}' already exists")
744
-
745
- self._swaig_functions[name] = SWAIGFunction(
559
+ self._tool_registry.define_tool(
746
560
  name=name,
747
561
  description=description,
748
562
  parameters=parameters,
@@ -764,16 +578,7 @@ class AgentBase(SWMLService):
764
578
  Returns:
765
579
  Self for method chaining
766
580
  """
767
- function_name = function_dict.get('function')
768
- if not function_name:
769
- raise ValueError("Function dictionary must contain 'function' field with the function name")
770
-
771
- if function_name in self._swaig_functions:
772
- raise ValueError(f"Tool with name '{function_name}' already exists")
773
-
774
- # Store the raw function dictionary for data_map tools
775
- # These don't have handlers since they execute on SignalWire's server
776
- self._swaig_functions[function_name] = function_dict
581
+ self._tool_registry.register_swaig_function(function_dict)
777
582
  return self
778
583
 
779
584
  def _tool_decorator(self, name=None, **kwargs):
@@ -786,71 +591,8 @@ class AgentBase(SWMLService):
786
591
  def example_function(self, param1):
787
592
  # ...
788
593
  """
789
- def decorator(func):
790
- nonlocal name
791
- if name is None:
792
- name = func.__name__
793
-
794
- parameters = kwargs.pop("parameters", {})
795
- description = kwargs.pop("description", func.__doc__ or f"Function {name}")
796
- secure = kwargs.pop("secure", True)
797
- fillers = kwargs.pop("fillers", None)
798
- webhook_url = kwargs.pop("webhook_url", None)
799
-
800
- self.define_tool(
801
- name=name,
802
- description=description,
803
- parameters=parameters,
804
- handler=func,
805
- secure=secure,
806
- fillers=fillers,
807
- webhook_url=webhook_url,
808
- **kwargs # Pass through any additional swaig_fields
809
- )
810
- return func
811
- return decorator
594
+ return ToolDecorator.create_instance_decorator(self._tool_registry)(name, **kwargs)
812
595
 
813
- def _register_class_decorated_tools(self):
814
- """
815
- Register tools defined with @AgentBase.tool class decorator
816
-
817
- This method scans the class for methods decorated with @AgentBase.tool
818
- and registers them automatically.
819
- """
820
- # Get the class of this instance
821
- cls = self.__class__
822
-
823
- # Loop through all attributes in the class
824
- for name in dir(cls):
825
- # Get the attribute
826
- attr = getattr(cls, name)
827
-
828
- # Check if it's a method decorated with @AgentBase.tool
829
- if inspect.ismethod(attr) or inspect.isfunction(attr):
830
- if hasattr(attr, "_is_tool") and getattr(attr, "_is_tool", False):
831
- # Extract tool information
832
- tool_name = getattr(attr, "_tool_name", name)
833
- tool_params = getattr(attr, "_tool_params", {})
834
-
835
- # Extract known parameters and pass through the rest as swaig_fields
836
- tool_params_copy = tool_params.copy()
837
- description = tool_params_copy.pop("description", attr.__doc__ or f"Function {tool_name}")
838
- parameters = tool_params_copy.pop("parameters", {})
839
- secure = tool_params_copy.pop("secure", True)
840
- fillers = tool_params_copy.pop("fillers", None)
841
- webhook_url = tool_params_copy.pop("webhook_url", None)
842
-
843
- # Register the tool with any remaining params as swaig_fields
844
- self.define_tool(
845
- name=tool_name,
846
- description=description,
847
- parameters=parameters,
848
- handler=attr.__get__(self, cls), # Bind the method to this instance
849
- secure=secure,
850
- fillers=fillers,
851
- webhook_url=webhook_url,
852
- **tool_params_copy # Pass through any additional swaig_fields
853
- )
854
596
 
855
597
  @classmethod
856
598
  def tool(cls, name=None, **kwargs):
@@ -863,12 +605,7 @@ class AgentBase(SWMLService):
863
605
  def example_function(self, param1):
864
606
  # ...
865
607
  """
866
- def decorator(func):
867
- setattr(func, "_is_tool", True)
868
- setattr(func, "_tool_name", name or func.__name__)
869
- setattr(func, "_tool_params", kwargs)
870
- return func
871
- return decorator
608
+ return ToolDecorator.create_class_decorator()(name, **kwargs)
872
609
 
873
610
  # ----------------------------------------------------------------------
874
611
  # Override Points for Subclasses
@@ -911,7 +648,7 @@ class AgentBase(SWMLService):
911
648
  "status": "healthy",
912
649
  "agent": self.get_name(),
913
650
  "route": self.route,
914
- "functions": len(self._swaig_functions)
651
+ "functions": len(self._tool_registry._swaig_functions)
915
652
  }
916
653
 
917
654
  @app.get("/ready")
@@ -922,7 +659,7 @@ class AgentBase(SWMLService):
922
659
  "status": "ready",
923
660
  "agent": self.get_name(),
924
661
  "route": self.route,
925
- "functions": len(self._swaig_functions)
662
+ "functions": len(self._tool_registry._swaig_functions)
926
663
  }
927
664
 
928
665
  # Add CORS middleware if needed
@@ -980,6 +717,11 @@ class AgentBase(SWMLService):
980
717
  Returns:
981
718
  Either a string prompt or a POM object as list of dicts
982
719
  """
720
+ # First check if prompt manager has a prompt
721
+ prompt_result = self._prompt_manager.get_prompt()
722
+ if prompt_result is not None:
723
+ return prompt_result
724
+
983
725
  # If using POM, return the POM structure
984
726
  if self._use_pom and self.pom:
985
727
  try:
@@ -1012,8 +754,8 @@ class AgentBase(SWMLService):
1012
754
  self.log.error("pom_rendering_failed", error=str(e))
1013
755
  # Fall back to raw text if POM fails
1014
756
 
1015
- # Return raw text (either explicitly set or default)
1016
- return self._raw_prompt or f"You are {self.name}, a helpful AI assistant."
757
+ # Return default text
758
+ return f"You are {self.name}, a helpful AI assistant."
1017
759
 
1018
760
  def get_post_prompt(self) -> Optional[str]:
1019
761
  """
@@ -1022,7 +764,7 @@ class AgentBase(SWMLService):
1022
764
  Returns:
1023
765
  Post-prompt text or None if not set
1024
766
  """
1025
- return self._post_prompt
767
+ return self._prompt_manager.get_post_prompt()
1026
768
 
1027
769
  def define_tools(self) -> List[SWAIGFunction]:
1028
770
  """
@@ -1034,7 +776,7 @@ class AgentBase(SWMLService):
1034
776
  This method can be overridden by subclasses.
1035
777
  """
1036
778
  tools = []
1037
- for func in self._swaig_functions.values():
779
+ for func in self._tool_registry._swaig_functions.values():
1038
780
  if isinstance(func, dict):
1039
781
  # Raw dictionary from register_swaig_function (e.g., DataMap)
1040
782
  tools.append(func)
@@ -1067,12 +809,12 @@ class AgentBase(SWMLService):
1067
809
  Function result
1068
810
  """
1069
811
  # Check if the function is registered
1070
- if name not in self._swaig_functions:
812
+ if name not in self._tool_registry._swaig_functions:
1071
813
  # If the function is not found, return an error
1072
814
  return {"response": f"Function '{name}' not found"}
1073
815
 
1074
816
  # Get the function
1075
- func = self._swaig_functions[name]
817
+ func = self._tool_registry._swaig_functions[name]
1076
818
 
1077
819
  # Check if this is a data_map function (raw dictionary)
1078
820
  if isinstance(func, dict):
@@ -1148,12 +890,12 @@ class AgentBase(SWMLService):
1148
890
  """
1149
891
  try:
1150
892
  # Skip validation for non-secure tools
1151
- if function_name not in self._swaig_functions:
893
+ if function_name not in self._tool_registry._swaig_functions:
1152
894
  self.log.warning("unknown_function", function=function_name)
1153
895
  return False
1154
896
 
1155
897
  # Get the function and check if it's secure
1156
- func = self._swaig_functions[function_name]
898
+ func = self._tool_registry._swaig_functions[function_name]
1157
899
  is_secure = True # Default to secure
1158
900
 
1159
901
  if isinstance(func, dict):
@@ -1330,9 +1072,23 @@ class AgentBase(SWMLService):
1330
1072
  # Fallback for local testing
1331
1073
  base_url = f"https://localhost:7071/api/{function_name}"
1332
1074
  else:
1333
- # Server mode
1334
- protocol = 'https' if self.ssl_cert and self.ssl_key else 'http'
1335
- base_url = f"{protocol}://{self.host}:{self.port}"
1075
+ # Server mode - check for proxy URL first
1076
+ if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
1077
+ # Use proxy URL when available (from reverse proxy detection)
1078
+ base_url = self._proxy_url_base.rstrip('/')
1079
+ else:
1080
+ # Fallback to local URL construction
1081
+ protocol = 'https' if getattr(self, 'ssl_enabled', False) else 'http'
1082
+
1083
+ # Determine host part - include port unless it's the standard port for the protocol
1084
+ if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
1085
+ # Use domain, but include port if it's not the standard HTTPS port (443)
1086
+ host_part = f"{self.domain}:{self.port}" if self.port != 443 else self.domain
1087
+ else:
1088
+ # Use host:port for HTTP or when no domain is specified
1089
+ host_part = f"{self.host}:{self.port}"
1090
+
1091
+ base_url = f"{protocol}://{host_part}"
1336
1092
 
1337
1093
  # Add route if not already included (for server mode)
1338
1094
  if mode == 'server' and self.route and not base_url.endswith(self.route):
@@ -1413,15 +1169,25 @@ class AgentBase(SWMLService):
1413
1169
  url = urlparse(base)
1414
1170
  base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
1415
1171
  else:
1416
- # For local URLs
1417
- if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1418
- host = "localhost"
1172
+ # Determine protocol based on SSL settings
1173
+ protocol = "https" if getattr(self, 'ssl_enabled', False) else "http"
1174
+
1175
+ # Determine host part - include port unless it's the standard port for the protocol
1176
+ if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
1177
+ # Use domain, but include port if it's not the standard HTTPS port (443)
1178
+ host_part = f"{self.domain}:{self.port}" if self.port != 443 else self.domain
1419
1179
  else:
1420
- host = self.host
1180
+ # For local URLs
1181
+ if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
1182
+ host = "localhost"
1183
+ else:
1184
+ host = self.host
1185
+
1186
+ host_part = f"{host}:{self.port}"
1421
1187
 
1422
1188
  # Always include auth credentials
1423
1189
  username, password = self._basic_auth
1424
- base = f"http://{username}:{password}@{host}:{self.port}"
1190
+ base = f"{protocol}://{username}:{password}@{host_part}"
1425
1191
 
1426
1192
  # Ensure the endpoint has a trailing slash to prevent redirects
1427
1193
  if endpoint in ["swaig", "post_prompt"]:
@@ -1483,7 +1249,7 @@ class AgentBase(SWMLService):
1483
1249
  swaig_obj = {}
1484
1250
 
1485
1251
  # Add defaults if we have functions
1486
- if self._swaig_functions:
1252
+ if self._tool_registry._swaig_functions:
1487
1253
  swaig_obj["defaults"] = {
1488
1254
  "web_hook_url": default_webhook_url
1489
1255
  }
@@ -1504,7 +1270,7 @@ class AgentBase(SWMLService):
1504
1270
  functions = []
1505
1271
 
1506
1272
  # Add each function to the functions array
1507
- for name, func in self._swaig_functions.items():
1273
+ for name, func in self._tool_registry._swaig_functions.items():
1508
1274
  if isinstance(func, dict):
1509
1275
  # For raw dictionaries (DataMap functions), use the entire dictionary as-is
1510
1276
  # This preserves data_map and any other special fields
@@ -1570,7 +1336,7 @@ class AgentBase(SWMLService):
1570
1336
  post_prompt_url = self._post_prompt_url_override
1571
1337
 
1572
1338
  # Add answer verb with auto-answer enabled
1573
- self.add_answer_verb()
1339
+ self.add_verb("answer", {})
1574
1340
 
1575
1341
  # Use the AI verb handler to build and validate the AI verb config
1576
1342
  ai_config = {}
@@ -1690,7 +1456,7 @@ class AgentBase(SWMLService):
1690
1456
 
1691
1457
  # Clear and rebuild the document with the modified AI config
1692
1458
  self.reset_document()
1693
- self.add_answer_verb()
1459
+ self.add_verb("answer", {})
1694
1460
  self.add_verb("ai", ai_config)
1695
1461
 
1696
1462
  # Return the rendered document as a string
@@ -1761,7 +1527,7 @@ class AgentBase(SWMLService):
1761
1527
  "status": "healthy",
1762
1528
  "agent": self.get_name(),
1763
1529
  "route": self.route,
1764
- "functions": len(self._swaig_functions)
1530
+ "functions": len(self._tool_registry._swaig_functions)
1765
1531
  }
1766
1532
 
1767
1533
  @app.get("/ready")
@@ -1770,7 +1536,7 @@ class AgentBase(SWMLService):
1770
1536
  """Readiness check endpoint for Kubernetes readiness probe"""
1771
1537
  # Check if agent is properly initialized
1772
1538
  ready = (
1773
- hasattr(self, '_swaig_functions') and
1539
+ hasattr(self, '_tool_registry') and
1774
1540
  hasattr(self, 'schema_utils') and
1775
1541
  self.schema_utils is not None
1776
1542
  )
@@ -1883,7 +1649,7 @@ class AgentBase(SWMLService):
1883
1649
  mode = force_mode or get_execution_mode()
1884
1650
 
1885
1651
  try:
1886
- if mode in ['cgi', 'cloud_function', 'azure_function']:
1652
+ if mode in ['cgi', 'azure_function']:
1887
1653
  response = self.handle_serverless_request(event, context, mode)
1888
1654
  print(response)
1889
1655
  return response
@@ -1996,31 +1762,6 @@ class AgentBase(SWMLService):
1996
1762
  "body": json.dumps({"error": "Unauthorized"})
1997
1763
  }
1998
1764
 
1999
- def _check_cloud_function_auth(self, request) -> bool:
2000
- """
2001
- Check basic auth in Cloud Function mode
2002
-
2003
- Args:
2004
- request: Cloud Function request object
2005
-
2006
- Returns:
2007
- True if auth is valid, False otherwise
2008
- """
2009
- # This would need to be implemented based on the specific
2010
- # cloud function framework being used (Flask, etc.)
2011
- # For now, return True to maintain existing behavior
2012
- return True
2013
-
2014
- def _send_cloud_function_auth_challenge(self):
2015
- """
2016
- Send authentication challenge in Cloud Function mode
2017
-
2018
- Returns:
2019
- Cloud Function response with 401 status
2020
- """
2021
- # This would need to be implemented based on the specific
2022
- # cloud function framework being used
2023
- return {"error": "Unauthorized", "status": 401}
2024
1765
 
2025
1766
  def handle_serverless_request(self, event=None, context=None, mode=None):
2026
1767
  """
@@ -2150,13 +1891,6 @@ class AgentBase(SWMLService):
2150
1891
 
2151
1892
  return self._handle_azure_function_request(event)
2152
1893
 
2153
- elif mode in ['cloud_function']:
2154
- # Legacy cloud function mode - deprecated
2155
- # Check authentication in Cloud Function mode
2156
- if not self._check_cloud_function_auth(event):
2157
- return self._send_cloud_function_auth_challenge()
2158
-
2159
- return self._handle_cloud_function_request(event)
2160
1894
 
2161
1895
  except Exception as e:
2162
1896
  import logging
@@ -2170,19 +1904,6 @@ class AgentBase(SWMLService):
2170
1904
  else:
2171
1905
  raise
2172
1906
 
2173
- def _handle_cloud_function_request(self, request):
2174
- """
2175
- Handle Cloud Function specific requests
2176
-
2177
- Args:
2178
- request: Cloud Function request object
2179
-
2180
- Returns:
2181
- Cloud Function response
2182
- """
2183
- # Platform-specific implementation would go here
2184
- # For now, return basic SWML response
2185
- return self._render_swml()
2186
1907
 
2187
1908
  def _execute_swaig_function(self, function_name: str, args: Optional[Dict[str, Any]] = None, call_id: Optional[str] = None, raw_data: Optional[Dict[str, Any]] = None):
2188
1909
  """
@@ -2210,8 +1931,8 @@ class AgentBase(SWMLService):
2210
1931
 
2211
1932
  try:
2212
1933
  # Validate function exists
2213
- if function_name not in self._swaig_functions:
2214
- req_log.warning("function_not_found", available_functions=list(self._swaig_functions.keys()))
1934
+ if function_name not in self._tool_registry._swaig_functions:
1935
+ req_log.warning("function_not_found", available_functions=list(self._tool_registry._swaig_functions.keys()))
2215
1936
  return {"error": f"Function '{function_name}' not found"}
2216
1937
 
2217
1938
  # Use empty args if not provided
@@ -2928,7 +2649,7 @@ class AgentBase(SWMLService):
2928
2649
  req_log.debug("token_found", token_length=len(token))
2929
2650
 
2930
2651
  # Check token validity but don't reject the request
2931
- if hasattr(self, '_session_manager') and function_name in self._swaig_functions:
2652
+ if hasattr(self, '_session_manager') and function_name in self._tool_registry._swaig_functions:
2932
2653
  is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
2933
2654
  if is_valid:
2934
2655
  req_log.debug("token_valid")
@@ -3214,8 +2935,7 @@ class AgentBase(SWMLService):
3214
2935
  if token:
3215
2936
  req_log.debug("token_found", token_length=len(token))
3216
2937
 
3217
- # Try to validate token, but continue processing regardless
3218
- # for backward compatibility with existing implementations
2938
+ # Try to validate token, but continue processing regardless
3219
2939
  if call_id and hasattr(self, '_session_manager'):
3220
2940
  try:
3221
2941
  is_valid = self._session_manager.validate_tool_token("post_prompt", token, call_id)
@@ -3445,7 +3165,7 @@ class AgentBase(SWMLService):
3445
3165
  Called when SWML is requested, with request data when available
3446
3166
 
3447
3167
  This method overrides SWMLService's on_request to properly handle SWML generation
3448
- for AI Agents. It forwards the call to on_swml_request for compatibility.
3168
+ for AI Agents.
3449
3169
 
3450
3170
  Args:
3451
3171
  request_data: Optional dictionary containing the parsed POST body
@@ -3454,13 +3174,11 @@ class AgentBase(SWMLService):
3454
3174
  Returns:
3455
3175
  None to use the default SWML rendering (which will call _render_swml)
3456
3176
  """
3457
- # First try to call on_swml_request if it exists (backward compatibility)
3177
+ # Call on_swml_request for customization
3458
3178
  if hasattr(self, 'on_swml_request') and callable(getattr(self, 'on_swml_request')):
3459
3179
  return self.on_swml_request(request_data, callback_path, None)
3460
3180
 
3461
3181
  # If no on_swml_request or it returned None, we'll proceed with default rendering
3462
- # We're not returning any modifications here because _render_swml will be called
3463
- # to generate the complete SWML document
3464
3182
  return None
3465
3183
 
3466
3184
  def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None, request: Optional[Request] = None) -> Optional[dict]: