signalwire-agents 0.1.9__py3-none-any.whl → 0.1.11__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 +39 -4
- signalwire_agents/agent_server.py +46 -2
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/test_swaig.py +2545 -0
- signalwire_agents/core/agent_base.py +691 -82
- signalwire_agents/core/contexts.py +289 -0
- signalwire_agents/core/data_map.py +499 -0
- signalwire_agents/core/function_result.py +57 -10
- signalwire_agents/core/skill_base.py +31 -1
- signalwire_agents/core/skill_manager.py +89 -23
- signalwire_agents/core/swaig_function.py +13 -1
- signalwire_agents/core/swml_handler.py +37 -13
- signalwire_agents/core/swml_service.py +37 -28
- signalwire_agents/skills/datasphere/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +229 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
- signalwire_agents/skills/datetime/skill.py +7 -3
- signalwire_agents/skills/joke/__init__.py +1 -0
- signalwire_agents/skills/joke/skill.py +88 -0
- signalwire_agents/skills/math/skill.py +8 -5
- signalwire_agents/skills/registry.py +23 -4
- signalwire_agents/skills/web_search/skill.py +58 -33
- signalwire_agents/skills/wikipedia/__init__.py +9 -0
- signalwire_agents/skills/wikipedia/skill.py +180 -0
- signalwire_agents/utils/__init__.py +2 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents/utils/serverless.py +38 -0
- signalwire_agents-0.1.11.dist-info/METADATA +756 -0
- signalwire_agents-0.1.11.dist-info/RECORD +58 -0
- {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -0
- signalwire_agents-0.1.9.dist-info/METADATA +0 -311
- signalwire_agents-0.1.9.dist-info/RECORD +0 -44
- {signalwire_agents-0.1.9.data → signalwire_agents-0.1.11.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,9 @@ import base64
|
|
21
21
|
import logging
|
22
22
|
import inspect
|
23
23
|
import functools
|
24
|
+
import re
|
25
|
+
import signal
|
26
|
+
import sys
|
24
27
|
from typing import Optional, Union, List, Dict, Any, Tuple, Callable, Type
|
25
28
|
from urllib.parse import urlparse, urlencode
|
26
29
|
|
@@ -51,11 +54,11 @@ try:
|
|
51
54
|
structlog.stdlib.add_logger_name,
|
52
55
|
structlog.stdlib.add_log_level,
|
53
56
|
structlog.stdlib.PositionalArgumentsFormatter(),
|
54
|
-
structlog.processors.TimeStamper(fmt="
|
57
|
+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
55
58
|
structlog.processors.StackInfoRenderer(),
|
56
59
|
structlog.processors.format_exc_info,
|
57
60
|
structlog.processors.UnicodeDecoder(),
|
58
|
-
structlog.
|
61
|
+
structlog.dev.ConsoleRenderer()
|
59
62
|
],
|
60
63
|
context_class=dict,
|
61
64
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
@@ -76,6 +79,8 @@ from signalwire_agents.core.state import StateManager, FileStateManager
|
|
76
79
|
from signalwire_agents.core.swml_service import SWMLService
|
77
80
|
from signalwire_agents.core.swml_handler import AIVerbHandler
|
78
81
|
from signalwire_agents.core.skill_manager import SkillManager
|
82
|
+
from signalwire_agents.utils.schema_utils import SchemaUtils
|
83
|
+
from signalwire_agents.utils.serverless import get_execution_mode, is_serverless_mode
|
79
84
|
|
80
85
|
# Create a logger
|
81
86
|
logger = structlog.get_logger("agent_base")
|
@@ -282,7 +287,9 @@ class AgentBase(SWMLService):
|
|
282
287
|
agent_id: Optional[str] = None,
|
283
288
|
native_functions: Optional[List[str]] = None,
|
284
289
|
schema_path: Optional[str] = None,
|
285
|
-
suppress_logs: bool = False
|
290
|
+
suppress_logs: bool = False,
|
291
|
+
enable_post_prompt_override: bool = False,
|
292
|
+
check_for_input_override: bool = False
|
286
293
|
):
|
287
294
|
"""
|
288
295
|
Initialize a new agent
|
@@ -306,6 +313,8 @@ class AgentBase(SWMLService):
|
|
306
313
|
native_functions: Optional list of native functions to include in the SWAIG object
|
307
314
|
schema_path: Optional path to the schema file
|
308
315
|
suppress_logs: Whether to suppress structured logs
|
316
|
+
enable_post_prompt_override: Whether to enable post-prompt override
|
317
|
+
check_for_input_override: Whether to enable check-for-input override
|
309
318
|
"""
|
310
319
|
# Import SWMLService here to avoid circular imports
|
311
320
|
from signalwire_agents.core.swml_service import SWMLService
|
@@ -325,7 +334,7 @@ class AgentBase(SWMLService):
|
|
325
334
|
|
326
335
|
# Log the schema path if found and not suppressing logs
|
327
336
|
if self.schema_utils and self.schema_utils.schema_path and not suppress_logs:
|
328
|
-
|
337
|
+
self.log.debug("using_schema_path", path=self.schema_utils.schema_path)
|
329
338
|
|
330
339
|
# Setup logger for this instance
|
331
340
|
self.log = logger.bind(agent=name)
|
@@ -360,7 +369,7 @@ class AgentBase(SWMLService):
|
|
360
369
|
self.pom = None
|
361
370
|
|
362
371
|
# Initialize tool registry (separate from SWMLService verb registry)
|
363
|
-
self._swaig_functions
|
372
|
+
self._swaig_functions = {}
|
364
373
|
|
365
374
|
# Initialize session manager
|
366
375
|
self._session_manager = SessionManager(token_expiry_secs=token_expiry_secs)
|
@@ -408,6 +417,15 @@ class AgentBase(SWMLService):
|
|
408
417
|
|
409
418
|
# Initialize skill manager
|
410
419
|
self.skill_manager = SkillManager(self)
|
420
|
+
|
421
|
+
# Initialize contexts system
|
422
|
+
self._contexts_builder = None
|
423
|
+
self._contexts_defined = False
|
424
|
+
|
425
|
+
self.schema_utils = SchemaUtils(schema_path)
|
426
|
+
if self.schema_utils and self.schema_utils.schema:
|
427
|
+
self.log.debug("schema_loaded", path=self.schema_utils.schema_path)
|
428
|
+
|
411
429
|
|
412
430
|
def _process_prompt_sections(self):
|
413
431
|
"""
|
@@ -510,6 +528,39 @@ class AgentBase(SWMLService):
|
|
510
528
|
# Prompt Building Methods
|
511
529
|
# ----------------------------------------------------------------------
|
512
530
|
|
531
|
+
def define_contexts(self) -> 'ContextBuilder':
|
532
|
+
"""
|
533
|
+
Define contexts and steps for this agent (alternative to POM/prompt)
|
534
|
+
|
535
|
+
Returns:
|
536
|
+
ContextBuilder for method chaining
|
537
|
+
|
538
|
+
Note:
|
539
|
+
Contexts can coexist with traditional prompts. The restriction is only
|
540
|
+
that you can't mix POM sections with raw text in the main prompt.
|
541
|
+
"""
|
542
|
+
# Import here to avoid circular imports
|
543
|
+
from signalwire_agents.core.contexts import ContextBuilder
|
544
|
+
|
545
|
+
if self._contexts_builder is None:
|
546
|
+
self._contexts_builder = ContextBuilder(self)
|
547
|
+
self._contexts_defined = True
|
548
|
+
|
549
|
+
return self._contexts_builder
|
550
|
+
|
551
|
+
def _validate_prompt_mode_exclusivity(self):
|
552
|
+
"""
|
553
|
+
Validate that POM sections and raw text are not mixed in the main prompt
|
554
|
+
|
555
|
+
Note: This does NOT prevent contexts from being used alongside traditional prompts
|
556
|
+
"""
|
557
|
+
# Only check for mixing POM sections with raw text in the main prompt
|
558
|
+
if self._raw_prompt and (self.pom and hasattr(self.pom, 'sections') and self.pom.sections):
|
559
|
+
raise ValueError(
|
560
|
+
"Cannot mix raw text prompt with POM sections in the main prompt. "
|
561
|
+
"Use either set_prompt_text() OR prompt_add_section() methods, not both."
|
562
|
+
)
|
563
|
+
|
513
564
|
def set_prompt_text(self, text: str) -> 'AgentBase':
|
514
565
|
"""
|
515
566
|
Set the prompt as raw text instead of using POM
|
@@ -520,6 +571,7 @@ class AgentBase(SWMLService):
|
|
520
571
|
Returns:
|
521
572
|
Self for method chaining
|
522
573
|
"""
|
574
|
+
self._validate_prompt_mode_exclusivity()
|
523
575
|
self._raw_prompt = text
|
524
576
|
return self
|
525
577
|
|
@@ -575,6 +627,7 @@ class AgentBase(SWMLService):
|
|
575
627
|
Returns:
|
576
628
|
Self for method chaining
|
577
629
|
"""
|
630
|
+
self._validate_prompt_mode_exclusivity()
|
578
631
|
if self._use_pom and self.pom:
|
579
632
|
# Create parameters for add_section based on what's supported
|
580
633
|
kwargs = {}
|
@@ -690,7 +743,9 @@ class AgentBase(SWMLService):
|
|
690
743
|
parameters: Dict[str, Any],
|
691
744
|
handler: Callable,
|
692
745
|
secure: bool = True,
|
693
|
-
fillers: Optional[Dict[str, List[str]]] = None
|
746
|
+
fillers: Optional[Dict[str, List[str]]] = None,
|
747
|
+
webhook_url: Optional[str] = None,
|
748
|
+
**swaig_fields
|
694
749
|
) -> 'AgentBase':
|
695
750
|
"""
|
696
751
|
Define a SWAIG function that the AI can call
|
@@ -702,6 +757,8 @@ class AgentBase(SWMLService):
|
|
702
757
|
handler: Function to call when invoked
|
703
758
|
secure: Whether to require token validation
|
704
759
|
fillers: Optional dict mapping language codes to arrays of filler phrases
|
760
|
+
webhook_url: Optional external webhook URL to use instead of local handling
|
761
|
+
**swaig_fields: Additional SWAIG fields to include in function definition
|
705
762
|
|
706
763
|
Returns:
|
707
764
|
Self for method chaining
|
@@ -715,10 +772,34 @@ class AgentBase(SWMLService):
|
|
715
772
|
parameters=parameters,
|
716
773
|
handler=handler,
|
717
774
|
secure=secure,
|
718
|
-
fillers=fillers
|
775
|
+
fillers=fillers,
|
776
|
+
webhook_url=webhook_url,
|
777
|
+
**swaig_fields
|
719
778
|
)
|
720
779
|
return self
|
721
780
|
|
781
|
+
def register_swaig_function(self, function_dict: Dict[str, Any]) -> 'AgentBase':
|
782
|
+
"""
|
783
|
+
Register a raw SWAIG function dictionary (e.g., from DataMap.to_swaig_function())
|
784
|
+
|
785
|
+
Args:
|
786
|
+
function_dict: Complete SWAIG function definition dictionary
|
787
|
+
|
788
|
+
Returns:
|
789
|
+
Self for method chaining
|
790
|
+
"""
|
791
|
+
function_name = function_dict.get('function')
|
792
|
+
if not function_name:
|
793
|
+
raise ValueError("Function dictionary must contain 'function' field with the function name")
|
794
|
+
|
795
|
+
if function_name in self._swaig_functions:
|
796
|
+
raise ValueError(f"Tool with name '{function_name}' already exists")
|
797
|
+
|
798
|
+
# Store the raw function dictionary for data_map tools
|
799
|
+
# These don't have handlers since they execute on SignalWire's server
|
800
|
+
self._swaig_functions[function_name] = function_dict
|
801
|
+
return self
|
802
|
+
|
722
803
|
def _tool_decorator(self, name=None, **kwargs):
|
723
804
|
"""
|
724
805
|
Decorator for defining SWAIG tools in a class
|
@@ -734,10 +815,11 @@ class AgentBase(SWMLService):
|
|
734
815
|
if name is None:
|
735
816
|
name = func.__name__
|
736
817
|
|
737
|
-
parameters = kwargs.
|
738
|
-
description = kwargs.
|
739
|
-
secure = kwargs.
|
740
|
-
fillers = kwargs.
|
818
|
+
parameters = kwargs.pop("parameters", {})
|
819
|
+
description = kwargs.pop("description", func.__doc__ or f"Function {name}")
|
820
|
+
secure = kwargs.pop("secure", True)
|
821
|
+
fillers = kwargs.pop("fillers", None)
|
822
|
+
webhook_url = kwargs.pop("webhook_url", None)
|
741
823
|
|
742
824
|
self.define_tool(
|
743
825
|
name=name,
|
@@ -745,7 +827,9 @@ class AgentBase(SWMLService):
|
|
745
827
|
parameters=parameters,
|
746
828
|
handler=func,
|
747
829
|
secure=secure,
|
748
|
-
fillers=fillers
|
830
|
+
fillers=fillers,
|
831
|
+
webhook_url=webhook_url,
|
832
|
+
**kwargs # Pass through any additional swaig_fields
|
749
833
|
)
|
750
834
|
return func
|
751
835
|
return decorator
|
@@ -772,20 +856,24 @@ class AgentBase(SWMLService):
|
|
772
856
|
tool_name = getattr(attr, "_tool_name", name)
|
773
857
|
tool_params = getattr(attr, "_tool_params", {})
|
774
858
|
|
775
|
-
#
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
859
|
+
# Extract known parameters and pass through the rest as swaig_fields
|
860
|
+
tool_params_copy = tool_params.copy()
|
861
|
+
description = tool_params_copy.pop("description", attr.__doc__ or f"Function {tool_name}")
|
862
|
+
parameters = tool_params_copy.pop("parameters", {})
|
863
|
+
secure = tool_params_copy.pop("secure", True)
|
864
|
+
fillers = tool_params_copy.pop("fillers", None)
|
865
|
+
webhook_url = tool_params_copy.pop("webhook_url", None)
|
780
866
|
|
781
|
-
# Register the tool
|
867
|
+
# Register the tool with any remaining params as swaig_fields
|
782
868
|
self.define_tool(
|
783
869
|
name=tool_name,
|
784
870
|
description=description,
|
785
871
|
parameters=parameters,
|
786
872
|
handler=attr.__get__(self, cls), # Bind the method to this instance
|
787
873
|
secure=secure,
|
788
|
-
fillers=fillers
|
874
|
+
fillers=fillers,
|
875
|
+
webhook_url=webhook_url,
|
876
|
+
**tool_params_copy # Pass through any additional swaig_fields
|
789
877
|
)
|
790
878
|
|
791
879
|
@classmethod
|
@@ -812,12 +900,102 @@ class AgentBase(SWMLService):
|
|
812
900
|
|
813
901
|
def get_name(self) -> str:
|
814
902
|
"""
|
815
|
-
Get
|
903
|
+
Get agent name
|
816
904
|
|
817
905
|
Returns:
|
818
|
-
Agent name
|
906
|
+
Agent name
|
819
907
|
"""
|
820
908
|
return self.name
|
909
|
+
|
910
|
+
def get_app(self):
|
911
|
+
"""
|
912
|
+
Get the FastAPI application instance for deployment adapters like Lambda/Mangum
|
913
|
+
|
914
|
+
This method ensures the FastAPI app is properly initialized and configured,
|
915
|
+
then returns it for use with deployment adapters like Mangum for AWS Lambda.
|
916
|
+
|
917
|
+
Returns:
|
918
|
+
FastAPI: The configured FastAPI application instance
|
919
|
+
"""
|
920
|
+
if self._app is None:
|
921
|
+
# Initialize the app if it hasn't been created yet
|
922
|
+
# This follows the same initialization logic as serve() but without running uvicorn
|
923
|
+
from fastapi import FastAPI
|
924
|
+
from fastapi.middleware.cors import CORSMiddleware
|
925
|
+
|
926
|
+
# Create a FastAPI app with explicit redirect_slashes=False
|
927
|
+
app = FastAPI(redirect_slashes=False)
|
928
|
+
|
929
|
+
# Add health and ready endpoints directly to the main app to avoid conflicts with catch-all
|
930
|
+
@app.get("/health")
|
931
|
+
@app.post("/health")
|
932
|
+
async def health_check():
|
933
|
+
"""Health check endpoint for Kubernetes liveness probe"""
|
934
|
+
return {
|
935
|
+
"status": "healthy",
|
936
|
+
"agent": self.get_name(),
|
937
|
+
"route": self.route,
|
938
|
+
"functions": len(self._swaig_functions)
|
939
|
+
}
|
940
|
+
|
941
|
+
@app.get("/ready")
|
942
|
+
@app.post("/ready")
|
943
|
+
async def readiness_check():
|
944
|
+
"""Readiness check endpoint for Kubernetes readiness probe"""
|
945
|
+
return {
|
946
|
+
"status": "ready",
|
947
|
+
"agent": self.get_name(),
|
948
|
+
"route": self.route,
|
949
|
+
"functions": len(self._swaig_functions)
|
950
|
+
}
|
951
|
+
|
952
|
+
# Add CORS middleware if needed
|
953
|
+
app.add_middleware(
|
954
|
+
CORSMiddleware,
|
955
|
+
allow_origins=["*"],
|
956
|
+
allow_credentials=True,
|
957
|
+
allow_methods=["*"],
|
958
|
+
allow_headers=["*"],
|
959
|
+
)
|
960
|
+
|
961
|
+
# Create router and register routes
|
962
|
+
router = self.as_router()
|
963
|
+
|
964
|
+
# Log registered routes for debugging
|
965
|
+
self.log.debug("router_routes_registered")
|
966
|
+
for route in router.routes:
|
967
|
+
if hasattr(route, "path"):
|
968
|
+
self.log.debug("router_route", path=route.path)
|
969
|
+
|
970
|
+
# Include the router
|
971
|
+
app.include_router(router, prefix=self.route)
|
972
|
+
|
973
|
+
# Register a catch-all route for debugging and troubleshooting
|
974
|
+
@app.get("/{full_path:path}")
|
975
|
+
@app.post("/{full_path:path}")
|
976
|
+
async def handle_all_routes(request: Request, full_path: str):
|
977
|
+
self.log.debug("request_received", path=full_path)
|
978
|
+
|
979
|
+
# Check if the path is meant for this agent
|
980
|
+
if not full_path.startswith(self.route.lstrip("/")):
|
981
|
+
return {"error": "Invalid route"}
|
982
|
+
|
983
|
+
# Extract the path relative to this agent's route
|
984
|
+
relative_path = full_path[len(self.route.lstrip("/")):]
|
985
|
+
relative_path = relative_path.lstrip("/")
|
986
|
+
self.log.debug("relative_path_extracted", path=relative_path)
|
987
|
+
|
988
|
+
return {"error": "Path not found"}
|
989
|
+
|
990
|
+
# Log all app routes for debugging
|
991
|
+
self.log.debug("app_routes_registered")
|
992
|
+
for route in app.routes:
|
993
|
+
if hasattr(route, "path"):
|
994
|
+
self.log.debug("app_route", path=route.path)
|
995
|
+
|
996
|
+
self._app = app
|
997
|
+
|
998
|
+
return self._app
|
821
999
|
|
822
1000
|
def get_prompt(self) -> Union[str, List[Dict[str, Any]]]:
|
823
1001
|
"""
|
@@ -855,7 +1033,7 @@ class AgentBase(SWMLService):
|
|
855
1033
|
return pom_data['_sections']
|
856
1034
|
# Fall through to default if nothing worked
|
857
1035
|
except Exception as e:
|
858
|
-
|
1036
|
+
self.log.error("pom_rendering_failed", error=str(e))
|
859
1037
|
# Fall back to raw text if POM fails
|
860
1038
|
|
861
1039
|
# Return raw text (either explicitly set or default)
|
@@ -875,11 +1053,19 @@ class AgentBase(SWMLService):
|
|
875
1053
|
Define the tools this agent can use
|
876
1054
|
|
877
1055
|
Returns:
|
878
|
-
List of SWAIGFunction objects
|
1056
|
+
List of SWAIGFunction objects or raw dictionaries (for data_map tools)
|
879
1057
|
|
880
1058
|
This method can be overridden by subclasses.
|
881
1059
|
"""
|
882
|
-
|
1060
|
+
tools = []
|
1061
|
+
for func in self._swaig_functions.values():
|
1062
|
+
if isinstance(func, dict):
|
1063
|
+
# Raw dictionary from register_swaig_function (e.g., DataMap)
|
1064
|
+
tools.append(func)
|
1065
|
+
else:
|
1066
|
+
# SWAIGFunction object from define_tool
|
1067
|
+
tools.append(func)
|
1068
|
+
return tools
|
883
1069
|
|
884
1070
|
def on_summary(self, summary: Optional[Dict[str, Any]], raw_data: Optional[Dict[str, Any]] = None) -> None:
|
885
1071
|
"""
|
@@ -912,7 +1098,18 @@ class AgentBase(SWMLService):
|
|
912
1098
|
# Get the function
|
913
1099
|
func = self._swaig_functions[name]
|
914
1100
|
|
915
|
-
#
|
1101
|
+
# Check if this is a data_map function (raw dictionary)
|
1102
|
+
if isinstance(func, dict):
|
1103
|
+
# Data_map functions execute on SignalWire's server, not here
|
1104
|
+
# This should never be called, but if it is, return an error
|
1105
|
+
return {"response": f"Data map function '{name}' should be executed by SignalWire server, not locally"}
|
1106
|
+
|
1107
|
+
# Check if this is an external webhook function
|
1108
|
+
if hasattr(func, 'webhook_url') and func.webhook_url:
|
1109
|
+
# External webhook functions should be called directly by SignalWire, not locally
|
1110
|
+
return {"response": f"External webhook function '{name}' should be executed by SignalWire at {func.webhook_url}, not locally"}
|
1111
|
+
|
1112
|
+
# Call the handler for regular SWAIG functions
|
916
1113
|
try:
|
917
1114
|
result = func.handler(args, raw_data)
|
918
1115
|
if result is None:
|
@@ -979,8 +1176,19 @@ class AgentBase(SWMLService):
|
|
979
1176
|
self.log.warning("unknown_function", function=function_name)
|
980
1177
|
return False
|
981
1178
|
|
1179
|
+
# Get the function and check if it's secure
|
1180
|
+
func = self._swaig_functions[function_name]
|
1181
|
+
is_secure = True # Default to secure
|
1182
|
+
|
1183
|
+
if isinstance(func, dict):
|
1184
|
+
# For raw dictionaries (DataMap functions), they're always secure
|
1185
|
+
is_secure = True
|
1186
|
+
else:
|
1187
|
+
# For SWAIGFunction objects, check the secure attribute
|
1188
|
+
is_secure = func.secure
|
1189
|
+
|
982
1190
|
# Always allow non-secure functions
|
983
|
-
if not
|
1191
|
+
if not is_secure:
|
984
1192
|
self.log.debug("non_secure_function_allowed", function=function_name)
|
985
1193
|
return True
|
986
1194
|
|
@@ -1107,25 +1315,56 @@ class AgentBase(SWMLService):
|
|
1107
1315
|
Returns:
|
1108
1316
|
Full URL including host, port, and route (with auth if requested)
|
1109
1317
|
"""
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1318
|
+
mode = get_execution_mode()
|
1319
|
+
|
1320
|
+
if mode == 'cgi':
|
1321
|
+
protocol = 'https' if os.getenv('HTTPS') == 'on' else 'http'
|
1322
|
+
host = os.getenv('HTTP_HOST') or os.getenv('SERVER_NAME') or 'localhost'
|
1323
|
+
script_name = os.getenv('SCRIPT_NAME', '')
|
1324
|
+
base_url = f"{protocol}://{host}{script_name}"
|
1325
|
+
elif mode == 'lambda':
|
1326
|
+
function_url = os.getenv('AWS_LAMBDA_FUNCTION_URL')
|
1327
|
+
if function_url and ('amazonaws.com' in function_url or 'on.aws' in function_url):
|
1328
|
+
base_url = function_url.rstrip('/')
|
1329
|
+
else:
|
1330
|
+
api_id = os.getenv('AWS_API_GATEWAY_ID')
|
1331
|
+
if api_id:
|
1332
|
+
region = os.getenv('AWS_REGION', 'us-east-1')
|
1333
|
+
stage = os.getenv('AWS_API_GATEWAY_STAGE', 'prod')
|
1334
|
+
base_url = f"https://{api_id}.execute-api.{region}.amazonaws.com/{stage}"
|
1335
|
+
else:
|
1336
|
+
import logging
|
1337
|
+
logging.warning("Lambda mode detected but no URL configuration found")
|
1338
|
+
base_url = "https://lambda-url-not-configured"
|
1339
|
+
elif mode == 'cloud_function':
|
1340
|
+
function_url = os.getenv('FUNCTION_URL')
|
1341
|
+
if function_url:
|
1342
|
+
base_url = function_url
|
1343
|
+
else:
|
1344
|
+
project = os.getenv('GOOGLE_CLOUD_PROJECT')
|
1345
|
+
if project:
|
1346
|
+
region = os.getenv('GOOGLE_CLOUD_REGION', 'us-central1')
|
1347
|
+
service = os.getenv('K_SERVICE', 'function')
|
1348
|
+
base_url = f"https://{region}-{project}.cloudfunctions.net/{service}"
|
1349
|
+
else:
|
1350
|
+
import logging
|
1351
|
+
logging.warning("Cloud Function mode detected but no URL configuration found")
|
1352
|
+
base_url = "https://cloud-function-url-not-configured"
|
1118
1353
|
else:
|
1119
|
-
#
|
1120
|
-
if self.
|
1121
|
-
|
1354
|
+
# Server mode - preserve existing logic
|
1355
|
+
if self._proxy_url_base:
|
1356
|
+
proxy_base = self._proxy_url_base.rstrip('/')
|
1357
|
+
route = self.route if self.route.startswith('/') else f"/{self.route}"
|
1358
|
+
base_url = f"{proxy_base}{route}"
|
1122
1359
|
else:
|
1123
|
-
host
|
1124
|
-
|
1125
|
-
|
1360
|
+
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
1361
|
+
host = "localhost"
|
1362
|
+
else:
|
1363
|
+
host = self.host
|
1364
|
+
base_url = f"http://{host}:{self.port}{self.route}"
|
1126
1365
|
|
1127
|
-
# Add auth if requested
|
1128
|
-
if include_auth:
|
1366
|
+
# Add auth if requested (only for server mode)
|
1367
|
+
if include_auth and mode == 'server':
|
1129
1368
|
username, password = self._basic_auth
|
1130
1369
|
url = urlparse(base_url)
|
1131
1370
|
return url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
@@ -1143,6 +1382,34 @@ class AgentBase(SWMLService):
|
|
1143
1382
|
Returns:
|
1144
1383
|
Fully constructed webhook URL
|
1145
1384
|
"""
|
1385
|
+
# Check for serverless environment and use appropriate URL generation
|
1386
|
+
mode = get_execution_mode()
|
1387
|
+
|
1388
|
+
if mode != 'server':
|
1389
|
+
# In serverless mode, use the serverless-appropriate URL
|
1390
|
+
base_url = self.get_full_url()
|
1391
|
+
|
1392
|
+
# For serverless, we don't need auth in webhook URLs since auth is handled differently
|
1393
|
+
# and we want to return the actual platform URL
|
1394
|
+
|
1395
|
+
# Ensure the endpoint has a trailing slash to prevent redirects
|
1396
|
+
if endpoint in ["swaig", "post_prompt"]:
|
1397
|
+
endpoint = f"{endpoint}/"
|
1398
|
+
|
1399
|
+
# Build the full webhook URL
|
1400
|
+
url = f"{base_url}/{endpoint}"
|
1401
|
+
|
1402
|
+
# Add query parameters if any (only if they have values)
|
1403
|
+
if query_params:
|
1404
|
+
# Remove any call_id from query params
|
1405
|
+
filtered_params = {k: v for k, v in query_params.items() if k != "call_id" and v}
|
1406
|
+
if filtered_params:
|
1407
|
+
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
1408
|
+
url = f"{url}?{params}"
|
1409
|
+
|
1410
|
+
return url
|
1411
|
+
|
1412
|
+
# Server mode - use existing logic with proxy/auth support
|
1146
1413
|
# Use the parent class's implementation if available and has the same method
|
1147
1414
|
if hasattr(super(), '_build_webhook_url'):
|
1148
1415
|
# Ensure _proxy_url_base is synchronized
|
@@ -1152,7 +1419,7 @@ class AgentBase(SWMLService):
|
|
1152
1419
|
# Call parent's implementation
|
1153
1420
|
return super()._build_webhook_url(endpoint, query_params)
|
1154
1421
|
|
1155
|
-
# Otherwise, fall back to our own implementation
|
1422
|
+
# Otherwise, fall back to our own implementation for server mode
|
1156
1423
|
# Base URL construction
|
1157
1424
|
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
1158
1425
|
# For proxy URLs
|
@@ -1251,33 +1518,46 @@ class AgentBase(SWMLService):
|
|
1251
1518
|
|
1252
1519
|
# Add each function to the functions array
|
1253
1520
|
for name, func in self._swaig_functions.items():
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1521
|
+
if isinstance(func, dict):
|
1522
|
+
# For raw dictionaries (DataMap functions), use the entire dictionary as-is
|
1523
|
+
# This preserves data_map and any other special fields
|
1524
|
+
function_entry = func.copy()
|
1525
|
+
|
1526
|
+
# Ensure the function name is set correctly
|
1527
|
+
function_entry["function"] = name
|
1528
|
+
|
1529
|
+
else:
|
1530
|
+
# For SWAIGFunction objects, build the entry manually
|
1531
|
+
# Check if it's secure and get token for secure functions when we have a call_id
|
1532
|
+
token = None
|
1533
|
+
if func.secure and call_id:
|
1534
|
+
token = self._create_tool_token(tool_name=name, call_id=call_id)
|
1535
|
+
|
1536
|
+
# Prepare function entry
|
1537
|
+
function_entry = {
|
1538
|
+
"function": name,
|
1539
|
+
"description": func.description,
|
1540
|
+
"parameters": {
|
1541
|
+
"type": "object",
|
1542
|
+
"properties": func.parameters
|
1543
|
+
}
|
1266
1544
|
}
|
1267
|
-
}
|
1268
|
-
|
1269
|
-
# Add fillers if present
|
1270
|
-
if func.fillers:
|
1271
|
-
function_entry["fillers"] = func.fillers
|
1272
1545
|
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
token_params = {"token": token}
|
1277
|
-
function_entry["web_hook_url"] = self._build_webhook_url("swaig", token_params)
|
1546
|
+
# Add fillers if present
|
1547
|
+
if func.fillers:
|
1548
|
+
function_entry["fillers"] = func.fillers
|
1278
1549
|
|
1279
|
-
|
1550
|
+
# Handle webhook URL
|
1551
|
+
if hasattr(func, 'webhook_url') and func.webhook_url:
|
1552
|
+
# External webhook function - use the provided URL directly
|
1553
|
+
function_entry["web_hook_url"] = func.webhook_url
|
1554
|
+
elif token:
|
1555
|
+
# Local function with token - build local webhook URL
|
1556
|
+
token_params = {"token": token}
|
1557
|
+
function_entry["web_hook_url"] = self._build_webhook_url("swaig", token_params)
|
1280
1558
|
|
1559
|
+
functions.append(function_entry)
|
1560
|
+
|
1281
1561
|
# Add functions array to SWAIG object if we have any
|
1282
1562
|
if functions:
|
1283
1563
|
swaig_obj["functions"] = functions
|
@@ -1312,14 +1592,29 @@ class AgentBase(SWMLService):
|
|
1312
1592
|
ai_handler = self.verb_registry.get_handler("ai")
|
1313
1593
|
if ai_handler:
|
1314
1594
|
try:
|
1315
|
-
#
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1595
|
+
# Check if we're in contexts mode
|
1596
|
+
if self._contexts_defined and self._contexts_builder:
|
1597
|
+
# Generate contexts instead of prompt
|
1598
|
+
contexts_dict = self._contexts_builder.to_dict()
|
1599
|
+
|
1600
|
+
# Build AI config with contexts
|
1601
|
+
ai_config = ai_handler.build_config(
|
1602
|
+
prompt_text=None,
|
1603
|
+
prompt_pom=None,
|
1604
|
+
contexts=contexts_dict,
|
1605
|
+
post_prompt=post_prompt,
|
1606
|
+
post_prompt_url=post_prompt_url,
|
1607
|
+
swaig=swaig_obj if swaig_obj else None
|
1608
|
+
)
|
1609
|
+
else:
|
1610
|
+
# Build AI config using the traditional prompt approach
|
1611
|
+
ai_config = ai_handler.build_config(
|
1612
|
+
prompt_text=None if prompt_is_pom else prompt,
|
1613
|
+
prompt_pom=prompt if prompt_is_pom else None,
|
1614
|
+
post_prompt=post_prompt,
|
1615
|
+
post_prompt_url=post_prompt_url,
|
1616
|
+
swaig=swaig_obj if swaig_obj else None
|
1617
|
+
)
|
1323
1618
|
|
1324
1619
|
# Add new configuration parameters to the AI config
|
1325
1620
|
|
@@ -1345,7 +1640,7 @@ class AgentBase(SWMLService):
|
|
1345
1640
|
|
1346
1641
|
except ValueError as e:
|
1347
1642
|
if not self._suppress_logs:
|
1348
|
-
|
1643
|
+
self.log.error("ai_verb_config_error", error=str(e))
|
1349
1644
|
else:
|
1350
1645
|
# Fallback if no handler (shouldn't happen but just in case)
|
1351
1646
|
ai_config = {
|
@@ -1438,9 +1733,9 @@ class AgentBase(SWMLService):
|
|
1438
1733
|
self._register_routes(router)
|
1439
1734
|
|
1440
1735
|
# Log all registered routes for debugging
|
1441
|
-
|
1736
|
+
self.log.debug("routes_registered", agent=self.name)
|
1442
1737
|
for route in router.routes:
|
1443
|
-
|
1738
|
+
self.log.debug("route_registered", path=route.path)
|
1444
1739
|
|
1445
1740
|
return router
|
1446
1741
|
|
@@ -1458,6 +1753,40 @@ class AgentBase(SWMLService):
|
|
1458
1753
|
# Create a FastAPI app with explicit redirect_slashes=False
|
1459
1754
|
app = FastAPI(redirect_slashes=False)
|
1460
1755
|
|
1756
|
+
# Add health and ready endpoints directly to the main app to avoid conflicts with catch-all
|
1757
|
+
@app.get("/health")
|
1758
|
+
@app.post("/health")
|
1759
|
+
async def health_check():
|
1760
|
+
"""Health check endpoint for Kubernetes liveness probe"""
|
1761
|
+
return {
|
1762
|
+
"status": "healthy",
|
1763
|
+
"agent": self.get_name(),
|
1764
|
+
"route": self.route,
|
1765
|
+
"functions": len(self._swaig_functions)
|
1766
|
+
}
|
1767
|
+
|
1768
|
+
@app.get("/ready")
|
1769
|
+
@app.post("/ready")
|
1770
|
+
async def readiness_check():
|
1771
|
+
"""Readiness check endpoint for Kubernetes readiness probe"""
|
1772
|
+
# Check if agent is properly initialized
|
1773
|
+
ready = (
|
1774
|
+
hasattr(self, '_swaig_functions') and
|
1775
|
+
hasattr(self, 'schema_utils') and
|
1776
|
+
self.schema_utils is not None
|
1777
|
+
)
|
1778
|
+
|
1779
|
+
status_code = 200 if ready else 503
|
1780
|
+
return Response(
|
1781
|
+
content=json.dumps({
|
1782
|
+
"status": "ready" if ready else "not_ready",
|
1783
|
+
"agent": self.get_name(),
|
1784
|
+
"initialized": ready
|
1785
|
+
}),
|
1786
|
+
status_code=status_code,
|
1787
|
+
media_type="application/json"
|
1788
|
+
)
|
1789
|
+
|
1461
1790
|
# Get router for this agent
|
1462
1791
|
router = self.as_router()
|
1463
1792
|
|
@@ -1465,7 +1794,7 @@ class AgentBase(SWMLService):
|
|
1465
1794
|
@app.get("/{full_path:path}")
|
1466
1795
|
@app.post("/{full_path:path}")
|
1467
1796
|
async def handle_all_routes(request: Request, full_path: str):
|
1468
|
-
|
1797
|
+
self.log.debug("request_received", path=full_path)
|
1469
1798
|
|
1470
1799
|
# Check if the path is meant for this agent
|
1471
1800
|
if not full_path.startswith(self.route.lstrip("/")):
|
@@ -1474,7 +1803,7 @@ class AgentBase(SWMLService):
|
|
1474
1803
|
# Extract the path relative to this agent's route
|
1475
1804
|
relative_path = full_path[len(self.route.lstrip("/")):]
|
1476
1805
|
relative_path = relative_path.lstrip("/")
|
1477
|
-
|
1806
|
+
self.log.debug("path_extracted", relative_path=relative_path)
|
1478
1807
|
|
1479
1808
|
# Perform routing based on the relative path
|
1480
1809
|
if not relative_path or relative_path == "/":
|
@@ -1509,11 +1838,11 @@ class AgentBase(SWMLService):
|
|
1509
1838
|
# Include router with prefix
|
1510
1839
|
app.include_router(router, prefix=self.route)
|
1511
1840
|
|
1512
|
-
#
|
1513
|
-
|
1841
|
+
# Log all app routes for debugging
|
1842
|
+
self.log.debug("app_routes_registered")
|
1514
1843
|
for route in app.routes:
|
1515
1844
|
if hasattr(route, "path"):
|
1516
|
-
|
1845
|
+
self.log.debug("app_route", path=route.path)
|
1517
1846
|
|
1518
1847
|
self._app = app
|
1519
1848
|
|
@@ -1522,12 +1851,290 @@ class AgentBase(SWMLService):
|
|
1522
1851
|
|
1523
1852
|
# Print the auth credentials with source
|
1524
1853
|
username, password, source = self.get_basic_auth_credentials(include_source=True)
|
1854
|
+
|
1855
|
+
# Log startup information using structured logging
|
1856
|
+
self.log.info("agent_starting",
|
1857
|
+
agent=self.name,
|
1858
|
+
url=f"http://{host}:{port}{self.route}",
|
1859
|
+
username=username,
|
1860
|
+
password_length=len(password),
|
1861
|
+
auth_source=source)
|
1862
|
+
|
1863
|
+
# Print user-friendly startup message (keep this for development UX)
|
1525
1864
|
print(f"Agent '{self.name}' is available at:")
|
1526
1865
|
print(f"URL: http://{host}:{port}{self.route}")
|
1527
1866
|
print(f"Basic Auth: {username}:{password} (source: {source})")
|
1528
1867
|
|
1529
1868
|
uvicorn.run(self._app, host=host, port=port)
|
1530
1869
|
|
1870
|
+
def run(self, event=None, context=None, force_mode=None, host: Optional[str] = None, port: Optional[int] = None):
|
1871
|
+
"""
|
1872
|
+
Smart run method that automatically detects environment and handles accordingly
|
1873
|
+
|
1874
|
+
Args:
|
1875
|
+
event: Serverless event object (Lambda, Cloud Functions)
|
1876
|
+
context: Serverless context object (Lambda, Cloud Functions)
|
1877
|
+
force_mode: Override automatic mode detection for testing
|
1878
|
+
host: Host override for server mode
|
1879
|
+
port: Port override for server mode
|
1880
|
+
|
1881
|
+
Returns:
|
1882
|
+
Response for serverless modes, None for server mode
|
1883
|
+
"""
|
1884
|
+
mode = force_mode or get_execution_mode()
|
1885
|
+
|
1886
|
+
try:
|
1887
|
+
if mode in ['cgi', 'cloud_function', 'azure_function']:
|
1888
|
+
response = self.handle_serverless_request(event, context, mode)
|
1889
|
+
print(response)
|
1890
|
+
return response
|
1891
|
+
elif mode == 'lambda':
|
1892
|
+
return self.handle_serverless_request(event, context, mode)
|
1893
|
+
else:
|
1894
|
+
# Server mode - use existing serve method
|
1895
|
+
self.serve(host, port)
|
1896
|
+
except Exception as e:
|
1897
|
+
import logging
|
1898
|
+
logging.error(f"Error in run method: {e}")
|
1899
|
+
if mode == 'lambda':
|
1900
|
+
return {
|
1901
|
+
"statusCode": 500,
|
1902
|
+
"headers": {"Content-Type": "application/json"},
|
1903
|
+
"body": json.dumps({"error": str(e)})
|
1904
|
+
}
|
1905
|
+
else:
|
1906
|
+
raise
|
1907
|
+
|
1908
|
+
def handle_serverless_request(self, event=None, context=None, mode=None):
|
1909
|
+
"""
|
1910
|
+
Handle serverless environment requests (CGI, Lambda, Cloud Functions)
|
1911
|
+
|
1912
|
+
Args:
|
1913
|
+
event: Serverless event object (Lambda, Cloud Functions)
|
1914
|
+
context: Serverless context object (Lambda, Cloud Functions)
|
1915
|
+
mode: Override execution mode (from force_mode in run())
|
1916
|
+
|
1917
|
+
Returns:
|
1918
|
+
Response appropriate for the serverless platform
|
1919
|
+
"""
|
1920
|
+
if mode is None:
|
1921
|
+
mode = get_execution_mode()
|
1922
|
+
|
1923
|
+
try:
|
1924
|
+
if mode == 'cgi':
|
1925
|
+
path_info = os.getenv('PATH_INFO', '').strip('/')
|
1926
|
+
if not path_info:
|
1927
|
+
return self._render_swml()
|
1928
|
+
else:
|
1929
|
+
# Parse CGI request for SWAIG function call
|
1930
|
+
args = {}
|
1931
|
+
call_id = None
|
1932
|
+
raw_data = None
|
1933
|
+
|
1934
|
+
# Try to parse POST data from stdin for CGI
|
1935
|
+
import sys
|
1936
|
+
content_length = os.getenv('CONTENT_LENGTH')
|
1937
|
+
if content_length and content_length.isdigit():
|
1938
|
+
try:
|
1939
|
+
post_data = sys.stdin.read(int(content_length))
|
1940
|
+
if post_data:
|
1941
|
+
raw_data = json.loads(post_data)
|
1942
|
+
call_id = raw_data.get("call_id")
|
1943
|
+
|
1944
|
+
# Extract arguments like the FastAPI handler does
|
1945
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
1946
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
1947
|
+
args = raw_data["argument"]["parsed"][0]
|
1948
|
+
elif "raw" in raw_data["argument"]:
|
1949
|
+
try:
|
1950
|
+
args = json.loads(raw_data["argument"]["raw"])
|
1951
|
+
except Exception:
|
1952
|
+
pass
|
1953
|
+
except Exception:
|
1954
|
+
# If parsing fails, continue with empty args
|
1955
|
+
pass
|
1956
|
+
|
1957
|
+
return self._execute_swaig_function(path_info, args, call_id, raw_data)
|
1958
|
+
|
1959
|
+
elif mode == 'lambda':
|
1960
|
+
if event:
|
1961
|
+
path = event.get('pathParameters', {}).get('proxy', '') if event.get('pathParameters') else ''
|
1962
|
+
if not path:
|
1963
|
+
swml_response = self._render_swml()
|
1964
|
+
return {
|
1965
|
+
"statusCode": 200,
|
1966
|
+
"headers": {"Content-Type": "application/json"},
|
1967
|
+
"body": swml_response
|
1968
|
+
}
|
1969
|
+
else:
|
1970
|
+
# Parse Lambda event for SWAIG function call
|
1971
|
+
args = {}
|
1972
|
+
call_id = None
|
1973
|
+
raw_data = None
|
1974
|
+
|
1975
|
+
# Parse request body if present
|
1976
|
+
body_content = event.get('body')
|
1977
|
+
if body_content:
|
1978
|
+
try:
|
1979
|
+
if isinstance(body_content, str):
|
1980
|
+
raw_data = json.loads(body_content)
|
1981
|
+
else:
|
1982
|
+
raw_data = body_content
|
1983
|
+
|
1984
|
+
call_id = raw_data.get("call_id")
|
1985
|
+
|
1986
|
+
# Extract arguments like the FastAPI handler does
|
1987
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
1988
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
1989
|
+
args = raw_data["argument"]["parsed"][0]
|
1990
|
+
elif "raw" in raw_data["argument"]:
|
1991
|
+
try:
|
1992
|
+
args = json.loads(raw_data["argument"]["raw"])
|
1993
|
+
except Exception:
|
1994
|
+
pass
|
1995
|
+
except Exception:
|
1996
|
+
# If parsing fails, continue with empty args
|
1997
|
+
pass
|
1998
|
+
|
1999
|
+
result = self._execute_swaig_function(path, args, call_id, raw_data)
|
2000
|
+
return {
|
2001
|
+
"statusCode": 200,
|
2002
|
+
"headers": {"Content-Type": "application/json"},
|
2003
|
+
"body": json.dumps(result) if isinstance(result, dict) else str(result)
|
2004
|
+
}
|
2005
|
+
else:
|
2006
|
+
# Handle case when event is None (direct Lambda call with no event)
|
2007
|
+
swml_response = self._render_swml()
|
2008
|
+
return {
|
2009
|
+
"statusCode": 200,
|
2010
|
+
"headers": {"Content-Type": "application/json"},
|
2011
|
+
"body": swml_response
|
2012
|
+
}
|
2013
|
+
|
2014
|
+
elif mode in ['cloud_function', 'azure_function']:
|
2015
|
+
return self._handle_cloud_function_request(event)
|
2016
|
+
|
2017
|
+
except Exception as e:
|
2018
|
+
import logging
|
2019
|
+
logging.error(f"Error in serverless request handler: {e}")
|
2020
|
+
if mode == 'lambda':
|
2021
|
+
return {
|
2022
|
+
"statusCode": 500,
|
2023
|
+
"headers": {"Content-Type": "application/json"},
|
2024
|
+
"body": json.dumps({"error": str(e)})
|
2025
|
+
}
|
2026
|
+
else:
|
2027
|
+
raise
|
2028
|
+
|
2029
|
+
def _handle_cloud_function_request(self, request):
|
2030
|
+
"""
|
2031
|
+
Handle Cloud Function specific requests
|
2032
|
+
|
2033
|
+
Args:
|
2034
|
+
request: Cloud Function request object
|
2035
|
+
|
2036
|
+
Returns:
|
2037
|
+
Cloud Function response
|
2038
|
+
"""
|
2039
|
+
# Platform-specific implementation would go here
|
2040
|
+
# For now, return basic SWML response
|
2041
|
+
return self._render_swml()
|
2042
|
+
|
2043
|
+
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):
|
2044
|
+
"""
|
2045
|
+
Execute a SWAIG function in serverless context
|
2046
|
+
|
2047
|
+
Args:
|
2048
|
+
function_name: Name of the function to execute
|
2049
|
+
args: Function arguments dictionary
|
2050
|
+
call_id: Optional call ID
|
2051
|
+
raw_data: Optional raw request data
|
2052
|
+
|
2053
|
+
Returns:
|
2054
|
+
Function execution result
|
2055
|
+
"""
|
2056
|
+
import structlog
|
2057
|
+
|
2058
|
+
# Use the existing logger
|
2059
|
+
req_log = self.log.bind(
|
2060
|
+
endpoint="serverless_swaig",
|
2061
|
+
function=function_name
|
2062
|
+
)
|
2063
|
+
|
2064
|
+
if call_id:
|
2065
|
+
req_log = req_log.bind(call_id=call_id)
|
2066
|
+
|
2067
|
+
req_log.debug("serverless_function_call_received")
|
2068
|
+
|
2069
|
+
try:
|
2070
|
+
# Validate function exists
|
2071
|
+
if function_name not in self._swaig_functions:
|
2072
|
+
req_log.warning("function_not_found", available_functions=list(self._swaig_functions.keys()))
|
2073
|
+
return {"error": f"Function '{function_name}' not found"}
|
2074
|
+
|
2075
|
+
# Use empty args if not provided
|
2076
|
+
if args is None:
|
2077
|
+
args = {}
|
2078
|
+
|
2079
|
+
# Use empty raw_data if not provided, but include function call structure
|
2080
|
+
if raw_data is None:
|
2081
|
+
raw_data = {
|
2082
|
+
"function": function_name,
|
2083
|
+
"argument": {
|
2084
|
+
"parsed": [args] if args else [],
|
2085
|
+
"raw": json.dumps(args) if args else "{}"
|
2086
|
+
}
|
2087
|
+
}
|
2088
|
+
if call_id:
|
2089
|
+
raw_data["call_id"] = call_id
|
2090
|
+
|
2091
|
+
req_log.debug("executing_function", args=json.dumps(args))
|
2092
|
+
|
2093
|
+
# Call the function using the existing on_function_call method
|
2094
|
+
result = self.on_function_call(function_name, args, raw_data)
|
2095
|
+
|
2096
|
+
# Convert result to dict if needed (same logic as in _handle_swaig_request)
|
2097
|
+
if isinstance(result, SwaigFunctionResult):
|
2098
|
+
result_dict = result.to_dict()
|
2099
|
+
elif isinstance(result, dict):
|
2100
|
+
result_dict = result
|
2101
|
+
else:
|
2102
|
+
result_dict = {"response": str(result)}
|
2103
|
+
|
2104
|
+
req_log.info("serverless_function_executed_successfully")
|
2105
|
+
req_log.debug("function_result", result=json.dumps(result_dict))
|
2106
|
+
return result_dict
|
2107
|
+
|
2108
|
+
except Exception as e:
|
2109
|
+
req_log.error("serverless_function_execution_error", error=str(e))
|
2110
|
+
return {"error": str(e), "function": function_name}
|
2111
|
+
|
2112
|
+
def setup_graceful_shutdown(self) -> None:
|
2113
|
+
"""
|
2114
|
+
Setup signal handlers for graceful shutdown (useful for Kubernetes)
|
2115
|
+
"""
|
2116
|
+
def signal_handler(signum, frame):
|
2117
|
+
self.log.info("shutdown_signal_received", signal=signum)
|
2118
|
+
|
2119
|
+
# Perform cleanup
|
2120
|
+
try:
|
2121
|
+
# Close any open resources
|
2122
|
+
if hasattr(self, '_session_manager'):
|
2123
|
+
# Could add cleanup logic here
|
2124
|
+
pass
|
2125
|
+
|
2126
|
+
self.log.info("cleanup_completed")
|
2127
|
+
except Exception as e:
|
2128
|
+
self.log.error("cleanup_error", error=str(e))
|
2129
|
+
finally:
|
2130
|
+
sys.exit(0)
|
2131
|
+
|
2132
|
+
# Register handlers for common termination signals
|
2133
|
+
signal.signal(signal.SIGTERM, signal_handler) # Kubernetes sends this
|
2134
|
+
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
2135
|
+
|
2136
|
+
self.log.debug("graceful_shutdown_handlers_registered")
|
2137
|
+
|
1531
2138
|
def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
1532
2139
|
"""
|
1533
2140
|
Customization point for subclasses to modify SWML based on request data
|
@@ -1552,6 +2159,8 @@ class AgentBase(SWMLService):
|
|
1552
2159
|
Args:
|
1553
2160
|
router: FastAPI router to register routes with
|
1554
2161
|
"""
|
2162
|
+
# Health check endpoints are now registered directly on the main app
|
2163
|
+
|
1555
2164
|
# Root endpoint (handles both with and without trailing slash)
|
1556
2165
|
@router.get("/")
|
1557
2166
|
@router.post("/")
|