devduck 0.1.1766644714__py3-none-any.whl → 0.3.0__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.

Potentially problematic release.


This version of devduck might be problematic. Click here for more details.

@@ -1,608 +0,0 @@
1
- """System prompt management tool for Strands Agents.
2
-
3
- This module provides a tool to view and modify system prompts used by the agent.
4
- It helps with dynamic adaptation of the agent's behavior and capabilities,
5
- and can persist changes by updating GitHub repository variables and local .prompt files.
6
-
7
- Key Features:
8
- 1. View current system prompt from any environment variable
9
- 2. Update system prompt (in-memory, .prompt file, and GitHub repository variable)
10
- 3. Add context information to system prompt
11
- 4. Reset system prompt to default
12
- 5. Support for custom variable names (SYSTEM_PROMPT, TOOL_BUILDER_SYSTEM_PROMPT, etc.)
13
- 6. Local file persistence via .prompt files with predictable fallback locations
14
-
15
- Usage Examples:
16
- ```python
17
- from strands import Agent
18
- from protocol_tools import system_prompt
19
-
20
- agent = Agent(tools=[system_prompt])
21
-
22
- # View current system prompt (default SYSTEM_PROMPT variable)
23
- result = agent.tool.system_prompt(action="view")
24
-
25
- # Update system prompt for tool builder
26
- result = agent.tool.system_prompt(
27
- action="update",
28
- prompt="You are a specialized tool builder agent...",
29
- repository="owner/repo",
30
- variable_name="TOOL_BUILDER_SYSTEM_PROMPT",
31
- )
32
-
33
- # Work with any custom variable name
34
- result = agent.tool.system_prompt(
35
- action="view", variable_name="MY_CUSTOM_PROMPT"
36
- )
37
- ```
38
- """
39
-
40
- import os
41
- import tempfile
42
- from pathlib import Path
43
- from typing import Any
44
-
45
- import requests
46
- from strands import tool
47
-
48
-
49
- def _get_github_token() -> str:
50
- """Get GitHub token from environment variable."""
51
- return os.environ.get("PAT_TOKEN", os.environ.get("GITHUB_TOKEN", ""))
52
-
53
-
54
- def _get_prompt_file_path(variable_name: str = "SYSTEM_PROMPT") -> Path:
55
- """Get the path to the .prompt file for a given variable name with fallback strategy.
56
-
57
- Tries locations in this order:
58
- 1. CWD (current working directory)
59
- 2. /tmp/devduck/prompts
60
- 3. tempdir/devduck/prompts
61
-
62
- Args:
63
- variable_name: Name of the variable (used to generate filename)
64
-
65
- Returns:
66
- Path to the .prompt file (first writable location)
67
- """
68
- # Convert variable name to lowercase filename
69
- # SYSTEM_PROMPT -> system_prompt.prompt
70
- # MY_CUSTOM_PROMPT -> my_custom_prompt.prompt
71
- filename = f"{variable_name.lower()}.prompt"
72
-
73
- # Try 1: CWD
74
- try:
75
- cwd_path = Path.cwd() / filename
76
- # Test if we can write to CWD
77
- cwd_path.touch(exist_ok=True)
78
- return cwd_path
79
- except (OSError, PermissionError):
80
- pass
81
-
82
- # Try 2: /tmp/devduck/prompts
83
- try:
84
- tmp_dir = Path("/tmp/devduck/prompts")
85
- tmp_dir.mkdir(parents=True, exist_ok=True)
86
- tmp_path = tmp_dir / filename
87
- # Test if we can write to /tmp/devduck
88
- tmp_path.touch(exist_ok=True)
89
- return tmp_path
90
- except (OSError, PermissionError):
91
- pass
92
-
93
- # Try 3: tempdir/devduck/prompts (system temp directory)
94
- temp_dir = Path(tempfile.gettempdir()) / "devduck" / "prompts"
95
- temp_dir.mkdir(parents=True, exist_ok=True)
96
- return temp_dir / filename
97
-
98
-
99
- def _read_prompt_file(variable_name: str = "SYSTEM_PROMPT") -> str:
100
- """Read prompt from .prompt file across all possible locations.
101
-
102
- Checks all locations in priority order and returns the first found.
103
-
104
- Args:
105
- variable_name: Name of the variable
106
-
107
- Returns:
108
- Content of the .prompt file or empty string if not found
109
- """
110
- filename = f"{variable_name.lower()}.prompt"
111
-
112
- # Check all possible locations in priority order
113
- possible_paths = [
114
- Path.cwd() / filename, # CWD
115
- Path("/tmp/devduck/prompts") / filename, # /tmp/devduck
116
- Path(tempfile.gettempdir()) / "devduck" / "prompts" / filename, # tempdir
117
- ]
118
-
119
- for prompt_file in possible_paths:
120
- try:
121
- if prompt_file.exists():
122
- return prompt_file.read_text(encoding="utf-8")
123
- except Exception:
124
- continue
125
-
126
- return ""
127
-
128
-
129
- def _write_prompt_file(
130
- prompt: str, variable_name: str = "SYSTEM_PROMPT"
131
- ) -> tuple[bool, str]:
132
- """Write prompt to .prompt file with fallback strategy.
133
-
134
- Args:
135
- prompt: The prompt content to write
136
- variable_name: Name of the variable
137
-
138
- Returns:
139
- Tuple of (success, path) where success is True if write succeeded
140
- """
141
- prompt_file = _get_prompt_file_path(variable_name)
142
- try:
143
- prompt_file.write_text(prompt, encoding="utf-8")
144
- return True, str(prompt_file)
145
- except Exception:
146
- return False, str(prompt_file)
147
-
148
-
149
- def _delete_prompt_file(variable_name: str = "SYSTEM_PROMPT") -> tuple[bool, str]:
150
- """Delete .prompt file from all possible locations.
151
-
152
- Args:
153
- variable_name: Name of the variable
154
-
155
- Returns:
156
- Tuple of (success, path) - success if any file was deleted
157
- """
158
- filename = f"{variable_name.lower()}.prompt"
159
- deleted = False
160
- deleted_path = ""
161
-
162
- # Try to delete from all possible locations
163
- possible_paths = [
164
- Path.cwd() / filename, # CWD
165
- Path("/tmp/devduck/prompts") / filename, # /tmp/devduck
166
- Path(tempfile.gettempdir()) / "devduck" / "prompts" / filename, # tempdir
167
- ]
168
-
169
- for prompt_file in possible_paths:
170
- try:
171
- if prompt_file.exists():
172
- prompt_file.unlink()
173
- deleted = True
174
- deleted_path = str(prompt_file)
175
- except Exception:
176
- continue
177
-
178
- return deleted, deleted_path
179
-
180
-
181
- def _get_github_repository_variable(
182
- repository: str, name: str, token: str
183
- ) -> dict[str, Any]:
184
- """Fetch a GitHub repository variable.
185
-
186
- Args:
187
- repository: The repository in format "owner/repo"
188
- name: The variable name
189
- token: GitHub token
190
-
191
- Returns:
192
- Dictionary with success status, message, and value if successful
193
- """
194
- # GitHub API endpoint for repository variables
195
- url = f"https://api.github.com/repos/{repository}/actions/variables/{name}"
196
-
197
- headers = {
198
- "Accept": "application/vnd.github+json",
199
- "Authorization": f"Bearer {token}",
200
- "X-GitHub-Api-Version": "2022-11-28",
201
- }
202
-
203
- try:
204
- response = requests.get(url, headers=headers, timeout=30)
205
-
206
- if response.status_code == 200:
207
- data = response.json()
208
- return {
209
- "success": True,
210
- "message": f"Variable {name} fetched successfully",
211
- "value": data.get("value", ""),
212
- }
213
- else:
214
- error_message = (
215
- f"Failed to fetch variable: {response.status_code} - {response.text}"
216
- )
217
- return {"success": False, "message": error_message, "value": ""}
218
- except Exception as e:
219
- return {
220
- "success": False,
221
- "message": f"Error fetching GitHub variable: {e!s}",
222
- "value": "",
223
- }
224
-
225
-
226
- def _get_system_prompt(
227
- repository: str | None = None, variable_name: str = "SYSTEM_PROMPT"
228
- ) -> str:
229
- """Get the current system prompt.
230
-
231
- Priority order:
232
- 1. Local environment variable
233
- 2. Local .prompt file (CWD → /tmp/devduck → tempdir)
234
- 3. GitHub repository variable (if repository specified)
235
-
236
- Args:
237
- repository: Optional GitHub repository in format "owner/repo"
238
- variable_name: Name of the environment/repository variable to use
239
-
240
- Returns:
241
- The system prompt string
242
- """
243
- # First check local environment
244
- local_prompt = os.environ.get(variable_name, "")
245
- if local_prompt:
246
- return local_prompt
247
-
248
- # Second, check .prompt file across all locations
249
- file_prompt = _read_prompt_file(variable_name)
250
- if file_prompt:
251
- # Load into environment for caching
252
- os.environ[variable_name] = file_prompt
253
- return file_prompt
254
-
255
- # Third, if repository is provided, try GitHub
256
- if repository:
257
- token = _get_github_token()
258
- if token:
259
- result = _get_github_repository_variable(
260
- repository=repository, name=variable_name, token=token
261
- )
262
-
263
- if result["success"] and result["value"]:
264
- # Store in local environment and file for future use
265
- os.environ[variable_name] = result["value"]
266
- _write_prompt_file(result["value"], variable_name)
267
- return str(result["value"])
268
-
269
- # Default to empty string if nothing found
270
- return ""
271
-
272
-
273
- def _update_system_prompt(
274
- new_prompt: str, variable_name: str = "SYSTEM_PROMPT"
275
- ) -> dict[str, Any]:
276
- """Update the system prompt in environment variable and .prompt file.
277
-
278
- Args:
279
- new_prompt: The new prompt content
280
- variable_name: Name of the variable
281
-
282
- Returns:
283
- Dictionary with success status and messages
284
- """
285
- # Update environment variable
286
- os.environ[variable_name] = new_prompt
287
-
288
- # Update .prompt file with fallback strategy
289
- file_success, file_path = _write_prompt_file(new_prompt, variable_name)
290
-
291
- return {"env_updated": True, "file_updated": file_success, "file_path": file_path}
292
-
293
-
294
- def _get_github_event_context() -> str:
295
- """Get GitHub event context information from environment variables."""
296
- event_context = []
297
-
298
- # GitHub repository information
299
- repo = os.environ.get("GITHUB_REPOSITORY", "")
300
- if repo:
301
- event_context.append(f"Repository: {repo}")
302
-
303
- # Event type
304
- event_name = os.environ.get("GITHUB_EVENT_NAME", "")
305
- if event_name:
306
- event_context.append(f"Event Type: {event_name}")
307
-
308
- # Actor
309
- actor = os.environ.get("GITHUB_ACTOR", "")
310
- if actor:
311
- event_context.append(f"Actor: {actor}")
312
-
313
- # Add more GitHub context variables as needed
314
- return "\n".join(event_context)
315
-
316
-
317
- def _update_github_repository_variable(
318
- repository: str, name: str, value: str, token: str
319
- ) -> dict[str, Any]:
320
- """Update a GitHub repository variable.
321
-
322
- Args:
323
- repository: The repository in format "owner/repo"
324
- name: The variable name
325
- value: The variable value
326
- token: GitHub token
327
-
328
- Returns:
329
- Dictionary with status and message
330
- """
331
- # GitHub API endpoint for repository variables
332
- url = f"https://api.github.com/repos/{repository}/actions/variables/{name}"
333
-
334
- headers = {
335
- "Accept": "application/vnd.github+json",
336
- "Authorization": f"Bearer {token}",
337
- "X-GitHub-Api-Version": "2022-11-28",
338
- }
339
-
340
- data = {"name": name, "value": value}
341
-
342
- response = requests.patch(url, headers=headers, json=data, timeout=30)
343
-
344
- if response.status_code == 204:
345
- return {"success": True, "message": f"Variable {name} updated successfully"}
346
- else:
347
- error_message = (
348
- f"Failed to update variable: {response.status_code} - {response.text}"
349
- )
350
- return {"success": False, "message": error_message}
351
-
352
-
353
- @tool
354
- def system_prompt(
355
- action: str,
356
- prompt: str | None = None,
357
- context: str | None = None,
358
- repository: str | None = None,
359
- variable_name: str = "SYSTEM_PROMPT",
360
- ) -> dict[str, str | list[dict[str, str]]]:
361
- """Manage the agent's system prompt.
362
-
363
- This tool allows viewing and modifying the system prompt used by the agent.
364
- It can be used to adapt the agent's behavior dynamically during runtime
365
- and can update GitHub repository variables and local .prompt files to persist changes.
366
-
367
- Args:
368
- action: The action to perform on the system prompt. One of:
369
- - "view": View the current system prompt
370
- - "update": Replace the current system prompt
371
- - "add_context": Add additional context to the system prompt
372
- - "reset": Reset to default (empty or environment-defined)
373
- - "get_github_context": Get GitHub event context
374
- prompt: New system prompt when using the "update" action
375
- context: Additional context to add when using the "add_context" action
376
- repository: GitHub repository in format "owner/repo" to update repository
377
- variable (e.g., "cagataycali/report-agent")
378
- variable_name: Name of the environment/repository variable to use
379
- (default: "SYSTEM_PROMPT")
380
-
381
- Returns:
382
- A dictionary with the operation status and current system prompt
383
-
384
- Example:
385
- ```python
386
- # View current system prompt
387
- result = system_prompt(action="view")
388
-
389
- # Update system prompt (saves to CWD → /tmp/devduck → tempdir)
390
- result = system_prompt(
391
- action="update", prompt="You are a specialized agent for task X..."
392
- )
393
-
394
- # Update GitHub repository variable (+ env var + .prompt file)
395
- result = system_prompt(
396
- action="update",
397
- prompt="You are a specialized agent for task X...",
398
- repository="owner/repo",
399
- )
400
-
401
- # Work with custom variable name
402
- result = system_prompt(
403
- action="update",
404
- prompt="You are a tool builder...",
405
- repository="owner/repo",
406
- variable_name="TOOL_BUILDER_SYSTEM_PROMPT",
407
- )
408
- ```
409
- """
410
- try:
411
- if action == "view":
412
- current_prompt = _get_system_prompt(repository, variable_name)
413
-
414
- # Determine source
415
- source_parts = []
416
- if os.environ.get(variable_name):
417
- source_parts.append("environment variable")
418
-
419
- # Check all possible file locations
420
- filename = f"{variable_name.lower()}.prompt"
421
- file_locations = [
422
- (Path.cwd() / filename, "CWD"),
423
- (Path("/tmp/devduck/prompts") / filename, "/tmp/devduck"),
424
- (
425
- Path(tempfile.gettempdir()) / "devduck" / "prompts" / filename,
426
- "tempdir",
427
- ),
428
- ]
429
-
430
- for file_path, location in file_locations:
431
- if file_path.exists():
432
- source_parts.append(f"file ({location}: {file_path})")
433
- break
434
-
435
- if repository:
436
- source_parts.append(f"GitHub ({repository})")
437
-
438
- source = " → ".join(source_parts) if source_parts else "not found"
439
-
440
- return {
441
- "status": "success",
442
- "content": [
443
- {
444
- "text": f"Current system prompt from {variable_name}:\nSource: {source}\n\n{current_prompt}"
445
- }
446
- ],
447
- }
448
-
449
- elif action == "update":
450
- if not prompt:
451
- return {
452
- "status": "error",
453
- "content": [
454
- {
455
- "text": "Error: prompt parameter is required for the update action"
456
- }
457
- ],
458
- }
459
-
460
- # Update in-memory environment variable and .prompt file
461
- update_result = _update_system_prompt(prompt, variable_name)
462
-
463
- messages = []
464
- messages.append(f"✓ Environment variable updated ({variable_name})")
465
-
466
- if update_result["file_updated"]:
467
- messages.append(f"✓ File saved ({update_result['file_path']})")
468
- else:
469
- messages.append(f"⚠ File save failed ({update_result['file_path']})")
470
-
471
- # If repository is specified, also update GitHub repository variable
472
- if repository:
473
- token = _get_github_token()
474
- if not token:
475
- messages.append(
476
- "⚠ GitHub token not available - skipped repository update"
477
- )
478
- else:
479
- result = _update_github_repository_variable(
480
- repository=repository,
481
- name=variable_name,
482
- value=prompt,
483
- token=token,
484
- )
485
-
486
- if result["success"]:
487
- messages.append(
488
- f"✓ GitHub repository variable updated ({repository})"
489
- )
490
- else:
491
- messages.append(f"⚠ GitHub update failed: {result['message']}")
492
-
493
- return {
494
- "status": "success",
495
- "content": [{"text": "\n".join(messages)}],
496
- }
497
-
498
- elif action == "add_context":
499
- if not context:
500
- return {
501
- "status": "error",
502
- "content": [
503
- {
504
- "text": "Error: context parameter is required for the add_context action"
505
- }
506
- ],
507
- }
508
-
509
- current_prompt = _get_system_prompt(repository, variable_name)
510
- new_prompt = f"{current_prompt}\n\n{context}" if current_prompt else context
511
-
512
- # Update in-memory environment variable and .prompt file
513
- update_result = _update_system_prompt(new_prompt, variable_name)
514
-
515
- messages = []
516
- messages.append(f"✓ Context added to {variable_name}")
517
- messages.append(f"✓ Environment variable updated")
518
-
519
- if update_result["file_updated"]:
520
- messages.append(f"✓ File saved ({update_result['file_path']})")
521
- else:
522
- messages.append(f"⚠ File save failed ({update_result['file_path']})")
523
-
524
- # If repository is specified, also update GitHub repository variable
525
- if repository:
526
- token = _get_github_token()
527
- if not token:
528
- messages.append(
529
- "⚠ GitHub token not available - skipped repository update"
530
- )
531
- else:
532
- result = _update_github_repository_variable(
533
- repository=repository,
534
- name=variable_name,
535
- value=new_prompt,
536
- token=token,
537
- )
538
-
539
- if result["success"]:
540
- messages.append(
541
- f"✓ GitHub repository variable updated ({repository})"
542
- )
543
- else:
544
- messages.append(f"⚠ GitHub update failed: {result['message']}")
545
-
546
- return {
547
- "status": "success",
548
- "content": [{"text": "\n".join(messages)}],
549
- }
550
-
551
- elif action == "reset":
552
- # Reset environment variable
553
- os.environ.pop(variable_name, None)
554
-
555
- # Delete .prompt file from all locations
556
- file_deleted, deleted_path = _delete_prompt_file(variable_name)
557
-
558
- messages = []
559
- messages.append(f"✓ Environment variable reset ({variable_name})")
560
-
561
- if file_deleted:
562
- messages.append(f"✓ File deleted ({deleted_path})")
563
- else:
564
- messages.append("⚠ File deletion failed or file doesn't exist")
565
-
566
- # If repository is specified, reset GitHub repository variable
567
- if repository:
568
- token = _get_github_token()
569
- if not token:
570
- messages.append(
571
- "⚠ GitHub token not available - skipped repository reset"
572
- )
573
- else:
574
- result = _update_github_repository_variable(
575
- repository=repository, name=variable_name, value="", token=token
576
- )
577
-
578
- if result["success"]:
579
- messages.append(
580
- f"✓ GitHub repository variable reset ({repository})"
581
- )
582
- else:
583
- messages.append(f"⚠ GitHub reset failed: {result['message']}")
584
-
585
- return {
586
- "status": "success",
587
- "content": [{"text": "\n".join(messages)}],
588
- }
589
-
590
- elif action == "get_github_context":
591
- github_context = _get_github_event_context()
592
- return {
593
- "status": "success",
594
- "content": [{"text": f"GitHub Event Context:\n\n{github_context}"}],
595
- }
596
-
597
- else:
598
- return {
599
- "status": "error",
600
- "content": [
601
- {
602
- "text": f"Error: Unknown action '{action}'. Valid actions are view, update, add_context, reset, get_github_context"
603
- }
604
- ],
605
- }
606
-
607
- except Exception as e:
608
- return {"status": "error", "content": [{"text": f"Error: {e!s}"}]}