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,263 @@
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
+ InfoGathererAgent - Prefab agent for collecting answers to a series of questions
12
+ """
13
+
14
+ from typing import List, Dict, Any, Optional, Union
15
+ import json
16
+
17
+ from signalwire_agents.core.agent_base import AgentBase
18
+ from signalwire_agents.core.function_result import SwaigFunctionResult
19
+
20
+
21
+ class InfoGathererAgent(AgentBase):
22
+ """
23
+ A prefab agent designed to collect answers to a series of questions.
24
+
25
+ This agent will:
26
+ 1. Ask if the user is ready to begin
27
+ 2. Ask each question in sequence
28
+ 3. Store the answers for later use
29
+
30
+ Example:
31
+ agent = InfoGathererAgent(
32
+ questions=[
33
+ {"key_name": "full_name", "question_text": "What is your full name?"},
34
+ {"key_name": "email", "question_text": "What is your email address?", "confirm": True},
35
+ {"key_name": "reason", "question_text": "How can I help you today?"}
36
+ ]
37
+ )
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ questions: List[Dict[str, str]],
43
+ name: str = "info_gatherer",
44
+ route: str = "/info_gatherer",
45
+ **kwargs
46
+ ):
47
+ """
48
+ Initialize an information gathering agent
49
+
50
+ Args:
51
+ questions: List of questions to ask, each with:
52
+ - key_name: Identifier for storing the answer
53
+ - question_text: The actual question to ask the user
54
+ - confirm: (Optional) If set to True, the agent will confirm the answer before submitting
55
+ name: Agent name for the route
56
+ route: HTTP route for this agent
57
+ **kwargs: Additional arguments for AgentBase
58
+ """
59
+ # Initialize the base agent
60
+ super().__init__(
61
+ name=name,
62
+ route=route,
63
+ use_pom=True,
64
+ **kwargs
65
+ )
66
+
67
+ # Validate questions format
68
+ self._validate_questions(questions)
69
+
70
+ # Set up global data with questions and initial state
71
+ self.set_global_data({
72
+ "questions": questions,
73
+ "question_index": 0,
74
+ "answers": []
75
+ })
76
+
77
+ # Build a minimal prompt
78
+ self._build_prompt()
79
+
80
+ # Configure additional agent settings
81
+ self._configure_agent_settings()
82
+
83
+ def _validate_questions(self, questions):
84
+ """Validate that questions are in the correct format"""
85
+ if not questions:
86
+ raise ValueError("At least one question is required")
87
+
88
+ for i, question in enumerate(questions):
89
+ if "key_name" not in question:
90
+ raise ValueError(f"Question {i+1} is missing 'key_name' field")
91
+ if "question_text" not in question:
92
+ raise ValueError(f"Question {i+1} is missing 'question_text' field")
93
+
94
+ def _build_prompt(self):
95
+ """Build a minimal prompt with just the objective"""
96
+ self.prompt_add_section(
97
+ "Objective",
98
+ 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."
99
+ )
100
+
101
+ def _configure_agent_settings(self):
102
+ """Configure additional agent settings"""
103
+ # Set AI behavior parameters
104
+ self.set_params({
105
+ "end_of_speech_timeout": 800,
106
+ "speech_event_timeout": 1000 # Slightly longer for thoughtful responses
107
+ })
108
+
109
+ def _generate_question_instruction(self, question_text: str, needs_confirmation: bool, is_first_question: bool = False) -> str:
110
+ """
111
+ Generate the instruction text for asking a question
112
+
113
+ Args:
114
+ question_text: The question to ask
115
+ needs_confirmation: Whether confirmation is required
116
+ is_first_question: Whether this is the first question or a subsequent one
117
+
118
+ Returns:
119
+ Formatted instruction text
120
+ """
121
+ # Start with the appropriate prefix based on whether this is the first question
122
+ if is_first_question:
123
+ instruction = f"Ask the user to answer the following question: {question_text}\n\n"
124
+ else:
125
+ instruction = f"Previous Answer recorded. Now ask the user to answer the following question: {question_text}\n\n"
126
+
127
+ # Add the common part
128
+ instruction += "Make sure the answer fits the scope and context of the question before submitting it. "
129
+
130
+ # Add confirmation guidance if needed
131
+ if needs_confirmation:
132
+ instruction += "Insist that the user confirms the answer as many times as needed until they say it is correct."
133
+ else:
134
+ instruction += "You don't need the user to confirm the answer to this question."
135
+
136
+ return instruction
137
+
138
+ @AgentBase.tool(
139
+ name="start_questions",
140
+ description="Start the question sequence with the first question",
141
+ parameters={}
142
+ )
143
+ def start_questions(self, args, raw_data):
144
+ """
145
+ Start the question sequence by retrieving the first question
146
+
147
+ This function gets the current question index from global_data
148
+ and returns the corresponding question.
149
+ """
150
+ # Get global data
151
+ global_data = raw_data.get("global_data", {})
152
+ questions = global_data.get("questions", [])
153
+ question_index = global_data.get("question_index", 0)
154
+
155
+ # Check if we have questions
156
+ if not questions or question_index >= len(questions):
157
+ return SwaigFunctionResult("I don't have any questions to ask.")
158
+
159
+ # Get the current question
160
+ current_question = questions[question_index]
161
+ question_text = current_question.get("question_text", "")
162
+ needs_confirmation = current_question.get("confirm", False)
163
+
164
+ # Generate instruction using the helper method
165
+ instruction = self._generate_question_instruction(
166
+ question_text=question_text,
167
+ needs_confirmation=needs_confirmation,
168
+ is_first_question=True
169
+ )
170
+
171
+ # Return a prompt to ask the question
172
+ return SwaigFunctionResult(instruction)
173
+
174
+ @AgentBase.tool(
175
+ name="submit_answer",
176
+ description="Submit an answer to the current question and move to the next one",
177
+ parameters={
178
+ "answer": {
179
+ "type": "string",
180
+ "description": "The user's answer to the current question"
181
+ }
182
+ }
183
+ )
184
+ def submit_answer(self, args, raw_data):
185
+ """
186
+ Submit an answer to the current question and move to the next one
187
+
188
+ This function:
189
+ 1. Stores the answer in global_data
190
+ 2. Increments the question index
191
+ 3. Returns the next question or completion message
192
+ """
193
+ # Get the answer
194
+ answer = args.get("answer", "")
195
+
196
+ # Get global data
197
+ global_data = raw_data.get("global_data", {})
198
+ questions = global_data.get("questions", [])
199
+ question_index = global_data.get("question_index", 0)
200
+ answers = global_data.get("answers", [])
201
+
202
+ # Check if we're within bounds
203
+ if question_index >= len(questions):
204
+ return SwaigFunctionResult("All questions have already been answered.")
205
+
206
+ # Get the current question
207
+ current_question = questions[question_index]
208
+ key_name = current_question.get("key_name", "")
209
+
210
+ # Store the answer
211
+ new_answer = {"key_name": key_name, "answer": answer}
212
+ new_answers = answers + [new_answer]
213
+
214
+ # Increment question index
215
+ new_question_index = question_index + 1
216
+
217
+ print(f"new_question_index: {new_question_index} len(questions): {len(questions)}")
218
+
219
+ # Check if we have more questions
220
+ if new_question_index < len(questions):
221
+
222
+ print(f"Asking next question: {new_question_index} question: {questions[new_question_index]}")
223
+
224
+ # Get the next question
225
+ next_question = questions[new_question_index]
226
+ next_question_text = next_question.get("question_text", "")
227
+ needs_confirmation = next_question.get("confirm", False)
228
+
229
+ # Generate instruction using the helper method
230
+ instruction = self._generate_question_instruction(
231
+ question_text=next_question_text,
232
+ needs_confirmation=needs_confirmation,
233
+ is_first_question=False
234
+ )
235
+
236
+ # Create response with the global data update and next question
237
+ result = SwaigFunctionResult(instruction)
238
+
239
+ # Add actions to update global data
240
+ result.add_actions([
241
+ {"set_global_data": {
242
+ "answers": new_answers,
243
+ "question_index": new_question_index
244
+ }}
245
+ ])
246
+
247
+ return result
248
+ else:
249
+ # No more questions - create response with global data update and completion message
250
+ result = SwaigFunctionResult(
251
+ "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."
252
+ )
253
+
254
+ # Add actions to update global data
255
+ result.add_actions([
256
+ {"set_global_data": {
257
+ "answers": new_answers,
258
+ "question_index": new_question_index
259
+ }}
260
+ ])
261
+
262
+ return result
263
+
@@ -0,0 +1,294 @@
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
+ ReceptionistAgent - Prefab agent for greeting callers and transferring them to appropriate departments
12
+ """
13
+
14
+ from typing import List, Dict, Any, Optional, Union
15
+ import json
16
+
17
+ from signalwire_agents.core.agent_base import AgentBase
18
+ from signalwire_agents.core.function_result import SwaigFunctionResult
19
+
20
+
21
+ class ReceptionistAgent(AgentBase):
22
+ """
23
+ A prefab agent designed to act as a receptionist that:
24
+ 1. Greets callers
25
+ 2. Collects basic information about their needs
26
+ 3. Transfers them to the appropriate department
27
+
28
+ Example:
29
+ agent = ReceptionistAgent(
30
+ departments=[
31
+ {"name": "sales", "description": "For product inquiries, pricing, and purchasing", "number": "+15551235555"},
32
+ {"name": "support", "description": "For technical help and troubleshooting", "number": "+15551236666"}
33
+ ]
34
+ )
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ departments: List[Dict[str, str]],
40
+ name: str = "receptionist",
41
+ route: str = "/receptionist",
42
+ greeting: str = "Thank you for calling. How can I help you today?",
43
+ voice: str = "elevenlabs.josh",
44
+ **kwargs
45
+ ):
46
+ """
47
+ Initialize a receptionist agent
48
+
49
+ Args:
50
+ departments: List of departments to transfer to, each with:
51
+ - name: Department identifier (e.g., "sales")
52
+ - description: Description of department's purpose
53
+ - number: Phone number for transfer
54
+ name: Agent name for the route
55
+ route: HTTP route for this agent
56
+ greeting: Initial greeting message
57
+ voice: Voice ID to use
58
+ **kwargs: Additional arguments for AgentBase
59
+ """
60
+ # Initialize the base agent
61
+ super().__init__(
62
+ name=name,
63
+ route=route,
64
+ use_pom=True,
65
+ **kwargs
66
+ )
67
+
68
+ # Validate departments format
69
+ self._validate_departments(departments)
70
+
71
+ # Store greeting
72
+ self._greeting = greeting
73
+
74
+ # Set up global data with departments and initial state
75
+ self.set_global_data({
76
+ "departments": departments,
77
+ "caller_info": {}
78
+ })
79
+
80
+ # Build a prompt
81
+ self._build_prompt()
82
+
83
+ # Configure agent settings
84
+ self._configure_agent_settings(voice)
85
+
86
+ # Register tools
87
+ self._register_tools()
88
+
89
+ def _validate_departments(self, departments):
90
+ """Validate that departments are in the correct format"""
91
+ if not departments:
92
+ raise ValueError("At least one department is required")
93
+
94
+ for i, dept in enumerate(departments):
95
+ if "name" not in dept:
96
+ raise ValueError(f"Department {i+1} is missing 'name' field")
97
+ if "description" not in dept:
98
+ raise ValueError(f"Department {i+1} is missing 'description' field")
99
+ if "number" not in dept:
100
+ raise ValueError(f"Department {i+1} is missing 'number' field")
101
+
102
+ def _build_prompt(self):
103
+ """Build the agent's prompt with personality, goals, and instructions"""
104
+
105
+ # Set personality
106
+ self.prompt_add_section(
107
+ "Personality",
108
+ body="You are a friendly and professional receptionist. You speak clearly and efficiently while maintaining a warm, helpful tone."
109
+ )
110
+
111
+ # Set goal
112
+ self.prompt_add_section(
113
+ "Goal",
114
+ body="Your goal is to greet callers, collect their basic information, and transfer them to the appropriate department."
115
+ )
116
+
117
+ # Set instructions
118
+ self.prompt_add_section(
119
+ "Instructions",
120
+ bullets=[
121
+ f"Begin by greeting the caller with: '{self._greeting}'",
122
+ "Collect their name and a brief description of their needs.",
123
+ "Based on their needs, determine which department would be most appropriate.",
124
+ "Use the collect_caller_info function when you have their name and reason for calling.",
125
+ "Use the transfer_call function to transfer them to the appropriate department.",
126
+ "Before transferring, always confirm with the caller that they're being transferred to the right department.",
127
+ "If a caller's request doesn't clearly match a department, ask follow-up questions to clarify."
128
+ ]
129
+ )
130
+
131
+ # Add context with department information
132
+ global_data = self._global_data
133
+ departments = global_data.get("departments", [])
134
+
135
+ department_bullets = []
136
+ for dept in departments:
137
+ department_bullets.append(f"{dept['name']}: {dept['description']}")
138
+
139
+ self.prompt_add_section(
140
+ "Available Departments",
141
+ bullets=department_bullets
142
+ )
143
+
144
+ # Add a post-prompt for summary generation
145
+ self.set_post_prompt("""
146
+ Return a JSON summary of the conversation:
147
+ {
148
+ "caller_name": "CALLER'S NAME",
149
+ "reason": "REASON FOR CALLING",
150
+ "department": "DEPARTMENT TRANSFERRED TO",
151
+ "satisfaction": "high/medium/low (estimated caller satisfaction)"
152
+ }
153
+ """)
154
+
155
+ def _configure_agent_settings(self, voice):
156
+ """Configure additional agent settings"""
157
+
158
+ # Set AI behavior parameters
159
+ self.set_params({
160
+ "end_of_speech_timeout": 700,
161
+ "speech_event_timeout": 1000,
162
+ "transfer_summary": True # Enable call summary transfer between agents
163
+ })
164
+
165
+ # Set language with specified voice
166
+ self.add_language(
167
+ name="English",
168
+ code="en-US",
169
+ voice=voice,
170
+ speech_fillers=["Let me get that information for you...", "One moment please..."],
171
+ function_fillers=["I'm processing that...", "Let me check which department can help you best..."]
172
+ )
173
+
174
+ def _register_tools(self):
175
+ """Register the tools this agent needs"""
176
+
177
+ # Define collect_caller_info tool
178
+ self.define_tool(
179
+ name="collect_caller_info",
180
+ description="Collect the caller's information for routing",
181
+ parameters={
182
+ "name": {
183
+ "type": "string",
184
+ "description": "The caller's name"
185
+ },
186
+ "reason": {
187
+ "type": "string",
188
+ "description": "The reason for the call"
189
+ }
190
+ },
191
+ handler=self._collect_caller_info_handler
192
+ )
193
+
194
+ # Define transfer_call tool
195
+ # First, get the department names from global data
196
+ global_data = self._global_data
197
+ departments = global_data.get("departments", [])
198
+ department_names = [dept["name"] for dept in departments]
199
+
200
+ self.define_tool(
201
+ name="transfer_call",
202
+ description="Transfer the caller to the appropriate department",
203
+ parameters={
204
+ "department": {
205
+ "type": "string",
206
+ "description": "The department to transfer to",
207
+ "enum": department_names
208
+ }
209
+ },
210
+ handler=self._transfer_call_handler
211
+ )
212
+
213
+ def _collect_caller_info_handler(self, args, raw_data):
214
+ """Handler for collect_caller_info tool"""
215
+
216
+ # Get the caller info
217
+ name = args.get("name", "")
218
+ reason = args.get("reason", "")
219
+
220
+ # Create response with global data update
221
+ result = SwaigFunctionResult(
222
+ f"Thank you, {name}. I've noted that you're calling about {reason}."
223
+ )
224
+
225
+ # Update global data with caller info
226
+ result.add_actions([
227
+ {"set_global_data": {
228
+ "caller_info": {
229
+ "name": name,
230
+ "reason": reason
231
+ }
232
+ }}
233
+ ])
234
+
235
+ return result
236
+
237
+ def _transfer_call_handler(self, args, raw_data):
238
+ """Handler for transfer_call tool"""
239
+
240
+ # Get the department
241
+ department_name = args.get("department", "")
242
+
243
+ # Get global data
244
+ global_data = raw_data.get("global_data", {})
245
+ caller_info = global_data.get("caller_info", {})
246
+ name = caller_info.get("name", "the caller")
247
+ departments = global_data.get("departments", [])
248
+
249
+ # Find the department in the list
250
+ department = None
251
+ for dept in departments:
252
+ if dept["name"] == department_name:
253
+ department = dept
254
+ break
255
+
256
+ # If department not found, return error
257
+ if not department:
258
+ return SwaigFunctionResult(f"Sorry, I couldn't find the {department_name} department.")
259
+
260
+ # Get transfer number
261
+ transfer_number = department.get("number", "")
262
+
263
+ # Create message for caller
264
+ message = f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!"
265
+
266
+ # Create result with transfer SWML
267
+ result = SwaigFunctionResult(message)
268
+
269
+ # Add the SWML to execute the transfer
270
+ result.add_swml([
271
+ {
272
+ "play": {
273
+ "url": f"say:{message}"
274
+ }
275
+ },
276
+ {
277
+ "connect": {
278
+ "to": transfer_number
279
+ }
280
+ }
281
+ ])
282
+
283
+ return result
284
+
285
+ def on_summary(self, summary, raw_data=None):
286
+ """
287
+ Process the conversation summary
288
+
289
+ Args:
290
+ summary: Summary data from the conversation
291
+ raw_data: The complete raw POST data from the request
292
+ """
293
+ # Subclasses can override this to handle the summary
294
+ pass