devduck 0.2.0__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.
- devduck/__init__.py +273 -99
- devduck/_version.py +2 -2
- devduck/tools/create_subagent.py +659 -0
- devduck/tools/store_in_kb.py +187 -0
- devduck/tools/tcp.py +0 -3
- devduck/tools/use_github.py +438 -0
- devduck/tools/websocket.py +1 -1
- {devduck-0.2.0.dist-info → devduck-0.3.0.dist-info}/METADATA +17 -8
- devduck-0.3.0.dist-info/RECORD +18 -0
- devduck/install.sh +0 -42
- devduck-0.2.0.dist-info/RECORD +0 -16
- {devduck-0.2.0.dist-info → devduck-0.3.0.dist-info}/WHEEL +0 -0
- {devduck-0.2.0.dist-info → devduck-0.3.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.2.0.dist-info → devduck-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.2.0.dist-info → devduck-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
}
|
devduck/tools/websocket.py
CHANGED
|
@@ -261,7 +261,7 @@ async def handle_websocket_client(websocket, system_prompt: str):
|
|
|
261
261
|
# Send welcome message
|
|
262
262
|
welcome = {
|
|
263
263
|
"type": "connected",
|
|
264
|
-
"data": "🦆 Welcome to DevDuck
|
|
264
|
+
"data": "🦆 Welcome to DevDuck!",
|
|
265
265
|
"timestamp": time.time(),
|
|
266
266
|
}
|
|
267
267
|
await websocket.send(json.dumps(welcome))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devduck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: 🦆 Extreme minimalist self-adapting AI agent - one file, self-healing, runtime dependencies
|
|
5
5
|
Author-email: duck <hey@devduck.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,15 +27,20 @@ Requires-Python: >=3.10
|
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE
|
|
29
29
|
Requires-Dist: strands-agents
|
|
30
|
+
Requires-Dist: prompt_toolkit
|
|
30
31
|
Requires-Dist: strands-agents[ollama]
|
|
31
|
-
Requires-Dist: strands-agents[openai]
|
|
32
|
-
Requires-Dist: strands-agents[anthropic]
|
|
33
32
|
Requires-Dist: strands-agents-tools
|
|
34
|
-
Requires-Dist: strands-
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: strands-agentcore-tools
|
|
34
|
+
Requires-Dist: beautifulsoup4
|
|
35
|
+
Requires-Dist: colorama
|
|
37
36
|
Requires-Dist: websockets
|
|
38
|
-
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: strands-agents[openai]; extra == "all"
|
|
39
|
+
Requires-Dist: strands-agents[anthropic]; extra == "all"
|
|
40
|
+
Requires-Dist: strands-fun-tools[audio]; extra == "all"
|
|
41
|
+
Requires-Dist: strands-fun-tools[vision]; extra == "all"
|
|
42
|
+
Requires-Dist: strands-fun-tools[all]; extra == "all"
|
|
43
|
+
Requires-Dist: strands-mcp-server; extra == "all"
|
|
39
44
|
Dynamic: license-file
|
|
40
45
|
|
|
41
46
|
# 🦆 DevDuck
|
|
@@ -47,10 +52,14 @@ Minimalist AI agent that fixes itself when things break.
|
|
|
47
52
|
## Install
|
|
48
53
|
|
|
49
54
|
```bash
|
|
55
|
+
# Minimal install
|
|
50
56
|
pipx install devduck
|
|
57
|
+
|
|
58
|
+
# Full install (all tools)
|
|
59
|
+
pipx install "devduck[all]"
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
Requires: Python 3.10+, Ollama running
|
|
62
|
+
Requires: Python 3.10+, Ollama running (or set MODEL_PROVIDER)
|
|
54
63
|
|
|
55
64
|
## Use
|
|
56
65
|
|