signalwire-agents 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +1 -1
  3. signalwire_agents/core/__init__.py +29 -0
  4. signalwire_agents/core/agent_base.py +2541 -0
  5. signalwire_agents/core/function_result.py +123 -0
  6. signalwire_agents/core/pom_builder.py +204 -0
  7. signalwire_agents/core/security/__init__.py +9 -0
  8. signalwire_agents/core/security/session_manager.py +179 -0
  9. signalwire_agents/core/state/__init__.py +17 -0
  10. signalwire_agents/core/state/file_state_manager.py +219 -0
  11. signalwire_agents/core/state/state_manager.py +101 -0
  12. signalwire_agents/core/swaig_function.py +172 -0
  13. signalwire_agents/core/swml_builder.py +214 -0
  14. signalwire_agents/core/swml_handler.py +227 -0
  15. signalwire_agents/core/swml_renderer.py +368 -0
  16. signalwire_agents/core/swml_service.py +1057 -0
  17. signalwire_agents/prefabs/__init__.py +26 -0
  18. signalwire_agents/prefabs/concierge.py +267 -0
  19. signalwire_agents/prefabs/faq_bot.py +305 -0
  20. signalwire_agents/prefabs/info_gatherer.py +263 -0
  21. signalwire_agents/prefabs/receptionist.py +294 -0
  22. signalwire_agents/prefabs/survey.py +378 -0
  23. signalwire_agents/utils/__init__.py +9 -0
  24. signalwire_agents/utils/pom_utils.py +9 -0
  25. signalwire_agents/utils/schema_utils.py +357 -0
  26. signalwire_agents/utils/token_generators.py +9 -0
  27. signalwire_agents/utils/validators.py +9 -0
  28. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +1 -1
  29. signalwire_agents-0.1.2.dist-info/RECORD +34 -0
  30. signalwire_agents-0.1.1.dist-info/RECORD +0 -9
  31. {signalwire_agents-0.1.1.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
  32. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +0 -0
  33. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/licenses/LICENSE +0 -0
  34. {signalwire_agents-0.1.1.dist-info → signalwire_agents-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,219 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """
11
+ File-based implementation of the StateManager interface
12
+ """
13
+
14
+ import os
15
+ import json
16
+ import tempfile
17
+ import time
18
+ from datetime import datetime, timedelta
19
+ from typing import Dict, Any, Optional, List
20
+
21
+ from .state_manager import StateManager
22
+
23
+
24
+ class FileStateManager(StateManager):
25
+ """
26
+ File-based state manager implementation
27
+
28
+ This implementation stores state data as JSON files in a directory.
29
+ Each call's state is stored in a separate file named by call_id.
30
+ Files older than expiry_days are automatically cleaned up.
31
+
32
+ This is suitable for development and low-volume deployments.
33
+ For production, consider using database or Redis implementations.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ storage_dir: Optional[str] = None,
39
+ expiry_days: float = 1.0
40
+ ):
41
+ """
42
+ Initialize the file state manager
43
+
44
+ Args:
45
+ storage_dir: Directory to store state files (default: system temp dir)
46
+ expiry_days: Days after which state files are considered expired
47
+ """
48
+ self.storage_dir = storage_dir or os.path.join(tempfile.gettempdir(), "signalwire_state")
49
+ self.expiry_days = expiry_days
50
+
51
+ # Create storage directory if it doesn't exist
52
+ if not os.path.exists(self.storage_dir):
53
+ os.makedirs(self.storage_dir)
54
+
55
+ def _get_file_path(self, call_id: str) -> str:
56
+ """Get the file path for a call_id"""
57
+ # Sanitize call_id to ensure it's safe for a filename
58
+ sanitized_id = "".join(c for c in call_id if c.isalnum() or c in "-_.")
59
+ return os.path.join(self.storage_dir, f"{sanitized_id}.json")
60
+
61
+ def store(self, call_id: str, data: Dict[str, Any]) -> bool:
62
+ """
63
+ Store state data for a call
64
+
65
+ Args:
66
+ call_id: Unique identifier for the call
67
+ data: Dictionary of state data to store
68
+
69
+ Returns:
70
+ True if successful, False otherwise
71
+ """
72
+ try:
73
+ # Add metadata including timestamp
74
+ state_data = {
75
+ "call_id": call_id,
76
+ "created_at": datetime.now().isoformat(),
77
+ "last_updated": datetime.now().isoformat(),
78
+ "data": data
79
+ }
80
+
81
+ file_path = self._get_file_path(call_id)
82
+ with open(file_path, "w") as f:
83
+ json.dump(state_data, f, indent=2)
84
+ return True
85
+ except Exception as e:
86
+ print(f"Error storing state for call {call_id}: {e}")
87
+ return False
88
+
89
+ def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
90
+ """
91
+ Retrieve state data for a call
92
+
93
+ Args:
94
+ call_id: Unique identifier for the call
95
+
96
+ Returns:
97
+ Dictionary of state data or None if not found
98
+ """
99
+ file_path = self._get_file_path(call_id)
100
+ if not os.path.exists(file_path):
101
+ return None
102
+
103
+ try:
104
+ with open(file_path, "r") as f:
105
+ state_data = json.load(f)
106
+
107
+ # Check if the file is expired
108
+ created_at = datetime.fromisoformat(state_data["created_at"])
109
+ if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
110
+ # Expired, so delete it and return None
111
+ os.remove(file_path)
112
+ return None
113
+
114
+ return state_data["data"]
115
+ except Exception as e:
116
+ print(f"Error retrieving state for call {call_id}: {e}")
117
+ return None
118
+
119
+ def update(self, call_id: str, data: Dict[str, Any]) -> bool:
120
+ """
121
+ Update state data for a call
122
+
123
+ Args:
124
+ call_id: Unique identifier for the call
125
+ data: Dictionary of state data to update (merged with existing)
126
+
127
+ Returns:
128
+ True if successful, False otherwise
129
+ """
130
+ file_path = self._get_file_path(call_id)
131
+ if not os.path.exists(file_path):
132
+ # If no existing data, just store new data
133
+ return self.store(call_id, data)
134
+
135
+ try:
136
+ # Read existing data
137
+ with open(file_path, "r") as f:
138
+ state_data = json.load(f)
139
+
140
+ # Update the data (deep merge)
141
+ existing_data = state_data["data"]
142
+ self._deep_update(existing_data, data)
143
+
144
+ # Update metadata
145
+ state_data["last_updated"] = datetime.now().isoformat()
146
+ state_data["data"] = existing_data
147
+
148
+ # Write back to file
149
+ with open(file_path, "w") as f:
150
+ json.dump(state_data, f, indent=2)
151
+
152
+ return True
153
+ except Exception as e:
154
+ print(f"Error updating state for call {call_id}: {e}")
155
+ return False
156
+
157
+ def delete(self, call_id: str) -> bool:
158
+ """
159
+ Delete state data for a call
160
+
161
+ Args:
162
+ call_id: Unique identifier for the call
163
+
164
+ Returns:
165
+ True if successful, False otherwise
166
+ """
167
+ file_path = self._get_file_path(call_id)
168
+ if not os.path.exists(file_path):
169
+ return False
170
+
171
+ try:
172
+ os.remove(file_path)
173
+ return True
174
+ except Exception as e:
175
+ print(f"Error deleting state for call {call_id}: {e}")
176
+ return False
177
+
178
+ def cleanup_expired(self) -> int:
179
+ """
180
+ Clean up expired state files
181
+
182
+ Returns:
183
+ Number of expired files cleaned up
184
+ """
185
+ count = 0
186
+ try:
187
+ # Get all state files
188
+ for filename in os.listdir(self.storage_dir):
189
+ if not filename.endswith(".json"):
190
+ continue
191
+
192
+ file_path = os.path.join(self.storage_dir, filename)
193
+
194
+ try:
195
+ # Read the file to check creation time
196
+ with open(file_path, "r") as f:
197
+ state_data = json.load(f)
198
+
199
+ # Check if the file is expired
200
+ created_at = datetime.fromisoformat(state_data["created_at"])
201
+ if (datetime.now() - created_at) > timedelta(days=self.expiry_days):
202
+ os.remove(file_path)
203
+ count += 1
204
+ except Exception:
205
+ # Skip files that can't be processed
206
+ continue
207
+
208
+ return count
209
+ except Exception as e:
210
+ print(f"Error cleaning up expired state files: {e}")
211
+ return count
212
+
213
+ def _deep_update(self, d: Dict, u: Dict) -> None:
214
+ """Recursively update a nested dictionary"""
215
+ for k, v in u.items():
216
+ if isinstance(v, dict) and k in d and isinstance(d[k], dict):
217
+ self._deep_update(d[k], v)
218
+ else:
219
+ d[k] = v
@@ -0,0 +1,101 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """
11
+ Abstract base class for state management
12
+ """
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import Dict, Any, Optional
16
+
17
+
18
+ class StateManager(ABC):
19
+ """
20
+ Abstract base class for state management
21
+
22
+ This defines the interface that all state manager implementations
23
+ must follow. State managers are responsible for storing, retrieving,
24
+ and managing call-specific state data.
25
+ """
26
+
27
+ @abstractmethod
28
+ def store(self, call_id: str, data: Dict[str, Any]) -> bool:
29
+ """
30
+ Store state data for a call
31
+
32
+ Args:
33
+ call_id: Unique identifier for the call
34
+ data: Dictionary of state data to store
35
+
36
+ Returns:
37
+ True if successful, False otherwise
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def retrieve(self, call_id: str) -> Optional[Dict[str, Any]]:
43
+ """
44
+ Retrieve state data for a call
45
+
46
+ Args:
47
+ call_id: Unique identifier for the call
48
+
49
+ Returns:
50
+ Dictionary of state data or None if not found
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ def update(self, call_id: str, data: Dict[str, Any]) -> bool:
56
+ """
57
+ Update state data for a call
58
+
59
+ Args:
60
+ call_id: Unique identifier for the call
61
+ data: Dictionary of state data to update (merged with existing)
62
+
63
+ Returns:
64
+ True if successful, False otherwise
65
+ """
66
+ pass
67
+
68
+ @abstractmethod
69
+ def delete(self, call_id: str) -> bool:
70
+ """
71
+ Delete state data for a call
72
+
73
+ Args:
74
+ call_id: Unique identifier for the call
75
+
76
+ Returns:
77
+ True if successful, False otherwise
78
+ """
79
+ pass
80
+
81
+ @abstractmethod
82
+ def cleanup_expired(self) -> int:
83
+ """
84
+ Clean up expired state data
85
+
86
+ Returns:
87
+ Number of expired items cleaned up
88
+ """
89
+ pass
90
+
91
+ def exists(self, call_id: str) -> bool:
92
+ """
93
+ Check if state exists for a call
94
+
95
+ Args:
96
+ call_id: Unique identifier for the call
97
+
98
+ Returns:
99
+ True if state exists, False otherwise
100
+ """
101
+ return self.retrieve(call_id) is not None
@@ -0,0 +1,172 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """
11
+ SwaigFunction class for defining and managing SWAIG function interfaces
12
+ """
13
+
14
+ from typing import Dict, Any, Optional, Callable, List, Type, Union
15
+ import inspect
16
+ import logging
17
+
18
+
19
+ class SWAIGFunction:
20
+ """
21
+ Represents a SWAIG function for AI integration
22
+ """
23
+ def __init__(
24
+ self,
25
+ name: str,
26
+ handler: Callable,
27
+ description: str,
28
+ parameters: Dict[str, Dict] = None,
29
+ secure: bool = False,
30
+ fillers: Optional[Dict[str, List[str]]] = None
31
+ ):
32
+ """
33
+ Initialize a new SWAIG function
34
+
35
+ Args:
36
+ name: Name of the function to appear in SWML
37
+ handler: Function to call when this SWAIG function is invoked
38
+ description: Human-readable description of the function
39
+ parameters: Dictionary of parameters, keys are parameter names, values are param definitions
40
+ secure: Whether this function requires token validation
41
+ fillers: Optional dictionary of filler phrases by language code
42
+ """
43
+ self.name = name
44
+ self.handler = handler
45
+ self.description = description
46
+ self.parameters = parameters or {}
47
+ self.secure = secure
48
+ self.fillers = fillers
49
+
50
+ def _ensure_parameter_structure(self) -> Dict:
51
+ """
52
+ Ensure the parameters are correctly structured for SWML
53
+
54
+ Returns:
55
+ Parameters dict with correct structure
56
+ """
57
+ if not self.parameters:
58
+ return {"type": "object", "properties": {}}
59
+
60
+ # Check if we already have the correct structure
61
+ if "type" in self.parameters and "properties" in self.parameters:
62
+ return self.parameters
63
+
64
+ # Otherwise, wrap the parameters in the expected structure
65
+ return {
66
+ "type": "object",
67
+ "properties": self.parameters
68
+ }
69
+
70
+ def __call__(self, *args, **kwargs):
71
+ """
72
+ Call the underlying handler function
73
+ """
74
+ return self.handler(*args, **kwargs)
75
+
76
+ def execute(self, args: Dict[str, Any], raw_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
77
+ """
78
+ Execute the function with the given arguments
79
+
80
+ Args:
81
+ args: Parsed arguments for the function
82
+ raw_data: Optional raw request data
83
+
84
+ Returns:
85
+ Function result as a dictionary (from SwaigFunctionResult.to_dict())
86
+ """
87
+ try:
88
+ # Raw data is mandatory, but we'll handle the case where it's null for robustness
89
+ if raw_data is None:
90
+ raw_data = {} # Provide an empty dict as fallback
91
+
92
+ # Call the handler with both args and raw_data
93
+ result = self.handler(args, raw_data)
94
+
95
+ # Import here to avoid circular imports
96
+ from signalwire_agents.core.function_result import SwaigFunctionResult
97
+
98
+ # Handle different result types - everything must end up as a SwaigFunctionResult
99
+ if isinstance(result, SwaigFunctionResult):
100
+ # Already a SwaigFunctionResult - just convert to dict
101
+ return result.to_dict()
102
+ elif isinstance(result, dict) and "response" in result:
103
+ # Already in the correct format - use as is
104
+ return result
105
+ elif isinstance(result, dict):
106
+ # Dictionary without response - create a SwaigFunctionResult
107
+ return SwaigFunctionResult("Function completed successfully").to_dict()
108
+ else:
109
+ # String or other type - create a SwaigFunctionResult with the string representation
110
+ return SwaigFunctionResult(str(result)).to_dict()
111
+
112
+ except Exception as e:
113
+ # Log the error for debugging but don't expose details to the AI
114
+ logging.error(f"Error executing SWAIG function {self.name}: {str(e)}")
115
+ # Return a generic error message
116
+ return SwaigFunctionResult(
117
+ "Sorry, I couldn't complete that action. Please try again or contact support if the issue persists."
118
+ ).to_dict()
119
+
120
+ def validate_args(self, args: Dict[str, Any]) -> bool:
121
+ """
122
+ Validate the arguments against the parameter schema
123
+
124
+ Args:
125
+ args: Arguments to validate
126
+
127
+ Returns:
128
+ True if valid, False otherwise
129
+ """
130
+ # TODO: Implement JSON Schema validation
131
+ return True
132
+
133
+ def to_swaig(self, base_url: str, token: Optional[str] = None, call_id: Optional[str] = None, include_auth: bool = True) -> Dict[str, Any]:
134
+ """
135
+ Convert this function to a SWAIG-compatible JSON object for SWML
136
+
137
+ Args:
138
+ base_url: Base URL for the webhook
139
+ token: Optional auth token to include
140
+ call_id: Optional call ID for session tracking
141
+ include_auth: Whether to include auth credentials in URL
142
+
143
+ Returns:
144
+ Dictionary representation for the SWAIG array in SWML
145
+ """
146
+ # All functions use a single /swaig endpoint
147
+ url = f"{base_url}/swaig"
148
+
149
+ # Add token and call_id parameters if provided
150
+ if token and call_id:
151
+ url = f"{url}?token={token}&call_id={call_id}"
152
+
153
+ # Create properly structured function definition
154
+ function_def = {
155
+ "function": self.name,
156
+ "description": self.description,
157
+ "parameters": self._ensure_parameter_structure(),
158
+ }
159
+
160
+ # Only add web_hook_url if not using defaults
161
+ # This will be handled by the defaults section in the SWAIG array
162
+ if url:
163
+ function_def["web_hook_url"] = url
164
+
165
+ # Add fillers if provided
166
+ if self.fillers and len(self.fillers) > 0:
167
+ function_def["fillers"] = self.fillers
168
+
169
+ return function_def
170
+
171
+ # Add an alias for backward compatibility
172
+ SwaigFunction = SWAIGFunction
@@ -0,0 +1,214 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """
11
+ SWML Builder - Fluent API for building SWML documents
12
+
13
+ This module provides a fluent builder API for creating SWML documents.
14
+ It allows for chaining method calls to build up a document step by step.
15
+ """
16
+
17
+ from typing import Dict, List, Any, Optional, Union, Self, TypeVar
18
+
19
+ from signalwire_agents.core.swml_service import SWMLService
20
+
21
+
22
+ T = TypeVar('T', bound='SWMLBuilder')
23
+
24
+
25
+ class SWMLBuilder:
26
+ """
27
+ Fluent builder for SWML documents
28
+
29
+ This class provides a fluent interface for building SWML documents
30
+ by chaining method calls. It delegates to an underlying SWMLService
31
+ instance for the actual document creation.
32
+ """
33
+
34
+ def __init__(self, service: SWMLService):
35
+ """
36
+ Initialize with a SWMLService instance
37
+
38
+ Args:
39
+ service: The SWMLService to delegate to
40
+ """
41
+ self.service = service
42
+
43
+ def answer(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> Self:
44
+ """
45
+ Add an 'answer' verb to the main section
46
+
47
+ Args:
48
+ max_duration: Maximum duration in seconds
49
+ codecs: Comma-separated list of codecs
50
+
51
+ Returns:
52
+ Self for method chaining
53
+ """
54
+ self.service.add_answer_verb(max_duration, codecs)
55
+ return self
56
+
57
+ def hangup(self, reason: Optional[str] = None) -> Self:
58
+ """
59
+ Add a 'hangup' verb to the main section
60
+
61
+ Args:
62
+ reason: Optional reason for hangup
63
+
64
+ Returns:
65
+ Self for method chaining
66
+ """
67
+ self.service.add_hangup_verb(reason)
68
+ return self
69
+
70
+ def ai(self,
71
+ prompt_text: Optional[str] = None,
72
+ prompt_pom: Optional[List[Dict[str, Any]]] = None,
73
+ post_prompt: Optional[str] = None,
74
+ post_prompt_url: Optional[str] = None,
75
+ swaig: Optional[Dict[str, Any]] = None,
76
+ **kwargs) -> Self:
77
+ """
78
+ Add an 'ai' verb to the main section
79
+
80
+ Args:
81
+ prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
82
+ prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
83
+ post_prompt: Optional post-prompt text
84
+ post_prompt_url: Optional URL for post-prompt processing
85
+ swaig: Optional SWAIG configuration
86
+ **kwargs: Additional AI parameters
87
+
88
+ Returns:
89
+ Self for method chaining
90
+ """
91
+ self.service.add_ai_verb(
92
+ prompt_text=prompt_text,
93
+ prompt_pom=prompt_pom,
94
+ post_prompt=post_prompt,
95
+ post_prompt_url=post_prompt_url,
96
+ swaig=swaig,
97
+ **kwargs
98
+ )
99
+ return self
100
+
101
+ def play(self, url: Optional[str] = None, urls: Optional[List[str]] = None,
102
+ volume: Optional[float] = None, say_voice: Optional[str] = None,
103
+ say_language: Optional[str] = None, say_gender: Optional[str] = None,
104
+ auto_answer: Optional[bool] = None) -> Self:
105
+ """
106
+ Add a 'play' verb to the main section
107
+
108
+ Args:
109
+ url: Single URL to play (mutually exclusive with urls)
110
+ urls: List of URLs to play (mutually exclusive with url)
111
+ volume: Volume level (-40 to 40)
112
+ say_voice: Voice for text-to-speech
113
+ say_language: Language for text-to-speech
114
+ say_gender: Gender for text-to-speech
115
+ auto_answer: Whether to auto-answer the call
116
+
117
+ Returns:
118
+ Self for method chaining
119
+ """
120
+ # Create base config
121
+ config = {}
122
+
123
+ # Add play config (either single URL or list)
124
+ if url is not None:
125
+ config["url"] = url
126
+ elif urls is not None:
127
+ config["urls"] = urls
128
+ else:
129
+ raise ValueError("Either url or urls must be provided")
130
+
131
+ # Add optional parameters
132
+ if volume is not None:
133
+ config["volume"] = volume
134
+ if say_voice is not None:
135
+ config["say_voice"] = say_voice
136
+ if say_language is not None:
137
+ config["say_language"] = say_language
138
+ if say_gender is not None:
139
+ config["say_gender"] = say_gender
140
+ if auto_answer is not None:
141
+ config["auto_answer"] = auto_answer
142
+
143
+ # Add the verb
144
+ self.service.add_verb("play", config)
145
+ return self
146
+
147
+ def say(self, text: str, voice: Optional[str] = None,
148
+ language: Optional[str] = None, gender: Optional[str] = None,
149
+ volume: Optional[float] = None) -> Self:
150
+ """
151
+ Add a 'play' verb with say: prefix for text-to-speech
152
+
153
+ Args:
154
+ text: Text to speak
155
+ voice: Voice for text-to-speech
156
+ language: Language for text-to-speech
157
+ gender: Gender for text-to-speech
158
+ volume: Volume level (-40 to 40)
159
+
160
+ Returns:
161
+ Self for method chaining
162
+ """
163
+ # Create play config with say: prefix
164
+ url = f"say:{text}"
165
+
166
+ # Add the verb
167
+ return self.play(
168
+ url=url,
169
+ say_voice=voice,
170
+ say_language=language,
171
+ say_gender=gender,
172
+ volume=volume
173
+ )
174
+
175
+ def add_section(self, section_name: str) -> Self:
176
+ """
177
+ Add a new section to the document
178
+
179
+ Args:
180
+ section_name: Name of the section to add
181
+
182
+ Returns:
183
+ Self for method chaining
184
+ """
185
+ self.service.add_section(section_name)
186
+ return self
187
+
188
+ def build(self) -> Dict[str, Any]:
189
+ """
190
+ Build and return the SWML document
191
+
192
+ Returns:
193
+ The complete SWML document as a dictionary
194
+ """
195
+ return self.service.get_document()
196
+
197
+ def render(self) -> str:
198
+ """
199
+ Build and render the SWML document as a JSON string
200
+
201
+ Returns:
202
+ The complete SWML document as a JSON string
203
+ """
204
+ return self.service.render_document()
205
+
206
+ def reset(self) -> Self:
207
+ """
208
+ Reset the document to an empty state
209
+
210
+ Returns:
211
+ Self for method chaining
212
+ """
213
+ self.service.reset_document()
214
+ return self