signalwire-agents 0.1.7__py3-none-any.whl → 0.1.8__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.
@@ -14,7 +14,11 @@ This module provides a fluent builder API for creating SWML documents.
14
14
  It allows for chaining method calls to build up a document step by step.
15
15
  """
16
16
 
17
- from typing import Dict, List, Any, Optional, Union, Self, TypeVar
17
+ from typing import Dict, List, Any, Optional, Union, TypeVar
18
+ try:
19
+ from typing import Self # Python 3.11+
20
+ except ImportError:
21
+ from typing_extensions import Self # For Python 3.9-3.10
18
22
 
19
23
  from signalwire_agents.core.swml_service import SWMLService
20
24
 
@@ -9,9 +9,12 @@ See LICENSE file in the project root for full license information.
9
9
 
10
10
  """
11
11
  InfoGathererAgent - Prefab agent for collecting answers to a series of questions
12
+
13
+ Supports both static (questions provided at init) and dynamic (questions determined
14
+ by a callback function) configuration modes.
12
15
  """
13
16
 
14
- from typing import List, Dict, Any, Optional, Union
17
+ from typing import List, Dict, Any, Optional, Union, Callable
15
18
  import json
16
19
 
17
20
  from signalwire_agents.core.agent_base import AgentBase
@@ -39,7 +42,7 @@ class InfoGathererAgent(AgentBase):
39
42
 
40
43
  def __init__(
41
44
  self,
42
- questions: List[Dict[str, str]],
45
+ questions: Optional[List[Dict[str, str]]] = None,
43
46
  name: str = "info_gatherer",
44
47
  route: str = "/info_gatherer",
45
48
  enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
@@ -49,7 +52,8 @@ class InfoGathererAgent(AgentBase):
49
52
  Initialize an information gathering agent
50
53
 
51
54
  Args:
52
- questions: List of questions to ask, each with:
55
+ questions: Optional list of questions to ask. If None, questions will be determined
56
+ dynamically via a callback function. Each question dict should have:
53
57
  - key_name: Identifier for storing the answer
54
58
  - question_text: The actual question to ask the user
55
59
  - confirm: (Optional) If set to True, the agent will confirm the answer before submitting
@@ -67,39 +71,84 @@ class InfoGathererAgent(AgentBase):
67
71
  **kwargs
68
72
  )
69
73
 
70
- # Validate questions format
71
- self._validate_questions(questions)
72
-
73
- # Set up global data with questions and initial state
74
- self.set_global_data({
75
- "questions": questions,
76
- "question_index": 0,
77
- "answers": []
78
- })
74
+ # Store whether we're in static or dynamic mode
75
+ self._static_questions = questions
76
+ self._question_callback = None
79
77
 
80
- # Build a minimal prompt
81
- self._build_prompt()
78
+ if questions is not None:
79
+ # Static mode: validate questions and set up immediately
80
+ self._validate_questions(questions)
81
+ self.set_global_data({
82
+ "questions": questions,
83
+ "question_index": 0,
84
+ "answers": []
85
+ })
86
+ # Build prompt for static configuration
87
+ self._build_prompt()
88
+ else:
89
+ # Dynamic mode: questions will be set up via callback in on_swml_request
90
+ # Build a generic prompt
91
+ self._build_prompt("dynamic")
82
92
 
83
93
  # Configure additional agent settings
84
94
  self._configure_agent_settings()
85
95
 
96
+ def set_question_callback(self, callback: Callable[[dict, dict, dict], List[Dict[str, str]]]):
97
+ """
98
+ Set a callback function for dynamic question configuration
99
+
100
+ Args:
101
+ callback: Function that takes (query_params, body_params, headers) and returns
102
+ a list of question dictionaries. Each question dict should have:
103
+ - key_name: Identifier for storing the answer
104
+ - question_text: The actual question to ask the user
105
+ - confirm: (Optional) If True, agent will confirm answer before submitting
106
+
107
+ Example:
108
+ def my_question_callback(query_params, body_params, headers):
109
+ question_set = query_params.get('set', 'default')
110
+ if question_set == 'support':
111
+ return [
112
+ {"key_name": "name", "question_text": "What is your name?"},
113
+ {"key_name": "issue", "question_text": "What's the issue?"}
114
+ ]
115
+ else:
116
+ return [{"key_name": "name", "question_text": "What is your name?"}]
117
+
118
+ agent.set_question_callback(my_question_callback)
119
+ """
120
+ self._question_callback = callback
121
+
86
122
  def _validate_questions(self, questions):
87
123
  """Validate that questions are in the correct format"""
88
124
  if not questions:
89
125
  raise ValueError("At least one question is required")
90
126
 
127
+ if not isinstance(questions, list):
128
+ raise ValueError("Questions must be a list")
129
+
91
130
  for i, question in enumerate(questions):
131
+ if not isinstance(question, dict):
132
+ raise ValueError(f"Question {i+1} must be a dictionary")
92
133
  if "key_name" not in question:
93
134
  raise ValueError(f"Question {i+1} is missing 'key_name' field")
94
135
  if "question_text" not in question:
95
136
  raise ValueError(f"Question {i+1} is missing 'question_text' field")
96
137
 
97
- def _build_prompt(self):
138
+ def _build_prompt(self, mode="static"):
98
139
  """Build a minimal prompt with just the objective"""
99
- self.prompt_add_section(
100
- "Objective",
101
- body="Your role is to get answers to a series of questions. Begin by asking the user if they are ready to answer some questions. If they confirm they are ready, call the start_questions function to begin the process."
102
- )
140
+ if mode == "dynamic":
141
+ # Generic prompt for dynamic mode - will be customized later
142
+ self.prompt_add_section(
143
+ "Objective",
144
+ body="Your role is to gather information by asking questions. Begin by asking the user if they are ready to answer some questions. If they confirm they are ready, call the start_questions function to begin the process."
145
+ )
146
+ else:
147
+ # Original static prompt
148
+ self.prompt_add_section(
149
+ "Objective",
150
+ body="Your role is to get answers to a series of questions. Begin by asking the user if they are ready to answer some questions. If they confirm they are ready, call the start_questions function to begin the process."
151
+ )
103
152
 
104
153
  def _configure_agent_settings(self):
105
154
  """Configure additional agent settings"""
@@ -109,6 +158,77 @@ class InfoGathererAgent(AgentBase):
109
158
  "speech_event_timeout": 1000 # Slightly longer for thoughtful responses
110
159
  })
111
160
 
161
+ def on_swml_request(self, request_data=None, callback_path=None, request=None):
162
+ """
163
+ Handle dynamic configuration using the callback function
164
+
165
+ This method is called when SWML is requested and allows us to configure
166
+ the agent just-in-time using the provided callback.
167
+ """
168
+ # Only process if we're in dynamic mode (no static questions)
169
+ if self._static_questions is not None:
170
+ return None
171
+
172
+ # If no callback is set, provide a basic fallback
173
+ if self._question_callback is None:
174
+ fallback_questions = [
175
+ {"key_name": "name", "question_text": "What is your name?"},
176
+ {"key_name": "message", "question_text": "How can I help you today?"}
177
+ ]
178
+ return {
179
+ "global_data": {
180
+ "questions": fallback_questions,
181
+ "question_index": 0,
182
+ "answers": []
183
+ }
184
+ }
185
+
186
+ # Extract request information for callback
187
+ query_params = {}
188
+ body_params = request_data or {}
189
+ headers = {}
190
+
191
+ if request and hasattr(request, 'query_params'):
192
+ query_params = dict(request.query_params)
193
+
194
+ if request and hasattr(request, 'headers'):
195
+ headers = dict(request.headers)
196
+
197
+ try:
198
+ # Call the user-provided callback to get questions
199
+ print(f"Calling question callback with query_params: {query_params}")
200
+ questions = self._question_callback(query_params, body_params, headers)
201
+ print(f"Callback returned {len(questions)} questions")
202
+
203
+ # Validate the returned questions
204
+ self._validate_questions(questions)
205
+
206
+ # Return global data modifications
207
+ return {
208
+ "global_data": {
209
+ "questions": questions,
210
+ "question_index": 0,
211
+ "answers": []
212
+ }
213
+ }
214
+
215
+ except Exception as e:
216
+ # Log error and fall back to basic questions
217
+ print(f"Error in question callback: {e}")
218
+ fallback_questions = [
219
+ {"key_name": "name", "question_text": "What is your name?"},
220
+ {"key_name": "message", "question_text": "How can I help you today?"}
221
+ ]
222
+ return {
223
+ "global_data": {
224
+ "questions": fallback_questions,
225
+ "question_index": 0,
226
+ "answers": []
227
+ }
228
+ }
229
+
230
+
231
+
112
232
  def _generate_question_instruction(self, question_text: str, needs_confirmation: bool, is_first_question: bool = False) -> str:
113
233
  """
