signalwire-agents 0.1.0__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 +10 -1
  2. signalwire_agents/agent_server.py +73 -44
  3. signalwire_agents/core/__init__.py +9 -0
  4. signalwire_agents/core/agent_base.py +125 -33
  5. signalwire_agents/core/function_result.py +31 -12
  6. signalwire_agents/core/pom_builder.py +9 -0
  7. signalwire_agents/core/security/__init__.py +9 -0
  8. signalwire_agents/core/security/session_manager.py +9 -0
  9. signalwire_agents/core/state/__init__.py +9 -0
  10. signalwire_agents/core/state/file_state_manager.py +9 -0
  11. signalwire_agents/core/state/state_manager.py +9 -0
  12. signalwire_agents/core/swaig_function.py +9 -0
  13. signalwire_agents/core/swml_builder.py +9 -0
  14. signalwire_agents/core/swml_handler.py +9 -0
  15. signalwire_agents/core/swml_renderer.py +9 -0
  16. signalwire_agents/core/swml_service.py +88 -40
  17. signalwire_agents/prefabs/__init__.py +12 -1
  18. signalwire_agents/prefabs/concierge.py +9 -18
  19. signalwire_agents/prefabs/faq_bot.py +9 -18
  20. signalwire_agents/prefabs/info_gatherer.py +193 -183
  21. signalwire_agents/prefabs/receptionist.py +294 -0
  22. signalwire_agents/prefabs/survey.py +9 -18
  23. signalwire_agents/utils/__init__.py +9 -0
  24. signalwire_agents/utils/pom_utils.py +9 -0
  25. signalwire_agents/utils/schema_utils.py +9 -0
  26. signalwire_agents/utils/token_generators.py +9 -0
  27. signalwire_agents/utils/validators.py +9 -0
  28. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +75 -30
  29. signalwire_agents-0.1.2.dist-info/RECORD +34 -0
  30. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +1 -1
  31. signalwire_agents-0.1.2.dist-info/licenses/LICENSE +21 -0
  32. signalwire_agents-0.1.0.dist-info/RECORD +0 -32
  33. {signalwire_agents-0.1.0.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
  34. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,18 @@
1
1
  """
2
- InfoGathererAgent - Prefab agent for collecting structured information from users
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
3
12
  """
4
13
 
5
14
  from typing import List, Dict, Any, Optional, Union
6
15
  import json
7
- import os
8
16
 
9
17
  from signalwire_agents.core.agent_base import AgentBase
10
18
  from signalwire_agents.core.function_result import SwaigFunctionResult
@@ -12,242 +20,244 @@ from signalwire_agents.core.function_result import SwaigFunctionResult
12
20
 
13
21
  class InfoGathererAgent(AgentBase):
14
22
  """
15
- A prefab agent designed to collect specific fields of information from a user.
23
+ A prefab agent designed to collect answers to a series of questions.
16
24
 
17
25
  This agent will:
18
- 1. Ask for each requested field
19
- 2. Confirm the collected information
20
- 3. Return a structured JSON summary
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
21
29
 
22
30
  Example:
23
31
  agent = InfoGathererAgent(
24
- fields=[
25
- {"name": "full_name", "prompt": "What is your full name?"},
26
- {"name": "reason", "prompt": "How can I help you today?"}
27
- ],
28
- confirmation_template="Thanks {full_name}, I'll help you with {reason}."
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
+ ]
29
37
  )
30
38
  """
31
39
 
32
40
  def __init__(
33
41
  self,
34
- fields: List[Dict[str, str]],
35
- confirmation_template: Optional[str] = None,
36
- summary_format: Optional[Dict[str, Any]] = None,
42
+ questions: List[Dict[str, str]],
37
43
  name: str = "info_gatherer",
38
44
  route: str = "/info_gatherer",
39
- schema_path: Optional[str] = None,
40
45
  **kwargs
41
46
  ):
42
47
  """
43
48
  Initialize an information gathering agent
44
49
 
45
50
  Args:
46
- fields: List of fields to collect, each with:
47
- - name: Field name (for storage)
48
- - prompt: Question to ask to collect the field
49
- - validation: Optional regex or description of valid inputs
50
- confirmation_template: Optional template string for confirming collected info
51
- Format with field names in {brackets}, e.g. "Thanks {name}!"
52
- summary_format: Optional JSON template for the post_prompt summary
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
53
55
  name: Agent name for the route
54
56
  route: HTTP route for this agent
55
- schema_path: Optional path to a custom schema
56
57
  **kwargs: Additional arguments for AgentBase
57
58
  """
58
- # Find schema.json if not provided
59
- if not schema_path:
60
- current_dir = os.path.dirname(os.path.abspath(__file__))
61
- parent_dir = os.path.dirname(os.path.dirname(current_dir))
62
-
63
- schema_locations = [
64
- os.path.join(current_dir, "schema.json"),
65
- os.path.join(parent_dir, "schema.json")
66
- ]
67
-
68
- for loc in schema_locations:
69
- if os.path.exists(loc):
70
- schema_path = loc
71
- break
72
-
73
59
  # Initialize the base agent
74
60
  super().__init__(
75
61
  name=name,
76
62
  route=route,
77
63
  use_pom=True,
78
- schema_path=schema_path,
79
64
  **kwargs
80
65
  )
81
66
 
82
- self.fields = fields
83
- self.confirmation_template = confirmation_template
84
- self.summary_format = summary_format
67
+ # Validate questions format
68
+ self._validate_questions(questions)
85
69
 
86
- # Build the prompt
87
- self._build_info_gatherer_prompt()
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
+ })
88
76
 
