signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  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/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,280 @@
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
+ import os
11
+ import json
12
+ import base64
13
+ from typing import Union, Tuple
14
+
15
+ from fastapi import Request
16
+
17
+
18
+ class AuthMixin:
19
+ """
20
+ Mixin class containing all authentication-related methods for AgentBase
21
+ """
22
+
23
+ def validate_basic_auth(self, username: str, password: str) -> bool:
24
+ """
25
+ Validate basic auth credentials
26
+
27
+ Args:
28
+ username: Username from request
29
+ password: Password from request
30
+
31
+ Returns:
32
+ True if valid, False otherwise
33
+
34
+ This method can be overridden by subclasses.
35
+ """
36
+ return (username, password) == self._basic_auth
37
+
38
+ def get_basic_auth_credentials(self, include_source: bool = False) -> Union[Tuple[str, str], Tuple[str, str, str]]:
39
+ """
40
+ Get the basic auth credentials
41
+
42
+ Args:
43
+ include_source: Whether to include the source of the credentials
44
+
45
+ Returns:
46
+ If include_source is False:
47
+ (username, password) tuple
48
+ If include_source is True:
49
+ (username, password, source) tuple, where source is one of:
50
+ "provided", "environment", or "generated"
51
+ """
52
+ username, password = self._basic_auth
53
+
54
+ if not include_source:
55
+ return (username, password)
56
+
57
+ # Determine source of credentials
58
+ env_user = os.environ.get('SWML_BASIC_AUTH_USER')
59
+ env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
60
+
61
+ # More robust source detection
62
+ if env_user and env_pass and username == env_user and password == env_pass:
63
+ source = "environment"
64
+ elif username.startswith("user_") and len(password) > 20: # Format of generated credentials
65
+ source = "generated"
66
+ else:
67
+ source = "provided"
68
+
69
+ return (username, password, source)
70
+
71
+ def _check_basic_auth(self, request: Request) -> bool:
72
+ """
73
+ Check basic auth from a request
74
+
75
+ Args:
76
+ request: FastAPI request object
77
+
78
+ Returns:
79
+ True if auth is valid, False otherwise
80
+ """
81
+ auth_header = request.headers.get("Authorization")
82
+ if not auth_header or not auth_header.startswith("Basic "):
83
+ return False
84
+
85
+ try:
86
+ # Decode the base64 credentials
87
+ credentials = base64.b64decode(auth_header[6:]).decode("utf-8")
88
+ username, password = credentials.split(":", 1)
89
+ return self.validate_basic_auth(username, password)
90
+ except Exception:
91
+ return False
92
+
93
+ def _check_cgi_auth(self) -> bool:
94
+ """
95
+ Check basic auth in CGI mode using environment variables
96
+
97
+ Returns:
98
+ True if auth is valid, False otherwise
99
+ """
100
+ # Check for HTTP_AUTHORIZATION environment variable
101
+ auth_header = os.getenv('HTTP_AUTHORIZATION')
102
+ if not auth_header:
103
+ # Also check for REMOTE_USER (if web server handled auth)
104
+ remote_user = os.getenv('REMOTE_USER')
105
+ if remote_user:
106
+ # If web server handled auth, trust it
107
+ return True
108
+ return False
109
+
110
+ if not auth_header.startswith('Basic '):
111
+ return False
112
+
113
+ try:
114
+ # Decode the base64 credentials
115
+ credentials = base64.b64decode(auth_header[6:]).decode("utf-8")
116
+ username, password = credentials.split(":", 1)
117
+ return self.validate_basic_auth(username, password)
118
+ except Exception:
119
+ return False
120
+
121
+ def _send_cgi_auth_challenge(self) -> str:
122
+ """
123
+ Send authentication challenge in CGI mode
124
+
125
+ Returns:
126
+ HTTP response with 401 status and WWW-Authenticate header
127
+ """
128
+ # In CGI, we need to output the complete HTTP response
129
+ response = "Status: 401 Unauthorized\r\n"
130
+ response += "WWW-Authenticate: Basic realm=\"SignalWire Agent\"\r\n"
131
+ response += "Content-Type: application/json\r\n"
132
+ response += "\r\n"
133
+ response += json.dumps({"error": "Unauthorized"})
134
+ return response
135
+
136
+ def _check_lambda_auth(self, event) -> bool:
137
+ """
138
+ Check basic auth in Lambda mode using event headers
139
+
140
+ Args:
141
+ event: Lambda event object containing headers
142
+
143
+ Returns:
144
+ True if auth is valid, False otherwise
145
+ """
146
+ if not event or 'headers' not in event:
147
+ return False
148
+
149
+ headers = event['headers']
150
+
151
+ # Check for authorization header (case-insensitive)
152
+ auth_header = None
153
+ for key, value in headers.items():
154
+ if key.lower() == 'authorization':
155
+ auth_header = value
156
+ break
157
+
158
+ if not auth_header or not auth_header.startswith('Basic '):
159
+ return False
160
+
161
+ try:
162
+ # Decode the base64 credentials
163
+ credentials = base64.b64decode(auth_header[6:]).decode("utf-8")
164
+ username, password = credentials.split(":", 1)
165
+ return self.validate_basic_auth(username, password)
166
+ except Exception:
167
+ return False
168
+
169
+ def _send_lambda_auth_challenge(self) -> dict:
170
+ """
171
+ Send authentication challenge in Lambda mode
172
+
173
+ Returns:
174
+ Lambda response with 401 status and WWW-Authenticate header
175
+ """
176
+ return {
177
+ "statusCode": 401,
178
+ "headers": {
179
+ "WWW-Authenticate": "Basic realm=\"SignalWire Agent\"",
180
+ "Content-Type": "application/json"
181
+ },
182
+ "body": json.dumps({"error": "Unauthorized"})
183
+ }
184
+
185
+ def _check_google_cloud_function_auth(self, request) -> bool:
186
+ """
187
+ Check basic auth in Google Cloud Functions mode using request headers
188
+
189
+ Args:
190
+ request: Flask request object or similar containing headers
191
+
192
+ Returns:
193
+ True if auth is valid, False otherwise
194
+ """
195
+ if not hasattr(request, 'headers'):
196
+ return False
197
+
198
+ # Check for authorization header (case-insensitive)
199
+ # Flask headers can be accessed directly with .get() which is case-insensitive
200
+ auth_header = request.headers.get('Authorization')
201
+
202
+ if not auth_header or not auth_header.startswith('Basic '):
203
+ return False
204
+
205
+ try:
206
+ import base64
207
+ encoded_credentials = auth_header[6:] # Remove 'Basic '
208
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
209
+ provided_username, provided_password = decoded_credentials.split(':', 1)
210
+
211
+ expected_username, expected_password = self.get_basic_auth_credentials()
212
+ return (provided_username == expected_username and
213
+ provided_password == expected_password)
214
+ except Exception:
215
+ return False
216
+
217
+ def _send_google_cloud_function_auth_challenge(self):
218
+ """
219
+ Send authentication challenge in Google Cloud Functions mode
220
+
221
+ Returns:
222
+ Flask-compatible response with 401 status and WWW-Authenticate header
223
+ """
224
+ from flask import Response
225
+ return Response(
226
+ response=json.dumps({"error": "Unauthorized"}),
227
+ status=401,
228
+ headers={
229
+ "WWW-Authenticate": "Basic realm=\"SignalWire Agent\"",
230
+ "Content-Type": "application/json"
231
+ }
232
+ )
233
+
234
+ def _check_azure_function_auth(self, req) -> bool:
235
+ """
236
+ Check basic auth in Azure Functions mode using request object
237
+
238
+ Args:
239
+ req: Azure Functions request object containing headers
240
+
241
+ Returns:
242
+ True if auth is valid, False otherwise
243
+ """
244
+ if not hasattr(req, 'headers'):
245
+ return False
246
+
247
+ # Check for authorization header - use .get() which works with both dict and Flask headers
248
+ auth_header = req.headers.get('Authorization')
249
+
250
+ if not auth_header or not auth_header.startswith('Basic '):
251
+ return False
252
+
253
+ try:
254
+ import base64
255
+ encoded_credentials = auth_header[6:] # Remove 'Basic '
256
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
257
+ provided_username, provided_password = decoded_credentials.split(':', 1)
258
+
259
+ expected_username, expected_password = self.get_basic_auth_credentials()
260
+ return (provided_username == expected_username and
261
+ provided_password == expected_password)
262
+ except Exception:
263
+ return False
264
+
265
+ def _send_azure_function_auth_challenge(self):
266
+ """
267
+ Send authentication challenge in Azure Functions mode
268
+
269
+ Returns:
270
+ Azure Functions response with 401 status and WWW-Authenticate header
271
+ """
272
+ import azure.functions as func
273
+ return func.HttpResponse(
274
+ body=json.dumps({"error": "Unauthorized"}),
275
+ status_code=401,
276
+ headers={
277
+ "WWW-Authenticate": "Basic realm=\"SignalWire Agent\"",
278
+ "Content-Type": "application/json"
279
+ }
280
+ )
@@ -0,0 +1,358 @@
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
+ from typing import Optional, Union, List, Dict, Any, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from signalwire_agents.core.agent_base import AgentBase
14
+ from signalwire_agents.core.contexts import ContextBuilder
15
+ else:
16
+ from signalwire_agents.core.contexts import ContextBuilder
17
+
18
+
19
+ class PromptMixin:
20
+ """
21
+ Mixin class containing all prompt-related methods for AgentBase
22
+ """
23
+
24
+ def _process_prompt_sections(self):
25
+ """
26
+ Process declarative PROMPT_SECTIONS attribute from a subclass
27
+
28
+ This auto-vivifies section methods and bootstraps the prompt
29
+ from class declaration, allowing for declarative agents.
30
+ """
31
+ # Skip if no PROMPT_SECTIONS defined or not using POM
32
+ cls = self.__class__
33
+ if not hasattr(cls, 'PROMPT_SECTIONS') or cls.PROMPT_SECTIONS is None or not self._use_pom:
34
+ return
35
+
36
+ sections = cls.PROMPT_SECTIONS
37
+
38
+ # If sections is a dictionary mapping section names to content
39
+ if isinstance(sections, dict):
40
+ for title, content in sections.items():
41
+ # Handle different content types
42
+ if isinstance(content, str):
43
+ # Plain text - add as body
44
+ self.prompt_add_section(title, body=content)
45
+ elif isinstance(content, list) and content: # Only add if non-empty
46
+ # List of strings - add as bullets
47
+ self.prompt_add_section(title, bullets=content)
48
+ elif isinstance(content, dict):
49
+ # Dictionary with body/bullets/subsections
50
+ body = content.get('body', '')
51
+ bullets = content.get('bullets', [])
52
+ numbered = content.get('numbered', False)
53
+ numbered_bullets = content.get('numberedBullets', False)
54
+
55
+ # Only create section if it has content
56
+ if body or bullets or 'subsections' in content:
57
+ # Create the section
58
+ self.prompt_add_section(
59
+ title,
60
+ body=body,
61
+ bullets=bullets if bullets else None,
62
+ numbered=numbered,
63
+ numbered_bullets=numbered_bullets
64
+ )
65
+
66
+ # Process subsections if any
67
+ subsections = content.get('subsections', [])
68
+ for subsection in subsections:
69
+ if 'title' in subsection:
70
+ sub_title = subsection['title']
71
+ sub_body = subsection.get('body', '')
72
+ sub_bullets = subsection.get('bullets', [])
73
+
74
+ # Only add subsection if it has content
75
+ if sub_body or sub_bullets:
76
+ self.prompt_add_subsection(
77
+ title,
78
+ sub_title,
79
+ body=sub_body,
80
+ bullets=sub_bullets if sub_bullets else None
81
+ )
82
+ # If sections is a list of section objects, use the POM format directly
83
+ elif isinstance(sections, list):
84
+ if self.pom:
85
+ # Process each section using auto-vivifying methods
86
+ for section in sections:
87
+ if 'title' in section:
88
+ title = section['title']
89
+ body = section.get('body', '')
90
+ bullets = section.get('bullets', [])
91
+ numbered = section.get('numbered', False)
92
+ numbered_bullets = section.get('numberedBullets', False)
93
+
94
+ # Only create section if it has content
95
+ if body or bullets or 'subsections' in section:
96
+ self.prompt_add_section(
97
+ title,
98
+ body=body,
99
+ bullets=bullets if bullets else None,
100
+ numbered=numbered,
101
+ numbered_bullets=numbered_bullets
102
+ )
103
+
104
+ # Process subsections if any
105
+ subsections = section.get('subsections', [])
106
+ for subsection in subsections:
107
+ if 'title' in subsection:
108
+ sub_title = subsection['title']
109
+ sub_body = subsection.get('body', '')
110
+ sub_bullets = subsection.get('bullets', [])
111
+
112
+ # Only add subsection if it has content
113
+ if sub_body or sub_bullets:
114
+ self.prompt_add_subsection(
115
+ title,
116
+ sub_title,
117
+ body=sub_body,
118
+ bullets=sub_bullets if sub_bullets else None
119
+ )
120
+
121
+ def define_contexts(self, contexts=None) -> Union['AgentBase', 'ContextBuilder']:
122
+ """
123
+ Define contexts and steps for this agent (alternative to POM/prompt)
124
+
125
+ Args:
126
+ contexts: Optional context configuration (dict or ContextBuilder)
127
+
128
+ Returns:
129
+ ContextBuilder for method chaining if no contexts provided
130
+
131
+ Note:
132
+ Contexts can coexist with traditional prompts. The restriction is only
133
+ that you can't mix POM sections with raw text in the main prompt.
134
+ """
135
+ if contexts is not None:
136
+ # New behavior - set contexts
137
+ self._prompt_manager.define_contexts(contexts)
138
+ return self
139
+ else:
140
+ # Legacy behavior - return ContextBuilder
141
+ if self._contexts_builder is None:
142
+ self._contexts_builder = ContextBuilder(self)
143
+ self._contexts_defined = True
144
+
145
+ return self._contexts_builder
146
+
147
+ @property
148
+ def contexts(self) -> 'ContextBuilder':
149
+ """
150
+ Get the ContextBuilder for this agent
151
+
152
+ Returns:
153
+ ContextBuilder instance for defining contexts
154
+ """
155
+ return self.define_contexts()
156
+
157
+ def _validate_prompt_mode_exclusivity(self):
158
+ """
159
+ Validate that POM sections and raw text are not mixed in the main prompt
160
+
161
+ Note: This does NOT prevent contexts from being used alongside traditional prompts
162
+ """
163
+ # Delegate to prompt manager
164
+ self._prompt_manager._validate_prompt_mode_exclusivity()
165
+
166
+ def set_prompt_text(self, text: str) -> 'AgentBase':
167
+ """
168
+ Set the prompt as raw text instead of using POM
169
+
170
+ Args:
171
+ text: The raw prompt text
172
+
173
+ Returns:
174
+ Self for method chaining
175
+ """
176
+ self._prompt_manager.set_prompt_text(text)
177
+ return self
178
+
179
+ def set_post_prompt(self, text: str) -> 'AgentBase':
180
+ """
181
+ Set the post-prompt text for summary generation
182
+
183
+ Args:
184
+ text: The post-prompt text
185
+
186
+ Returns:
187
+ Self for method chaining
188
+ """
189
+ self._prompt_manager.set_post_prompt(text)
190
+ return self
191
+
192
+ def set_prompt_pom(self, pom: List[Dict[str, Any]]) -> 'AgentBase':
193
+ """
194
+ Set the prompt as a POM dictionary
195
+
196
+ Args:
197
+ pom: POM dictionary structure
198
+
199
+ Returns:
200
+ Self for method chaining
201
+ """
202
+ self._prompt_manager.set_prompt_pom(pom)
203
+ return self
204
+
205
+ def prompt_add_section(
206
+ self,
207
+ title: str,
208
+ body: str = "",
209
+ bullets: Optional[List[str]] = None,
210
+ numbered: bool = False,
211
+ numbered_bullets: bool = False,
212
+ subsections: Optional[List[Dict[str, Any]]] = None
213
+ ) -> 'AgentBase':
214
+ """
215
+ Add a section to the prompt
216
+
217
+ Args:
218
+ title: Section title
219
+ body: Optional section body text
220
+ bullets: Optional list of bullet points
221
+ numbered: Whether this section should be numbered
222
+ numbered_bullets: Whether bullets should be numbered
223
+ subsections: Optional list of subsection objects
224
+
225
+ Returns:
226
+ Self for method chaining
227
+ """
228
+ self._prompt_manager.prompt_add_section(
229
+ title=title,
230
+ body=body,
231
+ bullets=bullets,
232
+ numbered=numbered,
233
+ numbered_bullets=numbered_bullets,
234
+ subsections=subsections
235
+ )
236
+ return self
237
+
238
+ def prompt_add_to_section(
239
+ self,
240
+ title: str,
241
+ body: Optional[str] = None,
242
+ bullet: Optional[str] = None,
243
+ bullets: Optional[List[str]] = None
244
+ ) -> 'AgentBase':
245
+ """
246
+ Add content to an existing section (creating it if needed)
247
+
248
+ Args:
249
+ title: Section title
250
+ body: Optional text to append to section body
251
+ bullet: Optional single bullet point to add
252
+ bullets: Optional list of bullet points to add
253
+
254
+ Returns:
255
+ Self for method chaining
256
+ """
257
+ self._prompt_manager.prompt_add_to_section(
258
+ title=title,
259
+ body=body,
260
+ bullet=bullet,
261
+ bullets=bullets
262
+ )
263
+ return self
264
+
265
+ def prompt_add_subsection(
266
+ self,
267
+ parent_title: str,
268
+ title: str,
269
+ body: str = "",
270
+ bullets: Optional[List[str]] = None
271
+ ) -> 'AgentBase':
272
+ """
273
+ Add a subsection to an existing section (creating parent if needed)
274
+
275
+ Args:
276
+ parent_title: Parent section title
277
+ title: Subsection title
278
+ body: Optional subsection body text
279
+ bullets: Optional list of bullet points
280
+
281
+ Returns:
282
+ Self for method chaining
283
+ """
284
+ self._prompt_manager.prompt_add_subsection(
285
+ parent_title=parent_title,
286
+ title=title,
287
+ body=body,
288
+ bullets=bullets
289
+ )
290
+ return self
291
+
292
+ def prompt_has_section(self, title: str) -> bool:
293
+ """
294
+ Check if a section exists in the prompt
295
+
296
+ Args:
297
+ title: Section title to check
298
+
299
+ Returns:
300
+ True if section exists, False otherwise
301
+ """
302
+ return self._prompt_manager.prompt_has_section(title)
303
+
304
+ def get_prompt(self) -> Union[str, List[Dict[str, Any]]]:
305
+ """
306
+ Get the prompt for the agent
307
+
308
+ Returns:
309
+ Either a string prompt or a POM object as list of dicts
310
+ """
311
+ # First check if prompt manager has a prompt
312
+ prompt_result = self._prompt_manager.get_prompt()
313
+ if prompt_result is not None:
314
+ return prompt_result
315
+
316
+ # If using POM, return the POM structure
317
+ if self._use_pom and self.pom:
318
+ try:
319
+ # Try different methods that might be available on the POM implementation
320
+ if hasattr(self.pom, 'render_dict'):
321
+ return self.pom.render_dict()
322
+ elif hasattr(self.pom, 'to_dict'):
323
+ return self.pom.to_dict()
324
+ elif hasattr(self.pom, 'to_list'):
325
+ return self.pom.to_list()
326
+ elif hasattr(self.pom, 'render'):
327
+ render_result = self.pom.render()
328
+ # If render returns a string, we need to convert it to JSON
329
+ if isinstance(render_result, str):
330
+ try:
331
+ import json
332
+ return json.loads(render_result)
333
+ except:
334
+ # If we can't parse as JSON, fall back to raw text
335
+ pass
336
+ return render_result
337
+ else:
338
+ # Last resort: attempt to convert the POM object directly to a list/dict
339
+ # This assumes the POM object has a reasonable __str__ or __repr__ method
340
+ pom_data = self.pom.__dict__
341
+ if '_sections' in pom_data and isinstance(pom_data['_sections'], list):
342
+ return pom_data['_sections']
343
+ # Fall through to default if nothing worked
344
+ except Exception as e:
345
+ self.log.error("pom_rendering_failed", error=str(e))
346
+ # Fall back to raw text if POM fails
347
+
348
+ # Return default text
349
+ return f"You are {self.name}, a helpful AI assistant."
350
+
351
+ def get_post_prompt(self) -> Optional[str]:
352
+ """
353
+ Get the post-prompt for the agent
354
+
355
+ Returns:
356
+ Post-prompt text or None if not set
357
+ """
358
+ return self._prompt_manager.get_post_prompt()