@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.
@@ -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()