agent-starter-pack 0.0.1b0__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 agent-starter-pack might be problematic. Click here for more details.

Files changed (162) hide show
  1. agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
  2. agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
  3. agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
  4. agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
  5. agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
  6. agents/agentic_rag_vertexai_search/README.md +22 -0
  7. agents/agentic_rag_vertexai_search/app/agent.py +145 -0
  8. agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
  9. agents/agentic_rag_vertexai_search/app/templates.py +53 -0
  10. agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  11. agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
  12. agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
  13. agents/crewai_coding_crew/README.md +34 -0
  14. agents/crewai_coding_crew/app/agent.py +86 -0
  15. agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
  16. agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
  17. agents/crewai_coding_crew/app/crew/crew.py +71 -0
  18. agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
  19. agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  20. agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
  21. agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
  22. agents/langgraph_base_react/README.md +9 -0
  23. agents/langgraph_base_react/app/agent.py +73 -0
  24. agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  25. agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
  26. agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
  27. agents/multimodal_live_api/README.md +50 -0
  28. agents/multimodal_live_api/app/agent.py +86 -0
  29. agents/multimodal_live_api/app/server.py +193 -0
  30. agents/multimodal_live_api/app/templates.py +51 -0
  31. agents/multimodal_live_api/app/vector_store.py +55 -0
  32. agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
  33. agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
  34. agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
  35. agents/multimodal_live_api/tests/unit/test_server.py +143 -0
  36. src/base_template/.gitignore +197 -0
  37. src/base_template/Makefile +37 -0
  38. src/base_template/README.md +91 -0
  39. src/base_template/app/utils/tracing.py +143 -0
  40. src/base_template/app/utils/typing.py +115 -0
  41. src/base_template/deployment/README.md +123 -0
  42. src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
  43. src/base_template/deployment/cd/staging.yaml +215 -0
  44. src/base_template/deployment/ci/pr_checks.yaml +51 -0
  45. src/base_template/deployment/terraform/apis.tf +34 -0
  46. src/base_template/deployment/terraform/build_triggers.tf +122 -0
  47. src/base_template/deployment/terraform/dev/apis.tf +42 -0
  48. src/base_template/deployment/terraform/dev/iam.tf +90 -0
  49. src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
  50. src/base_template/deployment/terraform/dev/providers.tf +29 -0
  51. src/base_template/deployment/terraform/dev/storage.tf +76 -0
  52. src/base_template/deployment/terraform/dev/variables.tf +126 -0
  53. src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
  54. src/base_template/deployment/terraform/iam.tf +130 -0
  55. src/base_template/deployment/terraform/locals.tf +50 -0
  56. src/base_template/deployment/terraform/log_sinks.tf +72 -0
  57. src/base_template/deployment/terraform/providers.tf +35 -0
  58. src/base_template/deployment/terraform/service_accounts.tf +42 -0
  59. src/base_template/deployment/terraform/storage.tf +100 -0
  60. src/base_template/deployment/terraform/variables.tf +202 -0
  61. src/base_template/deployment/terraform/vars/env.tfvars +43 -0
  62. src/base_template/pyproject.toml +113 -0
  63. src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
  64. src/cli/commands/create.py +534 -0
  65. src/cli/commands/setup_cicd.py +730 -0
  66. src/cli/main.py +35 -0
  67. src/cli/utils/__init__.py +35 -0
  68. src/cli/utils/cicd.py +662 -0
  69. src/cli/utils/gcp.py +120 -0
  70. src/cli/utils/logging.py +51 -0
  71. src/cli/utils/template.py +644 -0
  72. src/data_ingestion/README.md +79 -0
  73. src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
  74. src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
  75. src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
  76. src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
  77. src/data_ingestion/pyproject.toml +17 -0
  78. src/data_ingestion/uv.lock +999 -0
  79. src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
  80. src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
  81. src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
  82. src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
  83. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
  84. src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
  85. src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
  86. src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
  87. src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
  88. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
  89. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
  90. src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
  91. src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
  92. src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
  93. src/deployment_targets/cloud_run/Dockerfile +29 -0
  94. src/deployment_targets/cloud_run/app/server.py +128 -0
  95. src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
  96. src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
  97. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
  98. src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
  99. src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
  100. src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
  101. src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
  102. src/deployment_targets/cloud_run/uv.lock +6952 -0
  103. src/frontends/live_api_react/frontend/package-lock.json +19405 -0
  104. src/frontends/live_api_react/frontend/package.json +56 -0
  105. src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
  106. src/frontends/live_api_react/frontend/public/index.html +62 -0
  107. src/frontends/live_api_react/frontend/public/robots.txt +3 -0
  108. src/frontends/live_api_react/frontend/src/App.scss +189 -0
  109. src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
  110. src/frontends/live_api_react/frontend/src/App.tsx +205 -0
  111. src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
  112. src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
  113. src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
  114. src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
  115. src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
  116. src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
  117. src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
  118. src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
  119. src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
  120. src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
  121. src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
  122. src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
  123. src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
  124. src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
  125. src/frontends/live_api_react/frontend/src/index.css +28 -0
  126. src/frontends/live_api_react/frontend/src/index.tsx +35 -0
  127. src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
  128. src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
  129. src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
  130. src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
  131. src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
  132. src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
  133. src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
  134. src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
  135. src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
  136. src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
  137. src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
  138. src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
  139. src/frontends/live_api_react/frontend/tsconfig.json +25 -0
  140. src/frontends/streamlit/frontend/side_bar.py +213 -0
  141. src/frontends/streamlit/frontend/streamlit_app.py +263 -0
  142. src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
  143. src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
  144. src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
  145. src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
  146. src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
  147. src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
  148. src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
  149. src/resources/containers/data_processing/Dockerfile +25 -0
  150. src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
  151. src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
  152. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
  153. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
  154. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
  155. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
  156. src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
  157. src/resources/setup_cicd/cicd_variables.tf +36 -0
  158. src/resources/setup_cicd/github.tf +85 -0
  159. src/resources/setup_cicd/providers.tf +39 -0
  160. src/utils/generate_locks.py +135 -0
  161. src/utils/lock_utils.py +82 -0
  162. src/utils/watch_and_rebuild.py +190 -0
