devduck 0.5.0__py3-none-any.whl → 0.5.2__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 +612 -1004
- devduck/_version.py +2 -2
- devduck/agentcore_handler.py +76 -0
- devduck/tools/__init__.py +44 -1
- devduck/tools/_tray_app.py +8 -0
- devduck/tools/agentcore_agents.py +197 -0
- devduck/tools/agentcore_config.py +441 -0
- devduck/tools/agentcore_invoke.py +422 -0
- devduck/tools/agentcore_logs.py +320 -0
- devduck-0.5.2.dist-info/METADATA +415 -0
- {devduck-0.5.0.dist-info → devduck-0.5.2.dist-info}/RECORD +15 -10
- {devduck-0.5.0.dist-info → devduck-0.5.2.dist-info}/entry_points.txt +0 -1
- devduck-0.5.0.dist-info/METADATA +0 -554
- {devduck-0.5.0.dist-info → devduck-0.5.2.dist-info}/WHEEL +0 -0
- {devduck-0.5.0.dist-info → devduck-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.5.0.dist-info → devduck-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""AgentCore Invoke Tool - Invoke deployed DevDuck instances on AgentCore."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from strands import tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@tool
|
|
10
|
+
def agentcore_invoke(
|
|
11
|
+
prompt: str,
|
|
12
|
+
agent_name: Optional[str] = None,
|
|
13
|
+
agent_id: Optional[str] = None,
|
|
14
|
+
agent_arn: Optional[str] = None,
|
|
15
|
+
session_id: Optional[str] = None,
|
|
16
|
+
mode: str = "streaming",
|
|
17
|
+
tools: Optional[str] = None,
|
|
18
|
+
model: Optional[str] = None,
|
|
19
|
+
system_prompt: Optional[str] = None,
|
|
20
|
+
region: str = "us-west-2",
|
|
21
|
+
agent: Optional[Any] = None,
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""Invoke a deployed DevDuck instance on AgentCore.
|
|
24
|
+
|
|
25
|
+
**Quick Start:**
|
|
26
|
+
```python
|
|
27
|
+
# Use agent_id directly (RECOMMENDED - no config needed):
|
|
28
|
+
agentcore_invoke(agent_id="devduck-abc123", prompt="hello")
|
|
29
|
+
|
|
30
|
+
# Or by name (requires local config):
|
|
31
|
+
agentcore_invoke(agent_name="my-agent", prompt="hello")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Priority:** agent_arn > agent_id > agent_name (config lookup)
|
|
35
|
+
|
|
36
|
+
**Note:** Agent names with hyphens are auto-converted to underscores for config lookup.
|
|
37
|
+
For example: "test-agent-1" becomes "test_agent_1" in the config file.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
prompt: Query/prompt to send to the agent
|
|
41
|
+
agent_name: Name of deployed agent (default: "devduck")
|
|
42
|
+
- Hyphens auto-converted to underscores for config lookup
|
|
43
|
+
- Example: "test-agent" → looks for "test_agent" in config
|
|
44
|
+
agent_id: Direct agent ID (e.g., "devduck-UrvQvkH6H7") - RECOMMENDED
|
|
45
|
+
- Bypasses config lookup entirely
|
|
46
|
+
- Get from agentcore_agents() or deployment output
|
|
47
|
+
agent_arn: Direct agent ARN - bypasses config lookup
|
|
48
|
+
session_id: Session ID for continuity (auto-generated if not provided)
|
|
49
|
+
mode: Invocation mode (streaming, sync, async) - default: streaming
|
|
50
|
+
tools: Comma-separated list of tools to enable (optional)
|
|
51
|
+
Example: "file_read,calculator,shell"
|
|
52
|
+
model: Model ID override (optional)
|
|
53
|
+
system_prompt: System prompt override (optional)
|
|
54
|
+
region: AWS region (default: us-west-2)
|
|
55
|
+
agent: Parent agent for streaming callbacks
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict with status and response
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
# Use agent ID (RECOMMENDED - fastest, no config needed)
|
|
62
|
+
agentcore_invoke(
|
|
63
|
+
prompt="analyze this code",
|
|
64
|
+
agent_id="devduck-UrvQvkH6H7"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Use agent name (requires config file)
|
|
68
|
+
agentcore_invoke(
|
|
69
|
+
prompt="analyze this code",
|
|
70
|
+
agent_name="my-agent" # Auto-converts to "my_agent" for lookup
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# With session continuity
|
|
74
|
+
agentcore_invoke(
|
|
75
|
+
prompt="continue our discussion",
|
|
76
|
+
agent_id="devduck-UrvQvkH6H7",
|
|
77
|
+
session_id="previous-session-123"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Custom configuration
|
|
81
|
+
agentcore_invoke(
|
|
82
|
+
prompt="analyze data",
|
|
83
|
+
agent_id="devduck-UrvQvkH6H7",
|
|
84
|
+
model="us.anthropic.claude-sonnet-4-20250514-v1:0",
|
|
85
|
+
tools="file_read,calculator,python_repl",
|
|
86
|
+
system_prompt="You are a data analyst"
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
import boto3
|
|
91
|
+
import yaml
|
|
92
|
+
from botocore.config import Config
|
|
93
|
+
from pathlib import Path
|
|
94
|
+
|
|
95
|
+
# Determine agent ARN - priority: agent_arn > agent_id > agent_name (config lookup)
|
|
96
|
+
final_agent_arn = None
|
|
97
|
+
|
|
98
|
+
if agent_arn:
|
|
99
|
+
# Direct ARN provided - use it
|
|
100
|
+
final_agent_arn = agent_arn
|
|
101
|
+
elif agent_id:
|
|
102
|
+
# Direct agent ID provided - construct ARN
|
|
103
|
+
# Get account ID from STS
|
|
104
|
+
sts = boto3.client("sts", region_name=region)
|
|
105
|
+
account_id = sts.get_caller_identity()["Account"]
|
|
106
|
+
final_agent_arn = (
|
|
107
|
+
f"arn:aws:bedrock-agentcore:{region}:{account_id}:runtime/{agent_id}"
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# Fall back to config lookup by agent_name
|
|
111
|
+
if not agent_name:
|
|
112
|
+
agent_name = "devduck" # Default
|
|
113
|
+
|
|
114
|
+
# Normalize agent name: hyphens → underscores (matches agentcore_config behavior)
|
|
115
|
+
agent_name = agent_name.replace("-", "_")
|
|
116
|
+
|
|
117
|
+
# Try to find config file
|
|
118
|
+
import devduck as devduck_module
|
|
119
|
+
|
|
120
|
+
devduck_module_path = Path(devduck_module.__file__).parent
|
|
121
|
+
|
|
122
|
+
# Check if we're in development mode (has parent .git folder)
|
|
123
|
+
dev_mode = (devduck_module_path.parent / ".git").exists()
|
|
124
|
+
|
|
125
|
+
if dev_mode:
|
|
126
|
+
# Development mode: use parent directory
|
|
127
|
+
devduck_dir = devduck_module_path.parent
|
|
128
|
+
else:
|
|
129
|
+
# Installed mode: use module directory directly
|
|
130
|
+
devduck_dir = devduck_module_path
|
|
131
|
+
|
|
132
|
+
config_path = devduck_dir / ".bedrock_agentcore.yaml"
|
|
133
|
+
|
|
134
|
+
if not config_path.exists():
|
|
135
|
+
# No config file - try to list available agents
|
|
136
|
+
try:
|
|
137
|
+
import sys
|
|
138
|
+
from pathlib import Path
|
|
139
|
+
|
|
140
|
+
# Add tools directory to path if not already there
|
|
141
|
+
tools_dir = Path(__file__).parent
|
|
142
|
+
if str(tools_dir) not in sys.path:
|
|
143
|
+
sys.path.insert(0, str(tools_dir))
|
|
144
|
+
|
|
145
|
+
from agentcore_agents import agentcore_agents
|
|
146
|
+
|
|
147
|
+
agents_result = agentcore_agents(action="list", region=region)
|
|
148
|
+
|
|
149
|
+
if agents_result.get("status") == "success":
|
|
150
|
+
# Extract all text content from the result
|
|
151
|
+
agents_list = "\n".join(
|
|
152
|
+
item.get("text", "")
|
|
153
|
+
for item in agents_result.get("content", [])
|
|
154
|
+
if "text" in item
|
|
155
|
+
)
|
|
156
|
+
return {
|
|
157
|
+
"status": "error",
|
|
158
|
+
"content": [
|
|
159
|
+
{
|
|
160
|
+
"text": "❌ No agent specified. Provide agent_id or agent_arn directly."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"text": "\n**💡 If you just launched an agent:**\n"
|
|
164
|
+
"Extract `agent_id` from the previous agentcore_config() result and use it directly.\n"
|
|
165
|
+
"Example: If result had agent_id='cagatay_test_8-JMYhdpEgu9', then:\n"
|
|
166
|
+
"agentcore_invoke(agent_id='cagatay_test_8-JMYhdpEgu9', prompt='hello')"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"text": "\n**Available agents in your account:**\n"
|
|
170
|
+
+ agents_list
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
# Debug: print why listing failed
|
|
176
|
+
import traceback
|
|
177
|
+
|
|
178
|
+
error_detail = traceback.format_exc()
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
"status": "error",
|
|
182
|
+
"content": [
|
|
183
|
+
{
|
|
184
|
+
"text": "❌ No agent specified. Provide agent_id, agent_arn, or agent_name with valid config."
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"text": "\n**💡 If you just launched an agent:**\n"
|
|
188
|
+
"Extract `agent_id` from the previous agentcore_config() result.\n"
|
|
189
|
+
"Example: agentcore_invoke(agent_id='agent-xyz123', prompt='hello')"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"text": "\n**To see all agents:** agentcore_agents(action='list')"
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
with open(config_path) as f:
|
|
198
|
+
config = yaml.safe_load(f)
|
|
199
|
+
|
|
200
|
+
# Get agent ARN from config
|
|
201
|
+
if "agents" not in config or agent_name not in config["agents"]:
|
|
202
|
+
available_agents = list(config.get("agents", {}).keys())
|
|
203
|
+
if available_agents:
|
|
204
|
+
return {
|
|
205
|
+
"status": "error",
|
|
206
|
+
"content": [
|
|
207
|
+
{"text": f"Agent '{agent_name}' not found in config."},
|
|
208
|
+
{
|
|
209
|
+
"text": f"Available agents in config: {', '.join(available_agents)}"
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
else:
|
|
214
|
+
return {
|
|
215
|
+
"status": "error",
|
|
216
|
+
"content": [
|
|
217
|
+
{
|
|
218
|
+
"text": f"Agent '{agent_name}' not found in config (no agents configured)"
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
final_agent_arn = (
|
|
224
|
+
config["agents"][agent_name]
|
|
225
|
+
.get("bedrock_agentcore", {})
|
|
226
|
+
.get("agent_arn")
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if not final_agent_arn:
|
|
230
|
+
# Agent in config but not deployed - try to list all agents to see if it exists elsewhere
|
|
231
|
+
try:
|
|
232
|
+
import sys
|
|
233
|
+
from pathlib import Path
|
|
234
|
+
|
|
235
|
+
# Add tools directory to path if not already there
|
|
236
|
+
tools_dir = Path(__file__).parent
|
|
237
|
+
if str(tools_dir) not in sys.path:
|
|
238
|
+
sys.path.insert(0, str(tools_dir))
|
|
239
|
+
|
|
240
|
+
from agentcore_agents import agentcore_agents
|
|
241
|
+
|
|
242
|
+
agents_result = agentcore_agents(action="list", region=region)
|
|
243
|
+
|
|
244
|
+
if agents_result.get("status") == "success":
|
|
245
|
+
# Extract all text content from the result
|
|
246
|
+
agents_list = "\n".join(
|
|
247
|
+
item.get("text", "")
|
|
248
|
+
for item in agents_result.get("content", [])
|
|
249
|
+
if "text" in item
|
|
250
|
+
)
|
|
251
|
+
return {
|
|
252
|
+
"status": "error",
|
|
253
|
+
"content": [
|
|
254
|
+
{
|
|
255
|
+
"text": f"❌ Agent '{agent_name}' in config but not deployed."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"text": f"\n**💡 Deploy it:** agentcore_launch(agent_name='{agent_name}')"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"text": "\n**Or invoke existing agents by ID:**\n"
|
|
262
|
+
+ agents_list
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
}
|
|
266
|
+
except Exception:
|
|
267
|
+
pass # Fall back to simple error
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"status": "error",
|
|
271
|
+
"content": [
|
|
272
|
+
{
|
|
273
|
+
"text": f"❌ Agent '{agent_name}' not deployed. Run agentcore_launch()."
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Generate session ID if not provided
|
|
279
|
+
if not session_id:
|
|
280
|
+
session_id = str(uuid.uuid4())
|
|
281
|
+
|
|
282
|
+
# Configure boto3 client
|
|
283
|
+
boto_config = Config(
|
|
284
|
+
read_timeout=900,
|
|
285
|
+
connect_timeout=60,
|
|
286
|
+
retries={"max_attempts": 3},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
client = boto3.client(
|
|
290
|
+
"bedrock-agentcore", region_name=region, config=boto_config
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Build payload with optional parameters
|
|
294
|
+
payload_data = {"prompt": prompt, "mode": mode}
|
|
295
|
+
|
|
296
|
+
if tools:
|
|
297
|
+
payload_data["tools"] = tools
|
|
298
|
+
if model:
|
|
299
|
+
payload_data["model"] = model
|
|
300
|
+
if system_prompt:
|
|
301
|
+
payload_data["system_prompt"] = system_prompt
|
|
302
|
+
|
|
303
|
+
payload_json = json.dumps(payload_data)
|
|
304
|
+
|
|
305
|
+
# Invoke agent
|
|
306
|
+
response = client.invoke_agent_runtime(
|
|
307
|
+
agentRuntimeArn=final_agent_arn,
|
|
308
|
+
qualifier="DEFAULT",
|
|
309
|
+
runtimeSessionId=session_id,
|
|
310
|
+
payload=payload_json,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Process response
|
|
314
|
+
events = []
|
|
315
|
+
content_type = response.get("contentType", "")
|
|
316
|
+
|
|
317
|
+
if "text/event-stream" in content_type:
|
|
318
|
+
# Streaming response - process SSE events
|
|
319
|
+
for chunk in response.get("response", []):
|
|
320
|
+
# Decode bytes to string with error handling
|
|
321
|
+
if isinstance(chunk, bytes):
|
|
322
|
+
try:
|
|
323
|
+
chunk = chunk.decode("utf-8")
|
|
324
|
+
except UnicodeDecodeError:
|
|
325
|
+
# Skip malformed chunks
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Split SSE stream by delimiter to get individual events
|
|
329
|
+
if isinstance(chunk, str):
|
|
330
|
+
# Split by "\n\ndata: " to separate events
|
|
331
|
+
parts = chunk.split("\n\ndata: ")
|
|
332
|
+
# First part may have "data: " prefix
|
|
333
|
+
if parts and parts[0].startswith("data: "):
|
|
334
|
+
parts[0] = parts[0][6:] # Remove "data: " prefix
|
|
335
|
+
|
|
336
|
+
# Process each event
|
|
337
|
+
for event_str in parts:
|
|
338
|
+
if not event_str.strip():
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
# Parse JSON event
|
|
343
|
+
event = json.loads(event_str)
|
|
344
|
+
|
|
345
|
+
# Stream to callback handler if available
|
|
346
|
+
if (
|
|
347
|
+
agent
|
|
348
|
+
and hasattr(agent, "callback_handler")
|
|
349
|
+
and agent.callback_handler
|
|
350
|
+
):
|
|
351
|
+
if isinstance(event, dict):
|
|
352
|
+
# Extract text for display from any event type
|
|
353
|
+
text_to_display = None
|
|
354
|
+
|
|
355
|
+
# Check if this is a wrapped AgentCore event
|
|
356
|
+
if "event" in event and isinstance(
|
|
357
|
+
event["event"], dict
|
|
358
|
+
):
|
|
359
|
+
inner = event["event"]
|
|
360
|
+
# Extract text from contentBlockDelta
|
|
361
|
+
if "contentBlockDelta" in inner:
|
|
362
|
+
text_to_display = (
|
|
363
|
+
inner["contentBlockDelta"]
|
|
364
|
+
.get("delta", {})
|
|
365
|
+
.get("text", "")
|
|
366
|
+
)
|
|
367
|
+
# Pass the inner event
|
|
368
|
+
if text_to_display:
|
|
369
|
+
agent.callback_handler(
|
|
370
|
+
data=text_to_display, **inner
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
agent.callback_handler(**inner)
|
|
374
|
+
# Check if this is a local agent event with 'data' field
|
|
375
|
+
elif "data" in event:
|
|
376
|
+
text_to_display = event.get("data", "")
|
|
377
|
+
# Copy event and remove only non-serializable object references
|
|
378
|
+
filtered = event.copy()
|
|
379
|
+
for key in [
|
|
380
|
+
"agent",
|
|
381
|
+
"event_loop_cycle_trace",
|
|
382
|
+
"event_loop_cycle_span",
|
|
383
|
+
]:
|
|
384
|
+
filtered.pop(key, None)
|
|
385
|
+
agent.callback_handler(**filtered)
|
|
386
|
+
else:
|
|
387
|
+
# Pass other events as-is
|
|
388
|
+
agent.callback_handler(**event)
|
|
389
|
+
|
|
390
|
+
# Collect for response
|
|
391
|
+
events.append(event)
|
|
392
|
+
except (json.JSONDecodeError, ValueError):
|
|
393
|
+
# Skip non-JSON content
|
|
394
|
+
continue
|
|
395
|
+
else:
|
|
396
|
+
# Non-streaming response
|
|
397
|
+
for event in response.get("response", []):
|
|
398
|
+
if isinstance(event, bytes):
|
|
399
|
+
try:
|
|
400
|
+
events.append(event.decode("utf-8"))
|
|
401
|
+
except UnicodeDecodeError:
|
|
402
|
+
events.append(str(event))
|
|
403
|
+
else:
|
|
404
|
+
events.append(event)
|
|
405
|
+
|
|
406
|
+
# Format response
|
|
407
|
+
response_text = (
|
|
408
|
+
"\n".join(str(e) for e in events) if events else "No response content"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
"status": "success",
|
|
413
|
+
"content": [
|
|
414
|
+
{"text": f"**Agent ARN:** {final_agent_arn}"},
|
|
415
|
+
{"text": f"**Agent Response:**\n{response_text}"},
|
|
416
|
+
{"text": f"**Session ID:** {session_id}"},
|
|
417
|
+
{"text": f"**Mode:** {mode}"},
|
|
418
|
+
],
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|