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.
- signalwire_agents/__init__.py +10 -1
- signalwire_agents/agent_server.py +73 -44
- signalwire_agents/core/__init__.py +9 -0
- signalwire_agents/core/agent_base.py +125 -33
- signalwire_agents/core/function_result.py +31 -12
- signalwire_agents/core/pom_builder.py +9 -0
- signalwire_agents/core/security/__init__.py +9 -0
- signalwire_agents/core/security/session_manager.py +9 -0
- signalwire_agents/core/state/__init__.py +9 -0
- signalwire_agents/core/state/file_state_manager.py +9 -0
- signalwire_agents/core/state/state_manager.py +9 -0
- signalwire_agents/core/swaig_function.py +9 -0
- signalwire_agents/core/swml_builder.py +9 -0
- signalwire_agents/core/swml_handler.py +9 -0
- signalwire_agents/core/swml_renderer.py +9 -0
- signalwire_agents/core/swml_service.py +88 -40
- signalwire_agents/prefabs/__init__.py +12 -1
- signalwire_agents/prefabs/concierge.py +9 -18
- signalwire_agents/prefabs/faq_bot.py +9 -18
- signalwire_agents/prefabs/info_gatherer.py +193 -183
- signalwire_agents/prefabs/receptionist.py +294 -0
- signalwire_agents/prefabs/survey.py +9 -18
- signalwire_agents/utils/__init__.py +9 -0
- signalwire_agents/utils/pom_utils.py +9 -0
- signalwire_agents/utils/schema_utils.py +9 -0
- signalwire_agents/utils/token_generators.py +9 -0
- signalwire_agents/utils/validators.py +9 -0
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +75 -30
- signalwire_agents-0.1.2.dist-info/RECORD +34 -0
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.2.dist-info/licenses/LICENSE +21 -0
- signalwire_agents-0.1.0.dist-info/RECORD +0 -32
- {signalwire_agents-0.1.0.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
- {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
|
-
|
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
|
23
|
+
A prefab agent designed to collect answers to a series of questions.
|
16
24
|
|
17
25
|
This agent will:
|
18
|
-
1. Ask
|
19
|
-
2.
|
20
|
-
3.
|
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
|
-
|
25
|
-
{"
|
26
|
-
{"
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
47
|
-
-
|
48
|
-
-
|
49
|
-
-
|
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
|
-
|
83
|
-
self.
|
84
|
-
self.summary_format = summary_format
|
67
|
+
# Validate questions format
|
68
|
+
self._validate_questions(questions)
|
85
69
|
|
86
|
-
#
|
87
|
-
self.
|
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
|
-
#
|
90
|
-
self.
|
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
|
96
|
-
"""
|
97
|
-
|
98
|
-
|
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
|
-
|
112
|
-
if
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
"
|
126
|
-
body="
|
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
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
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
|
-
|
136
|
+
return instruction
|
159
137
|
|
160
|
-
|
161
|
-
""
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
#
|
176
|
-
|
177
|
-
"
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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="
|
188
|
-
description="
|
175
|
+
name="submit_answer",
|
176
|
+
description="Submit an answer to the current question and move to the next one",
|
189
177
|
parameters={
|
190
|
-
"
|
191
|
-
"type": "string",
|
192
|
-
"description": "The name of the field to validate"
|
193
|
-
},
|
194
|
-
"value": {
|
178
|
+
"answer": {
|
195
179
|
"type": "string",
|
196
|
-
"description": "The
|
180
|
+
"description": "The user's answer to the current question"
|
197
181
|
}
|
198
182
|
}
|
199
183
|
)
|
200
|
-
def
|
184
|
+
def submit_answer(self, args, raw_data):
|
201
185
|
"""
|
202
|
-
|
186
|
+
Submit an answer to the current question and move to the next one
|
203
187
|
|
204
|
-
This function
|
205
|
-
|
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
|
-
|
208
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
#
|
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
|
+
|