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,423 +0,0 @@
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
- print("\n")
411
-
412
- return {
413
- "status": "success",
414
- "content": [
415
- {"text": f"**Agent ARN:** {final_agent_arn}"},
416
- {"text": f"**Agent Response:**\n{response_text}"},
417
- {"text": f"**Session ID:** {session_id}"},
418
- {"text": f"**Mode:** {mode}"},
419
- ],
420
- }
421
-
422
- except Exception as e:
423
- return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}