89
- # Set up the post-prompt template
90
- self._setup_post_prompt()
77
+ # Build a minimal prompt
78
+ self._build_prompt()
91
79
 
92
80
  # Configure additional agent settings
93
81
  self._configure_agent_settings()
94
82
 
95
- def _build_info_gatherer_prompt(self):
96
- """Build the agent prompt for information gathering"""
97
- # Create base instructions
98
- instructions = [
99
- "Ask for ONLY ONE piece of information at a time.",
100
- "Confirm each answer before moving to the next question.",
101
- "Do not ask for information not in your field list.",
102
- "Be polite but direct with your questions."
103
- ]
104
-
105
- # Add field-specific instructions
106
- for i, field in enumerate(self.fields, 1):
107
- field_name = field.get("name")
108
- field_prompt = field.get("prompt")
109
- validation = field.get("validation", "")
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")
110
87
 
111
- field_text = f"{i}. {field_name}: \"{field_prompt}\""
112
- if validation:
113
- field_text += f" ({validation})"
114
-
115
- instructions.append(field_text)
116
-
117
- # Add confirmation instruction if a template is provided
118
- if self.confirmation_template:
119
- instructions.append(
120
- f"After collecting all fields, confirm with: {self.confirmation_template}"
121
- )
122
-
123
- # Create the prompt sections directly using prompt_add_section
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"""
124
96
  self.prompt_add_section(
125
- "Personality",
126
- body="You are a friendly and efficient virtual assistant."
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."
127
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
128
112
 
129
- self.prompt_add_section(
130
- "Goal",
131
- body="Your job is to collect specific information from the user."
132
- )
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"
133
126
 
134
- self.prompt_add_section(
135
- "Instructions",
136
- bullets=instructions
137
- )
138
-
139
- def _setup_post_prompt(self):
140
- """Set up the post-prompt for summary formatting"""
141
- # Build a JSON template for the collected data
142
- if not self.summary_format:
143
- # Default format: a flat dictionary of field values
144
- field_list = ", ".join([f'"{f["name"]}": "%{{{f["name"]}}}"' for f in self.fields])
145
- post_prompt = f"""
146
- Return a JSON object with all the information collected:
147
- {{
148
- {field_list}
149
- }}
150
- """
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."
151
133
  else:
152
- # Format is provided as a template - just serialize it
153
- post_prompt = f"""
154
- Return the following JSON structure with the collected information:
155
- {json.dumps(self.summary_format, indent=2)}
156
- """
134
+ instruction += "You don't need the user to confirm the answer to this question."
157
135
 
158
- self.set_post_prompt(post_prompt)
136
+ return instruction
159
137
 
160
- def _configure_agent_settings(self):
161
- """Configure additional agent settings"""
162
- # Add field names as hints to help the AI recognize them
163
- field_names = [field.get("name") for field in self.fields if "name" in field]
164
- self.add_hints(field_names)
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
165
146
 
166
- # Set AI behavior parameters for better information collection
167
- self.set_params({
168
- "wait_for_user": False,
169
- "end_of_speech_timeout": 1200, # Slightly longer for thoughtful responses
170
- "ai_volume": 5,
171
- "digit_timeout": 3000, # 3 seconds for DTMF input timeout
172
- "energy_level": 50 # Medium energy threshold
173
- })
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)
174
154
 
175
- # Add global data with the fields structure
176
- self.set_global_data({
177
- "fields": [
178
- {
179
- "name": field.get("name"),
180
- "prompt": field.get("prompt")
181
- }
182
- for field in self.fields
183
- ]
184
- })
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)
185
173
 
186
174
  @AgentBase.tool(
187
- name="validate_field",
188
- description="Validate if the provided value is valid for a specific field",
175
+ name="submit_answer",
176
+ description="Submit an answer to the current question and move to the next one",
189
177
  parameters={
190
- "field_name": {
191
- "type": "string",
192
- "description": "The name of the field to validate"
193
- },
194
- "value": {
178
+ "answer": {
195
179
  "type": "string",
196
- "description": "The value provided by the user"
180
+ "description": "The user's answer to the current question"
197
181
  }
198
182
  }
199
183
  )
200
- def validate_field(self, args, raw_data):
184
+ def submit_answer(self, args, raw_data):
201
185
  """
