@intentsolutionsio/jeremy-adk-orchestrator 2.1.0
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.
- package/.claude-plugin/plugin.json +23 -0
- package/LICENSE +21 -0
- package/README.md +776 -0
- package/agents/a2a-protocol-manager.md +411 -0
- package/package.json +44 -0
- package/skills/adk-deployment-specialist/SKILL.md +54 -0
- package/skills/adk-deployment-specialist/references/ARD.md +71 -0
- package/skills/adk-deployment-specialist/references/PRD.md +67 -0
- package/skills/adk-deployment-specialist/references/errors.md +106 -0
- package/skills/adk-deployment-specialist/references/examples.md +89 -0
- package/skills/adk-deployment-specialist/references/how-it-works.md +191 -0
- package/skills/adk-deployment-specialist/references/workflow-examples.md +167 -0
- package/skills/adk-deployment-specialist/scripts/deploy-agent.sh +157 -0
- package/skills/adk-deployment-specialist/scripts/test-a2a-protocol.py +277 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# deploy-agent.sh - Deploy ADK agent to Vertex AI Agent Engine
|
|
3
|
+
# Uses the Python SDK (vertexai.Client) since there is no gcloud CLI for Agent Engine.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Colors
|
|
8
|
+
GREEN='\033[0;32m'
|
|
9
|
+
YELLOW='\033[1;33m'
|
|
10
|
+
RED='\033[0;31m'
|
|
11
|
+
NC='\033[0m'
|
|
12
|
+
|
|
13
|
+
# Configuration
|
|
14
|
+
AGENT_DIR="${1:-}"
|
|
15
|
+
PROJECT_ID="${2:-${GCP_PROJECT_ID:-}}"
|
|
16
|
+
REGION="${3:-us-central1}"
|
|
17
|
+
DISPLAY_NAME="${4:-}"
|
|
18
|
+
|
|
19
|
+
usage() {
|
|
20
|
+
cat <<EOF
|
|
21
|
+
Usage: $0 <AGENT_DIR> [PROJECT_ID] [REGION] [DISPLAY_NAME]
|
|
22
|
+
|
|
23
|
+
Deploy ADK agent to Vertex AI Agent Engine using the Python SDK.
|
|
24
|
+
|
|
25
|
+
NOTE: There is no gcloud CLI for Agent Engine. This script uses the
|
|
26
|
+
vertexai Python SDK (vertexai.Client.agent_engines.create).
|
|
27
|
+
|
|
28
|
+
Arguments:
|
|
29
|
+
AGENT_DIR Directory containing agent code (must have agent.py)
|
|
30
|
+
PROJECT_ID GCP project ID (default: \$GCP_PROJECT_ID)
|
|
31
|
+
REGION GCP region (default: us-central1)
|
|
32
|
+
DISPLAY_NAME Agent display name (default: directory name)
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
$0 ./my-agent my-project us-central1 my-agent
|
|
36
|
+
GCP_PROJECT_ID=my-project $0 ./agent
|
|
37
|
+
|
|
38
|
+
Requirements:
|
|
39
|
+
pip install google-adk>=1.15.1 google-cloud-aiplatform>=1.120.0
|
|
40
|
+
|
|
41
|
+
EOF
|
|
42
|
+
exit 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if [[ -z "$AGENT_DIR" ]]; then
|
|
46
|
+
echo "Error: AGENT_DIR is required"
|
|
47
|
+
usage
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [[ ! -d "$AGENT_DIR" ]]; then
|
|
51
|
+
echo -e "${RED}Error: Agent directory not found: $AGENT_DIR${NC}"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [[ -z "$PROJECT_ID" ]]; then
|
|
56
|
+
echo "Error: PROJECT_ID is required (set GCP_PROJECT_ID env var or provide as argument)"
|
|
57
|
+
usage
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Default display name to directory basename
|
|
61
|
+
if [[ -z "$DISPLAY_NAME" ]]; then
|
|
62
|
+
DISPLAY_NAME=$(basename "$AGENT_DIR")
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo -e "${GREEN}Deploying ADK Agent to Vertex AI Agent Engine${NC}"
|
|
66
|
+
echo "Agent Dir: $AGENT_DIR"
|
|
67
|
+
echo "Project: $PROJECT_ID"
|
|
68
|
+
echo "Region: $REGION"
|
|
69
|
+
echo "Display Name: $DISPLAY_NAME"
|
|
70
|
+
echo ""
|
|
71
|
+
|
|
72
|
+
# Check Python SDK is installed
|
|
73
|
+
if ! python3 -c "import vertexai" 2>/dev/null; then
|
|
74
|
+
echo -e "${YELLOW}Vertex AI SDK not found. Installing...${NC}"
|
|
75
|
+
pip install google-cloud-aiplatform[agent_engines]>=1.120.0 google-adk>=1.15.1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Validate agent files
|
|
79
|
+
echo "Validating agent directory..."
|
|
80
|
+
if [[ ! -f "$AGENT_DIR/agent.py" ]]; then
|
|
81
|
+
echo -e "${RED}Error: agent.py not found in $AGENT_DIR${NC}"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if ! python3 -m py_compile "$AGENT_DIR/agent.py"; then
|
|
86
|
+
echo -e "${RED}Error: agent.py has syntax errors${NC}"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
echo -e "${GREEN}Agent files valid${NC}"
|
|
90
|
+
|
|
91
|
+
# Check for requirements.txt
|
|
92
|
+
REQUIREMENTS_FILE="$AGENT_DIR/requirements.txt"
|
|
93
|
+
if [[ ! -f "$REQUIREMENTS_FILE" ]]; then
|
|
94
|
+
echo -e "${YELLOW}Warning: No requirements.txt found. Using default ADK dependencies.${NC}"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Deploy using Python SDK
|
|
98
|
+
echo ""
|
|
99
|
+
echo "Deploying agent via vertexai.Client.agent_engines.create()..."
|
|
100
|
+
echo ""
|
|
101
|
+
|
|
102
|
+
python3 -c "
|
|
103
|
+
import sys
|
|
104
|
+
sys.path.insert(0, '${AGENT_DIR}')
|
|
105
|
+
|
|
106
|
+
import vertexai
|
|
107
|
+
|
|
108
|
+
# Import the agent from the agent directory
|
|
109
|
+
from agent import root_agent
|
|
110
|
+
|
|
111
|
+
# Initialize client
|
|
112
|
+
client = vertexai.Client(project='${PROJECT_ID}', location='${REGION}')
|
|
113
|
+
|
|
114
|
+
# Read requirements if available
|
|
115
|
+
requirements = ['google-adk>=1.15.1']
|
|
116
|
+
try:
|
|
117
|
+
with open('${REQUIREMENTS_FILE}', 'r') as f:
|
|
118
|
+
requirements = [
|
|
119
|
+
line.strip() for line in f
|
|
120
|
+
if line.strip() and not line.startswith('#')
|
|
121
|
+
]
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
# Deploy to Agent Engine
|
|
126
|
+
print('Creating reasoning engine...')
|
|
127
|
+
remote_agent = client.agent_engines.create(
|
|
128
|
+
agent_engine=root_agent,
|
|
129
|
+
requirements=requirements,
|
|
130
|
+
display_name='${DISPLAY_NAME}',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
print()
|
|
134
|
+
print('Agent deployed successfully!')
|
|
135
|
+
print(f'Resource Name: {remote_agent.resource_name}')
|
|
136
|
+
print()
|
|
137
|
+
print('To query this agent:')
|
|
138
|
+
print(f\" python3 -c \\\"import vertexai; c = vertexai.Client(project='{PROJECT_ID}', location='{REGION}'); a = c.agent_engines.get(name='{{}}'.format(remote_agent.resource_name)); print(a.query(input='Hello'))\\\"\")
|
|
139
|
+
print()
|
|
140
|
+
print('To list all agents:')
|
|
141
|
+
print(f\" python3 -c \\\"import vertexai; c = vertexai.Client(project='{PROJECT_ID}', location='{REGION}'); [print(a.display_name, a.resource_name) for a in c.agent_engines.list()]\\\"\")
|
|
142
|
+
"
|
|
143
|
+
|
|
144
|
+
DEPLOY_STATUS=$?
|
|
145
|
+
|
|
146
|
+
if [[ $DEPLOY_STATUS -eq 0 ]]; then
|
|
147
|
+
echo ""
|
|
148
|
+
echo -e "${GREEN}Deployment complete${NC}"
|
|
149
|
+
else
|
|
150
|
+
echo ""
|
|
151
|
+
echo -e "${RED}Deployment failed (exit code: $DEPLOY_STATUS)${NC}"
|
|
152
|
+
echo "Check logs for details. Common issues:"
|
|
153
|
+
echo " - Missing IAM roles (need roles/aiplatform.admin)"
|
|
154
|
+
echo " - Quota exceeded (default: 10 reasoning engines per project)"
|
|
155
|
+
echo " - Invalid agent definition (check tool function signatures)"
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
test-a2a-protocol.py - Test A2A protocol endpoints for deployed ADK agent
|
|
4
|
+
|
|
5
|
+
Tests:
|
|
6
|
+
- AgentCard retrieval
|
|
7
|
+
- Task submission
|
|
8
|
+
- Status polling
|
|
9
|
+
- Protocol compliance
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
import subprocess
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_access_token() -> str:
|
|
20
|
+
"""Get GCP access token"""
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["gcloud", "auth", "print-access-token"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True
|
|
25
|
+
)
|
|
26
|
+
return result.stdout.strip()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_agent_card(agent_url: str, token: str) -> Dict:
|
|
30
|
+
"""Test AgentCard endpoint"""
|
|
31
|
+
import urllib.request
|
|
32
|
+
import urllib.error
|
|
33
|
+
|
|
34
|
+
print("Testing AgentCard endpoint...")
|
|
35
|
+
agent_card_url = f"{agent_url}/.well-known/agent-card"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
req = urllib.request.Request(
|
|
39
|
+
agent_card_url,
|
|
40
|
+
headers={"Authorization": f"Bearer {token}"}
|
|
41
|
+
)
|
|
42
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
43
|
+
agent_card = json.loads(response.read().decode())
|
|
44
|
+
|
|
45
|
+
print("✓ AgentCard retrieved successfully")
|
|
46
|
+
print(f" Name: {agent_card.get('name', 'unknown')}")
|
|
47
|
+
print(f" Description: {agent_card.get('description', 'unknown')}")
|
|
48
|
+
print(f" Version: {agent_card.get('version', 'unknown')}")
|
|
49
|
+
|
|
50
|
+
# Validate required fields
|
|
51
|
+
required_fields = ["name", "description", "capabilities"]
|
|
52
|
+
missing = [f for f in required_fields if f not in agent_card]
|
|
53
|
+
|
|
54
|
+
if missing:
|
|
55
|
+
print(f"⚠ Missing required fields: {', '.join(missing)}")
|
|
56
|
+
return {"status": "partial", "card": agent_card, "missing": missing}
|
|
57
|
+
|
|
58
|
+
print("✓ All required fields present")
|
|
59
|
+
return {"status": "success", "card": agent_card}
|
|
60
|
+
|
|
61
|
+
except urllib.error.HTTPError as e:
|
|
62
|
+
print(f"✗ AgentCard request failed: {e.code} {e.reason}")
|
|
63
|
+
return {"status": "failed", "error": str(e)}
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"✗ Error retrieving AgentCard: {e}")
|
|
66
|
+
return {"status": "failed", "error": str(e)}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_task_submission(agent_url: str, token: str, message: str) -> Optional[str]:
|
|
70
|
+
"""Test task submission"""
|
|
71
|
+
import urllib.request
|
|
72
|
+
import urllib.error
|
|
73
|
+
|
|
74
|
+
print("\nTesting Task Submission API (A2A JSON-RPC)...")
|
|
75
|
+
task_url = agent_url # A2A uses a single endpoint with JSON-RPC methods
|
|
76
|
+
|
|
77
|
+
payload = {
|
|
78
|
+
"jsonrpc": "2.0",
|
|
79
|
+
"method": "tasks/send",
|
|
80
|
+
"params": {
|
|
81
|
+
"id": f"test-task-{int(time.time())}",
|
|
82
|
+
"message": {
|
|
83
|
+
"role": "user",
|
|
84
|
+
"parts": [{"text": message}],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"id": f"req-{int(time.time())}",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
req = urllib.request.Request(
|
|
92
|
+
task_url,
|
|
93
|
+
data=json.dumps(payload).encode(),
|
|
94
|
+
headers={
|
|
95
|
+
"Authorization": f"Bearer {token}",
|
|
96
|
+
"Content-Type": "application/json"
|
|
97
|
+
},
|
|
98
|
+
method="POST"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
102
|
+
result = json.loads(response.read().decode())
|
|
103
|
+
|
|
104
|
+
task_id = result.get("task_id")
|
|
105
|
+
print(f"✓ Task submitted successfully")
|
|
106
|
+
print(f" Task ID: {task_id}")
|
|
107
|
+
print(f" Status: {result.get('status', 'unknown')}")
|
|
108
|
+
|
|
109
|
+
return task_id
|
|
110
|
+
|
|
111
|
+
except urllib.error.HTTPError as e:
|
|
112
|
+
print(f"✗ Task submission failed: {e.code} {e.reason}")
|
|
113
|
+
try:
|
|
114
|
+
error_body = e.read().decode()
|
|
115
|
+
print(f" Error details: {error_body}")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
return None
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"✗ Error submitting task: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_task_status(agent_url: str, token: str, task_id: str, max_wait: int = 60) -> bool:
|
|
125
|
+
"""Test task status polling"""
|
|
126
|
+
import urllib.request
|
|
127
|
+
import urllib.error
|
|
128
|
+
|
|
129
|
+
print("\nTesting Task Status API (A2A JSON-RPC tasks/get)...")
|
|
130
|
+
status_url = agent_url # A2A uses single endpoint with JSON-RPC methods
|
|
131
|
+
|
|
132
|
+
start_time = time.time()
|
|
133
|
+
|
|
134
|
+
while time.time() - start_time < max_wait:
|
|
135
|
+
try:
|
|
136
|
+
status_payload = json.dumps({
|
|
137
|
+
"jsonrpc": "2.0",
|
|
138
|
+
"method": "tasks/get",
|
|
139
|
+
"params": {"id": task_id},
|
|
140
|
+
"id": f"status-{int(time.time())}",
|
|
141
|
+
}).encode()
|
|
142
|
+
|
|
143
|
+
req = urllib.request.Request(
|
|
144
|
+
status_url,
|
|
145
|
+
data=status_payload,
|
|
146
|
+
headers={
|
|
147
|
+
"Authorization": f"Bearer {token}",
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
method="POST",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
154
|
+
result = json.loads(response.read().decode())
|
|
155
|
+
|
|
156
|
+
# A2A task statuses: submitted, working, input-required, completed, failed, canceled
|
|
157
|
+
task_result = result.get("result", {})
|
|
158
|
+
status = task_result.get("status", {}).get("state", "unknown")
|
|
159
|
+
|
|
160
|
+
print(f" Status: {status}")
|
|
161
|
+
|
|
162
|
+
if status == "completed":
|
|
163
|
+
print("Task completed successfully")
|
|
164
|
+
artifacts = task_result.get("artifacts", [])
|
|
165
|
+
if artifacts:
|
|
166
|
+
first_part = artifacts[0].get("parts", [{}])[0].get("text", "")
|
|
167
|
+
if first_part:
|
|
168
|
+
print(f" Response: {first_part[:100]}...")
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
elif status == "failed":
|
|
172
|
+
error_msg = task_result.get("status", {}).get("message", "unknown error")
|
|
173
|
+
print(f"Task failed: {error_msg}")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
elif status in ["submitted", "working"]:
|
|
177
|
+
time.sleep(5)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
else:
|
|
181
|
+
print(f"Unknown status: {status}")
|
|
182
|
+
time.sleep(5)
|
|
183
|
+
|
|
184
|
+
except urllib.error.HTTPError as e:
|
|
185
|
+
print(f"✗ Status check failed: {e.code} {e.reason}")
|
|
186
|
+
return False
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"✗ Error checking status: {e}")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
print(f"⚠ Timeout waiting for task completion ({max_wait}s)")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_protocol_tests(agent_url: str, test_message: str = "Hello, test message"):
|
|
196
|
+
"""Run all A2A protocol tests"""
|
|
197
|
+
print("=" * 70)
|
|
198
|
+
print("A2A Protocol Compliance Test")
|
|
199
|
+
print("=" * 70)
|
|
200
|
+
print(f"Agent URL: {agent_url}")
|
|
201
|
+
print(f"Test Message: {test_message}")
|
|
202
|
+
print("=" * 70)
|
|
203
|
+
print()
|
|
204
|
+
|
|
205
|
+
# Get access token
|
|
206
|
+
token = get_access_token()
|
|
207
|
+
if not token:
|
|
208
|
+
print("✗ Failed to get access token")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
# Test 1: AgentCard
|
|
212
|
+
card_result = test_agent_card(agent_url, token)
|
|
213
|
+
|
|
214
|
+
# Test 2: Task Submission
|
|
215
|
+
task_id = test_task_submission(agent_url, token, test_message)
|
|
216
|
+
|
|
217
|
+
# Test 3: Status Polling (if task was submitted)
|
|
218
|
+
status_success = False
|
|
219
|
+
if task_id:
|
|
220
|
+
status_success = test_task_status(agent_url, token, task_id)
|
|
221
|
+
|
|
222
|
+
# Summary
|
|
223
|
+
print("\n" + "=" * 70)
|
|
224
|
+
print("Test Summary")
|
|
225
|
+
print("=" * 70)
|
|
226
|
+
|
|
227
|
+
tests_passed = 0
|
|
228
|
+
tests_total = 3
|
|
229
|
+
|
|
230
|
+
if card_result.get("status") == "success":
|
|
231
|
+
print("✓ AgentCard Test: PASSED")
|
|
232
|
+
tests_passed += 1
|
|
233
|
+
else:
|
|
234
|
+
print("✗ AgentCard Test: FAILED")
|
|
235
|
+
|
|
236
|
+
if task_id:
|
|
237
|
+
print("✓ Task Submission Test: PASSED")
|
|
238
|
+
tests_passed += 1
|
|
239
|
+
else:
|
|
240
|
+
print("✗ Task Submission Test: FAILED")
|
|
241
|
+
|
|
242
|
+
if status_success:
|
|
243
|
+
print("✓ Task Status Test: PASSED")
|
|
244
|
+
tests_passed += 1
|
|
245
|
+
else:
|
|
246
|
+
print("✗ Task Status Test: FAILED")
|
|
247
|
+
|
|
248
|
+
print(f"\nResult: {tests_passed}/{tests_total} tests passed")
|
|
249
|
+
|
|
250
|
+
if tests_passed == tests_total:
|
|
251
|
+
print("🟢 A2A Protocol: COMPLIANT")
|
|
252
|
+
sys.exit(0)
|
|
253
|
+
elif tests_passed >= 2:
|
|
254
|
+
print("🟡 A2A Protocol: PARTIALLY COMPLIANT")
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
else:
|
|
257
|
+
print("🔴 A2A Protocol: NOT COMPLIANT")
|
|
258
|
+
sys.exit(2)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main():
|
|
262
|
+
if len(sys.argv) < 2:
|
|
263
|
+
print("Usage: test-a2a-protocol.py <AGENT_URL> [TEST_MESSAGE]")
|
|
264
|
+
print("\nTest A2A protocol compliance for deployed ADK agent")
|
|
265
|
+
print("\nExample:")
|
|
266
|
+
print(" test-a2a-protocol.py https://my-agent-xyz.run.app")
|
|
267
|
+
print(" test-a2a-protocol.py https://my-agent-xyz.run.app 'Deploy a GKE cluster'")
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
|
|
270
|
+
agent_url = sys.argv[1].rstrip("/")
|
|
271
|
+
test_message = sys.argv[2] if len(sys.argv) > 2 else "Hello, this is a test message"
|
|
272
|
+
|
|
273
|
+
run_protocol_tests(agent_url, test_message)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
main()
|