signalwire-agents 0.1.12__py3-none-any.whl → 0.1.14__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 +28 -11
- signalwire_agents/cli/build_search.py +174 -14
- signalwire_agents/cli/test_swaig.py +159 -114
- signalwire_agents/core/agent_base.py +7 -38
- signalwire_agents/core/logging_config.py +143 -14
- signalwire_agents/core/skill_manager.py +2 -2
- signalwire_agents/core/swaig_function.py +2 -3
- signalwire_agents/core/swml_renderer.py +43 -28
- signalwire_agents/core/swml_service.py +5 -45
- signalwire_agents/search/document_processor.py +275 -14
- signalwire_agents/search/index_builder.py +45 -10
- signalwire_agents/search/query_processor.py +27 -12
- signalwire_agents/skills/__init__.py +1 -1
- signalwire_agents/skills/native_vector_search/skill.py +24 -6
- signalwire_agents/skills/registry.py +58 -42
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/METADATA +1 -1
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/RECORD +22 -22
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/entry_points.txt +1 -1
- {signalwire_agents-0.1.12.data → signalwire_agents-0.1.14.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.12.dist-info → signalwire_agents-0.1.14.dist-info}/top_level.txt +0 -0
@@ -46,8 +46,14 @@ Examples:
|
|
46
46
|
swaig-test examples/my_agent.py --dump-swml --raw | jq '.sections.main[1].ai.SWAIG.functions'
|
47
47
|
"""
|
48
48
|
|
49
|
+
# CRITICAL: Set environment variable BEFORE any imports to suppress logs for --raw
|
49
50
|
import sys
|
50
51
|
import os
|
52
|
+
|
53
|
+
if "--raw" in sys.argv or "--dump-swml" in sys.argv:
|
54
|
+
os.environ["SIGNALWIRE_LOG_MODE"] = "off"
|
55
|
+
|
56
|
+
import warnings
|
51
57
|
import json
|
52
58
|
import importlib.util
|
53
59
|
import argparse
|
@@ -56,13 +62,18 @@ import time
|
|
56
62
|
import hashlib
|
57
63
|
import re
|
58
64
|
import requests
|
59
|
-
import warnings
|
60
65
|
from pathlib import Path
|
61
66
|
from typing import Dict, Any, Optional, List, Tuple
|
62
67
|
from datetime import datetime
|
63
68
|
import logging
|
64
69
|
import inspect
|
65
70
|
|
71
|
+
# Store original print function before any potential suppression
|
72
|
+
original_print = print
|
73
|
+
|
74
|
+
# Add the parent directory to the path so we can import signalwire_agents
|
75
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
76
|
+
|
66
77
|
try:
|
67
78
|
# Try to import the AgentBase class
|
68
79
|
from signalwire_agents.core.agent_base import AgentBase
|
@@ -72,22 +83,15 @@ except ImportError:
|
|
72
83
|
AgentBase = None
|
73
84
|
SwaigFunctionResult = None
|
74
85
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
# Add the parent directory to the path so we can import signalwire_agents
|
86
|
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
87
|
-
|
88
|
-
from signalwire_agents import AgentBase
|
89
|
-
from signalwire_agents.core.function_result import SwaigFunctionResult
|
90
|
-
|
86
|
+
# Reset logging configuration if --raw flag was set
|
87
|
+
# This must happen AFTER signalwire_agents imports but BEFORE any logging is used
|
88
|
+
if "--raw" in sys.argv or "--dump-swml" in sys.argv:
|
89
|
+
try:
|
90
|
+
from signalwire_agents.core.logging_config import reset_logging_configuration, configure_logging
|
91
|
+
reset_logging_configuration()
|
92
|
+
configure_logging() # Reconfigure with the new environment variable
|
93
|
+
except ImportError:
|
94
|
+
pass
|
91
95
|
|
92
96
|
# ===== MOCK REQUEST OBJECTS FOR DYNAMIC AGENT TESTING =====
|
93
97
|
|
@@ -666,7 +670,6 @@ def handle_dump_swml(agent: 'AgentBase', args: argparse.Namespace) -> int:
|
|
666
670
|
Exit code (0 for success, 1 for error)
|
667
671
|
"""
|
668
672
|
if not args.raw:
|
669
|
-
print("\nGenerating SWML document...")
|
670
673
|
if args.verbose:
|
671
674
|
print(f"Agent: {agent.get_name()}")
|
672
675
|
print(f"Route: {agent.route}")
|
@@ -767,20 +770,13 @@ def handle_dump_swml(agent: 'AgentBase', args: argparse.Namespace) -> int:
|
|
767
770
|
# Output only the raw JSON for piping to jq/yq
|
768
771
|
print(swml_doc)
|
769
772
|
else:
|
770
|
-
#
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
# Parse and show formatted JSON for better readability
|
778
|
-
try:
|
779
|
-
swml_parsed = json.loads(swml_doc)
|
780
|
-
print("\nFormatted SWML:")
|
781
|
-
print(json.dumps(swml_parsed, indent=2))
|
782
|
-
except json.JSONDecodeError:
|
783
|
-
print("\nNote: SWML document is not valid JSON format")
|
773
|
+
# Output formatted JSON (like raw but pretty-printed)
|
774
|
+
try:
|
775
|
+
swml_parsed = json.loads(swml_doc)
|
776
|
+
print(json.dumps(swml_parsed, indent=2))
|
777
|
+
except json.JSONDecodeError:
|
778
|
+
# If not valid JSON, show raw
|
779
|
+
print(swml_doc)
|
784
780
|
|
785
781
|
return 0
|
786
782
|
|
@@ -1375,6 +1371,94 @@ def execute_external_webhook_function(func: 'SWAIGFunction', function_name: str,
|
|
1375
1371
|
return {"error": error_msg}
|
1376
1372
|
|
1377
1373
|
|
1374
|
+
def display_agent_tools(agent: 'AgentBase', verbose: bool = False) -> None:
|
1375
|
+
"""
|
1376
|
+
Display the available SWAIG functions for an agent
|
1377
|
+
|
1378
|
+
Args:
|
1379
|
+
agent: The agent instance
|
1380
|
+
verbose: Whether to show verbose details
|
1381
|
+
"""
|
1382
|
+
print("\nAvailable SWAIG functions:")
|
1383
|
+
if hasattr(agent, '_swaig_functions') and agent._swaig_functions:
|
1384
|
+
for name, func in agent._swaig_functions.items():
|
1385
|
+
if isinstance(func, dict):
|
1386
|
+
# DataMap function
|
1387
|
+
description = func.get('description', 'DataMap function (serverless)')
|
1388
|
+
print(f" {name} - {description}")
|
1389
|
+
|
1390
|
+
# Show parameters for DataMap functions
|
1391
|
+
if 'parameters' in func and func['parameters']:
|
1392
|
+
params = func['parameters']
|
1393
|
+
# Handle both formats: direct properties dict or full schema
|
1394
|
+
if 'properties' in params:
|
1395
|
+
properties = params['properties']
|
1396
|
+
required_fields = params.get('required', [])
|
1397
|
+
else:
|
1398
|
+
properties = params
|
1399
|
+
required_fields = []
|
1400
|
+
|
1401
|
+
if properties:
|
1402
|
+
print(f" Parameters:")
|
1403
|
+
for param_name, param_def in properties.items():
|
1404
|
+
param_type = param_def.get('type', 'unknown')
|
1405
|
+
param_desc = param_def.get('description', 'No description')
|
1406
|
+
is_required = param_name in required_fields
|
1407
|
+
required_marker = " (required)" if is_required else ""
|
1408
|
+
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
1409
|
+
else:
|
1410
|
+
print(f" Parameters: None")
|
1411
|
+
else:
|
1412
|
+
print(f" Parameters: None")
|
1413
|
+
|
1414
|
+
if verbose:
|
1415
|
+
print(f" Config: {json.dumps(func, indent=6)}")
|
1416
|
+
else:
|
1417
|
+
# Regular SWAIG function
|
1418
|
+
func_type = ""
|
1419
|
+
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
1420
|
+
func_type = " (EXTERNAL webhook)"
|
1421
|
+
elif hasattr(func, 'webhook_url') and func.webhook_url:
|
1422
|
+
func_type = " (webhook)"
|
1423
|
+
else:
|
1424
|
+
func_type = " (LOCAL webhook)"
|
1425
|
+
|
1426
|
+
print(f" {name} - {func.description}{func_type}")
|
1427
|
+
|
1428
|
+
# Show external URL if applicable
|
1429
|
+
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
1430
|
+
print(f" External URL: {func.webhook_url}")
|
1431
|
+
|
1432
|
+
# Show parameters
|
1433
|
+
if hasattr(func, 'parameters') and func.parameters:
|
1434
|
+
params = func.parameters
|
1435
|
+
# Handle both formats: direct properties dict or full schema
|
1436
|
+
if 'properties' in params:
|
1437
|
+
properties = params['properties']
|
1438
|
+
required_fields = params.get('required', [])
|
1439
|
+
else:
|
1440
|
+
properties = params
|
1441
|
+
required_fields = []
|
1442
|
+
|
1443
|
+
if properties:
|
1444
|
+
print(f" Parameters:")
|
1445
|
+
for param_name, param_def in properties.items():
|
1446
|
+
param_type = param_def.get('type', 'unknown')
|
1447
|
+
param_desc = param_def.get('description', 'No description')
|
1448
|
+
is_required = param_name in required_fields
|
1449
|
+
required_marker = " (required)" if is_required else ""
|
1450
|
+
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
1451
|
+
else:
|
1452
|
+
print(f" Parameters: None")
|
1453
|
+
else:
|
1454
|
+
print(f" Parameters: None")
|
1455
|
+
|
1456
|
+
if verbose:
|
1457
|
+
print(f" Function object: {func}")
|
1458
|
+
else:
|
1459
|
+
print(" No SWAIG functions registered")
|
1460
|
+
|
1461
|
+
|
1378
1462
|
def discover_agents_in_file(agent_path: str) -> List[Dict[str, Any]]:
|
1379
1463
|
"""
|
1380
1464
|
Discover all available agents in a Python file without instantiating them
|
@@ -2315,7 +2399,29 @@ Examples:
|
|
2315
2399
|
|
2316
2400
|
print()
|
2317
2401
|
|
2318
|
-
if
|
2402
|
+
# Show tools if there's only one agent or if --agent-class is specified
|
2403
|
+
show_tools = False
|
2404
|
+
selected_agent = None
|
2405
|
+
|
2406
|
+
if len(agents) == 1:
|
2407
|
+
# Single agent - show tools automatically
|
2408
|
+
show_tools = True
|
2409
|
+
selected_agent = agents[0]['object']
|
2410
|
+
print("This file contains a single agent, no --agent-class needed.")
|
2411
|
+
elif args.agent_class:
|
2412
|
+
# Specific agent class requested - show tools for that agent
|
2413
|
+
for agent_info in agents:
|
2414
|
+
if agent_info['class_name'] == args.agent_class:
|
2415
|
+
show_tools = True
|
2416
|
+
selected_agent = agent_info['object']
|
2417
|
+
break
|
2418
|
+
|
2419
|
+
if not selected_agent:
|
2420
|
+
print(f"Error: Agent class '{args.agent_class}' not found.")
|
2421
|
+
print(f"Available agents: {[a['class_name'] for a in agents]}")
|
2422
|
+
return 1
|
2423
|
+
else:
|
2424
|
+
# Multiple agents, no specific class - show usage examples
|
2319
2425
|
print("To use a specific agent with this tool:")
|
2320
2426
|
print(f" swaig-test {args.agent_path} [tool_name] [args] --agent-class <AgentClassName>")
|
2321
2427
|
print()
|
@@ -2324,8 +2430,24 @@ Examples:
|
|
2324
2430
|
print(f" swaig-test {args.agent_path} --list-tools --agent-class {agent_info['class_name']}")
|
2325
2431
|
print(f" swaig-test {args.agent_path} --dump-swml --agent-class {agent_info['class_name']}")
|
2326
2432
|
print()
|
2327
|
-
|
2328
|
-
|
2433
|
+
|
2434
|
+
# Show tools if we have a selected agent
|
2435
|
+
if show_tools and selected_agent:
|
2436
|
+
try:
|
2437
|
+
# If it's a class, try to instantiate it
|
2438
|
+
if not isinstance(selected_agent, AgentBase):
|
2439
|
+
if isinstance(selected_agent, type) and issubclass(selected_agent, AgentBase):
|
2440
|
+
selected_agent = selected_agent()
|
2441
|
+
else:
|
2442
|
+
print(f"Warning: Cannot instantiate agent to show tools")
|
2443
|
+
return 0
|
2444
|
+
|
2445
|
+
display_agent_tools(selected_agent, args.verbose)
|
2446
|
+
except Exception as e:
|
2447
|
+
print(f"Warning: Could not load agent tools: {e}")
|
2448
|
+
if args.verbose:
|
2449
|
+
import traceback
|
2450
|
+
traceback.print_exc()
|
2329
2451
|
|
2330
2452
|
return 0
|
2331
2453
|
|
@@ -2374,84 +2496,7 @@ Examples:
|
|
2374
2496
|
|
2375
2497
|
# List tools if requested
|
2376
2498
|
if args.list_tools:
|
2377
|
-
|
2378
|
-
if hasattr(agent, '_swaig_functions') and agent._swaig_functions:
|
2379
|
-
for name, func in agent._swaig_functions.items():
|
2380
|
-
if isinstance(func, dict):
|
2381
|
-
# DataMap function
|
2382
|
-
description = func.get('description', 'DataMap function (serverless)')
|
2383
|
-
print(f" {name} - {description}")
|
2384
|
-
|
2385
|
-
# Show parameters for DataMap functions
|
2386
|
-
if 'parameters' in func and func['parameters']:
|
2387
|
-
params = func['parameters']
|
2388
|
-
# Handle both formats: direct properties dict or full schema
|
2389
|
-
if 'properties' in params:
|
2390
|
-
properties = params['properties']
|
2391
|
-
required_fields = params.get('required', [])
|
2392
|
-
else:
|
2393
|
-
properties = params
|
2394
|
-
required_fields = []
|
2395
|
-
|
2396
|
-
if properties:
|
2397
|
-
print(f" Parameters:")
|
2398
|
-
for param_name, param_def in properties.items():
|
2399
|
-
param_type = param_def.get('type', 'unknown')
|
2400
|
-
param_desc = param_def.get('description', 'No description')
|
2401
|
-
is_required = param_name in required_fields
|
2402
|
-
required_marker = " (required)" if is_required else ""
|
2403
|
-
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
2404
|
-
else:
|
2405
|
-
print(f" Parameters: None")
|
2406
|
-
else:
|
2407
|
-
print(f" Parameters: None")
|
2408
|
-
|
2409
|
-
if args.verbose:
|
2410
|
-
print(f" Config: {json.dumps(func, indent=6)}")
|
2411
|
-
else:
|
2412
|
-
# Regular SWAIG function
|
2413
|
-
func_type = ""
|
2414
|
-
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
2415
|
-
func_type = " (EXTERNAL webhook)"
|
2416
|
-
elif hasattr(func, 'webhook_url') and func.webhook_url:
|
2417
|
-
func_type = " (webhook)"
|
2418
|
-
else:
|
2419
|
-
func_type = " (LOCAL webhook)"
|
2420
|
-
|
2421
|
-
print(f" {name} - {func.description}{func_type}")
|
2422
|
-
|
2423
|
-
# Show external URL if applicable
|
2424
|
-
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
2425
|
-
print(f" External URL: {func.webhook_url}")
|
2426
|
-
|
2427
|
-
# Show parameters
|
2428
|
-
if hasattr(func, 'parameters') and func.parameters:
|
2429
|
-
params = func.parameters
|
2430
|
-
# Handle both formats: direct properties dict or full schema
|
2431
|
-
if 'properties' in params:
|
2432
|
-
properties = params['properties']
|
2433
|
-
required_fields = params.get('required', [])
|
2434
|
-
else:
|
2435
|
-
properties = params
|
2436
|
-
required_fields = []
|
2437
|
-
|
2438
|
-
if properties:
|
2439
|
-
print(f" Parameters:")
|
2440
|
-
for param_name, param_def in properties.items():
|
2441
|
-
param_type = param_def.get('type', 'unknown')
|
2442
|
-
param_desc = param_def.get('description', 'No description')
|
2443
|
-
is_required = param_name in required_fields
|
2444
|
-
required_marker = " (required)" if is_required else ""
|
2445
|
-
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
2446
|
-
else:
|
2447
|
-
print(f" Parameters: None")
|
2448
|
-
else:
|
2449
|
-
print(f" Parameters: None")
|
2450
|
-
|
2451
|
-
if args.verbose:
|
2452
|
-
print(f" Function object: {func}")
|
2453
|
-
else:
|
2454
|
-
print(" No SWAIG functions registered")
|
2499
|
+
display_agent_tools(agent, args.verbose)
|
2455
2500
|
return 0
|
2456
2501
|
|
2457
2502
|
# Dump SWML if requested
|
@@ -2606,4 +2651,4 @@ def console_entry_point():
|
|
2606
2651
|
|
2607
2652
|
|
2608
2653
|
if __name__ == "__main__":
|
2609
|
-
sys.exit(main())
|
2654
|
+
sys.exit(main())
|
@@ -44,31 +44,7 @@ except ImportError:
|
|
44
44
|
"uvicorn is required. Install it with: pip install uvicorn"
|
45
45
|
)
|
46
46
|
|
47
|
-
|
48
|
-
import structlog
|
49
|
-
# Configure structlog only if not already configured
|
50
|
-
if not structlog.is_configured():
|
51
|
-
structlog.configure(
|
52
|
-
processors=[
|
53
|
-
structlog.stdlib.filter_by_level,
|
54
|
-
structlog.stdlib.add_logger_name,
|
55
|
-
structlog.stdlib.add_log_level,
|
56
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
57
|
-
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
58
|
-
structlog.processors.StackInfoRenderer(),
|
59
|
-
structlog.processors.format_exc_info,
|
60
|
-
structlog.processors.UnicodeDecoder(),
|
61
|
-
structlog.dev.ConsoleRenderer()
|
62
|
-
],
|
63
|
-
context_class=dict,
|
64
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
65
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
66
|
-
cache_logger_on_first_use=True,
|
67
|
-
)
|
68
|
-
except ImportError:
|
69
|
-
raise ImportError(
|
70
|
-
"structlog is required. Install it with: pip install structlog"
|
71
|
-
)
|
47
|
+
|
72
48
|
|
73
49
|
from signalwire_agents.core.pom_builder import PomBuilder
|
74
50
|
from signalwire_agents.core.swaig_function import SWAIGFunction
|
@@ -82,8 +58,8 @@ from signalwire_agents.core.skill_manager import SkillManager
|
|
82
58
|
from signalwire_agents.utils.schema_utils import SchemaUtils
|
83
59
|
from signalwire_agents.core.logging_config import get_logger, get_execution_mode
|
84
60
|
|
85
|
-
# Create a logger
|
86
|
-
logger =
|
61
|
+
# Create a logger using centralized system
|
62
|
+
logger = get_logger("agent_base")
|
87
63
|
|
88
64
|
class EphemeralAgentConfig:
|
89
65
|
"""
|
@@ -1363,8 +1339,8 @@ class AgentBase(SWMLService):
|
|
1363
1339
|
host = self.host
|
1364
1340
|
base_url = f"http://{host}:{self.port}{self.route}"
|
1365
1341
|
|
1366
|
-
# Add auth if requested (
|
1367
|
-
if include_auth
|
1342
|
+
# Add auth if requested (applies to all modes now)
|
1343
|
+
if include_auth:
|
1368
1344
|
username, password = self._basic_auth
|
1369
1345
|
url = urlparse(base_url)
|
1370
1346
|
return url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
@@ -1386,11 +1362,8 @@ class AgentBase(SWMLService):
|
|
1386
1362
|
mode = get_execution_mode()
|
1387
1363
|
|
1388
1364
|
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
|
1365
|
+
# In serverless mode, use the serverless-appropriate URL with auth
|
1366
|
+
base_url = self.get_full_url(include_auth=True)
|
1394
1367
|
|
1395
1368
|
# Ensure the endpoint has a trailing slash to prevent redirects
|
1396
1369
|
if endpoint in ["swaig", "post_prompt"]:
|
@@ -2053,8 +2026,6 @@ class AgentBase(SWMLService):
|
|
2053
2026
|
Returns:
|
2054
2027
|
Function execution result
|
2055
2028
|
"""
|
2056
|
-
import structlog
|
2057
|
-
|
2058
2029
|
# Use the existing logger
|
2059
2030
|
req_log = self.log.bind(
|
2060
2031
|
endpoint="serverless_swaig",
|
@@ -2227,8 +2198,6 @@ class AgentBase(SWMLService):
|
|
2227
2198
|
|
2228
2199
|
self.log.info("callback_endpoint_registered", path=callback_path)
|
2229
2200
|
|
2230
|
-
@classmethod
|
2231
|
-
|
2232
2201
|
# ----------------------------------------------------------------------
|
2233
2202
|
# AI Verb Configuration Methods
|
2234
2203
|
# ----------------------------------------------------------------------
|
@@ -89,12 +89,44 @@ class StructuredLoggerWrapper:
|
|
89
89
|
# Also support the 'warn' alias
|
90
90
|
warn = warning
|
91
91
|
|
92
|
+
def bind(self, **kwargs) -> 'StructuredLoggerWrapper':
|
93
|
+
"""
|
94
|
+
Create a new logger instance with bound context data
|
95
|
+
|
96
|
+
This maintains compatibility with structlog's bind() method.
|
97
|
+
The bound data will be included in all subsequent log messages.
|
98
|
+
"""
|
99
|
+
# Create a new wrapper that includes the bound context
|
100
|
+
return BoundStructuredLoggerWrapper(self._logger, kwargs)
|
101
|
+
|
92
102
|
# Support direct access to underlying logger attributes if needed
|
93
103
|
def __getattr__(self, name: str) -> Any:
|
94
104
|
"""Delegate any unknown attributes to the underlying logger"""
|
95
105
|
return getattr(self._logger, name)
|
96
106
|
|
97
107
|
|
108
|
+
class BoundStructuredLoggerWrapper(StructuredLoggerWrapper):
|
109
|
+
"""
|
110
|
+
A structured logger wrapper that includes bound context data in all messages
|
111
|
+
"""
|
112
|
+
|
113
|
+
def __init__(self, logger: logging.Logger, bound_data: Dict[str, Any]):
|
114
|
+
super().__init__(logger)
|
115
|
+
self._bound_data = bound_data
|
116
|
+
|
117
|
+
def _format_structured_message(self, message: str, **kwargs) -> str:
|
118
|
+
"""Format a message with both bound data and additional keyword arguments"""
|
119
|
+
# Combine bound data with additional kwargs
|
120
|
+
all_kwargs = {**self._bound_data, **kwargs}
|
121
|
+
return super()._format_structured_message(message, **all_kwargs)
|
122
|
+
|
123
|
+
def bind(self, **kwargs) -> 'BoundStructuredLoggerWrapper':
|
124
|
+
"""Create a new logger with additional bound context"""
|
125
|
+
# Combine existing bound data with new data
|
126
|
+
new_bound_data = {**self._bound_data, **kwargs}
|
127
|
+
return BoundStructuredLoggerWrapper(self._logger, new_bound_data)
|
128
|
+
|
129
|
+
|
98
130
|
def get_execution_mode() -> str:
|
99
131
|
"""
|
100
132
|
Determine the execution mode based on environment variables
|
@@ -111,6 +143,16 @@ def get_execution_mode() -> str:
|
|
111
143
|
return 'server'
|
112
144
|
|
113
145
|
|
146
|
+
def reset_logging_configuration():
|
147
|
+
"""
|
148
|
+
Reset the logging configuration flag to allow reconfiguration
|
149
|
+
|
150
|
+
This is useful when environment variables change after initial configuration.
|
151
|
+
"""
|
152
|
+
global _logging_configured
|
153
|
+
_logging_configured = False
|
154
|
+
|
155
|
+
|
114
156
|
def configure_logging():
|
115
157
|
"""
|
116
158
|
Configure logging system once, globally, based on environment variables
|
@@ -182,31 +224,39 @@ def _configure_off_mode():
|
|
182
224
|
|
183
225
|
|
184
226
|
def _configure_stderr_mode(log_level: str):
|
185
|
-
"""Configure logging to stderr"""
|
227
|
+
"""Configure logging to stderr with colored formatting"""
|
186
228
|
# Clear existing handlers
|
187
229
|
logging.getLogger().handlers.clear()
|
188
230
|
|
189
231
|
# Convert log level
|
190
232
|
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
191
233
|
|
192
|
-
#
|
193
|
-
logging.
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
)
|
234
|
+
# Create handler with colored formatter
|
235
|
+
handler = logging.StreamHandler(sys.stderr)
|
236
|
+
handler.setFormatter(ColoredFormatter())
|
237
|
+
|
238
|
+
# Configure root logger
|
239
|
+
root_logger = logging.getLogger()
|
240
|
+
root_logger.setLevel(numeric_level)
|
241
|
+
root_logger.addHandler(handler)
|
198
242
|
|
199
243
|
|
200
244
|
def _configure_default_mode(log_level: str):
|
201
|
-
"""Configure standard logging behavior"""
|
245
|
+
"""Configure standard logging behavior with colored formatting"""
|
246
|
+
# Clear existing handlers
|
247
|
+
logging.getLogger().handlers.clear()
|
248
|
+
|
202
249
|
# Convert log level
|
203
250
|
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
204
251
|
|
205
|
-
#
|
206
|
-
logging.
|
207
|
-
|
208
|
-
|
209
|
-
|
252
|
+
# Create handler with colored formatter
|
253
|
+
handler = logging.StreamHandler()
|
254
|
+
handler.setFormatter(ColoredFormatter())
|
255
|
+
|
256
|
+
# Configure root logger
|
257
|
+
root_logger = logging.getLogger()
|
258
|
+
root_logger.setLevel(numeric_level)
|
259
|
+
root_logger.addHandler(handler)
|
210
260
|
|
211
261
|
|
212
262
|
def get_logger(name: str) -> StructuredLoggerWrapper:
|
@@ -229,4 +279,83 @@ def get_logger(name: str) -> StructuredLoggerWrapper:
|
|
229
279
|
python_logger = logging.getLogger(name)
|
230
280
|
|
231
281
|
# Wrap it with our structured logging interface
|
232
|
-
return StructuredLoggerWrapper(python_logger)
|
282
|
+
return StructuredLoggerWrapper(python_logger)
|
283
|
+
|
284
|
+
|
285
|
+
class ColoredFormatter(logging.Formatter):
|
286
|
+
"""
|
287
|
+
A beautiful colored logging formatter that makes logs easy to read and visually appealing
|
288
|
+
"""
|
289
|
+
|
290
|
+
# ANSI color codes
|
291
|
+
COLORS = {
|
292
|
+
'DEBUG': '\033[36m', # Cyan
|
293
|
+
'INFO': '\033[32m', # Green
|
294
|
+
'WARNING': '\033[33m', # Yellow
|
295
|
+
'ERROR': '\033[31m', # Red
|
296
|
+
'CRITICAL': '\033[35m', # Magenta
|
297
|
+
'RESET': '\033[0m', # Reset
|
298
|
+
'BOLD': '\033[1m', # Bold
|
299
|
+
'DIM': '\033[2m', # Dim
|
300
|
+
'WHITE': '\033[37m', # White
|
301
|
+
'BLUE': '\033[34m', # Blue
|
302
|
+
'BLACK': '\033[30m', # Black (for brackets)
|
303
|
+
}
|
304
|
+
|
305
|
+
def __init__(self):
|
306
|
+
super().__init__()
|
307
|
+
|
308
|
+
def format(self, record):
|
309
|
+
# Check if we should use colors (not in raw mode, and stdout is a tty)
|
310
|
+
use_colors = (
|
311
|
+
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and
|
312
|
+
os.getenv('SIGNALWIRE_LOG_MODE') != 'off' and
|
313
|
+
'--raw' not in sys.argv and '--dump-swml' not in sys.argv
|
314
|
+
)
|
315
|
+
|
316
|
+
if use_colors:
|
317
|
+
# Get colors
|
318
|
+
level_color = self.COLORS.get(record.levelname, self.COLORS['WHITE'])
|
319
|
+
reset = self.COLORS['RESET']
|
320
|
+
dim = self.COLORS['DIM']
|
321
|
+
bold = self.COLORS['BOLD']
|
322
|
+
blue = self.COLORS['BLUE']
|
323
|
+
black = self.COLORS['BLACK']
|
324
|
+
|
325
|
+
# Format timestamp in a compact, readable way
|
326
|
+
timestamp = self.formatTime(record, '%H:%M:%S')
|
327
|
+
|
328
|
+
# Format level with appropriate color and consistent width
|
329
|
+
level_name = f"{level_color}{record.levelname:<8}{reset}"
|
330
|
+
|
331
|
+
# Format logger name - keep it short and readable
|
332
|
+
logger_name = record.name
|
333
|
+
if len(logger_name) > 15:
|
334
|
+
# Truncate long logger names but keep the end (most specific part)
|
335
|
+
logger_name = "..." + logger_name[-12:]
|
336
|
+
|
337
|
+
# Get function and line info if available
|
338
|
+
func_info = ""
|
339
|
+
if hasattr(record, 'funcName') and hasattr(record, 'lineno'):
|
340
|
+
func_name = getattr(record, 'funcName', '')
|
341
|
+
line_no = getattr(record, 'lineno', 0)
|
342
|
+
if func_name and func_name != '<module>':
|
343
|
+
func_info = f" {dim}({func_name}:{line_no}){reset}"
|
344
|
+
|
345
|
+
# Format the message
|
346
|
+
message = record.getMessage()
|
347
|
+
|
348
|
+
# Create the final formatted message with a clean, readable layout
|
349
|
+
formatted = (
|
350
|
+
f"{black}[{reset}{dim}{timestamp}{reset}{black}]{reset} "
|
351
|
+
f"{level_name} "
|
352
|
+
f"{blue}{logger_name:<15}{reset}"
|
353
|
+
f"{func_info} "
|
354
|
+
f"{message}"
|
355
|
+
)
|
356
|
+
|
357
|
+
return formatted
|
358
|
+
else:
|
359
|
+
# Non-colored format (fallback for files, pipes, etc.)
|
360
|
+
timestamp = self.formatTime(record, '%Y-%m-%d %H:%M:%S')
|
361
|
+
return f"{timestamp} {record.levelname:<8} {record.name} {record.getMessage()}"
|
@@ -8,7 +8,7 @@ See LICENSE file in the project root for full license information.
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
from typing import Dict, List, Type, Any, Optional
|
11
|
-
import
|
11
|
+
from signalwire_agents.core.logging_config import get_logger
|
12
12
|
from signalwire_agents.core.skill_base import SkillBase
|
13
13
|
|
14
14
|
class SkillManager:
|
@@ -17,7 +17,7 @@ class SkillManager:
|
|
17
17
|
def __init__(self, agent):
|
18
18
|
self.agent = agent
|
19
19
|
self.loaded_skills: Dict[str, SkillBase] = {}
|
20
|
-
self.logger =
|
20
|
+
self.logger = get_logger("skill_manager")
|
21
21
|
|
22
22
|
def load_skill(self, skill_name: str, skill_class: Type[SkillBase] = None, params: Optional[Dict[str, Any]] = None) -> tuple[bool, str]:
|
23
23
|
"""
|
@@ -15,6 +15,8 @@ from typing import Dict, Any, Optional, Callable, List, Type, Union
|
|
15
15
|
import inspect
|
16
16
|
import logging
|
17
17
|
|
18
|
+
# Import here to avoid circular imports
|
19
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
18
20
|
|
19
21
|
class SWAIGFunction:
|
20
22
|
"""
|
@@ -101,9 +103,6 @@ class SWAIGFunction:
|
|
101
103
|
# Call the handler with both args and raw_data
|
102
104
|
result = self.handler(args, raw_data)
|
103
105
|
|
104
|
-
# Import here to avoid circular imports
|
105
|
-
from signalwire_agents.core.function_result import SwaigFunctionResult
|
106
|
-
|
107
106
|
# Handle different result types - everything must end up as a SwaigFunctionResult
|
108
107
|
if isinstance(result, SwaigFunctionResult):
|
109
108
|
# Already a SwaigFunctionResult - just convert to dict
|