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

Files changed (37) hide show
  1. devduck/__init__.py +1439 -483
  2. devduck/__main__.py +7 -0
  3. devduck/_version.py +34 -0
  4. devduck/agentcore_handler.py +76 -0
  5. devduck/test_redduck.py +0 -1
  6. devduck/tools/__init__.py +47 -0
  7. devduck/tools/_ambient_input.py +423 -0
  8. devduck/tools/_tray_app.py +530 -0
  9. devduck/tools/agentcore_agents.py +197 -0
  10. devduck/tools/agentcore_config.py +441 -0
  11. devduck/tools/agentcore_invoke.py +423 -0
  12. devduck/tools/agentcore_logs.py +320 -0
  13. devduck/tools/ambient.py +157 -0
  14. devduck/tools/create_subagent.py +659 -0
  15. devduck/tools/fetch_github_tool.py +201 -0
  16. devduck/tools/install_tools.py +409 -0
  17. devduck/tools/ipc.py +546 -0
  18. devduck/tools/mcp_server.py +600 -0
  19. devduck/tools/scraper.py +935 -0
  20. devduck/tools/speech_to_speech.py +850 -0
  21. devduck/tools/state_manager.py +292 -0
  22. devduck/tools/store_in_kb.py +187 -0
  23. devduck/tools/system_prompt.py +608 -0
  24. devduck/tools/tcp.py +263 -94
  25. devduck/tools/tray.py +247 -0
  26. devduck/tools/use_github.py +438 -0
  27. devduck/tools/websocket.py +498 -0
  28. devduck-0.1.1766644714.dist-info/METADATA +717 -0
  29. devduck-0.1.1766644714.dist-info/RECORD +33 -0
  30. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
  31. devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
  32. devduck/install.sh +0 -42
  33. devduck-0.1.0.dist-info/METADATA +0 -106
  34. devduck-0.1.0.dist-info/RECORD +0 -11
  35. devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
  36. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
  37. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/top_level.txt +0 -0