src/cli/utils/cicd.py ADDED
@@ -0,0 +1,662 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utilities for CI/CD setup and management."""
16
+
17
+ import json
18
+ import re
19
+ import subprocess
20
+ import time
21
+ from dataclasses import dataclass
22
+ from enum import Enum
23
+ from pathlib import Path
24
+
25
+ from rich.console import Console
26
+ from rich.prompt import IntPrompt, Prompt
27
+
28
+ console = Console()
29
+
30
+
31
+ def setup_git_provider(non_interactive: bool = False) -> str:
32
+ """Interactive selection of git provider."""
33
+ if non_interactive:
34
+ return "github" # Default to GitHub in non-interactive mode
35
+
36
+ console.print("\n> Git Provider Configuration", style="bold blue")
37
+ providers = [
38
+ ("github", "GitHub"),
39
+ # Add more providers here in the future
40
+ # ("gitlab", "GitLab (Coming soon)"),
41
+ # ("bitbucket", "Bitbucket (Coming soon)"),
42
+ ]
43
+
44
+ console.print("Available Git providers:")
45
+ for i, (id, name) in enumerate(providers, 1):
46
+ if id == "github":
47
+ console.print(f"{i}. {name}")
48
+ else:
49
+ console.print(f"{i}. {name}", style="dim")
50
+
51
+ choice = IntPrompt.ask("Select your Git provider", default=1)
52
+
53
+ git_provider = providers[choice - 1][0]
54
+ if git_provider != "github":
55
+ console.print("⚠️ Only GitHub is currently supported.", style="bold yellow")
56
+ raise ValueError("Unsupported git provider")
57
+
58
+ return git_provider
59
+
60
+
61
+ def setup_repository_name(
62
+ default_prefix: str = "genai-app", non_interactive: bool = False
63
+ ) -> tuple[str, str]:
64
+ """Interactive setup of repository name and owner."""
65
+ if non_interactive:
66
+ timestamp = int(time.time())
67
+ # Return empty string instead of None to match return type
68
+ return f"{default_prefix}-{timestamp}", ""
69
+
70
+ console.print("\n> Repository Configuration", style="bold blue")
71
+
72
+ # Get current GitHub username
73
+ result = run_command(["gh", "api", "user", "--jq", ".login"], capture_output=True)
74
+ github_username = result.stdout.strip()
75
+
76
+ # Get repository name
77
+ repo_name = Prompt.ask(
78
+ "Enter repository name", default=f"{default_prefix}-{int(time.time())}"
79
+ )
80
+
81
+ # Get repository owner (default to current user)
82
+ repo_owner = Prompt.ask(
83
+ "Enter repository owner (organization or username)", default=github_username
84
+ )
85
+
86
+ return repo_name, repo_owner
87
+
88
+
89
+ def create_github_connection(
90
+ project_id: str,
91
+ region: str,
92
+ connection_name: str,
93
+ repository_name: str,
94
+ repository_owner: str,
95
+ ) -> tuple[str, str]:
96
+ """Create and verify GitHub connection using gcloud command.
97
+
98
+ Args:
99
+ project_id: GCP project ID
100
+ region: GCP region
101
+ connection_name: Name for the GitHub connection
102
+ repository_name: Name of repository to create
103
+ repository_owner: Owner of repository
104
+
105
+ Returns:
106
+ tuple[str, str]: The OAuth token secret ID and the app installation ID
107
+ """
108
+ console.print("\n🔗 Creating GitHub connection...")
109
+
110
+ # Create repository if details provided
111
+ try:
112
+ # Check if repo exists
113
+ result = run_command(
114
+ [
115
+ "gh",
116
+ "repo",
117
+ "view",
118
+ f"{repository_owner}/{repository_name}",
119
+ "--json",
120
+ "name",
121
+ ],
122
+ capture_output=True,
123
+ check=False,
124
+ )
125
+
126
+ if result.returncode != 0:
127
+ # Repository doesn't exist, create it
128
+ console.print(
129
+ f"\n📦 Creating GitHub repository: {repository_owner}/{repository_name}"
130
+ )
131
+ run_command(
132
+ [
133
+ "gh",
134
+ "repo",
135
+ "create",
136
+ f"{repository_owner}/{repository_name}",
137
+ "--private",
138
+ "--description",
139
+ "Repository created by Terraform",
140
+ ]
141
+ )
142
+ console.print("✅ GitHub repository created")
143
+ else:
144
+ console.print("✅ Using existing GitHub repository")
145
+ except subprocess.CalledProcessError as e:
146
+ console.print(f"❌ Failed to create/check repository: {e!s}", style="bold red")
147
+ raise
148
+
149
+ def try_create_connection() -> subprocess.CompletedProcess[str]:
150
+ cmd = [
151
+ "gcloud",
152
+ "builds",
153
+ "connections",
154
+ "create",
155
+ "github",
156
+ connection_name,
157
+ f"--region={region}",
158
+ f"--project={project_id}",
159
+ ]
160
+
161
+ # Display the command being run
162
+ console.print(f"\n🔄 Running command: {' '.join(cmd)}")
163
+
164
+ # Use Popen to get control over stdin
165
+ process = subprocess.Popen(
166
+ cmd,
167
+ stdin=subprocess.PIPE,
168
+ stdout=subprocess.PIPE,
169
+ stderr=subprocess.PIPE,
170
+ text=True,
171
+ )
172
+
173
+ # Send 'y' followed by enter key to handle both the API enablement prompt and any other prompts
174
+ stdout, stderr = process.communicate(input="y\n")
175
+
176
+ # Create a CompletedProcess-like object for compatibility
177
+ return subprocess.CompletedProcess(
178
+ args=cmd, returncode=process.returncode, stdout=stdout, stderr=stderr
179
+ )
180
+
181
+ # Try initial connection creation
182
+ result = try_create_connection()
183
+
184
+ if result.returncode == 0:
185
+ console.print("✅ GitHub connection created successfully")
186
+ else:
187
+ stderr = str(result.stderr)
188
+ console.print(stderr)
189
+
190
+ if "ALREADY_EXISTS" in stderr:
191
+ console.print("✅ Using existing GitHub connection")
192
+ else:
193
+ console.print(
194
+ f"❌ Failed to create GitHub connection: {stderr}", style="bold red"
195
+ )
196
+ raise subprocess.CalledProcessError(
197
+ result.returncode, result.args, result.stdout, stderr
198
+ )
199
+
200
+ console.print("\n⚠️ Important:", style="bold yellow")
201
+ console.print(
202
+ "1. Please visit the URL below to authorize Cloud Build (if not already authorized)"
203
+ )
204
+ console.print("2. After authorization, the setup will continue automatically")
205
+ console.print("\nChecking connection status...")
206
+
207
+ # Poll for connection readiness
208
+ max_retries = 30 # 5 minutes total with 10s sleep
209
+ for attempt in range(max_retries):
210
+ try:
211
+ result = run_command(
212
+ [
213
+ "gcloud",
214
+ "builds",
215
+ "connections",
216
+ "describe",
217
+ connection_name,
218
+ f"--region={region}",
219
+ f"--project={project_id}",
220
+ "--format=json",
221
+ ],
222
+ capture_output=True,
223
+ )
224
+
225
+ status = json.loads(result.stdout).get("installationState", {}).get("stage")
226
+
227
+ if status == "COMPLETE":
228
+ console.print("✅ GitHub connection is authorized and ready")
229
+
230
+ # Get the secret version and app installation ID
231
+ connection_data = json.loads(result.stdout)
232
+ github_config = connection_data.get("githubConfig", {})
233
+
234
+ oauth_token_secret_version = github_config.get(
235
+ "authorizerCredential", {}
236
+ ).get("oauthTokenSecretVersion")
237
+ app_installation_id = github_config.get("appInstallationId")
238
+
239
+ if not oauth_token_secret_version or not app_installation_id:
240
+ raise ValueError(
241
+ "Could not find OAuth token secret version or app installation ID in connection details"
242
+ )
243
+
244
+ # Extract just the secret ID from the full path
245
+ # Format: "projects/PROJECT_ID/secrets/SECRET_ID/versions/VERSION"
246
+ secret_id = oauth_token_secret_version.split("/secrets/")[1].split(
247
+ "/versions/"
248
+ )[0]
249
+
250
+ console.print(f"✅ Retrieved OAuth token secret ID: {secret_id}")
251
+ console.print(
252
+ f"✅ Retrieved app installation ID: {app_installation_id}"
253
+ )
254
+
255
+ return secret_id, app_installation_id
256
+ elif status == "PENDING_USER_OAUTH" or status == "PENDING_INSTALL_APP":
257
+ if attempt < max_retries - 1: # Don't print waiting on last attempt
258
+ console.print("⏳ Waiting for GitHub authorization...")
259
+ time.sleep(10)
260
+ continue
261
+ else:
262
+ raise Exception(f"Unexpected connection status: {status}")
263
+
264
+ except subprocess.CalledProcessError as e:
265
+ console.print(
266
+ f"❌ Failed to check connection status: {e}", style="bold red"
267
+ )
268
+ raise
269
+
270
+ raise TimeoutError("GitHub connection authorization timed out after 5 minutes")
271
+
272
+
273
+ @dataclass
274
+ class ProjectConfig:
275
+ staging_project_id: str
276
+ prod_project_id: str
277
+ cicd_project_id: str
278
+ agent: str
279
+ deployment_target: str
280
+ region: str = "us-central1"
281
+ dev_project_id: str | None = None
282
+ project_name: str | None = None
283
+ repository_name: str | None = None
284
+ repository_owner: str | None = None
285
+ host_connection_name: str | None = None
286
+ github_pat: str | None = None
287
+ github_app_installation_id: str | None = None
288
+ git_provider: str = "github"
289
+
290
+
291
+ def print_cicd_summary(
292
+ config: ProjectConfig, github_username: str, repo_url: str, cloud_build_url: str
293
+ ) -> None:
294
+ """Print a summary of the CI/CD setup."""
295
+ console.print("\n🎉 CI/CD Infrastructure Setup Complete!", style="bold green")
296
+ console.print("====================================")
297
+ console.print("\n📊 Resource Summary:")
298
+ console.print(f" • Development Project: {config.dev_project_id}")
299
+ console.print(f" • Staging Project: {config.staging_project_id}")
300
+ console.print(f" • Production Project: {config.prod_project_id}")
301
+ console.print(f" • CICD Project: {config.cicd_project_id}")
302
+ console.print(f" • Repository: {config.repository_name}")
303
+ console.print(f" • Region: {config.region}")
304
+
305
+ console.print("\n🔗 Important Links:")
306
+ console.print(f" • GitHub Repository: {repo_url}")
307
+ console.print(f" • Cloud Build Console: {cloud_build_url}")
308
+
309
+ console.print("\n📝 Next Steps:", style="bold blue")
310
+ console.print("1. Push your code to the repository")
311
+ console.print("2. Create and merge a pull request to trigger CI/CD pipelines")
312
+ console.print("3. Monitor builds in the Cloud Build console")
313
+ console.print(
314
+ "4. After successful staging deployment, approve production deployment in Cloud Build"
315
+ )
316
+ console.print(
317
+ "\n🌟 Enjoy building your new Agent! Happy coding! 🚀", style="bold green"
318
+ )
319
+
320
+
321
+ def ensure_apis_enabled(project_id: str, apis: list[str]) -> None:
322
+ """Check and enable required APIs and set up necessary permissions.
323
+
324
+ Args:
325
+ project_id: GCP project ID where APIs should be enabled
326
+ apis: List of API service names to check and enable
327
+ """
328
+ console.print("\n🔍 Checking required APIs...")
329
+ for api in apis:
330
+ try:
331
+ # Check if API is enabled
332
+ result = run_command(
333
+ [
334
+ "gcloud",
335
+ "services",
336
+ "list",
337
+ f"--project={project_id}",
338
+ f"--filter=config.name:{api}",
339
+ "--format=json",
340
+ ],
341
+ capture_output=True,
342
+ )
343
+
344
+ services = json.loads(result.stdout)
345
+ if not services: # API not enabled
346
+ console.print(f"📡 Enabling {api}...")
347
+ run_command(
348
+ ["gcloud", "services", "enable", api, f"--project={project_id}"]
349
+ )
350
+ console.print(f"✅ Enabled {api}")
351
+ else:
352
+ console.print(f"✅ {api} already enabled")
353
+ except subprocess.CalledProcessError as e:
354
+ console.print(f"❌ Failed to check/enable {api}: {e!s}", style="bold red")
355
+ raise
356
+
357
+ # Get the Cloud Build service account
358
+ console.print("\n🔑 Setting up service account permissions...")
359
+ try:
360
+ result = run_command(
361
+ ["gcloud", "projects", "get-iam-policy", project_id, "--format=json"],
362
+ capture_output=True,
363
+ )
364
+
365
+ project_number = run_command(
366
+ [
367
+ "gcloud",
368
+ "projects",
369
+ "describe",
370
+ project_id,
371
+ "--format=value(projectNumber)",
372
+ ],
373
+ capture_output=True,
374
+ ).stdout.strip()
375
+
376
+ cloudbuild_sa = (
377
+ f"service-{project_number}@gcp-sa-cloudbuild.iam.gserviceaccount.com"
378
+ )
379
+
380
+ # Grant Secret Manager Admin role to Cloud Build service account
381
+ console.print(f"📦 Granting Secret Manager Admin role to {cloudbuild_sa}...")
382
+ run_command(
383
+ [
384
+ "gcloud",
385
+ "projects",
386
+ "add-iam-policy-binding",
387
+ project_id,
388
+ f"--member=serviceAccount:{cloudbuild_sa}",
389
+ "--role=roles/secretmanager.admin",
390
+ ]
391
+ )
392
+ console.print("✅ Permissions granted to Cloud Build service account")
393
+
394
+ except subprocess.CalledProcessError as e:
395
+ console.print(
396
+ f"❌ Failed to set up service account permissions: {e!s}", style="bold red"
397
+ )
398
+ raise
399
+
400
+ # Add a small delay to allow API enablement and IAM changes to propagate
401
+ time.sleep(10)
402
+
403
+
404
+ def run_command(
405
+ cmd: list[str] | str,
406
+ check: bool = True,
407
+ cwd: Path | None = None,
408
+ capture_output: bool = False,
409
+ shell: bool = False,
410
+ ) -> subprocess.CompletedProcess:
411
+ """Run a command and display it to the user"""
412
+ # Format command for display
413
+ cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd)
414
+ print(f"\n🔄 Running command: {cmd_str}")
415
+ if cwd:
416
+ print(f"📂 In directory: {cwd}")
417
+
418
+ # Run the command
419
+ result = subprocess.run(
420
+ cmd, check=check, cwd=cwd, capture_output=capture_output, text=True, shell=shell
421
+ )
422
+
423
+ # Display output if captured
424
+ if capture_output and result.stdout:
425
+ print(f"📤 Output:\n{result.stdout.strip()}")
426
+ if capture_output and result.stderr:
427
+ print(f"⚠️ Error output:\n{result.stderr}")
428
+
429
+ return result
430
+
431
+
432
+ def create_github_repository(repository_owner: str, repository_name: str) -> None:
433
+ """Create GitHub repository if it doesn't exist.
434
+
435
+ Args:
436
+ repository_owner: Owner of the repository
437
+ repository_name: Name of the repository to create
438
+ """
439
+ try:
440
+ # Check if repo exists
441
+ result = run_command(
442
+ [
443
+ "gh",
444
+ "repo",
445
+ "view",
446
+ f"{repository_owner}/{repository_name}",
447
+ "--json",
448
+ "name",
449
+ ],
450
+ capture_output=True,
451
+ check=False,
452
+ )
453
+
454
+ if result.returncode != 0:
455
+ # Repository doesn't exist, create it
456
+ console.print(
457
+ f"\n📦 Creating GitHub repository: {repository_owner}/{repository_name}"
458
+ )
459
+ run_command(
460
+ [
461
+ "gh",
462
+ "repo",
463
+ "create",
464
+ f"{repository_owner}/{repository_name}",
465
+ "--private",
466
+ "--description",
467
+ "Repository created by Terraform",
468
+ ]
469
+ )
470
+ console.print("✅ GitHub repository created")
471
+ else:
472
+ console.print("✅ Using existing GitHub repository")
473
+ except subprocess.CalledProcessError as e:
474
+ console.print(f"❌ Failed to create/check repository: {e!s}", style="bold red")
475
+ raise
476
+
477
+
478
+ class Environment(Enum):
479
+ DEV = "dev"
480
+ STAGING = "staging"
481
+ PROD = "prod"
482
+ CICD = "cicd"
483
+
484
+
485
+ class E2EDeployment:
486
+ def __init__(self, config: ProjectConfig) -> None:
487
+ self.config = config
488
+ self.projects: dict[Environment, Path] = {}
489
+
490
+ # Generate project name if not provided
491
+ if not self.config.project_name:
492
+ # Create a meaningful default project name based on agent and deployment target
493
+ prefix = f"{self.config.agent}-{self.config.deployment_target}"
494
+ # Clean up prefix to be filesystem compatible
495
+ prefix = re.sub(r"[^a-zA-Z0-9-]", "-", prefix.lower())
496
+ timestamp = int(time.time())
497
+ self.config.project_name = f"{prefix}-{timestamp}"
498
+
499
+ def update_terraform_vars(self, project_dir: Path, is_dev: bool = False) -> None:
500
+ """Update terraform variables with project configuration"""
501
+ if is_dev:
502
+ # Dev environment only needs one project ID
503
+ tf_vars_path = (
504
+ project_dir / "deployment" / "terraform" / "dev" / "vars" / "env.tfvars"
505
+ )
506
+
507
+ with open(tf_vars_path) as f:
508
+ content = f.read()
509
+
510
+ # Replace dev project ID
511
+ content = re.sub(
512
+ r'dev_project_id\s*=\s*"[^"]*"',
513
+ f'dev_project_id = "{self.config.dev_project_id}"',
514
+ content,
515
+ )
516
+ else:
517
+ # Path to production needs staging, prod, and CICD project IDs
518
+ tf_vars_path = (
519
+ project_dir / "deployment" / "terraform" / "vars" / "env.tfvars"
520
+ )
521
+
522
+ with open(tf_vars_path) as f:
523
+ content = f.read()
524
+
525
+ # Replace all project IDs
526
+ content = re.sub(
527
+ r'staging_project_id\s*=\s*"[^"]*"',
528
+ f'staging_project_id = "{self.config.staging_project_id}"',
529
+ content,
530
+ )
531
+ content = re.sub(
532
+ r'prod_project_id\s*=\s*"[^"]*"',
533
+ f'prod_project_id = "{self.config.prod_project_id}"',
534
+ content,
535
+ )
536
+ content = re.sub(
537
+ r'cicd_runner_project_id\s*=\s*"[^"]*"',
538
+ f'cicd_runner_project_id = "{self.config.cicd_project_id}"',
539
+ content,
540
+ )
541
+
542
+ # Add host connection and repository name
543
+ content = re.sub(
544
+ r'host_connection_name\s*=\s*"[^"]*"',
545
+ f'host_connection_name = "{self.config.host_connection_name}"',
546
+ content,
547
+ )
548
+ content = re.sub(
549
+ r'repository_name\s*=\s*"[^"]*"',
550
+ f'repository_name = "{self.config.repository_name}"',
551
+ content,
552
+ )
553
+
554
+ # Write updated content
555
+ with open(tf_vars_path, "w") as f:
556
+ f.write(content)
557
+
558
+ def setup_terraform_state(self, project_dir: Path, env: Environment) -> None:
559
+ """Setup terraform state configuration for dev or prod environment"""
560
+ # Determine terraform directories - we need both for full setup
561
+ tf_dirs = []
562
+ if env == Environment.DEV:
563
+ tf_dirs = [project_dir / "deployment" / "terraform" / "dev"]
564
+ else:
565
+ # For prod/staging, set up both root and dev terraform
566
+ tf_dirs = [
567
+ project_dir / "deployment" / "terraform",
568
+ project_dir / "deployment" / "terraform" / "dev",
569
+ ]
570
+
571
+ bucket_name = f"{self.config.cicd_project_id}-terraform-state"
572
+
573
+ # Ensure bucket exists and is accessible
574
+ try:
575
+ result = run_command(
576
+ ["gsutil", "ls", "-b", f"gs://{bucket_name}"],
577
+ check=False,
578
+ capture_output=True,
579
+ )
580
+
581
+ if result.returncode != 0:
582
+ print(f"\n📦 Creating Terraform state bucket: {bucket_name}")
583
+ run_command(
584
+ [
585
+ "gsutil",
586
+ "mb",
587
+ "-p",
588
+ self.config.cicd_project_id,
589
+ "-l",
590
+ self.config.region,
591
+ f"gs://{bucket_name}",
592
+ ]
593
+ )
594
+
595
+ run_command(
596
+ ["gsutil", "versioning", "set", "on", f"gs://{bucket_name}"]
597
+ )
598
+ except subprocess.CalledProcessError as e:
599
+ print(f"\n❌ Failed to setup state bucket: {e}")
600
+ raise
601
+
602
+ # Create backend.tf in each required directory
603
+ for tf_dir in tf_dirs:
604
+ # Use different state prefixes for dev and prod/staging to keep states separate
605
+ is_dev_dir = str(tf_dir).endswith("/dev")
606
+ state_prefix = "dev" if is_dev_dir else "prod"
607
+
608
+ backend_file = tf_dir / "backend.tf"
609
+ with open(backend_file, "w") as f:
610
+ f.write(f'''terraform {{
611
+ backend "gcs" {{
612
+ bucket = "{bucket_name}"
613
+ prefix = "{state_prefix}"
614
+ }}
615
+ }}
616
+ ''')
617
+ print(
618
+ f"\n✅ Terraform state configured in {tf_dir} to use bucket: {bucket_name} with prefix: {state_prefix}"
619
+ )
620
+
621
+ def setup_terraform(
622
+ self, project_dir: Path, env: Environment, local_state: bool = False
623
+ ) -> None:
624
+ """Initialize and apply Terraform for the given environment"""
625
+ print(f"\n🏗️ Setting up Terraform for {env.value} environment")
626
+
627
+ # Setup state configuration for all required directories if using remote state
628
+ if not local_state:
629
+ self.setup_terraform_state(project_dir, env)
630
+
631
+ # Determine which directories to process and their corresponding var files
632
+ tf_configs = []
633
+ if env == Environment.DEV:
634
+ tf_configs = [
635
+ (project_dir / "deployment" / "terraform" / "dev", "vars/env.tfvars")
636
+ ]
637
+ else:
638
+ # For prod/staging, we need both directories but with their own var files
639
+ tf_configs = [
640
+ (
641
+ project_dir / "deployment" / "terraform",
642
+ "vars/env.tfvars",
643
+ ), # Prod vars
644
+ (
645
+ project_dir / "deployment" / "terraform" / "dev",
646
+ "vars/env.tfvars",
647
+ ), # Dev vars
648
+ ]
649
+
650
+ # Initialize and apply Terraform for each directory
651
+ for tf_dir, var_file in tf_configs:
652
+ print(f"\n🔧 Initializing Terraform in {tf_dir}...")
653
+ if local_state:
654
+ run_command(["terraform", "init", "-backend=false"], cwd=tf_dir)
655
+ else:
656
+ run_command(["terraform", "init"], cwd=tf_dir)
657
+
658
+ print(f"\n🚀 Applying Terraform configuration in {tf_dir}...")
659
+ run_command(
660
+ ["terraform", "apply", f"-var-file={var_file}", "-auto-approve"],
661
+ cwd=tf_dir,
662
+ )