202
- Validate if a provided value is valid for a specific field
186
+ Submit an answer to the current question and move to the next one
203
187
 
204
- This function checks if a user's input meets any validation criteria
205
- specified for the field.
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
206
192
  """
207
- field_name = args.get("field_name", "")
208
- value = args.get("value", "")
209
-
210
- # Find the field by name
211
- field = None
212
- for f in self.fields:
213
- if f.get("name") == field_name:
214
- field = f
215
- break
216
-
217
- if not field:
218
- return SwaigFunctionResult(f"Error: Field '{field_name}' not found in configuration.")
219
-
220
- # Check if the field has validation requirements
221
- validation = field.get("validation", "")
222
-
223
- # Simple validation check (in a real implementation, you would perform
224
- # more sophisticated validation based on the validation rules)
225
- if validation and not value.strip():
226
- return SwaigFunctionResult({
227
- "response": f"The field '{field_name}' cannot be empty.",
228
- "valid": False
229
- })
230
-
231
- # For this simple example, we'll consider any non-empty value valid
232
- return SwaigFunctionResult({
233
- "response": f"The value for '{field_name}' is valid.",
234
- "valid": True
235
- })
236
-
237
- def on_summary(self, summary, raw_data=None):
238
- """
239
- Process the collected information summary
193
+ # Get the answer
194
+ answer = args.get("answer", "")
240
195
 
241
- Args:
242
- summary: Dictionary of collected field values
243
- raw_data: The complete raw POST data from the request
244
-
245
- Override this method in subclasses to use the collected data.
246
- """
247
- if summary:
248
- if isinstance(summary, dict):
249
- print(f"Information collected: {json.dumps(summary, indent=2)}")
250
- else:
251
- print(f"Information collected: {summary}")
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", [])
252
201
 
253
- # Subclasses should override this to save or process the collected data
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
+