114
234
  Generate the instruction text for asking a question
@@ -239,13 +359,11 @@ class InfoGathererAgent(AgentBase):
239
359
  # Create response with the global data update and next question
240
360
  result = SwaigFunctionResult(instruction)
241
361
 
242
- # Add actions to update global data
243
- result.add_actions([
244
- {"set_global_data": {
245
- "answers": new_answers,
246
- "question_index": new_question_index
247
- }}
248
- ])
362
+ # Use the helper method to update global data
363
+ result.update_global_data({
364
+ "answers": new_answers,
365
+ "question_index": new_question_index
366
+ })
249
367
 
250
368
  return result
251
369
  else:
@@ -254,13 +372,11 @@ class InfoGathererAgent(AgentBase):
254
372
  "Thank you! All questions have been answered. You can now summarize the information collected or ask if there's anything else the user would like to discuss."
255
373
  )
256
374
 
257
- # Add actions to update global data
258
- result.add_actions([
259
- {"set_global_data": {
260
- "answers": new_answers,
261
- "question_index": new_question_index
262
- }}
263
- ])
375
+ # Use the helper method to update global data
376
+ result.update_global_data({
377
+ "answers": new_answers,
378
+ "question_index": new_question_index
379
+ })
264
380
 
265
381
  return result
266
382
 