devduck/tools/tray.py ADDED
@@ -0,0 +1,247 @@
1
+ """
2
+ Tray app control tool - integrated with devduck
3
+ """
4
+
5
+ from strands import tool
6
+ from typing import Dict, Any, List
7
+ import subprocess
8
+ import socket
9
+ import json
10
+ import tempfile
11
+ import os
12
+ import sys
13
+ import time
14
+ import signal
15
+ from pathlib import Path
16
+
17
+ # Global state
18
+ _tray_process = None
19
+
20
+
21
+ def _send_ipc_command(socket_path: str, command: Dict) -> Dict:
22
+ """Send IPC command to tray app"""
23
+ try:
24
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
25
+ client.settimeout(10.0)
26
+ client.connect(socket_path)
27
+
28
+ # Send command
29
+ message = json.dumps(command).encode("utf-8")
30
+ client.sendall(message)
31
+
32
+ # Receive response
33
+ response_data = b""
34
+ while True:
35
+ chunk = client.recv(4096)
36
+ if not chunk:
37
+ break
38
+ response_data += chunk
39
+ # Check if we have complete JSON
40
+ try:
41
+ json.loads(response_data.decode("utf-8"))
42
+ break
43
+ except:
44
+ continue
45
+
46
+ client.close()
47
+
48
+ if not response_data:
49
+ return {"status": "error", "message": "Empty response"}
50
+
51
+ return json.loads(response_data.decode("utf-8"))
52
+ except socket.timeout:
53
+ return {"status": "error", "message": "Timeout"}
54
+ except Exception as e:
55
+ return {"status": "error", "message": str(e)}
56
+
57
+
58
+ @tool
59
+ def tray(
60
+ action: str,
61
+ items: List[Dict[str, Any]] = None,
62
+ title: str = None,
63
+ message: Dict[str, Any] = None,
64
+ text: str = None,
65
+ ) -> Dict[str, Any]:
66
+ """Control system tray app with devduck integration.
67
+
68
+ Returns:
69
+ Dict with status and content
70
+ """
71
+ global _tray_process
72
+
73
+ socket_path = os.path.join(tempfile.gettempdir(), "devduck_tray.sock")
74
+
75
+ if action == "start":
76
+ if _tray_process and _tray_process.poll() is None:
77
+ return {"status": "success", "content": [{"text": "✓ Already running"}]}
78
+
79
+ # Get tray script path
80
+ tools_dir = Path(__file__).parent
81
+ tray_script = tools_dir / "_tray_app.py"
82
+
83
+ if not tray_script.exists():
84
+ return {
85
+ "status": "error",
86
+ "content": [{"text": f"❌ Tray app not found: {tray_script}"}],
87
+ }
88
+
89
+ _tray_process = subprocess.Popen(
90
+ [sys.executable, str(tray_script)],
91
+ stdout=subprocess.DEVNULL,
92
+ stderr=subprocess.DEVNULL,
93
+ )
94
+
95
+ time.sleep(1.5)
96
+
97
+ return {
98
+ "status": "success",
99
+ "content": [{"text": f"✓ Tray app started (PID: {_tray_process.pid})"}],
100
+ }
101
+
102
+ elif action == "stop":
103
+ if _tray_process:
104
+ try:
105
+ os.kill(_tray_process.pid, signal.SIGTERM)
106
+ _tray_process.wait(timeout=3)
107
+ except:
108
+ pass
109
+ _tray_process = None
110
+
111
+ return {"status": "success", "content": [{"text": "✓ Stopped"}]}
112
+
113
+ elif action == "status":
114
+ is_running = _tray_process and _tray_process.poll() is None
115
+ return {"status": "success", "content": [{"text": f"Running: {is_running}"}]}
116
+
117
+ elif action == "update_menu":
118
+ if not items:
119
+ return {
120
+ "status": "error",
121
+ "content": [{"text": "items parameter required"}],
122
+ }
123
+
124
+ result = _send_ipc_command(
125
+ socket_path, {"action": "update_menu", "items": items}
126
+ )
127
+
128
+ if result.get("status") == "success":
129
+ return {
130
+ "status": "success",
131
+ "content": [{"text": f"✓ Menu updated ({len(items)} items)"}],
132
+ }
133
+ else:
134
+ return {
135
+ "status": "error",
136
+ "content": [
137
+ {"text": f"Failed: {result.get('message', 'Unknown error')}"}
138
+ ],
139
+ }
140
+
141
+ elif action == "update_title":
142
+ if not title:
143
+ return {
144
+ "status": "error",
145
+ "content": [{"text": "title parameter required"}],
146
+ }
147
+
148
+ result = _send_ipc_command(
149
+ socket_path, {"action": "update_title", "title": title}
150
+ )
151
+
152
+ if result.get("status") == "success":
153
+ return {"status": "success", "content": [{"text": f"✓ Title: {title}"}]}
154
+ else:
155
+ return {
156
+ "status": "error",
157
+ "content": [{"text": f"Failed: {result.get('message')}"}],
158
+ }
159
+
160
+ elif action == "set_progress":
161
+ """Set progress indicator: idle, thinking, processing, complete, error"""
162
+ if not text:
163
+ return {
164
+ "status": "error",
165
+ "content": [
166
+ {
167
+ "text": "text parameter required (idle/thinking/processing/complete/error)"
168
+ }
169
+ ],
170
+ }
171
+
172
+ result = _send_ipc_command(
173
+ socket_path, {"action": "set_progress", "progress": text}
174
+ )
175
+
176
+ if result.get("status") == "success":
177
+ return {"status": "success", "content": [{"text": f"✓ Progress: {text}"}]}
178
+ else:
179
+ return {
180
+ "status": "error",
181
+ "content": [{"text": f"Failed: {result.get('message')}"}],
182
+ }
183
+
184
+ elif action == "notify":
185
+ if not message:
186
+ return {
187
+ "status": "error",
188
+ "content": [{"text": "message parameter required"}],
189
+ }
190
+
191
+ result = _send_ipc_command(
192
+ socket_path, {"action": "notify", "message": message}
193
+ )
194
+
195
+ if result.get("status") == "success":
196
+ return {"status": "success", "content": [{"text": "✓ Notification sent"}]}
197
+ else:
198
+ return {
199
+ "status": "error",
200
+ "content": [{"text": f"Failed: {result.get('message')}"}],
201
+ }
202
+
203
+ elif action == "show_input":
204
+ result = _send_ipc_command(socket_path, {"action": "show_input"})
205
+
206
+ if result.get("status") == "success":
207
+ return {"status": "success", "content": [{"text": "✓ Input shown"}]}
208
+ else:
209
+ return {
210
+ "status": "error",
211
+ "content": [{"text": f"Failed: {result.get('message')}"}],
212
+ }
213
+
214
+ elif action == "stream_text":
215
+ if not text:
216
+ return {"status": "error", "content": [{"text": "text parameter required"}]}
217
+
218
+ result = _send_ipc_command(socket_path, {"action": "stream_text", "text": text})
219
+
220
+ if result.get("status") == "success":
221
+ return {"status": "success", "content": [{"text": "✓ Text streamed"}]}
222
+ else:
223
+ return {
224
+ "status": "error",
225
+ "content": [{"text": f"Failed: {result.get('message')}"}],
226
+ }
227
+
228
+ elif action in ["toggle_tcp", "toggle_ws", "toggle_mcp"]:
229
+ result = _send_ipc_command(socket_path, {"action": action})
230
+
231
+ if result.get("status") == "success":
232
+ return {"status": "success", "content": [{"text": f"✓ {action} executed"}]}
233
+ else:
234
+ return {
235
+ "status": "error",
236
+ "content": [{"text": f"Failed: {result.get('message')}"}],
237
+ }
238
+
239
+ else:
240
+ return {
241
+ "status": "error",
242
+ "content": [
243
+ {
244
+ "text": f"Unknown action: {action}. Available: start, stop, status, update_menu, update_title, set_progress, notify, show_input, stream_text, toggle_tcp, toggle_ws, toggle_mcp"
245
+ }
246
+ ],
247
+ }
@@ -0,0 +1,438 @@
1
+ """GitHub GraphQL API integration tool for Strands Agents.
2
+
3
+ This module provides a comprehensive interface to GitHub's v4 GraphQL API,
4
+ allowing you to execute any GitHub GraphQL query or mutation directly from your Strands Agent.
5
+ The tool handles authentication, parameter validation, response formatting,
6
+ and provides user-friendly error messages with schema recommendations.
7
+
8
+ Key Features:
9
+
10
+ 1. Universal GitHub GraphQL Access:
11
+ • Access to GitHub's full GraphQL API (v4)
12
+ • Support for both queries and mutations
13
+ • Authentication via GITHUB_TOKEN environment variable
14
+ • Rate limit awareness and error handling
15
+
16
+ 2. Safety Features:
17
+ • Confirmation prompts for mutative operations (mutations)
18
+ • Parameter validation with helpful error messages
19
+ • Error handling with detailed feedback
20
+ • Query complexity analysis
21
+
22
+ 3. Response Handling:
23
+ • JSON formatting of responses
24
+ • Error message extraction from GraphQL responses
25
+ • Rate limit information display
26
+ • Pretty printing of operation details
27
+
28
+ 4. Usage Examples:
29
+ ```python
30
+ from strands import Agent
31
+ from tools.use_github import use_github
32
+
33
+ agent = Agent(tools=[use_github])
34
+
35
+ # Get repository information
36
+ result = agent.tool.use_github(
37
+ query_type="query",
38
+ query='''
39
+ query($owner: String!, $name: String!) {
40
+ repository(owner: $owner, name: $name) {
41
+ name
42
+ description
43
+ stargazerCount
44
+ forkCount
45
+ }
46
+ }
47
+ ''',
48
+ variables={"owner": "octocat", "name": "Hello-World"},
49
+ label="Get repository information",
50
+ )
51
+ ```
52
+
53
+ See the use_github function docstring for more details on parameters and usage.
54
+ """
55
+
56
+ import json
57
+ import logging
58
+ import os
59
+ from typing import Any
60
+
61
+ import requests
62
+ from colorama import Fore, Style, init
63
+ from rich.console import Console
64
+ from rich.panel import Panel
65
+ from strands import tool
66
+
67
+ # Initialize colorama
68
+ init(autoreset=True)
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+ # GitHub GraphQL API endpoint
73
+ GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
74
+
75
+
76
+ def create_console() -> Console:
77
+ """Create a Rich console instance."""
78
+ return Console()
79
+
80
+
81
+ def get_user_input(prompt: str) -> str:
82
+ """Simple user input function with styled prompt."""
83
+ # Remove Rich markup for simple input
84
+ clean_prompt = prompt.replace("<yellow><bold>", "").replace(
85
+ "</bold> [y/*]</yellow>", " [y/*] "
86
+ )
87
+ return input(clean_prompt)
88
+
89
+
90
+ # Common mutation keywords that indicate potentially destructive operations
91
+ MUTATIVE_KEYWORDS = [
92
+ "create",
93
+ "update",
94
+ "delete",
95
+ "add",
96
+ "remove",
97
+ "merge",
98
+ "close",
99
+ "reopen",
100
+ "lock",
101
+ "unlock",
102
+ "pin",
103
+ "unpin",
104
+ "transfer",
105
+ "archive",
106
+ "unarchive",
107
+ "enable",
108
+ "disable",
109
+ "accept",
110
+ "decline",
111
+ "dismiss",
112
+ "submit",
113
+ "request",
114
+ "cancel",
115
+ "convert",
116
+ ]
117
+
118
+
119
+ def get_github_token() -> str | None:
120
+ """Get GitHub token from environment variables.
121
+
122
+ Returns:
123
+ GitHub token string or None if not found
124
+ """
125
+ return os.environ.get("GITHUB_TOKEN", "")
126
+
127
+
128
+ def is_mutation_query(query: str) -> bool:
129
+ """Check if a GraphQL query is a mutation based on keywords and structure.
130
+
131
+ Args:
132
+ query: GraphQL query string
133
+
134
+ Returns:
135
+ True if the query appears to be a mutation
136
+ """
137
+ query_lower = query.lower().strip()
138
+
139
+ # Check if query starts with "mutation"
140
+ if query_lower.startswith("mutation"):
141
+ return True
142
+
143
+ # Check for mutative keywords in the query
144
+ return any(keyword in query_lower for keyword in MUTATIVE_KEYWORDS)
145
+
146
+
147
+ def execute_github_graphql(
148
+ query: str, variables: dict[str, Any] | None = None, token: str | None = None
149
+ ) -> dict[str, Any]:
150
+ """Execute a GraphQL query against GitHub's API.
151
+
152
+ Args:
153
+ query: GraphQL query string
154
+ variables: Optional variables for the query
155
+ token: GitHub authentication token
156
+
157
+ Returns:
158
+ Dictionary containing the GraphQL response
159
+
160
+ Raises:
161
+ requests.RequestException: If the request fails
162
+ ValueError: If authentication fails
163
+ """
164
+ if not token:
165
+ raise ValueError(
166
+ "GitHub token is required. Set GITHUB_TOKEN environment variable."
167
+ )
168
+
169
+ headers = {
170
+ "Authorization": f"Bearer {token}",
171
+ "Content-Type": "application/json",
172
+ "Accept": "application/vnd.github.v4+json",
173
+ "User-Agent": "Strands-Agent-GitHub-Tool/1.0",
174
+ }
175
+
176
+ payload = {"query": query, "variables": variables or {}}
177
+
178
+ response = requests.post(
179
+ GITHUB_GRAPHQL_URL, headers=headers, json=payload, timeout=30
180
+ )
181
+
182
+ response.raise_for_status()
183
+ response_data: dict[str, Any] = response.json()
184
+ return response_data
185
+
186
+
187
+ def format_github_response(response: dict[str, Any]) -> str:
188
+ """Format GitHub GraphQL response for display.
189
+
190
+ Args:
191
+ response: GitHub GraphQL response dictionary
192
+
193
+ Returns:
194
+ Formatted string representation of the response
195
+ """
196
+ formatted_parts = []
197
+
198
+ # Handle errors
199
+ if "errors" in response:
200
+ formatted_parts.append(f"{Fore.RED}Errors:{Style.RESET_ALL}")
201
+ for error in response["errors"]:
202
+ formatted_parts.append(f" - {error.get('message', 'Unknown error')}")
203
+ if "locations" in error:
204
+ locations = error["locations"]
205
+ formatted_parts.append(f" Locations: {locations}")
206
+
207
+ # Handle data
208
+ if "data" in response:
209
+ formatted_parts.append(f"{Fore.GREEN}Data:{Style.RESET_ALL}")
210
+ formatted_parts.append(json.dumps(response["data"], indent=2))
211
+
212
+ # Handle rate limit info
213
+ if "extensions" in response and "cost" in response["extensions"]:
214
+ cost_info = response["extensions"]["cost"]
215
+ formatted_parts.append(f"{Fore.YELLOW}Rate Limit Info:{Style.RESET_ALL}")
216
+ formatted_parts.append(
217
+ f" - Query Cost: {cost_info.get('requestedQueryCost', 'N/A')}"
218
+ )
219
+ formatted_parts.append(f" - Node Count: {cost_info.get('nodeCount', 'N/A')}")
220
+ if "rateLimit" in cost_info:
221
+ rate_limit = cost_info["rateLimit"]
222
+ formatted_parts.append(
223
+ f" - Remaining: {rate_limit.get('remaining', 'N/A')}"
224
+ )
225
+ formatted_parts.append(f" - Reset At: {rate_limit.get('resetAt', 'N/A')}")
226
+
227
+ return "\n".join(formatted_parts)
228
+
229
+
230
+ @tool
231
+ def use_github(
232
+ query_type: str,
233
+ query: str,
234
+ label: str,
235
+ variables: dict[str, Any] | None = None,
236
+ ) -> dict[str, Any]:
237
+ """Execute GitHub GraphQL API operations with comprehensive error handling and validation.
238
+
239
+ This tool provides a universal interface to GitHub's GraphQL API (v4), allowing you to execute
240
+ any query or mutation supported by GitHub's GraphQL schema. It handles authentication via
241
+ GITHUB_TOKEN, parameter validation, response formatting, and provides helpful error messages.
242
+
243
+ How It Works:
244
+ ------------
245
+ 1. The tool validates the GitHub token from environment variables
246
+ 2. For mutations or potentially destructive operations, it prompts for confirmation
247
+ 3. It executes the GraphQL query/mutation against GitHub's API
248
+ 4. Responses are processed and formatted with proper error handling
249
+ 5. Rate limit information is displayed when available
250
+
251
+ Common Usage Scenarios:
252
+ ---------------------
253
+ - Repository Management: Get repository info, create/update repositories
254
+ - Issue & PR Operations: Create, update, close issues and pull requests
255
+ - User & Organization Data: Retrieve user profiles, organization details
256
+ - Project Management: Manage GitHub Projects, milestones, and labels
257
+ - Git Operations: Access commit history, branches, and tags
258
+ - Security: Manage webhooks, deploy keys, and security settings
259
+
260
+ Example Queries:
261
+ ---------------
262
+ Repository Information:
263
+ ```graphql
264
+ query($owner: String!, $name: String!) {
265
+ repository(owner: $owner, name: $name) {
266
+ name
267
+ description
268
+ stargazerCount
269
+ forkCount
270
+ issues(states: OPEN) {
271
+ totalCount
272
+ }
273
+ pullRequests(states: OPEN) {
274
+ totalCount
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ Create Issue Mutation:
281
+ ```graphql
282
+ mutation($repositoryId: ID!, $title: String!, $body: String) {
283
+ createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) {
284
+ issue {
285
+ number
286
+ title
287
+ url
288
+ }
289
+ }
290
+ }
291
+ ```
292
+
293
+ Args:
294
+ query_type: Type of GraphQL operation ("query" or "mutation")
295
+ query: The GraphQL query or mutation string
296
+ label: Human-readable description of the GitHub operation
297
+ variables: Optional dictionary of variables for the query
298
+
299
+ Returns:
300
+ Dict containing status and response content:
301
+ {
302
+ "status": "success|error",
303
+ "content": [{"text": "Response message"}]
304
+ }
305
+
306
+ Notes:
307
+ - Requires GITHUB_TOKEN environment variable to be set
308
+ - Mutations require user confirmation in non-dev environments
309
+ - You can disable confirmation by setting BYPASS_TOOL_CONSENT=true
310
+ - The tool automatically handles rate limiting information
311
+ - GraphQL errors are formatted and displayed clearly
312
+ - All responses are JSON formatted for easy parsing
313
+
314
+ Environment Variables:
315
+ - GITHUB_TOKEN: Required GitHub personal access token or app token
316
+ - BYPASS_TOOL_CONSENT: Set to "true" to skip confirmation prompts
317
+ """
318
+ console = create_console()
319
+
320
+ # Set default for variables if None
321
+ if variables is None:
322
+ variables = {}
323
+
324
+ STRANDS_BYPASS_TOOL_CONSENT = (
325
+ os.environ.get("BYPASS_TOOL_CONSENT", "").lower() == "true"
326
+ )
327
+
328
+ # Create a panel for GitHub Operation Details
329
+ operation_details = f"{Fore.CYAN}Type:{Style.RESET_ALL} {query_type}\n"
330
+ operation_details += f"{Fore.CYAN}Query:{Style.RESET_ALL}\n{query}\n"
331
+ if variables:
332
+ operation_details += f"{Fore.CYAN}Variables:{Style.RESET_ALL}\n"
333
+ for key, value in variables.items():
334
+ operation_details += f" - {key}: {value}\n"
335
+
336
+ console.print(Panel(operation_details, title=label, expand=False))
337
+
338
+ logger.debug(
339
+ f"Invoking GitHub GraphQL: query_type = {query_type}, variables = {variables}"
340
+ )
341
+
342
+ # Get GitHub token
343
+ github_token = get_github_token()
344
+ if not github_token:
345
+ return {
346
+ "status": "error",
347
+ "content": [
348
+ {
349
+ "text": "GitHub token not found. Please set the GITHUB_TOKEN environment variable.\n"
350
+ "You can create a token at: https://github.com/settings/tokens"
351
+ }
352
+ ],
353
+ }
354
+
355
+ # Check if the operation is potentially mutative
356
+ is_mutative = query_type.lower() == "mutation" or is_mutation_query(query)
357
+
358
+ if is_mutative and not STRANDS_BYPASS_TOOL_CONSENT:
359
+ # Prompt for confirmation before executing the operation
360
+ confirm = get_user_input(
361
+ f"<yellow><bold>This appears to be a mutative operation ({query_type}). "
362
+ f"Do you want to proceed?</bold> [y/*]</yellow>"
363
+ )
364
+ if confirm.lower() != "y":
365
+ return {
366
+ "status": "error",
367
+ "content": [
368
+ {"text": f"Operation canceled by user. Reason: {confirm}."}
369
+ ],
370
+ }
371
+
372
+ try:
373
+ # Execute the GraphQL query
374
+ response = execute_github_graphql(query, variables, github_token)
375
+
376
+ # Format the response
377
+ formatted_response = format_github_response(response)
378
+
379
+ # Check if there were GraphQL errors
380
+ if "errors" in response:
381
+ return {
382
+ "status": "error",
383
+ "content": [
384
+ {"text": "GraphQL query completed with errors:"},
385
+ {"text": formatted_response},
386
+ ],
387
+ }
388
+
389
+ return {
390
+ "status": "success",
391
+ "content": [{"text": formatted_response}],
392
+ }
393
+
394
+ except requests.exceptions.HTTPError as http_err:
395
+ if http_err.response.status_code == 401:
396
+ return {
397
+ "status": "error",
398
+ "content": [
399
+ {
400
+ "text": "Authentication failed. Please check your GITHUB_TOKEN.\n"
401
+ "Make sure the token has the required permissions for this operation."
402
+ }
403
+ ],
404
+ }
405
+ elif http_err.response.status_code == 403:
406
+ return {
407
+ "status": "error",
408
+ "content": [
409
+ {
410
+ "text": "Forbidden. Your token may not have sufficient permissions for this operation.\n"
411
+ f"HTTP Error: {http_err}"
412
+ }
413
+ ],
414
+ }
415
+ else:
416
+ return {
417
+ "status": "error",
418
+ "content": [{"text": f"HTTP Error: {http_err}"}],
419
+ }
420
+
421
+ except requests.exceptions.RequestException as req_err:
422
+ return {
423
+ "status": "error",
424
+ "content": [{"text": f"Request Error: {req_err}"}],
425
+ }
426
+
427
+ except ValueError as val_err:
428
+ return {
429
+ "status": "error",
430
+ "content": [{"text": f"Configuration Error: {val_err}"}],
431
+ }
432
+
433
+ except Exception as ex:
434
+ logger.warning(f"GitHub GraphQL call threw exception: {type(ex).__name__}")
435
+ return {
436
+ "status": "error",
437
+ "content": [{"text": f"GitHub GraphQL call threw exception: {ex!s}"}],
438
+ }