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.
- agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
- agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
- agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
- agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
- agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
- agents/agentic_rag_vertexai_search/README.md +22 -0
- agents/agentic_rag_vertexai_search/app/agent.py +145 -0
- agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
- agents/agentic_rag_vertexai_search/app/templates.py +53 -0
- agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
- agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
- agents/crewai_coding_crew/README.md +34 -0
- agents/crewai_coding_crew/app/agent.py +86 -0
- agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
- agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
- agents/crewai_coding_crew/app/crew/crew.py +71 -0
- agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
- agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
- agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
- agents/langgraph_base_react/README.md +9 -0
- agents/langgraph_base_react/app/agent.py +73 -0
- agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
- agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
- agents/multimodal_live_api/README.md +50 -0
- agents/multimodal_live_api/app/agent.py +86 -0
- agents/multimodal_live_api/app/server.py +193 -0
- agents/multimodal_live_api/app/templates.py +51 -0
- agents/multimodal_live_api/app/vector_store.py +55 -0
- agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
- agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
- agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
- agents/multimodal_live_api/tests/unit/test_server.py +143 -0
- src/base_template/.gitignore +197 -0
- src/base_template/Makefile +37 -0
- src/base_template/README.md +91 -0
- src/base_template/app/utils/tracing.py +143 -0
- src/base_template/app/utils/typing.py +115 -0
- src/base_template/deployment/README.md +123 -0
- src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
- src/base_template/deployment/cd/staging.yaml +215 -0
- src/base_template/deployment/ci/pr_checks.yaml +51 -0
- src/base_template/deployment/terraform/apis.tf +34 -0
- src/base_template/deployment/terraform/build_triggers.tf +122 -0
- src/base_template/deployment/terraform/dev/apis.tf +42 -0
- src/base_template/deployment/terraform/dev/iam.tf +90 -0
- src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
- src/base_template/deployment/terraform/dev/providers.tf +29 -0
- src/base_template/deployment/terraform/dev/storage.tf +76 -0
- src/base_template/deployment/terraform/dev/variables.tf +126 -0
- src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
- src/base_template/deployment/terraform/iam.tf +130 -0
- src/base_template/deployment/terraform/locals.tf +50 -0
- src/base_template/deployment/terraform/log_sinks.tf +72 -0
- src/base_template/deployment/terraform/providers.tf +35 -0
- src/base_template/deployment/terraform/service_accounts.tf +42 -0
- src/base_template/deployment/terraform/storage.tf +100 -0
- src/base_template/deployment/terraform/variables.tf +202 -0
- src/base_template/deployment/terraform/vars/env.tfvars +43 -0
- src/base_template/pyproject.toml +113 -0
- src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
- src/cli/commands/create.py +534 -0
- src/cli/commands/setup_cicd.py +730 -0
- src/cli/main.py +35 -0
- src/cli/utils/__init__.py +35 -0
- src/cli/utils/cicd.py +662 -0
- src/cli/utils/gcp.py +120 -0
- src/cli/utils/logging.py +51 -0
- src/cli/utils/template.py +644 -0
- src/data_ingestion/README.md +79 -0
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
- src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
- src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
- src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
- src/data_ingestion/pyproject.toml +17 -0
- src/data_ingestion/uv.lock +999 -0
- src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
- src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
- src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
- src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
- src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
- src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
- src/deployment_targets/cloud_run/Dockerfile +29 -0
- src/deployment_targets/cloud_run/app/server.py +128 -0
- src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
- src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
- src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
- src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
- src/deployment_targets/cloud_run/uv.lock +6952 -0
- src/frontends/live_api_react/frontend/package-lock.json +19405 -0
- src/frontends/live_api_react/frontend/package.json +56 -0
- src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
- src/frontends/live_api_react/frontend/public/index.html +62 -0
- src/frontends/live_api_react/frontend/public/robots.txt +3 -0
- src/frontends/live_api_react/frontend/src/App.scss +189 -0
- src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
- src/frontends/live_api_react/frontend/src/App.tsx +205 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
- src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
- src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
- src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
- src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
- src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
- src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
- src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
- src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
- src/frontends/live_api_react/frontend/src/index.css +28 -0
- src/frontends/live_api_react/frontend/src/index.tsx +35 -0
- src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
- src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
- src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
- src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
- src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
- src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
- src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
- src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
- src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
- src/frontends/live_api_react/frontend/tsconfig.json +25 -0
- src/frontends/streamlit/frontend/side_bar.py +213 -0
- src/frontends/streamlit/frontend/streamlit_app.py +263 -0
- src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
- src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
- src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
- src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
- src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
- src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
- src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
- src/resources/containers/data_processing/Dockerfile +25 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
- src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
- src/resources/setup_cicd/cicd_variables.tf +36 -0
- src/resources/setup_cicd/github.tf +85 -0
- src/resources/setup_cicd/providers.tf +39 -0
- src/utils/generate_locks.py +135 -0
- src/utils/lock_utils.py +82 -0
- 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]
|