devduck 0.2.0__py3-none-any.whl → 0.4.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.

@@ -190,9 +190,11 @@ def _start_mcp_server(
190
190
  agent_invoke_tool = types.Tool(
191
191
  name="devduck",
192
192
  description=(
193
- "Invoke the full DevDuck agent with a natural language prompt. "
194
- "Use this for complex queries that require reasoning across multiple tools "
195
- "or when you need a conversational response from the agent."
193
+ "Invoke a FULL DevDuck instance with complete capabilities. "
194
+ "Each invocation creates a fresh DevDuck agent with self-healing, "
195
+ "hot-reload, all tools, knowledge base integration, and system prompt building. "
196
+ "Use this for complex queries requiring reasoning, multi-tool orchestration, "
197
+ "or when you need the complete DevDuck experience via MCP."
196
198
  ),
197
199
  inputSchema={
198
200
  "type": "object",
@@ -228,7 +230,7 @@ def _start_mcp_server(
228
230
  try:
229
231
  logger.debug(f"call_tool: name={name}, arguments={arguments}")
230
232
 
231
- # Handle agent invocation tool - use the devduck instance directly
233
+ # Handle agent invocation tool - create a full DevDuck instance
232
234
  if name == "devduck" and expose_agent:
233
235
  prompt = arguments.get("prompt")
234
236
  if not prompt:
@@ -241,8 +243,34 @@ def _start_mcp_server(
241
243
 
242
244
  logger.debug(f"Invoking devduck with prompt: {prompt[:100]}...")
243
245
 
244
- # Use the devduck agent directly (don't create a new instance)
245
- result = agent(prompt)
246
+ # Create a NEW DevDuck instance for this MCP invocation
247
+ # This gives full DevDuck power: self-healing, hot-reload, all tools, etc.
248
+ try:
249
+ from devduck import DevDuck
250
+
251
+ # Create fresh DevDuck instance (no auto-start to avoid recursion)
252
+ mcp_devduck = DevDuck(auto_start_servers=False)
253
+ mcp_agent = mcp_devduck.agent
254
+
255
+ if not mcp_agent:
256
+ return [
257
+ types.TextContent(
258
+ type="text",
259
+ text="❌ Error: Failed to create DevDuck instance",
260
+ )
261
+ ]
262
+
263
+ # Execute with full DevDuck capabilities
264
+ result = mcp_agent(prompt)
265
+
266
+ except Exception as e:
267
+ logger.error(f"DevDuck creation failed: {e}", exc_info=True)
268
+ return [
269
+ types.TextContent(
270
+ type="text",
271
+ text=f"❌ Error creating DevDuck instance: {str(e)}",
272
+ )
273
+ ]
246
274
 
247
275
  # Extract text response from agent result
248
276
  response_text = str(result)
@@ -0,0 +1,187 @@
1
+ """Tool for storing data in Bedrock Knowledge Base asynchronously."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from typing import Any
10
+
11
+ import boto3
12
+ from strands import tool
13
+
14
+ # Set up logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _store_in_kb_background(
19
+ content: str, title: str, kb_id: str, region_name: str
20
+ ) -> None:
21
+ """Background worker function that performs the actual KB storage.
22
+
23
+ This runs in a separate thread and handles all the KB operations.
24
+ Whole validation is done in the main thread before calling this function.
25
+
26
+ Args:
27
+ content: The text content to store
28
+ title: The title for the content
29
+ kb_id: The knowledge base ID
30
+ region_name: The AWS region to use
31
+ """
32
+ try:
33
+ # Generate document ID with timestamp for traceability
34
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
35
+ doc_id = f"memory_{timestamp}_{str(uuid.uuid4())[:8]}"
36
+
37
+ # Package content with title for better organization
38
+ content_with_metadata = {
39
+ "title": title,
40
+ "action": "store",
41
+ "content": content,
42
+ }
43
+
44
+ # Initialize Bedrock agent client
45
+ bedrock_agent_client = boto3.client("bedrock-agent", region_name=region_name)
46
+
47
+ # Get the data source ID associated with the knowledge base
48
+ data_sources = bedrock_agent_client.list_data_sources(knowledgeBaseId=kb_id)
49
+
50
+ if not data_sources.get("dataSourceSummaries"):
51
+ logger.error(
52
+ f"No data sources found for knowledge base {kb_id}, region {region_name}."
53
+ )
54
+ return
55
+
56
+ # Look for a CUSTOM data source type first, as it's required for inline content ingestion
57
+ data_source_id = None
58
+ source_type = None
59
+
60
+ for ds in data_sources["dataSourceSummaries"]:
61
+ # Get the data source details to check its type
62
+ ds_detail = bedrock_agent_client.get_data_source(
63
+ knowledgeBaseId=kb_id, dataSourceId=ds["dataSourceId"]
64
+ )
65
+
66
+ # Check if this is a CUSTOM type data source
67
+ if ds_detail["dataSource"]["dataSourceConfiguration"]["type"] == "CUSTOM":
68
+ data_source_id = ds["dataSourceId"]
69
+ source_type = "CUSTOM"
70
+ logger.debug(f"Found CUSTOM data source: {data_source_id}")
71
+ break
72
+
73
+ # If no CUSTOM data source found, use the first available one but log a warning
74
+ if not data_source_id and data_sources["dataSourceSummaries"]:
75
+ data_source_id = data_sources["dataSourceSummaries"][0]["dataSourceId"]
76
+ ds_detail = bedrock_agent_client.get_data_source(
77
+ knowledgeBaseId=kb_id, dataSourceId=data_source_id
78
+ )
79
+ source_type = ds_detail["dataSource"]["dataSourceConfiguration"]["type"]
80
+ logger.debug(
81
+ f"No CUSTOM data source found. Using {source_type} data source: {data_source_id}"
82
+ )
83
+
84
+ if not data_source_id:
85
+ logger.error(f"No suitable data source found for knowledge base {kb_id}.")
86
+ return
87
+
88
+ # Prepare document for ingestion based on the data source type
89
+ if source_type == "CUSTOM":
90
+ ingest_request = {
91
+ "knowledgeBaseId": kb_id,
92
+ "dataSourceId": data_source_id,
93
+ "documents": [
94
+ {
95
+ "content": {
96
+ "dataSourceType": "CUSTOM",
97
+ "custom": {
98
+ "customDocumentIdentifier": {"id": doc_id},
99
+ "inlineContent": {
100
+ "textContent": {
101
+ "data": json.dumps(content_with_metadata)
102
+ },
103
+ "type": "TEXT",
104
+ },
105
+ "sourceType": "IN_LINE",
106
+ },
107
+ }
108
+ }
109
+ ],
110
+ }
111
+ elif source_type == "S3":
112
+ # S3 source types need a different ingestion approach
113
+ logger.error(
114
+ "S3 data source type is not supported for direct ingestion with this tool."
115
+ )
116
+ return
117
+ else:
118
+ logger.error(f"Unsupported data source type: {source_type}")
119
+ return
120
+
121
+ # Ingest document into knowledge base
122
+ _ = bedrock_agent_client.ingest_knowledge_base_documents(**ingest_request)
123
+
124
+ # Log success
125
+ logger.info(
126
+ f"Successfully ingested document into knowledge base {kb_id}: {doc_id}"
127
+ )
128
+
129
+ except Exception as e:
130
+ logger.error(f"Error ingesting into knowledge base: {e!s}")
131
+
132
+
133
+ @tool
134
+ def store_in_kb(
135
+ content: str, title: str | None = None, knowledge_base_id: str | None = None
136
+ ) -> dict[str, Any]:
137
+ """Store content in a Bedrock Knowledge Base using real-time ingestion.
138
+
139
+ This version runs asynchronously in a background thread and returns immediately.
140
+
141
+ Args:
142
+ content: The text content to store in the knowledge base.
143
+ title: Optional title for the content. If not provided, a timestamp will be used.
144
+ knowledge_base_id: Optional knowledge base ID. If not provided, will use the STRANDS_KNOWLEDGE_BASE_ID env.
145
+
146
+ Returns:
147
+ A dictionary containing the result of the operation.
148
+ """
149
+ # All validation done in main thread before spawning background thread
150
+
151
+ # Validate content first
152
+ if not content or not content.strip():
153
+ return {"status": "error", "content": [{"text": "❌ Content cannot be empty"}]}
154
+
155
+ # Resolve and validate knowledge base ID early (addresses environment variable race condition)
156
+ kb_id = knowledge_base_id or os.getenv("STRANDS_KNOWLEDGE_BASE_ID")
157
+ if not kb_id:
158
+ return {
159
+ "status": "error",
160
+ "content": [
161
+ {
162
+ "text": "❌ No knowledge base ID provided or found in environment variables STRANDS_KNOWLEDGE_BASE_ID"
163
+ }
164
+ ],
165
+ }
166
+
167
+ region_name = os.getenv("AWS_REGION", "us-west-2")
168
+
169
+ doc_title = title or f"Strands Memory {time.strftime('%Y%m%d_%H%M%S')}"
170
+
171
+ thread = threading.Thread(
172
+ target=_store_in_kb_background,
173
+ args=(content, doc_title, kb_id, region_name),
174
+ daemon=True,
175
+ )
176
+ thread.start()
177
+
178
+ # Return immediately with status
179
+ return {
180
+ "status": "success",
181
+ "content": [
182
+ {"text": "✅ Started background task to store content in knowledge base:"},
183
+ {"text": f"📝 Title: {doc_title}"},
184
+ {"text": f"🗄️ Knowledge Base ID: {kb_id}"},
185
+ {"text": "⏱️ Processing in background..."},
186
+ ],
187
+ }
devduck/tools/tcp.py CHANGED
@@ -218,9 +218,6 @@ def handle_client(
218
218
  try:
219
219
  # Send welcome message
220
220
  welcome_msg = "🦆 Welcome to DevDuck TCP Server!\n"
221
- welcome_msg += (
222
- "Real-time streaming enabled - responses stream as they're generated.\n"
223
- )
224
221
  welcome_msg += "Send a message or 'exit' to close the connection.\n\n"
225
222
  streaming_handler._send(welcome_msg)
226
223
 
@@ -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
+ }