@@ -40,7 +40,7 @@ class ReceptionistAgent(AgentBase):
40
40
  name: str = "receptionist",
41
41
  route: str = "/receptionist",
42
42
  greeting: str = "Thank you for calling. How can I help you today?",
43
- voice: str = "elevenlabs.josh",
43
+ voice: str = "rime.spore",
44
44
  enable_state_tracking: bool = True, # Enable state tracking by default
45
45
  **kwargs
46
46
  ):
@@ -261,28 +261,20 @@ class ReceptionistAgent(AgentBase):
261
261
  # Get transfer number
262
262
  transfer_number = department.get("number", "")
263
263
 
264
- # Create result with transfer SWML
265
- result = SwaigFunctionResult(f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!")
264
+ # Create result with transfer using the connect helper method
265
+ # post_process=True allows the AI to speak the response before executing the transfer
266
+ result = SwaigFunctionResult(
267
+ f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!",
268
+ post_process=True
269
+ )
266
270
 
267
- # Add the SWML to execute the transfer
268
- # Add actions to update global data
269
- result.add_actions([
270
- {
271
- "SWML": {
272
- "sections": {
273
- "main": [
274
- {
275
- "connect": {
276
- "to": transfer_number
277
- }
278
- }
279
- ]
280
- },
281
- "version": "1.0.0"
282
- },
283
- "transfer": "true"
284
- }
285
- ])
271
+ # Use the connect helper instead of manually constructing SWML
272
+ # final=True means this is a permanent transfer (call exits the agent)
273
+ result.connect(transfer_number, final=True)
274
+
275
+ # Alternative: Immediate transfer without AI speaking (faster but less friendly)
276
+ # result = SwaigFunctionResult() # No response text needed
277
+ # result.connect(transfer_number, final=True) # Executes immediately from function call
286
278
 
287
279
  return result
288
280
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  Project-URL: Homepage, https://github.com/signalwire/signalwire-ai-agents
@@ -35,6 +35,7 @@ A Python SDK for creating, hosting, and securing SignalWire AI agents as microse
35
35
  - **Self-Contained Agents**: Each agent is both a web app and an AI persona
36
36
  - **Prompt Object Model**: Structured prompt composition using POM
37
37
  - **SWAIG Integration**: Easily define and handle AI tools/functions
38
+ - **Dynamic Configuration**: Configure agents per-request for multi-tenant apps and personalization
38
39
  - **Custom Routing**: Dynamic request handling for different paths and content
39
40
  - **SIP Integration**: Route SIP calls to agents based on SIP usernames
40
41
  - **Security Built-In**: Session management, function-specific security tokens, and basic auth
@@ -165,6 +166,73 @@ Available prefabs include:
165
166
  - `FAQBotAgent`: Answers questions based on a knowledge base
166
167
  - `ConciergeAgent`: Routes users to specialized agents
167
168
  - `SurveyAgent`: Conducts structured surveys with questions and rating scales
169
+ - `ReceptionistAgent`: Greets callers and transfers them to appropriate departments
170
+
171
+ ## Dynamic Agent Configuration
172
+
173
+ Configure agents dynamically based on request parameters for multi-tenant applications, A/B testing, and personalization.
174
+
175
+ ### Static vs Dynamic Configuration
176
+
177
+ - **Static**: Agent configured once at startup (traditional approach)
178
+ - **Dynamic**: Agent configured fresh for each request based on parameters
179
+
180
+ ### Basic Example
181
+
182
+ ```python
183
+ from signalwire_agents import AgentBase
184
+
185
+ class DynamicAgent(AgentBase):
186
+ def __init__(self):
187
+ super().__init__(name="dynamic-agent", route="/dynamic")
188
+
189
+ # Set up dynamic configuration callback
190
+ self.set_dynamic_config_callback(self.configure_per_request)
191
+
192
+ def configure_per_request(self, query_params, body_params, headers, agent):
193
+ """Configure agent based on request parameters"""
194
+
195
+ # Extract parameters from request
196
+ tier = query_params.get('tier', 'standard')
197
+ language = query_params.get('language', 'en')
198
+ customer_id = query_params.get('customer_id')
199
+
200
+ # Configure voice and language
201
+ if language == 'es':
202
+ agent.add_language("Spanish", "es-ES", "rime.spore:mistv2")
203
+ else:
204
+ agent.add_language("English", "en-US", "rime.spore:mistv2")
205
+
206
+ # Configure based on service tier
207
+ if tier == 'premium':
208
+ agent.set_params({"end_of_speech_timeout": 300}) # Faster response
209
+ agent.prompt_add_section("Service Level", "You provide premium support.")
210
+ else:
211
+ agent.set_params({"end_of_speech_timeout": 500}) # Standard response
212
+ agent.prompt_add_section("Service Level", "You provide standard support.")
213
+
214
+ # Personalize with customer data
215
+ global_data = {"tier": tier, "language": language}
216
+ if customer_id:
217
+ global_data["customer_id"] = customer_id
218
+ agent.set_global_data(global_data)
219
+
220
+ # Usage examples:
221
+ # curl "http://localhost:3000/dynamic?tier=premium&language=es&customer_id=123"
222
+ # curl "http://localhost:3000/dynamic?tier=standard&language=en"
223
+ ```
224
+
225
+ ### Use Cases
226
+
227
+ - **Multi-tenant SaaS**: Different configurations per customer/organization
228
+ - **A/B Testing**: Test different agent behaviors with different user groups
229
+ - **Personalization**: Customize voice, prompts, and behavior per user
230
+ - **Localization**: Language and cultural adaptation based on user location
231
+ - **Dynamic Pricing**: Adjust features and capabilities based on subscription tiers
232
+
233
+ The `EphemeralAgentConfig` object provides all the same familiar methods as `AgentBase` (like `add_language()`, `prompt_add_section()`, `set_global_data()`) but applies them per-request instead of at startup.
234
+
235
+ For detailed documentation and advanced examples, see the [Agent Guide](docs/agent_guide.md#dynamic-agent-configuration).
168
236
 
169
237
  ## Configuration
170
238
 
@@ -189,7 +257,7 @@ To enable HTTPS directly (without a reverse proxy), set `SWML_SSL_ENABLED` to "t
189
257
 
190
258
  The package includes comprehensive documentation in the `docs/` directory:
191
259
 
192
- - [Agent Guide](docs/agent_guide.md) - Detailed guide to creating and customizing agents
260
+ - [Agent Guide](docs/agent_guide.md) - Detailed guide to creating and customizing agents, including dynamic configuration
193
261
  - [Architecture](docs/architecture.md) - Overview of the SDK architecture and core concepts
194
262
  - [SWML Service Guide](docs/swml_service_guide.md) - Guide to the underlying SWML service
195
263
 
@@ -1,12 +1,12 @@
1
- signalwire_agents/__init__.py,sha256=WMXWZxEE3QuDgEKy6MBcL6hxdSDA3_gFd2XAQYmEwMg,800
1
+ signalwire_agents/__init__.py,sha256=0epKP_LHIiz6nCNxn-hQvDfdYGfKvTxpu9oe-OThvEc,800
2
2
  signalwire_agents/agent_server.py,sha256=se_YzOQE5UUoRUKCbTnOg9qr4G3qN7iVuQLutwXEwFU,12850
3
3
  signalwire_agents/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
4
4
  signalwire_agents/core/__init__.py,sha256=mVDLbpq1pg_WwiqsQR28NNZwJ6-VUXFIfg-vN7pk0ew,806
5
- signalwire_agents/core/agent_base.py,sha256=D8ZiRRYDxF24cOjXoT253SWODEvmDZ-l8kwWMnr18Bw,103118
6
- signalwire_agents/core/function_result.py,sha256=vD8eBDJBQNNnss1jShadfogCZw_prODB6eBkTuVgZKA,3538
5
+ signalwire_agents/core/agent_base.py,sha256=GLnjisSPNEqJMG4-eb3TS_fBtNLPbvgjd1cTVWxqxJ4,112500
6
+ signalwire_agents/core/function_result.py,sha256=OXzm4GOvA6TKbzZFsdz3iBU5vQs9LXxsmQue7zWVolE,43538
7
7
  signalwire_agents/core/pom_builder.py,sha256=ywuiIfP8BeLBPo_G4X1teZlG6zTCMkW71CZnmyoDTAQ,6636
8
8
  signalwire_agents/core/swaig_function.py,sha256=WoHGQuCmU9L9k39pttRunmfRtIa_PnNRn9W0Xq3MfIk,6316
9
- signalwire_agents/core/swml_builder.py,sha256=Y_eHThVVso-_Hz4f73eEuu3HJstsM9PtBl-36GvAXhI,6594
9
+ signalwire_agents/core/swml_builder.py,sha256=Q1ikU9pedgjW888mjbqDFv-jMDvDZ-tZgfyMfu4qQN0,6719
10
10
  signalwire_agents/core/swml_handler.py,sha256=KvphI_YY47VWGVXaix_N3SuQSyygHEUr9We6xESQK44,7002
11
11
  signalwire_agents/core/swml_renderer.py,sha256=iobMWWoBR7dkHndI3Qlwf4C0fg2p7DmAU2Rb7ZmmLhA,13891
12
12
  signalwire_agents/core/swml_service.py,sha256=4zTtpIKDtL59MzKgkBdpPmBCT6s1wVIikWEzZ1hqnyc,49309
@@ -18,17 +18,17 @@ signalwire_agents/core/state/state_manager.py,sha256=76B4mDutMb826dK4c_IJhOXH09B
18
18
  signalwire_agents/prefabs/__init__.py,sha256=MW11J63XH7KxF2MWguRsMFM9iqMWexaEO9ynDPL_PDM,715
19
19
  signalwire_agents/prefabs/concierge.py,sha256=FQxSUBAVH2ECtNs4GGa3f0EPiOrAKaz14h7byNFy5K8,10106
20
20
  signalwire_agents/prefabs/faq_bot.py,sha256=NrUn5AGmtdzYTyxTHPt8BZ14ZN1sh4xKA2SVQUlfPPQ,10834
21
- signalwire_agents/prefabs/info_gatherer.py,sha256=NkRc6FSaslDMhvF3a2rAvTqD9V8t17OV6_awwtQPPRM,10219
22
- signalwire_agents/prefabs/receptionist.py,sha256=Bpb_okc5MnxEdrrO-PkXXo5Zp9awgyt4bvYJOahSbYE,10555
21
+ signalwire_agents/prefabs/info_gatherer.py,sha256=dr9UUgNGX7MIKdCMth3jDVLf6OrHov5G6_zIuNvnrOY,15468
22
+ signalwire_agents/prefabs/receptionist.py,sha256=8EyQ6M0Egs3g7KKWukHFiO9UPoVUxT4MLkvyT3V8o64,10585
23
23
  signalwire_agents/prefabs/survey.py,sha256=IIIQfgvMlfVNjEEEdWUn4lAJqVsCDlBsIAkOJ1ckyAE,14796
24
24
  signalwire_agents/utils/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
25
25
  signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
26
26
  signalwire_agents/utils/schema_utils.py,sha256=LvFCFvJTQk_xYK0B-NXbkXKEF7Zmv-LqpV_vfpPnOb4,13473
27
27
  signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
28
28
  signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
29
- signalwire_agents-0.1.7.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
30
- signalwire_agents-0.1.7.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
31
- signalwire_agents-0.1.7.dist-info/METADATA,sha256=rlAExY7ealJ5HzZIx-IOHnH9MZn3zfXcuISBgnIRJbw,7486
32
- signalwire_agents-0.1.7.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
33
- signalwire_agents-0.1.7.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
34
- signalwire_agents-0.1.7.dist-info/RECORD,,
29
+ signalwire_agents-0.1.8.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
30
+ signalwire_agents-0.1.8.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
31
+ signalwire_agents-0.1.8.dist-info/METADATA,sha256=kTDyXqy-gZ6JMfVCr6EIsj-EB2hKtgN5wxp1VPBnNek,10507
32
+ signalwire_agents-0.1.8.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
33
+ signalwire_agents-0.1.8.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
34
+ signalwire_agents-0.1.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5