signalwire-agents 0.1.23__py3-none-any.whl → 0.1.25__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 (64) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +2 -1
  3. signalwire_agents/cli/config.py +61 -0
  4. signalwire_agents/cli/core/__init__.py +1 -0
  5. signalwire_agents/cli/core/agent_loader.py +254 -0
  6. signalwire_agents/cli/core/argparse_helpers.py +164 -0
  7. signalwire_agents/cli/core/dynamic_config.py +62 -0
  8. signalwire_agents/cli/execution/__init__.py +1 -0
  9. signalwire_agents/cli/execution/datamap_exec.py +437 -0
  10. signalwire_agents/cli/execution/webhook_exec.py +125 -0
  11. signalwire_agents/cli/output/__init__.py +1 -0
  12. signalwire_agents/cli/output/output_formatter.py +132 -0
  13. signalwire_agents/cli/output/swml_dump.py +177 -0
  14. signalwire_agents/cli/simulation/__init__.py +1 -0
  15. signalwire_agents/cli/simulation/data_generation.py +365 -0
  16. signalwire_agents/cli/simulation/data_overrides.py +187 -0
  17. signalwire_agents/cli/simulation/mock_env.py +271 -0
  18. signalwire_agents/cli/test_swaig.py +522 -2539
  19. signalwire_agents/cli/types.py +72 -0
  20. signalwire_agents/core/agent/__init__.py +1 -3
  21. signalwire_agents/core/agent/config/__init__.py +1 -3
  22. signalwire_agents/core/agent/prompt/manager.py +25 -7
  23. signalwire_agents/core/agent/tools/decorator.py +2 -0
  24. signalwire_agents/core/agent/tools/registry.py +8 -0
  25. signalwire_agents/core/agent_base.py +492 -3053
  26. signalwire_agents/core/function_result.py +31 -42
  27. signalwire_agents/core/mixins/__init__.py +28 -0
  28. signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
  29. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  30. signalwire_agents/core/mixins/prompt_mixin.py +345 -0
  31. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  32. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  33. signalwire_agents/core/mixins/state_mixin.py +219 -0
  34. signalwire_agents/core/mixins/tool_mixin.py +295 -0
  35. signalwire_agents/core/mixins/web_mixin.py +1130 -0
  36. signalwire_agents/core/skill_manager.py +3 -1
  37. signalwire_agents/core/swaig_function.py +10 -1
  38. signalwire_agents/core/swml_service.py +140 -58
  39. signalwire_agents/skills/README.md +452 -0
  40. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  41. signalwire_agents/skills/datasphere/README.md +210 -0
  42. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  43. signalwire_agents/skills/datetime/README.md +132 -0
  44. signalwire_agents/skills/joke/README.md +149 -0
  45. signalwire_agents/skills/math/README.md +161 -0
  46. signalwire_agents/skills/native_vector_search/skill.py +33 -13
  47. signalwire_agents/skills/play_background_file/README.md +218 -0
  48. signalwire_agents/skills/spider/README.md +236 -0
  49. signalwire_agents/skills/spider/__init__.py +4 -0
  50. signalwire_agents/skills/spider/skill.py +479 -0
  51. signalwire_agents/skills/swml_transfer/README.md +395 -0
  52. signalwire_agents/skills/swml_transfer/__init__.py +1 -0
  53. signalwire_agents/skills/swml_transfer/skill.py +257 -0
  54. signalwire_agents/skills/weather_api/README.md +178 -0
  55. signalwire_agents/skills/web_search/README.md +163 -0
  56. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  57. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/METADATA +47 -2
  58. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
  59. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/entry_points.txt +1 -1
  60. signalwire_agents/core/agent/config/ephemeral.py +0 -176
  61. signalwire_agents-0.1.23.data/data/schema.json +0 -5611
  62. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/WHEEL +0 -0
  63. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
  64. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,287 @@
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
+ auth_header = None
200
+ for key in request.headers:
201
+ if key.lower() == 'authorization':
202
+ auth_header = request.headers[key]
203
+ break
204
+
205
+ if not auth_header or not auth_header.startswith('Basic '):
206
+ return False
207
+
208
+ try:
209
+ import base64
210
+ encoded_credentials = auth_header[6:] # Remove 'Basic '
211
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
212
+ provided_username, provided_password = decoded_credentials.split(':', 1)
213
+
214
+ expected_username, expected_password = self.get_basic_auth_credentials()
215
+ return (provided_username == expected_username and
216
+ provided_password == expected_password)
217
+ except Exception:
218
+ return False
219
+
220
+ def _send_google_cloud_function_auth_challenge(self):
221
+ """
222
+ Send authentication challenge in Google Cloud Functions mode
223
+
224
+ Returns:
225
+ Flask-compatible response with 401 status and WWW-Authenticate header
226
+ """
227
+ from flask import Response
228
+ return Response(
229
+ response=json.dumps({"error": "Unauthorized"}),
230
+ status=401,
231
+ headers={
232
+ "WWW-Authenticate": "Basic realm=\"SignalWire Agent\"",
233
+ "Content-Type": "application/json"
234
+ }
235
+ )
236
+
237
+ def _check_azure_function_auth(self, req) -> bool:
238
+ """
239
+ Check basic auth in Azure Functions mode using request object
240
+
241
+ Args:
242
+ req: Azure Functions request object containing headers
243
+
244
+ Returns:
245
+ True if auth is valid, False otherwise
246
+ """
247
+ if not hasattr(req, 'headers'):
248
+ return False
249
+
250
+ # Check for authorization header (case-insensitive)
251
+ auth_header = None
252
+ for key, value in req.headers.items():
253
+ if key.lower() == 'authorization':
254
+ auth_header = value
255
+ break
256
+
257
+ if not auth_header or not auth_header.startswith('Basic '):
258
+ return False
259
+
260
+ try:
261
+ import base64
262
+ encoded_credentials = auth_header[6:] # Remove 'Basic '
263
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
264
+ provided_username, provided_password = decoded_credentials.split(':', 1)
265
+
266
+ expected_username, expected_password = self.get_basic_auth_credentials()
267
+ return (provided_username == expected_username and
268
+ provided_password == expected_password)
269
+ except Exception:
270
+ return False
271
+
272
+ def _send_azure_function_auth_challenge(self):
273
+ """
274
+ Send authentication challenge in Azure Functions mode
275
+
276
+ Returns:
277
+ Azure Functions response with 401 status and WWW-Authenticate header
278
+ """
279
+ import azure.functions as func
280
+ return func.HttpResponse(
281
+ body=json.dumps({"error": "Unauthorized"}),
282
+ status_code=401,
283
+ headers={
284
+ "WWW-Authenticate": "Basic realm=\"SignalWire Agent\"",
285
+ "Content-Type": "application/json"
286
+ }
287
+ )
@@ -0,0 +1,345 @@
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
11
+
12
+
13
+ class PromptMixin:
14
+ """
15
+ Mixin class containing all prompt-related methods for AgentBase
16
+ """
17
+
18
+ def _process_prompt_sections(self):
19
+ """
20
+ Process declarative PROMPT_SECTIONS attribute from a subclass
21
+
22
+ This auto-vivifies section methods and bootstraps the prompt
23
+ from class declaration, allowing for declarative agents.
24
+ """
25
+ # Skip if no PROMPT_SECTIONS defined or not using POM
26
+ cls = self.__class__
27
+ if not hasattr(cls, 'PROMPT_SECTIONS') or cls.PROMPT_SECTIONS is None or not self._use_pom:
28
+ return
29
+
30
+ sections = cls.PROMPT_SECTIONS
31
+
32
+ # If sections is a dictionary mapping section names to content
33
+ if isinstance(sections, dict):
34
+ for title, content in sections.items():
35
+ # Handle different content types
36
+ if isinstance(content, str):
37
+ # Plain text - add as body
38
+ self.prompt_add_section(title, body=content)
39
+ elif isinstance(content, list) and content: # Only add if non-empty
40
+ # List of strings - add as bullets
41
+ self.prompt_add_section(title, bullets=content)
42
+ elif isinstance(content, dict):
43
+ # Dictionary with body/bullets/subsections
44
+ body = content.get('body', '')
45
+ bullets = content.get('bullets', [])
46
+ numbered = content.get('numbered', False)
47
+ numbered_bullets = content.get('numberedBullets', False)
48
+
49
+ # Only create section if it has content
50
+ if body or bullets or 'subsections' in content:
51
+ # Create the section
52
+ self.prompt_add_section(
53
+ title,
54
+ body=body,
55
+ bullets=bullets if bullets else None,
56
+ numbered=numbered,
57
+ numbered_bullets=numbered_bullets
58
+ )
59
+
60
+ # Process subsections if any
61
+ subsections = content.get('subsections', [])
62
+ for subsection in subsections:
63
+ if 'title' in subsection:
64
+ sub_title = subsection['title']
65
+ sub_body = subsection.get('body', '')
66
+ sub_bullets = subsection.get('bullets', [])
67
+
68
+ # Only add subsection if it has content
69
+ if sub_body or sub_bullets:
70
+ self.prompt_add_subsection(
71
+ title,
72
+ sub_title,
73
+ body=sub_body,
74
+ bullets=sub_bullets if sub_bullets else None
75
+ )
76
+ # If sections is a list of section objects, use the POM format directly
77
+ elif isinstance(sections, list):
78
+ if self.pom:
79
+ # Process each section using auto-vivifying methods
80
+ for section in sections:
81
+ if 'title' in section:
82
+ title = section['title']
83
+ body = section.get('body', '')
84
+ bullets = section.get('bullets', [])
85
+ numbered = section.get('numbered', False)
86
+ numbered_bullets = section.get('numberedBullets', False)
87
+
88
+ # Only create section if it has content
89
+ if body or bullets or 'subsections' in section:
90
+ self.prompt_add_section(
91
+ title,
92
+ body=body,
93
+ bullets=bullets if bullets else None,
94
+ numbered=numbered,
95
+ numbered_bullets=numbered_bullets
96
+ )
97
+
98
+ # Process subsections if any
99
+ subsections = section.get('subsections', [])
100
+ for subsection in subsections:
101
+ if 'title' in subsection:
102
+ sub_title = subsection['title']
103
+ sub_body = subsection.get('body', '')
104
+ sub_bullets = subsection.get('bullets', [])
105
+
106
+ # Only add subsection if it has content
107
+ if sub_body or sub_bullets:
108
+ self.prompt_add_subsection(
109
+ title,
110
+ sub_title,
111
+ body=sub_body,
112
+ bullets=sub_bullets if sub_bullets else None
113
+ )
114
+
115
+ def define_contexts(self, contexts=None) -> Optional['ContextBuilder']:
116
+ """
117
+ Define contexts and steps for this agent (alternative to POM/prompt)
118
+
119
+ Args:
120
+ contexts: Optional context configuration (dict or ContextBuilder)
121
+
122
+ Returns:
123
+ ContextBuilder for method chaining if no contexts provided
124
+
125
+ Note:
126
+ Contexts can coexist with traditional prompts. The restriction is only
127
+ that you can't mix POM sections with raw text in the main prompt.
128
+ """
129
+ if contexts is not None:
130
+ # New behavior - set contexts
131
+ self._prompt_manager.define_contexts(contexts)
132
+ return self
133
+ else:
134
+ # Legacy behavior - return ContextBuilder
135
+ # Import here to avoid circular imports
136
+ from signalwire_agents.core.contexts import ContextBuilder
137
+
138
+ if self._contexts_builder is None:
139
+ self._contexts_builder = ContextBuilder(self)
140
+ self._contexts_defined = True
141
+
142
+ return self._contexts_builder
143
+
144
+ def _validate_prompt_mode_exclusivity(self):
145
+ """
146
+ Validate that POM sections and raw text are not mixed in the main prompt
147
+
148
+ Note: This does NOT prevent contexts from being used alongside traditional prompts
149
+ """
150
+ # Delegate to prompt manager
151
+ self._prompt_manager._validate_prompt_mode_exclusivity()
152
+
153
+ def set_prompt_text(self, text: str) -> 'AgentBase':
154
+ """
155
+ Set the prompt as raw text instead of using POM
156
+
157
+ Args:
158
+ text: The raw prompt text
159
+
160
+ Returns:
161
+ Self for method chaining
162
+ """
163
+ self._prompt_manager.set_prompt_text(text)
164
+ return self
165
+
166
+ def set_post_prompt(self, text: str) -> 'AgentBase':
167
+ """
168
+ Set the post-prompt text for summary generation
169
+
170
+ Args:
171
+ text: The post-prompt text
172
+
173
+ Returns:
174
+ Self for method chaining
175
+ """
176
+ self._prompt_manager.set_post_prompt(text)
177
+ return self
178
+
179
+ def set_prompt_pom(self, pom: List[Dict[str, Any]]) -> 'AgentBase':
180
+ """
181
+ Set the prompt as a POM dictionary
182
+
183
+ Args:
184
+ pom: POM dictionary structure
185
+
186
+ Returns:
187
+ Self for method chaining
188
+ """
189
+ self._prompt_manager.set_prompt_pom(pom)
190
+ return self
191
+
192
+ def prompt_add_section(
193
+ self,
194
+ title: str,
195
+ body: str = "",
196
+ bullets: Optional[List[str]] = None,
197
+ numbered: bool = False,
198
+ numbered_bullets: bool = False,
199
+ subsections: Optional[List[Dict[str, Any]]] = None
200
+ ) -> 'AgentBase':
201
+ """
202
+ Add a section to the prompt
203
+
204
+ Args:
205
+ title: Section title
206
+ body: Optional section body text
207
+ bullets: Optional list of bullet points
208
+ numbered: Whether this section should be numbered
209
+ numbered_bullets: Whether bullets should be numbered
210
+ subsections: Optional list of subsection objects
211
+
212
+ Returns:
213
+ Self for method chaining
214
+ """
215
+ self._prompt_manager.prompt_add_section(
216
+ title=title,
217
+ body=body,
218
+ bullets=bullets,
219
+ numbered=numbered,
220
+ numbered_bullets=numbered_bullets,
221
+ subsections=subsections
222
+ )
223
+ return self
224
+
225
+ def prompt_add_to_section(
226
+ self,
227
+ title: str,
228
+ body: Optional[str] = None,
229
+ bullet: Optional[str] = None,
230
+ bullets: Optional[List[str]] = None
231
+ ) -> 'AgentBase':
232
+ """
233
+ Add content to an existing section (creating it if needed)
234
+
235
+ Args:
236
+ title: Section title
237
+ body: Optional text to append to section body
238
+ bullet: Optional single bullet point to add
239
+ bullets: Optional list of bullet points to add
240
+
241
+ Returns:
242
+ Self for method chaining
243
+ """
244
+ self._prompt_manager.prompt_add_to_section(
245
+ title=title,
246
+ body=body,
247
+ bullet=bullet,
248
+ bullets=bullets
249
+ )
250
+ return self
251
+
252
+ def prompt_add_subsection(
253
+ self,
254
+ parent_title: str,
255
+ title: str,
256
+ body: str = "",
257
+ bullets: Optional[List[str]] = None
258
+ ) -> 'AgentBase':
259
+ """
260
+ Add a subsection to an existing section (creating parent if needed)
261
+
262
+ Args:
263
+ parent_title: Parent section title
264
+ title: Subsection title
265
+ body: Optional subsection body text
266
+ bullets: Optional list of bullet points
267
+
268
+ Returns:
269
+ Self for method chaining
270
+ """
271
+ self._prompt_manager.prompt_add_subsection(
272
+ parent_title=parent_title,
273
+ title=title,
274
+ body=body,
275
+ bullets=bullets
276
+ )
277
+ return self
278
+
279
+ def prompt_has_section(self, title: str) -> bool:
280
+ """
281
+ Check if a section exists in the prompt
282
+
283
+ Args:
284
+ title: Section title to check
285
+
286
+ Returns:
287
+ True if section exists, False otherwise
288
+ """
289
+ return self._prompt_manager.prompt_has_section(title)
290
+
291
+ def get_prompt(self) -> Union[str, List[Dict[str, Any]]]:
292
+ """
293
+ Get the prompt for the agent
294
+
295
+ Returns:
296
+ Either a string prompt or a POM object as list of dicts
297
+ """
298
+ # First check if prompt manager has a prompt
299
+ prompt_result = self._prompt_manager.get_prompt()
300
+ if prompt_result is not None:
301
+ return prompt_result
302
+
303
+ # If using POM, return the POM structure
304
+ if self._use_pom and self.pom:
305
+ try:
306
+ # Try different methods that might be available on the POM implementation
307
+ if hasattr(self.pom, 'render_dict'):
308
+ return self.pom.render_dict()
309
+ elif hasattr(self.pom, 'to_dict'):
310
+ return self.pom.to_dict()
311
+ elif hasattr(self.pom, 'to_list'):
312
+ return self.pom.to_list()
313
+ elif hasattr(self.pom, 'render'):
314
+ render_result = self.pom.render()
315
+ # If render returns a string, we need to convert it to JSON
316
+ if isinstance(render_result, str):
317
+ try:
318
+ import json
319
+ return json.loads(render_result)
320
+ except:
321
+ # If we can't parse as JSON, fall back to raw text
322
+ pass
323
+ return render_result
324
+ else:
325
+ # Last resort: attempt to convert the POM object directly to a list/dict
326
+ # This assumes the POM object has a reasonable __str__ or __repr__ method
327
+ pom_data = self.pom.__dict__
328
+ if '_sections' in pom_data and isinstance(pom_data['_sections'], list):
329
+ return pom_data['_sections']
330
+ # Fall through to default if nothing worked
331
+ except Exception as e:
332
+ self.log.error("pom_rendering_failed", error=str(e))
333
+ # Fall back to raw text if POM fails
334
+
335
+ # Return default text
336
+ return f"You are {self.name}, a helpful AI assistant."
337
+
338
+ def get_post_prompt(self) -> Optional[str]:
339
+ """
340
+ Get the post-prompt for the agent
341
+
342
+ Returns:
343
+ Post-prompt text or None if not set
344
+ """
345
+ return self._prompt_manager.get_post_prompt()