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
@@ -0,0 +1,644 @@
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
+ import logging
16
+ import os
17
+ import pathlib
18
+ import shutil
19
+ import tempfile
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ import yaml
24
+ from cookiecutter.main import cookiecutter
25
+
26
+
27
+ @dataclass
28
+ class TemplateConfig:
29
+ name: str
30
+ description: str
31
+ settings: dict[str, bool | list[str]]
32
+
33
+ @classmethod
34
+ def from_file(cls, config_path: pathlib.Path) -> "TemplateConfig":
35
+ """Load template config from file with validation"""
36
+ try:
37
+ with open(config_path) as f:
38
+ data = yaml.safe_load(f)
39
+
40
+ if not isinstance(data, dict):
41
+ raise ValueError(f"Invalid template config format in {config_path}")
42
+
43
+ required_fields = ["name", "description", "settings"]
44
+ missing_fields = [f for f in required_fields if f not in data]
45
+ if missing_fields:
46
+ raise ValueError(
47
+ f"Missing required fields in template config: {missing_fields}"
48
+ )
49
+
50
+ return cls(
51
+ name=data["name"],
52
+ description=data["description"],
53
+ settings=data["settings"],
54
+ )
55
+ except yaml.YAMLError as err:
56
+ raise ValueError(f"Invalid YAML in template config: {err}") from err
57
+ except Exception as err:
58
+ raise ValueError(f"Error loading template config: {err}") from err
59
+
60
+
61
+ OVERWRITE_FOLDERS = ["app", "frontend", "tests", "notebooks"]
62
+ TEMPLATE_CONFIG_FILE = ".templateconfig.yaml"
63
+ DEPLOYMENT_FOLDERS = ["cloud_run", "agent_engine"]
64
+ DEFAULT_FRONTEND = "streamlit"
65
+
66
+
67
+ def get_available_agents(deployment_target: str | None = None) -> dict:
68
+ """Dynamically load available agents from the agents directory.
69
+
70
+ Args:
71
+ deployment_target: Optional deployment target to filter agents
72
+ """
73
+ # Define priority agents that should appear first
74
+ PRIORITY_AGENTS = [
75
+ "langgraph_base_react" # Add other priority agents here as needed
76
+ ]
77
+
78
+ agents_list = []
79
+ priority_agents = []
80
+ agents_dir = pathlib.Path(__file__).parent.parent.parent.parent / "agents"
81
+
82
+ for agent_dir in agents_dir.iterdir():
83
+ if agent_dir.is_dir() and not agent_dir.name.startswith("__"):
84
+ template_config_path = agent_dir / "template" / ".templateconfig.yaml"
85
+ if template_config_path.exists():
86
+ try:
87
+ with open(template_config_path) as f:
88
+ config = yaml.safe_load(f)
89
+ agent_name = agent_dir.name
90
+
91
+ # Skip if deployment target specified and agent doesn't support it
92
+ if deployment_target:
93
+ targets = config.get("settings", {}).get(
94
+ "deployment_targets", []
95
+ )
96
+ if isinstance(targets, str):
97
+ targets = [targets]
98
+ if deployment_target not in targets:
99
+ continue
100
+
101
+ description = config.get("description", "No description available")
102
+ agent_info = {"name": agent_name, "description": description}
103
+
104
+ # Add to priority list or regular list based on agent name
105
+ if agent_name in PRIORITY_AGENTS:
106
+ priority_agents.append(agent_info)
107
+ else:
108
+ agents_list.append(agent_info)
109
+ except Exception as e:
110
+ logging.warning(f"Could not load agent from {agent_dir}: {e}")
111
+
112
+ # Only sort the non-priority agents
113
+ agents_list.sort(key=lambda x: x["name"])
114
+
115
+ # Combine priority agents with regular agents (no sorting of priority_agents)
116
+ combined_agents = priority_agents + agents_list
117
+
118
+ # Convert to numbered dictionary starting from 1
119
+ agents = {i + 1: agent for i, agent in enumerate(combined_agents)}
120
+
121
+ return agents
122
+
123
+
124
+ def load_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
125
+ """Read .templateconfig.yaml file to get agent configuration."""
126
+ config_file = template_dir / TEMPLATE_CONFIG_FILE
127
+ if not config_file.exists():
128
+ return {}
129
+
130
+ try:
131
+ with open(config_file) as f:
132
+ config = yaml.safe_load(f)
133
+ return config if config else {}
134
+ except Exception as e:
135
+ logging.error(f"Error loading template config: {e}")
136
+ return {}
137
+
138
+
139
+ def get_deployment_targets(agent_name: str) -> list:
140
+ """Get available deployment targets for the selected agent."""
141
+ template_path = (
142
+ pathlib.Path(__file__).parent.parent.parent.parent
143
+ / "agents"
144
+ / agent_name
145
+ / "template"
146
+ )
147
+ config = load_template_config(template_path)
148
+
149
+ if not config:
150
+ return []
151
+
152
+ targets = config.get("settings", {}).get("deployment_targets", [])
153
+ return targets if isinstance(targets, list) else [targets]
154
+
155
+
156
+ def prompt_deployment_target(agent_name: str) -> str:
157
+ """Ask user to select a deployment target for the agent."""
158
+ targets = get_deployment_targets(agent_name)
159
+
160
+ # Define deployment target friendly names and descriptions
161
+ TARGET_INFO = {
162
+ "agent_engine": {
163
+ "display_name": "Vertex AI Agent Engine",
164
+ "description": "Vertex AI Managed platform for scalable agent deployments",
165
+ },
166
+ "cloud_run": {
167
+ "display_name": "Cloud Run",
168
+ "description": "GCP Serverless container execution",
169
+ },
170
+ }
171
+
172
+ if not targets:
173
+ return ""
174
+
175
+ from rich.console import Console
176
+
177
+ console = Console()
178
+ console.print("\n> Please select a deployment target:")
179
+ for idx, target in enumerate(targets, 1):
180
+ info = TARGET_INFO.get(target, {})
181
+ display_name = info.get("display_name", target)
182
+ description = info.get("description", "")
183
+ console.print(f"{idx}. {display_name} - {description}")
184
+
185
+ from rich.prompt import IntPrompt
186
+
187
+ choice = IntPrompt.ask(
188
+ "\nEnter the number of your deployment target choice",
189
+ default=1,
190
+ show_default=True,
191
+ )
192
+ return targets[choice - 1]
193
+
194
+
195
+ def prompt_data_ingestion(agent_name: str) -> bool:
196
+ """Ask user if they want to include data pipeline if the agent supports it."""
197
+ template_path = (
198
+ pathlib.Path(__file__).parent.parent.parent.parent
199
+ / "agents"
200
+ / agent_name
201
+ / "template"
202
+ )
203
+ config = load_template_config(template_path)
204
+
205
+ if config:
206
+ # If requires_data_ingestion is true, return True without prompting
207
+ if config.get("settings", {}).get("requires_data_ingestion"):
208
+ return True
209
+
210
+ # Only prompt if the agent has optional data processing support
211
+ if "data_ingestion" in config.get("settings", {}):
212
+ from rich.prompt import Prompt
213
+
214
+ return (
215
+ Prompt.ask(
216
+ "\n> This agent supports a data pipeline. Would you like to include it?",
217
+ choices=["y", "n"],
218
+ default="n",
219
+ ).lower()
220
+ == "y"
221
+ )
222
+ return False
223
+
224
+
225
+ def get_template_path(agent_name: str, debug: bool = False) -> str:
226
+ """Get the absolute path to the agent template directory."""
227
+ current_dir = pathlib.Path(__file__).parent.parent.parent.parent
228
+ template_path = current_dir / "agents" / agent_name / "template"
229
+ if debug:
230
+ logging.debug(f"Looking for template in: {template_path}")
231
+ logging.debug(f"Template exists: {template_path.exists()}")
232
+ if template_path.exists():
233
+ logging.debug(f"Template contents: {list(template_path.iterdir())}")
234
+
235
+ if not template_path.exists():
236
+ raise ValueError(f"Template directory not found at {template_path}")
237
+
238
+ return str(template_path)
239
+
240
+
241
+ def copy_data_ingestion_files(project_template: pathlib.Path) -> None:
242
+ """Copy data processing files to the project template.
243
+
244
+ Args:
245
+ project_template: Path to the project template directory
246
+ """
247
+ data_ingestion_src = pathlib.Path(__file__).parent.parent.parent / "data_ingestion"
248
+ data_ingestion_dst = project_template / "data_ingestion"
249
+
250
+ if data_ingestion_src.exists():
251
+ logging.debug(
252
+ f"Copying data processing files from {data_ingestion_src} to {data_ingestion_dst}"
253
+ )
254
+ copy_files(data_ingestion_src, data_ingestion_dst, overwrite=True)
255
+ else:
256
+ logging.warning(
257
+ f"Data processing source directory not found at {data_ingestion_src}"
258
+ )
259
+
260
+
261
+ def process_template(
262
+ agent_name: str,
263
+ template_dir: str,
264
+ project_name: str,
265
+ deployment_target: str | None = None,
266
+ include_data_ingestion: bool = False,
267
+ output_dir: pathlib.Path | None = None,
268
+ ) -> None:
269
+ """Process the template directory and create a new project.
270
+
271
+ Args:
272
+ agent_name: Name of the agent template to use
273
+ template_dir: Directory containing the template files
274
+ project_name: Name of the project to create
275
+ deployment_target: Optional deployment target (agent_engine or cloud_run)
276
+ include_data_ingestion: Whether to include data pipeline components
277
+ output_dir: Optional output directory path, defaults to current directory
278
+ """
279
+ logging.debug(f"Processing template from {template_dir}")
280
+ logging.debug(f"Project name: {project_name}")
281
+ logging.debug(f"Include pipeline: {include_data_ingestion}")
282
+ logging.debug(f"Output directory: {output_dir}")
283
+
284
+ # Get paths
285
+ agent_path = pathlib.Path(template_dir).parent # Get parent of template dir
286
+ logging.debug(f"agent path: {agent_path}")
287
+ logging.debug(f"agent path exists: {agent_path.exists()}")
288
+ logging.debug(
289
+ f"agent path contents: {list(agent_path.iterdir()) if agent_path.exists() else 'N/A'}"
290
+ )
291
+
292
+ base_template_path = pathlib.Path(__file__).parent.parent.parent / "base_template"
293
+
294
+ # Use provided output_dir or current directory
295
+ destination_dir = output_dir if output_dir else pathlib.Path.cwd()
296
+
297
+ # Create output directory if it doesn't exist
298
+ if not destination_dir.exists():
299
+ destination_dir.mkdir(parents=True)
300
+
301
+ # Create a new temporary directory and use it as our working directory
302
+ with tempfile.TemporaryDirectory() as temp_dir:
303
+ temp_path = pathlib.Path(temp_dir)
304
+
305
+ # Important: Store the original working directory
306
+ original_dir = pathlib.Path.cwd()
307
+
308
+ try:
309
+ os.chdir(temp_path) # Change to temp directory
310
+
311
+ # Create the cookiecutter template structure
312
+ cookiecutter_template = temp_path / "template"
313
+ cookiecutter_template.mkdir(parents=True)
314
+ project_template = cookiecutter_template / "{{cookiecutter.project_name}}"
315
+ project_template.mkdir(parents=True)
316
+
317
+ # 1. First copy base template files
318
+ base_template_path = (
319
+ pathlib.Path(__file__).parent.parent.parent / "base_template"
320
+ )
321
+ copy_files(base_template_path, project_template, agent_name, overwrite=True)
322
+ logging.debug(f"1. Copied base template from {base_template_path}")
323
+
324
+ # 2. Process deployment target if specified
325
+ if deployment_target and deployment_target in DEPLOYMENT_FOLDERS:
326
+ deployment_path = (
327
+ pathlib.Path(__file__).parent.parent.parent
328
+ / "deployment_targets"
329
+ / deployment_target
330
+ )
331
+ if deployment_path.exists():
332
+ copy_files(
333
+ deployment_path,
334
+ project_template,
335
+ agent_name=agent_name,
336
+ overwrite=True,
337
+ )
338
+ logging.debug(
339
+ f"2. Processed deployment files for target: {deployment_target}"
340
+ )
341
+
342
+ # 3. Copy data ingestion files if needed
343
+ template_config = load_template_config(pathlib.Path(template_dir))
344
+ requires_data_ingestion = template_config.get("settings", {}).get(
345
+ "requires_data_ingestion", False
346
+ )
347
+ should_include_data_ingestion = include_data_ingestion or requires_data_ingestion
348
+
349
+ if should_include_data_ingestion:
350
+ logging.debug("3. Including data processing files")
351
+ copy_data_ingestion_files(project_template)
352
+
353
+ # 4. Process frontend files
354
+ frontend_type = template_config.get("settings", {}).get(
355
+ "frontend_type", DEFAULT_FRONTEND
356
+ )
357
+ copy_frontend_files(frontend_type, project_template)
358
+ logging.debug(f"4. Processed frontend files for type: {frontend_type}")
359
+
360
+ # 5. Finally, copy agent-specific files to override everything else
361
+ if agent_path.exists():
362
+ for folder in OVERWRITE_FOLDERS:
363
+ agent_folder = agent_path / folder
364
+ project_folder = project_template / folder
365
+ if agent_folder.exists():
366
+ logging.debug(f"5. Copying agent folder {folder} with override")
367
+ copy_files(
368
+ agent_folder, project_folder, agent_name, overwrite=True
369
+ )
370
+
371
+ # Copy agent README.md if it exists
372
+ agent_readme = agent_path / "README.md"
373
+ if agent_readme.exists():
374
+ agent_readme_dest = project_template / "agent_README.md"
375
+ shutil.copy2(agent_readme, agent_readme_dest)
376
+ logging.debug(
377
+ f"Copied agent README from {agent_readme} to {agent_readme_dest}"
378
+ )
379
+
380
+ # Load and validate template config first
381
+ template_path = pathlib.Path(template_dir)
382
+ config = load_template_config(template_path)
383
+ if not config:
384
+ raise ValueError(f"Could not load template config from {template_path}")
385
+
386
+ # Validate deployment target
387
+ available_targets = config.get("settings", {}).get("deployment_targets", [])
388
+ if isinstance(available_targets, str):
389
+ available_targets = [available_targets]
390
+
391
+ if deployment_target and deployment_target not in available_targets:
392
+ raise ValueError(
393
+ f"Invalid deployment target '{deployment_target}'. Available targets: {available_targets}"
394
+ )
395
+
396
+ # Load template config
397
+ template_config = load_template_config(pathlib.Path(template_dir))
398
+
399
+ # Check if data processing should be included
400
+ requires_data_ingestion = template_config.get("settings", {}).get(
401
+ "requires_data_ingestion", False
402
+ )
403
+ should_include_data_ingestion = include_data_ingestion or requires_data_ingestion
404
+
405
+ if should_include_data_ingestion:
406
+ logging.debug(
407
+ "Including data processing files based on template config or user request"
408
+ )
409
+ copy_data_ingestion_files(project_template)
410
+
411
+ # Create cookiecutter.json in the template root
412
+ # Process extra dependencies
413
+ extra_deps = template_config.get("settings", {}).get(
414
+ "extra_dependencies", []
415
+ )
416
+ otel_instrumentations = get_otel_instrumentations(dependencies=extra_deps)
417
+
418
+ # Get frontend type from template config
419
+ frontend_type = template_config.get("settings", {}).get(
420
+ "frontend_type", DEFAULT_FRONTEND
421
+ )
422
+
423
+ cookiecutter_config = {
424
+ "project_name": "my-project",
425
+ "agent_name": agent_name,
426
+ "agent_description": template_config.get("description", ""),
427
+ "deployment_target": deployment_target or "",
428
+ "frontend_type": frontend_type,
429
+ "extra_dependencies": [extra_deps],
430
+ "otel_instrumentations": otel_instrumentations,
431
+ "data_ingestion": should_include_data_ingestion,
432
+ "_copy_without_render": [
433
+ "*.ipynb", # Don't render notebooks
434
+ "*.json", # Don't render JSON files
435
+ "frontend/*", # Don't render frontend directory
436
+ "tests/*", # Don't render tests directory
437
+ "notebooks/*", # Don't render notebooks directory
438
+ ".git/*", # Don't render git directory
439
+ "__pycache__/*", # Don't render cache
440
+ "**/__pycache__/*",
441
+ ".pytest_cache/*",
442
+ ".venv/*",
443
+ "*templates.py", # Don't render templates files
444
+ "!*.py", # render Python files
445
+ "!Makefile", # DO render Makefile
446
+ "!README.md", # DO render README.md
447
+ ],
448
+ }
449
+
450
+ with open(cookiecutter_template / "cookiecutter.json", "w") as f:
451
+ import json
452
+
453
+ json.dump(cookiecutter_config, f, indent=4)
454
+
455
+ logging.debug(f"Template structure created at {cookiecutter_template}")
456
+ logging.debug(
457
+ f"Directory contents: {list(cookiecutter_template.iterdir())}"
458
+ )
459
+
460
+ # Process the template
461
+ cookiecutter(
462
+ str(cookiecutter_template),
463
+ no_input=True,
464
+ extra_context={
465
+ "project_name": project_name,
466
+ "agent_name": agent_name,
467
+ },
468
+ )
469
+ logging.debug("Template processing completed successfully")
470
+
471
+ # Move the generated project to the final destination
472
+ output_dir = temp_path / project_name
473
+ final_destination = destination_dir / project_name
474
+
475
+ logging.debug(f"Moving project from {output_dir} to {final_destination}")
476
+
477
+ if output_dir.exists():
478
+ if final_destination.exists():
479
+ shutil.rmtree(final_destination)
480
+ shutil.copytree(output_dir, final_destination, dirs_exist_ok=True)
481
+ logging.debug(f"Project successfully created at {final_destination}")
482
+
483
+ # After copying template files, handle the lock file
484
+ if deployment_target:
485
+ # Get the source lock file path
486
+ lock_path = (
487
+ pathlib.Path(__file__).parent.parent.parent.parent
488
+ / "src"
489
+ / "resources"
490
+ / "locks"
491
+ / f"uv-{agent_name}-{deployment_target}.lock"
492
+ )
493
+ logging.debug(f"Looking for lock file at: {lock_path}")
494
+ logging.debug(f"Lock file exists: {lock_path.exists()}")
495
+ if not lock_path.exists():
496
+ raise FileNotFoundError(f"Lock file not found: {lock_path}")
497
+ # Copy and rename to uv.lock in the project directory
498
+ shutil.copy2(lock_path, final_destination / "uv.lock")
499
+ logging.debug(
500
+ f"Copied lock file from {lock_path} to {final_destination}/uv.lock"
501
+ )
502
+
503
+ # Replace cookiecutter project name with actual project name in lock file
504
+ lock_file_path = final_destination / "uv.lock"
505
+ with open(lock_file_path, "r+", encoding="utf-8") as f:
506
+ content = f.read()
507
+ f.seek(0)
508
+ f.write(
509
+ content.replace(
510
+ "{{cookiecutter.project_name}}", project_name
511
+ )
512
+ )
513
+ f.truncate()
514
+ logging.debug(
515
+ f"Updated project name in lock file at {lock_file_path}"
516
+ )
517
+ else:
518
+ logging.error(f"Generated project directory not found at {output_dir}")
519
+ raise FileNotFoundError(
520
+ f"Generated project directory not found at {output_dir}"
521
+ )
522
+
523
+ except Exception as e:
524
+ logging.error(f"Failed to process template: {e!s}")
525
+ raise
526
+
527
+ finally:
528
+ # Always restore the original working directory
529
+ os.chdir(original_dir)
530
+
531
+
532
+ def should_exclude_path(path: pathlib.Path, agent_name: str) -> bool:
533
+ """Determine if a path should be excluded based on the agent type."""
534
+ if agent_name == "multimodal_live_api":
535
+ # Exclude the unit test utils folder and app/utils folder for multimodal_live_api
536
+ if "tests/unit/test_utils" in str(path) or "app/utils" in str(path):
537
+ logging.debug(f"Excluding path for multimodal_live_api: {path}")
538
+ return True
539
+ return False
540
+
541
+
542
+ def copy_files(
543
+ src: pathlib.Path,
544
+ dst: pathlib.Path,
545
+ agent_name: str | None = None,
546
+ overwrite: bool = False,
547
+ ) -> None:
548
+ """
549
+ Copy files with configurable behavior for exclusions and overwrites.
550
+
551
+ Args:
552
+ src: Source path
553
+ dst: Destination path
554
+ agent_name: Name of the agent (for agent-specific exclusions)
555
+ overwrite: Whether to overwrite existing files (True) or skip them (False)
556
+ """
557
+
558
+ def should_skip(path: pathlib.Path) -> bool:
559
+ """Determine if a file/directory should be skipped during copying."""
560
+ if path.suffix in [".pyc"]:
561
+ return True
562
+ if "__pycache__" in str(path) or path.name == "__pycache__":
563
+ return True
564
+ if agent_name is not None and should_exclude_path(path, agent_name):
565
+ return True
566
+ return False
567
+
568
+ if src.is_dir():
569
+ if not dst.exists():
570
+ dst.mkdir(parents=True)
571
+ for item in src.iterdir():
572
+ if should_skip(item):
573
+ logging.debug(f"Skipping file/directory: {item}")
574
+ continue
575
+ d = dst / item.name
576
+ if item.is_dir():
577
+ copy_files(item, d, agent_name, overwrite)
578
+ else:
579
+ if overwrite or not d.exists():
580
+ logging.debug(f"Copying file: {item} -> {d}")
581
+ shutil.copy2(item, d)
582
+ else:
583
+ logging.debug(f"Skipping existing file: {d}")
584
+ else:
585
+ if not should_skip(src):
586
+ if overwrite or not dst.exists():
587
+ shutil.copy2(src, dst)
588
+
589
+
590
+ def copy_frontend_files(frontend_type: str, project_template: pathlib.Path) -> None:
591
+ """Copy files from the specified frontend folder directly to project root."""
592
+ # Use default frontend if none specified
593
+ frontend_type = frontend_type or DEFAULT_FRONTEND
594
+
595
+ # Get the frontends directory path
596
+ frontends_path = (
597
+ pathlib.Path(__file__).parent.parent.parent / "frontends" / frontend_type
598
+ )
599
+
600
+ if frontends_path.exists():
601
+ logging.debug(f"Copying frontend files from {frontends_path}")
602
+ # Copy frontend files directly to project root instead of a nested frontend directory
603
+ copy_files(frontends_path, project_template, overwrite=True)
604
+ else:
605
+ logging.warning(f"Frontend type directory not found: {frontends_path}")
606
+ if frontend_type != DEFAULT_FRONTEND:
607
+ logging.info(f"Falling back to default frontend: {DEFAULT_FRONTEND}")
608
+ copy_frontend_files(DEFAULT_FRONTEND, project_template)
609
+
610
+
611
+ def copy_deployment_files(
612
+ deployment_target: str, agent_name: str, project_template: pathlib.Path
613
+ ) -> None:
614
+ """Copy files from the specified deployment target folder."""
615
+ if not deployment_target:
616
+ return
617
+
618
+ deployment_path = (
619
+ pathlib.Path(__file__).parent.parent.parent
620
+ / "deployment_targets"
621
+ / deployment_target
622
+ )
623
+
624
+ if deployment_path.exists():
625
+ logging.debug(f"Copying deployment files from {deployment_path}")
626
+ # Pass agent_name to respect agent-specific exclusions
627
+ copy_files(
628
+ deployment_path, project_template, agent_name=agent_name, overwrite=True
629
+ )
630
+ else:
631
+ logging.warning(f"Deployment target directory not found: {deployment_path}")
632
+
633
+
634
+ def get_otel_instrumentations(dependencies: list) -> list[list[str]]:
635
+ """Returns OpenTelemetry instrumentation statements for enabled dependencies."""
636
+ otel_deps = {
637
+ "langgraph": "Instruments.LANGCHAIN",
638
+ "crewai": "Instruments.CREW",
639
+ }
640
+ imports = []
641
+ for dep in dependencies:
642
+ if any(otel_dep in dep for otel_dep in otel_deps):
643
+ imports.append(otel_deps[next(key for key in otel_deps if key in dep)])
644
+ return [imports]