signalwire-agents 0.1.23__py3-none-any.whl → 0.1.24__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 +1 -1
- signalwire_agents/agent_server.py +2 -1
- signalwire_agents/cli/config.py +61 -0
- signalwire_agents/cli/core/__init__.py +1 -0
- signalwire_agents/cli/core/agent_loader.py +254 -0
- signalwire_agents/cli/core/argparse_helpers.py +164 -0
- signalwire_agents/cli/core/dynamic_config.py +62 -0
- signalwire_agents/cli/execution/__init__.py +1 -0
- signalwire_agents/cli/execution/datamap_exec.py +437 -0
- signalwire_agents/cli/execution/webhook_exec.py +125 -0
- signalwire_agents/cli/output/__init__.py +1 -0
- signalwire_agents/cli/output/output_formatter.py +132 -0
- signalwire_agents/cli/output/swml_dump.py +177 -0
- signalwire_agents/cli/simulation/__init__.py +1 -0
- signalwire_agents/cli/simulation/data_generation.py +365 -0
- signalwire_agents/cli/simulation/data_overrides.py +187 -0
- signalwire_agents/cli/simulation/mock_env.py +271 -0
- signalwire_agents/cli/test_swaig.py +522 -2539
- signalwire_agents/cli/types.py +72 -0
- signalwire_agents/core/agent/__init__.py +1 -3
- signalwire_agents/core/agent/config/__init__.py +1 -3
- signalwire_agents/core/agent/prompt/manager.py +25 -7
- signalwire_agents/core/agent/tools/decorator.py +2 -0
- signalwire_agents/core/agent/tools/registry.py +8 -0
- signalwire_agents/core/agent_base.py +492 -3053
- signalwire_agents/core/function_result.py +31 -42
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
- signalwire_agents/core/mixins/auth_mixin.py +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +345 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +219 -0
- signalwire_agents/core/mixins/tool_mixin.py +295 -0
- signalwire_agents/core/mixins/web_mixin.py +1130 -0
- signalwire_agents/core/skill_manager.py +3 -1
- signalwire_agents/core/swaig_function.py +10 -1
- signalwire_agents/core/swml_service.py +140 -58
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/native_vector_search/skill.py +33 -13
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +4 -0
- signalwire_agents/skills/spider/skill.py +479 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +1 -0
- signalwire_agents/skills/swml_transfer/skill.py +257 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/METADATA +47 -2
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/RECORD +62 -22
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/entry_points.txt +1 -1
- signalwire_agents/core/agent/config/ephemeral.py +0 -176
- signalwire_agents-0.1.23.data/data/schema.json +0 -5611
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.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()
|