signalwire-agents 0.1.6__py3-none-any.whl → 1.0.7__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 (140) hide show
  1. signalwire_agents/__init__.py +130 -4
  2. signalwire_agents/agent_server.py +438 -32
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +18 -0
  5. signalwire_agents/cli/build_search.py +1367 -0
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/execution/__init__.py +10 -0
  13. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  14. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  15. signalwire_agents/cli/init_project.py +1225 -0
  16. signalwire_agents/cli/output/__init__.py +10 -0
  17. signalwire_agents/cli/output/output_formatter.py +255 -0
  18. signalwire_agents/cli/output/swml_dump.py +186 -0
  19. signalwire_agents/cli/simulation/__init__.py +10 -0
  20. signalwire_agents/cli/simulation/data_generation.py +374 -0
  21. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  22. signalwire_agents/cli/simulation/mock_env.py +282 -0
  23. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  24. signalwire_agents/cli/test_swaig.py +809 -0
  25. signalwire_agents/cli/types.py +81 -0
  26. signalwire_agents/core/__init__.py +2 -2
  27. signalwire_agents/core/agent/__init__.py +12 -0
  28. signalwire_agents/core/agent/config/__init__.py +12 -0
  29. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  30. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  31. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  32. signalwire_agents/core/agent/prompt/manager.py +306 -0
  33. signalwire_agents/core/agent/routing/__init__.py +9 -0
  34. signalwire_agents/core/agent/security/__init__.py +9 -0
  35. signalwire_agents/core/agent/swml/__init__.py +9 -0
  36. signalwire_agents/core/agent/tools/__init__.py +15 -0
  37. signalwire_agents/core/agent/tools/decorator.py +97 -0
  38. signalwire_agents/core/agent/tools/registry.py +210 -0
  39. signalwire_agents/core/agent_base.py +959 -2166
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +707 -0
  43. signalwire_agents/core/data_map.py +487 -0
  44. signalwire_agents/core/function_result.py +1150 -1
  45. signalwire_agents/core/logging_config.py +376 -0
  46. signalwire_agents/core/mixins/__init__.py +28 -0
  47. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  48. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  49. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  50. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  51. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  52. signalwire_agents/core/mixins/state_mixin.py +153 -0
  53. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  54. signalwire_agents/core/mixins/web_mixin.py +1134 -0
  55. signalwire_agents/core/security/session_manager.py +174 -86
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +200 -0
  58. signalwire_agents/core/skill_manager.py +244 -0
  59. signalwire_agents/core/swaig_function.py +33 -9
  60. signalwire_agents/core/swml_builder.py +212 -12
  61. signalwire_agents/core/swml_handler.py +43 -13
  62. signalwire_agents/core/swml_renderer.py +123 -297
  63. signalwire_agents/core/swml_service.py +277 -260
  64. signalwire_agents/prefabs/concierge.py +6 -2
  65. signalwire_agents/prefabs/info_gatherer.py +149 -33
  66. signalwire_agents/prefabs/receptionist.py +14 -22
  67. signalwire_agents/prefabs/survey.py +6 -2
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +137 -0
  70. signalwire_agents/search/document_processor.py +1223 -0
  71. signalwire_agents/search/index_builder.py +804 -0
  72. signalwire_agents/search/migration.py +418 -0
  73. signalwire_agents/search/models.py +30 -0
  74. signalwire_agents/search/pgvector_backend.py +752 -0
  75. signalwire_agents/search/query_processor.py +502 -0
  76. signalwire_agents/search/search_engine.py +1264 -0
  77. signalwire_agents/search/search_service.py +574 -0
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +23 -0
  80. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  81. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  82. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  83. signalwire_agents/skills/datasphere/README.md +210 -0
  84. signalwire_agents/skills/datasphere/__init__.py +12 -0
  85. signalwire_agents/skills/datasphere/skill.py +310 -0
  86. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  87. signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
  88. signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
  89. signalwire_agents/skills/datetime/README.md +132 -0
  90. signalwire_agents/skills/datetime/__init__.py +10 -0
  91. signalwire_agents/skills/datetime/skill.py +126 -0
  92. signalwire_agents/skills/joke/README.md +149 -0
  93. signalwire_agents/skills/joke/__init__.py +10 -0
  94. signalwire_agents/skills/joke/skill.py +109 -0
  95. signalwire_agents/skills/math/README.md +161 -0
  96. signalwire_agents/skills/math/__init__.py +10 -0
  97. signalwire_agents/skills/math/skill.py +105 -0
  98. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  99. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  100. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  101. signalwire_agents/skills/native_vector_search/README.md +210 -0
  102. signalwire_agents/skills/native_vector_search/__init__.py +10 -0
  103. signalwire_agents/skills/native_vector_search/skill.py +820 -0
  104. signalwire_agents/skills/play_background_file/README.md +218 -0
  105. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  106. signalwire_agents/skills/play_background_file/skill.py +242 -0
  107. signalwire_agents/skills/registry.py +459 -0
  108. signalwire_agents/skills/spider/README.md +236 -0
  109. signalwire_agents/skills/spider/__init__.py +13 -0
  110. signalwire_agents/skills/spider/skill.py +598 -0
  111. signalwire_agents/skills/swml_transfer/README.md +395 -0
  112. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  113. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  114. signalwire_agents/skills/weather_api/README.md +178 -0
  115. signalwire_agents/skills/weather_api/__init__.py +12 -0
  116. signalwire_agents/skills/weather_api/skill.py +191 -0
  117. signalwire_agents/skills/web_search/README.md +163 -0
  118. signalwire_agents/skills/web_search/__init__.py +10 -0
  119. signalwire_agents/skills/web_search/skill.py +739 -0
  120. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  121. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  122. signalwire_agents/skills/wikipedia_search/skill.py +210 -0
  123. signalwire_agents/utils/__init__.py +14 -0
  124. signalwire_agents/utils/schema_utils.py +111 -44
  125. signalwire_agents/web/__init__.py +17 -0
  126. signalwire_agents/web/web_service.py +559 -0
  127. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  128. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  129. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  130. signalwire_agents-1.0.7.dist-info/METADATA +992 -0
  131. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  132. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
  133. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  134. signalwire_agents/core/state/file_state_manager.py +0 -219
  135. signalwire_agents/core/state/state_manager.py +0 -101
  136. signalwire_agents-0.1.6.data/data/schema.json +0 -5611
  137. signalwire_agents-0.1.6.dist-info/METADATA +0 -199
  138. signalwire_agents-0.1.6.dist-info/RECORD +0 -34
  139. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  140. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,707 @@
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
+ Contexts and Steps System for SignalWire Agents
12
+
13
+ This module provides an alternative to traditional POM-based prompts by allowing
14
+ agents to be defined as structured contexts with sequential steps. Each step
15
+ contains its own prompt, completion criteria, and function restrictions.
16
+ """
17
+
18
+ from typing import Dict, List, Optional, Union, Any
19
+
20
+
21
+ class Step:
22
+ """Represents a single step within a context"""
23
+
24
+ def __init__(self, name: str):
25
+ self.name = name
26
+ self._text: Optional[str] = None
27
+ self._step_criteria: Optional[str] = None
28
+ self._functions: Optional[Union[str, List[str]]] = None
29
+ self._valid_steps: Optional[List[str]] = None
30
+ self._valid_contexts: Optional[List[str]] = None
31
+
32
+ # POM-style sections for rich prompts
33
+ self._sections: List[Dict[str, Any]] = []
34
+
35
+ # Reset object for context switching from steps
36
+ self._reset_system_prompt: Optional[str] = None
37
+ self._reset_user_prompt: Optional[str] = None
38
+ self._reset_consolidate: bool = False
39
+ self._reset_full_reset: bool = False
40
+
41
+ def set_text(self, text: str) -> 'Step':
42
+ """
43
+ Set the step's prompt text directly
44
+
45
+ Args:
46
+ text: The prompt text for this step
47
+
48
+ Returns:
49
+ Self for method chaining
50
+ """
51
+ if self._sections:
52
+ raise ValueError("Cannot use set_text() when POM sections have been added. Use one approach or the other.")
53
+ self._text = text
54
+ return self
55
+
56
+ def add_section(self, title: str, body: str) -> 'Step':
57
+ """
58
+ Add a POM section to the step
59
+
60
+ Args:
61
+ title: Section title
62
+ body: Section body text
63
+
64
+ Returns:
65
+ Self for method chaining
66
+ """
67
+ if self._text is not None:
68
+ raise ValueError("Cannot add POM sections when set_text() has been used. Use one approach or the other.")
69
+ self._sections.append({"title": title, "body": body})
70
+ return self
71
+
72
+ def add_bullets(self, title: str, bullets: List[str]) -> 'Step':
73
+ """
74
+ Add a POM section with bullet points
75
+
76
+ Args:
77
+ title: Section title
78
+ bullets: List of bullet points
79
+
80
+ Returns:
81
+ Self for method chaining
82
+ """
83
+ if self._text is not None:
84
+ raise ValueError("Cannot add POM sections when set_text() has been used. Use one approach or the other.")
85
+ self._sections.append({"title": title, "bullets": bullets})
86
+ return self
87
+
88
+ def set_step_criteria(self, criteria: str) -> 'Step':
89
+ """
90
+ Set the criteria for determining when this step is complete
91
+
92
+ Args:
93
+ criteria: Description of step completion criteria
94
+
95
+ Returns:
96
+ Self for method chaining
97
+ """
98
+ self._step_criteria = criteria
99
+ return self
100
+
101
+ def set_functions(self, functions: Union[str, List[str]]) -> 'Step':
102
+ """
103
+ Set which functions are available in this step
104
+
105
+ Args:
106
+ functions: "none" to disable all functions, or list of function names
107
+
108
+ Returns:
109
+ Self for method chaining
110
+ """
111
+ self._functions = functions
112
+ return self
113
+
114
+ def set_valid_steps(self, steps: List[str]) -> 'Step':
115
+ """
116
+ Set which steps can be navigated to from this step
117
+
118
+ Args:
119
+ steps: List of valid step names (include "next" for sequential flow)
120
+
121
+ Returns:
122
+ Self for method chaining
123
+ """
124
+ self._valid_steps = steps
125
+ return self
126
+
127
+ def set_valid_contexts(self, contexts: List[str]) -> 'Step':
128
+ """
129
+ Set which contexts can be navigated to from this step
130
+
131
+ Args:
132
+ contexts: List of valid context names
133
+
134
+ Returns:
135
+ Self for method chaining
136
+ """
137
+ self._valid_contexts = contexts
138
+ return self
139
+
140
+ def set_reset_system_prompt(self, system_prompt: str) -> 'Step':
141
+ """
142
+ Set system prompt for context switching when this step navigates to a context
143
+
144
+ Args:
145
+ system_prompt: New system prompt for context switching
146
+
147
+ Returns:
148
+ Self for method chaining
149
+ """
150
+ self._reset_system_prompt = system_prompt
151
+ return self
152
+
153
+ def set_reset_user_prompt(self, user_prompt: str) -> 'Step':
154
+ """
155
+ Set user prompt for context switching when this step navigates to a context
156
+
157
+ Args:
158
+ user_prompt: User message to inject for context switching
159
+
160
+ Returns:
161
+ Self for method chaining
162
+ """
163
+ self._reset_user_prompt = user_prompt
164
+ return self
165
+
166
+ def set_reset_consolidate(self, consolidate: bool) -> 'Step':
167
+ """
168
+ Set whether to consolidate conversation when this step switches contexts
169
+
170
+ Args:
171
+ consolidate: Whether to consolidate previous conversation
172
+
173
+ Returns:
174
+ Self for method chaining
175
+ """
176
+ self._reset_consolidate = consolidate
177
+ return self
178
+
179
+ def set_reset_full_reset(self, full_reset: bool) -> 'Step':
180
+ """
181
+ Set whether to do full reset when this step switches contexts
182
+
183
+ Args:
184
+ full_reset: Whether to completely rewrite system prompt vs inject
185
+
186
+ Returns:
187
+ Self for method chaining
188
+ """
189
+ self._reset_full_reset = full_reset
190
+ return self
191
+
192
+ def _render_text(self) -> str:
193
+ """Render the step's prompt text"""
194
+ if self._text is not None:
195
+ return self._text
196
+
197
+ if not self._sections:
198
+ raise ValueError(f"Step '{self.name}' has no text or POM sections defined")
199
+
200
+ # Convert POM sections to markdown
201
+ markdown_parts = []
202
+ for section in self._sections:
203
+ if "bullets" in section:
204
+ markdown_parts.append(f"## {section['title']}")
205
+ for bullet in section["bullets"]:
206
+ markdown_parts.append(f"- {bullet}")
207
+ else:
208
+ markdown_parts.append(f"## {section['title']}")
209
+ markdown_parts.append(section["body"])
210
+ markdown_parts.append("") # Add spacing
211
+
212
+ return "\n".join(markdown_parts).strip()
213
+
214
+ def to_dict(self) -> Dict[str, Any]:
215
+ """Convert step to dictionary for SWML generation"""
216
+ step_dict = {
217
+ "name": self.name,
218
+ "text": self._render_text()
219
+ }
220
+
221
+ if self._step_criteria:
222
+ step_dict["step_criteria"] = self._step_criteria
223
+
224
+ if self._functions is not None:
225
+ step_dict["functions"] = self._functions
226
+
227
+ if self._valid_steps is not None:
228
+ step_dict["valid_steps"] = self._valid_steps
229
+
230
+ if self._valid_contexts is not None:
231
+ step_dict["valid_contexts"] = self._valid_contexts
232
+
233
+ # Add reset object if any reset parameters are set
234
+ reset_obj = {}
235
+ if self._reset_system_prompt is not None:
236
+ reset_obj["system_prompt"] = self._reset_system_prompt
237
+ if self._reset_user_prompt is not None:
238
+ reset_obj["user_prompt"] = self._reset_user_prompt
239
+ if self._reset_consolidate:
240
+ reset_obj["consolidate"] = self._reset_consolidate
241
+ if self._reset_full_reset:
242
+ reset_obj["full_reset"] = self._reset_full_reset
243
+
244
+ if reset_obj:
245
+ step_dict["reset"] = reset_obj
246
+
247
+ return step_dict
248
+
249
+
250
+ class Context:
251
+ """Represents a single context containing multiple steps"""
252
+
253
+ def __init__(self, name: str):
254
+ self.name = name
255
+ self._steps: Dict[str, Step] = {}
256
+ self._step_order: List[str] = []
257
+ self._valid_contexts: Optional[List[str]] = None
258
+
259
+ # Context entry parameters
260
+ self._post_prompt: Optional[str] = None
261
+ self._system_prompt: Optional[str] = None
262
+ self._system_prompt_sections: List[Dict[str, Any]] = [] # For POM-style system prompts
263
+ self._consolidate: bool = False
264
+ self._full_reset: bool = False
265
+ self._user_prompt: Optional[str] = None
266
+ self._isolated: bool = False
267
+
268
+ # Context prompt (separate from system_prompt)
269
+ self._prompt_text: Optional[str] = None
270
+ self._prompt_sections: List[Dict[str, Any]] = []
271
+
272
+ # Context fillers
273
+ self._enter_fillers: Optional[Dict[str, List[str]]] = None
274
+ self._exit_fillers: Optional[Dict[str, List[str]]] = None
275
+
276
+ def add_step(self, name: str) -> Step:
277
+ """
278
+ Add a new step to this context
279
+
280
+ Args:
281
+ name: Step name
282
+
283
+ Returns:
284
+ Step object for method chaining
285
+ """
286
+ if name in self._steps:
287
+ raise ValueError(f"Step '{name}' already exists in context '{self.name}'")
288
+
289
+ step = Step(name)
290
+ self._steps[name] = step
291
+ self._step_order.append(name)
292
+ return step
293
+
294
+ def set_valid_contexts(self, contexts: List[str]) -> 'Context':
295
+ """
296
+ Set which contexts can be navigated to from this context
297
+
298
+ Args:
299
+ contexts: List of valid context names
300
+
301
+ Returns:
302
+ Self for method chaining
303
+ """
304
+ self._valid_contexts = contexts
305
+ return self
306
+
307
+ def set_post_prompt(self, post_prompt: str) -> 'Context':
308
+ """
309
+ Set post prompt override for this context
310
+
311
+ Args:
312
+ post_prompt: Post prompt text to use when this context is active
313
+
314
+ Returns:
315
+ Self for method chaining
316
+ """
317
+ self._post_prompt = post_prompt
318
+ return self
319
+
320
+ def set_system_prompt(self, system_prompt: str) -> 'Context':
321
+ """
322
+ Set system prompt for context switching (triggers context reset)
323
+
324
+ Args:
325
+ system_prompt: New system prompt for when this context is entered
326
+
327
+ Returns:
328
+ Self for method chaining
329
+ """
330
+ if self._system_prompt_sections:
331
+ raise ValueError("Cannot use set_system_prompt() when POM sections have been added for system prompt. Use one approach or the other.")
332
+ self._system_prompt = system_prompt
333
+ return self
334
+
335
+ def set_consolidate(self, consolidate: bool) -> 'Context':
336
+ """
337
+ Set whether to consolidate conversation history when entering this context
338
+
339
+ Args:
340
+ consolidate: Whether to consolidate previous conversation
341
+
342
+ Returns:
343
+ Self for method chaining
344
+ """
345
+ self._consolidate = consolidate
346
+ return self
347
+
348
+ def set_full_reset(self, full_reset: bool) -> 'Context':
349
+ """
350
+ Set whether to do full reset when entering this context
351
+
352
+ Args:
353
+ full_reset: Whether to completely rewrite system prompt vs inject
354
+
355
+ Returns:
356
+ Self for method chaining
357
+ """
358
+ self._full_reset = full_reset
359
+ return self
360
+
361
+ def set_user_prompt(self, user_prompt: str) -> 'Context':
362
+ """
363
+ Set user prompt to inject when entering this context
364
+
365
+ Args:
366
+ user_prompt: User message to inject for context
367
+
368
+ Returns:
369
+ Self for method chaining
370
+ """
371
+ self._user_prompt = user_prompt
372
+ return self
373
+
374
+ def set_isolated(self, isolated: bool) -> 'Context':
375
+ """
376
+ Set whether to truncate conversation history when entering this context
377
+
378
+ Args:
379
+ isolated: Whether to truncate conversation on context switch
380
+
381
+ Returns:
382
+ Self for method chaining
383
+ """
384
+ self._isolated = isolated
385
+ return self
386
+
387
+ def add_system_section(self, title: str, body: str) -> 'Context':
388
+ """
389
+ Add a POM section to the system prompt
390
+
391
+ Args:
392
+ title: Section title
393
+ body: Section body text
394
+
395
+ Returns:
396
+ Self for method chaining
397
+ """
398
+ if self._system_prompt is not None:
399
+ raise ValueError("Cannot add POM sections for system prompt when set_system_prompt() has been used. Use one approach or the other.")
400
+ self._system_prompt_sections.append({"title": title, "body": body})
401
+ return self
402
+
403
+ def add_system_bullets(self, title: str, bullets: List[str]) -> 'Context':
404
+ """
405
+ Add a POM section with bullet points to the system prompt
406
+
407
+ Args:
408
+ title: Section title
409
+ bullets: List of bullet points
410
+
411
+ Returns:
412
+ Self for method chaining
413
+ """
414
+ if self._system_prompt is not None:
415
+ raise ValueError("Cannot add POM sections for system prompt when set_system_prompt() has been used. Use one approach or the other.")
416
+ self._system_prompt_sections.append({"title": title, "bullets": bullets})
417
+ return self
418
+
419
+ def set_prompt(self, prompt: str) -> 'Context':
420
+ """
421
+ Set the context's prompt text directly
422
+
423
+ Args:
424
+ prompt: The prompt text for this context
425
+
426
+ Returns:
427
+ Self for method chaining
428
+ """
429
+ if self._prompt_sections:
430
+ raise ValueError("Cannot use set_prompt() when POM sections have been added. Use one approach or the other.")
431
+ self._prompt_text = prompt
432
+ return self
433
+
434
+ def add_section(self, title: str, body: str) -> 'Context':
435
+ """
436
+ Add a POM section to the context prompt
437
+
438
+ Args:
439
+ title: Section title
440
+ body: Section body text
441
+
442
+ Returns:
443
+ Self for method chaining
444
+ """
445
+ if self._prompt_text is not None:
446
+ raise ValueError("Cannot add POM sections when set_prompt() has been used. Use one approach or the other.")
447
+ self._prompt_sections.append({"title": title, "body": body})
448
+ return self
449
+
450
+ def add_bullets(self, title: str, bullets: List[str]) -> 'Context':
451
+ """
452
+ Add a POM section with bullet points to the context prompt
453
+
454
+ Args:
455
+ title: Section title
456
+ bullets: List of bullet points
457
+
458
+ Returns:
459
+ Self for method chaining
460
+ """
461
+ if self._prompt_text is not None:
462
+ raise ValueError("Cannot add POM sections when set_prompt() has been used. Use one approach or the other.")
463
+ self._prompt_sections.append({"title": title, "bullets": bullets})
464
+ return self
465
+
466
+ def set_enter_fillers(self, enter_fillers: Dict[str, List[str]]) -> 'Context':
467
+ """
468
+ Set fillers that the AI says when entering this context
469
+
470
+ Args:
471
+ enter_fillers: Dictionary mapping language codes (or "default") to lists of filler phrases
472
+ Example: {"en-US": ["Welcome...", "Hello..."], "default": ["Entering..."]}
473
+
474
+ Returns:
475
+ Self for method chaining
476
+ """
477
+ if enter_fillers and isinstance(enter_fillers, dict):
478
+ self._enter_fillers = enter_fillers
479
+ return self
480
+
481
+ def set_exit_fillers(self, exit_fillers: Dict[str, List[str]]) -> 'Context':
482
+ """
483
+ Set fillers that the AI says when exiting this context
484
+
485
+ Args:
486
+ exit_fillers: Dictionary mapping language codes (or "default") to lists of filler phrases
487
+ Example: {"en-US": ["Goodbye...", "Thank you..."], "default": ["Exiting..."]}
488
+
489
+ Returns:
490
+ Self for method chaining
491
+ """
492
+ if exit_fillers and isinstance(exit_fillers, dict):
493
+ self._exit_fillers = exit_fillers
494
+ return self
495
+
496
+ def add_enter_filler(self, language_code: str, fillers: List[str]) -> 'Context':
497
+ """
498
+ Add enter fillers for a specific language
499
+
500
+ Args:
501
+ language_code: Language code (e.g., "en-US", "es") or "default" for catch-all
502
+ fillers: List of filler phrases for entering this context
503
+
504
+ Returns:
505
+ Self for method chaining
506
+ """
507
+ if language_code and fillers and isinstance(fillers, list):
508
+ if self._enter_fillers is None:
509
+ self._enter_fillers = {}
510
+ self._enter_fillers[language_code] = fillers
511
+ return self
512
+
513
+ def add_exit_filler(self, language_code: str, fillers: List[str]) -> 'Context':
514
+ """
515
+ Add exit fillers for a specific language
516
+
517
+ Args:
518
+ language_code: Language code (e.g., "en-US", "es") or "default" for catch-all
519
+ fillers: List of filler phrases for exiting this context
520
+
521
+ Returns:
522
+ Self for method chaining
523
+ """
524
+ if language_code and fillers and isinstance(fillers, list):
525
+ if self._exit_fillers is None:
526
+ self._exit_fillers = {}
527
+ self._exit_fillers[language_code] = fillers
528
+ return self
529
+
530
+ def _render_prompt(self) -> Optional[str]:
531
+ """Render the context's prompt text"""
532
+ if self._prompt_text is not None:
533
+ return self._prompt_text
534
+
535
+ if not self._prompt_sections:
536
+ return None
537
+
538
+ # Convert POM sections to markdown
539
+ markdown_parts = []
540
+ for section in self._prompt_sections:
541
+ if "bullets" in section:
542
+ markdown_parts.append(f"## {section['title']}")
543
+ for bullet in section["bullets"]:
544
+ markdown_parts.append(f"- {bullet}")
545
+ else:
546
+ markdown_parts.append(f"## {section['title']}")
547
+ markdown_parts.append(section["body"])
548
+ markdown_parts.append("") # Add spacing
549
+
550
+ return "\n".join(markdown_parts).strip()
551
+
552
+ def _render_system_prompt(self) -> Optional[str]:
553
+ """Render the system prompt text"""
554
+ if self._system_prompt is not None:
555
+ return self._system_prompt
556
+
557
+ if not self._system_prompt_sections:
558
+ return None
559
+
560
+ # Convert POM sections to markdown
561
+ markdown_parts = []
562
+ for section in self._system_prompt_sections:
563
+ if "bullets" in section:
564
+ markdown_parts.append(f"## {section['title']}")
565
+ for bullet in section["bullets"]:
566
+ markdown_parts.append(f"- {bullet}")
567
+ else:
568
+ markdown_parts.append(f"## {section['title']}")
569
+ markdown_parts.append(section["body"])
570
+ markdown_parts.append("") # Add spacing
571
+
572
+ return "\n".join(markdown_parts).strip()
573
+
574
+ def to_dict(self) -> Dict[str, Any]:
575
+ """Convert context to dictionary for SWML generation"""
576
+ if not self._steps:
577
+ raise ValueError(f"Context '{self.name}' has no steps defined")
578
+
579
+ context_dict = {
580
+ "steps": [self._steps[name].to_dict() for name in self._step_order]
581
+ }
582
+
583
+ if self._valid_contexts is not None:
584
+ context_dict["valid_contexts"] = self._valid_contexts
585
+
586
+ # Add context entry parameters
587
+ if self._post_prompt is not None:
588
+ context_dict["post_prompt"] = self._post_prompt
589
+
590
+ rendered_system_prompt = self._render_system_prompt()
591
+ if rendered_system_prompt is not None:
592
+ context_dict["system_prompt"] = rendered_system_prompt
593
+
594
+ if self._consolidate:
595
+ context_dict["consolidate"] = self._consolidate
596
+
597
+ if self._full_reset:
598
+ context_dict["full_reset"] = self._full_reset
599
+
600
+ if self._user_prompt is not None:
601
+ context_dict["user_prompt"] = self._user_prompt
602
+
603
+ if self._isolated:
604
+ context_dict["isolated"] = self._isolated
605
+
606
+ # Add context prompt - use POM structure if sections exist, otherwise use string
607
+ if self._prompt_sections:
608
+ # Use structured POM format
609
+ context_dict["pom"] = self._prompt_sections
610
+ elif self._prompt_text is not None:
611
+ # Use string format
612
+ context_dict["prompt"] = self._prompt_text
613
+
614
+ # Add enter and exit fillers if defined
615
+ if self._enter_fillers is not None:
616
+ context_dict["enter_fillers"] = self._enter_fillers
617
+
618
+ if self._exit_fillers is not None:
619
+ context_dict["exit_fillers"] = self._exit_fillers
620
+
621
+ return context_dict
622
+
623
+
624
+ class ContextBuilder:
625
+ """Main builder class for creating contexts and steps"""
626
+
627
+ def __init__(self, agent):
628
+ self._agent = agent
629
+ self._contexts: Dict[str, Context] = {}
630
+ self._context_order: List[str] = []
631
+
632
+ def add_context(self, name: str) -> Context:
633
+ """
634
+ Add a new context
635
+
636
+ Args:
637
+ name: Context name
638
+
639
+ Returns:
640
+ Context object for method chaining
641
+ """
642
+ if name in self._contexts:
643
+ raise ValueError(f"Context '{name}' already exists")
644
+
645
+ context = Context(name)
646
+ self._contexts[name] = context
647
+ self._context_order.append(name)
648
+ return context
649
+
650
+ def validate(self) -> None:
651
+ """Validate the contexts configuration"""
652
+ if not self._contexts:
653
+ raise ValueError("At least one context must be defined")
654
+
655
+ # If only one context, it must be named "default"
656
+ if len(self._contexts) == 1:
657
+ context_name = list(self._contexts.keys())[0]
658
+ if context_name != "default":
659
+ raise ValueError("When using a single context, it must be named 'default'")
660
+
661
+ # Validate each context has at least one step
662
+ for context_name, context in self._contexts.items():
663
+ if not context._steps:
664
+ raise ValueError(f"Context '{context_name}' must have at least one step")
665
+
666
+ # Validate step references in valid_steps
667
+ for context_name, context in self._contexts.items():
668
+ for step_name, step in context._steps.items():
669
+ if step._valid_steps:
670
+ for valid_step in step._valid_steps:
671
+ if valid_step != "next" and valid_step not in context._steps:
672
+ raise ValueError(
673
+ f"Step '{step_name}' in context '{context_name}' "
674
+ f"references unknown step '{valid_step}'"
675
+ )
676
+
677
+ # Validate context references in valid_contexts
678
+ for context_name, context in self._contexts.items():
679
+ if context._valid_contexts:
680
+ for valid_context in context._valid_contexts:
681
+ if valid_context not in self._contexts:
682
+ raise ValueError(
683
+ f"Context '{context_name}' references unknown context '{valid_context}'"
684
+ )
685
+
686
+ def to_dict(self) -> Dict[str, Any]:
687
+ """Convert all contexts to dictionary for SWML generation"""
688
+ self.validate()
689
+
690
+ return {
691
+ context_name: context.to_dict()
692
+ for context_name in self._context_order
693
+ for context_name, context in [(context_name, self._contexts[context_name])]
694
+ }
695
+
696
+
697
+ def create_simple_context(name: str = "default") -> Context:
698
+ """
699
+ Helper function to create a simple single context
700
+
701
+ Args:
702
+ name: Context name (defaults to "default")
703
+
704
+ Returns:
705
+ Context object for method chaining
706
+ """
707
+ return Context(name)