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.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/agent_server.py +50 -11
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +14 -0
- signalwire_agents/core/agent/config/__init__.py +14 -0
- signalwire_agents/core/agent/config/ephemeral.py +176 -0
- signalwire_agents/core/agent/deployment/__init__.py +0 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +0 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +288 -0
- signalwire_agents/core/agent/routing/__init__.py +0 -0
- signalwire_agents/core/agent/security/__init__.py +0 -0
- signalwire_agents/core/agent/swml/__init__.py +0 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +95 -0
- signalwire_agents/core/agent/tools/registry.py +192 -0
- signalwire_agents/core/agent_base.py +131 -413
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/skill_manager.py +0 -17
- signalwire_agents/core/swaig_function.py +0 -2
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +25 -94
- signalwire_agents/search/index_builder.py +1 -1
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +3 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +192 -0
- signalwire_agents/skills/play_background_file/__init__.py +3 -0
- signalwire_agents/skills/play_background_file/skill.py +197 -0
- signalwire_agents/skills/weather_api/__init__.py +3 -0
- signalwire_agents/skills/weather_api/skill.py +154 -0
- {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/METADATA +5 -8
- {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/RECORD +37 -18
- {signalwire_agents-0.1.20.data → signalwire_agents-0.1.23.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/entry_points.txt +0 -0
- {signalwire_agents-0.1.20.dist-info → signalwire_agents-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {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.
|
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
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
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
|
-
#
|
534
|
-
|
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.
|
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.
|
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
|
-
|
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.
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
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
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
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
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
1016
|
-
return
|
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.
|
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
|
-
|
1335
|
-
|
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
|
-
#
|
1417
|
-
|
1418
|
-
|
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
|
-
|
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"
|
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.
|
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.
|
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, '
|
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', '
|
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.
|
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
|
-
#
|
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]:
|