Skip to content

Commit 1303b7a

Browse files
authored
feat: add support for flat structure remote templates (#676)
* feat: add support for flat structure remote templates Add support for remote templates where agent code is in the root directory rather than a subdirectory (e.g., adk-python samples). - Add _detect_flat_structure() to auto-detect flat templates - Update _infer_agent_directory_for_adk() to handle flat structures - Add copy_flat_structure_agent_files() for proper file placement - Allow -dir . flag to explicitly indicate flat structure - Derive target directory name from template folder name This enables using templates like: https://github.com/google/adk-python/tree/main/contributing/samples/bigquery
1 parent eadb1b4 commit 1303b7a

File tree

5 files changed

+726
-13
lines changed

5 files changed

+726
-13
lines changed

agent_starter_pack/cli/utils/remote_template.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,40 @@ def fetch_remote_template(
364364
) from e
365365

366366

367+
def _detect_flat_structure(template_dir: pathlib.Path) -> bool:
368+
"""Detect if template has a flat structure (agent code in root).
369+
370+
A flat structure means:
371+
- agent.py exists directly in the template root
372+
- No obvious agent subdirectory exists (like 'app/' or folder_name as package)
373+
374+
Args:
375+
template_dir: Path to template directory
376+
377+
Returns:
378+
True if template has flat structure, False otherwise
379+
"""
380+
# Check if agent.py exists in root
381+
agent_py_in_root = (template_dir / "agent.py").exists()
382+
if not agent_py_in_root:
383+
return False
384+
385+
# Check for common agent subdirectory patterns
386+
folder_name = template_dir.name.replace("-", "_")
387+
potential_agent_dirs = ["app", folder_name]
388+
389+
for subdir in potential_agent_dirs:
390+
subdir_path = template_dir / subdir
391+
if subdir_path.is_dir() and (subdir_path / "agent.py").exists():
392+
# Found a proper agent subdirectory
393+
return False
394+
395+
logging.debug(
396+
f"Detected flat structure in {template_dir}: agent.py in root, no agent subdirectory"
397+
)
398+
return True
399+
400+
367401
def _infer_agent_directory_for_adk(
368402
template_dir: pathlib.Path, is_adk_sample: bool
369403
) -> dict[str, Any]:
@@ -379,19 +413,35 @@ def _infer_agent_directory_for_adk(
379413
if not is_adk_sample:
380414
return {}
381415

416+
# Check for flat structure (agent code in root)
417+
is_flat = _detect_flat_structure(template_dir)
418+
382419
# Convert folder name to Python package convention (hyphens to underscores)
383420
folder_name = template_dir.name
384-
agent_directory = folder_name.replace("-", "_")
421+
target_agent_directory = folder_name.replace("-", "_")
422+
423+
if is_flat:
424+
logging.debug(
425+
f"Flat structure detected: source is '.', target agent_directory is '{target_agent_directory}'"
426+
)
427+
return {
428+
"settings": {
429+
"agent_directory": target_agent_directory,
430+
"source_agent_directory": ".", # Special value for flat structure
431+
},
432+
"has_explicit_config": False,
433+
"is_flat_structure": True,
434+
}
385435

386436
logging.debug(
387-
f"Inferred agent_directory '{agent_directory}' from folder name '{folder_name}' for ADK sample"
437+
f"Inferred agent_directory '{target_agent_directory}' from folder name '{folder_name}' for ADK sample"
388438
)
389439

390440
return {
391441
"settings": {
392-
"agent_directory": agent_directory,
442+
"agent_directory": target_agent_directory,
393443
},
394-
"has_explicit_config": False, # Track that this was inferred
444+
"has_explicit_config": False,
395445
}
396446

397447

@@ -481,6 +531,19 @@ def load_remote_template_config(
481531
logging.warning(f"Failed to apply ADK inference for {template_dir}: {e}")
482532
# Continue with default configuration
483533

534+
# Detect flat structure for all remote templates (not just ADK samples)
535+
# This handles cases where agent.py is in root with no subdirectory
536+
if not has_explicit_config and not is_adk_sample:
537+
if _detect_flat_structure(template_dir):
538+
folder_name = template_dir.name.replace("-", "_")
539+
config.setdefault("settings", {})
540+
config["settings"]["agent_directory"] = folder_name
541+
config["settings"]["source_agent_directory"] = "."
542+
config["is_flat_structure"] = True
543+
logging.debug(
544+
f"Detected flat structure for non-ADK template: source='.', target='{folder_name}'"
545+
)
546+
484547
# Add metadata about configuration source
485548
config["has_explicit_config"] = bool(has_explicit_config)
486549

agent_starter_pack/cli/utils/template.py

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,29 @@ def add_base_template_dependencies_interactively(
230230
return False
231231

232232

233-
def validate_agent_directory_name(agent_dir: str) -> None:
233+
def validate_agent_directory_name(agent_dir: str, allow_dot: bool = False) -> None:
234234
"""Validate that an agent directory name is a valid Python identifier.
235235
236236
Args:
237237
agent_dir: The agent directory name to validate
238+
allow_dot: If True, allows "." as a special value indicating flat structure
238239
239240
Raises:
240241
ValueError: If the agent directory name is not a valid Python identifier
242+
243+
Note:
244+
The special value "." indicates flat structure - agent code is in the
245+
template root. When "." is used, the target directory name will be
246+
derived from the template folder name.
241247
"""
248+
if agent_dir == ".":
249+
if allow_dot:
250+
return # "." is valid when explicitly allowed (will be resolved later)
251+
raise ValueError(
252+
"Agent directory '.' is not valid in this context. "
253+
"Use '.' only to indicate flat structure templates."
254+
)
255+
242256
if "-" in agent_dir:
243257
raise ValueError(
244258
f"Agent directory '{agent_dir}' contains hyphens (-) which are not allowed. "
@@ -879,7 +893,11 @@ def process_template(
879893
def get_agent_directory(
880894
template_config: dict[str, Any], cli_overrides: dict[str, Any] | None = None
881895
) -> str:
882-
"""Get agent directory with CLI override support."""
896+
"""Get agent directory with CLI override support.
897+
898+
Handles the special case where agent_directory is "." (flat structure),
899+
deriving the target directory name from the remote template folder name.
900+
"""
883901
agent_dir = None
884902
if (
885903
cli_overrides
@@ -892,6 +910,20 @@ def get_agent_directory(
892910
"agent_directory", "app"
893911
)
894912

913+
# Handle "." (flat structure) - derive target from folder name
914+
if agent_dir == ".":
915+
if remote_template_path:
916+
# Derive from remote template folder name
917+
folder_name = remote_template_path.name.replace("-", "_")
918+
logging.debug(
919+
f"Flat structure (-dir .): deriving target '{folder_name}' from folder name"
920+
)
921+
agent_dir = folder_name
922+
else:
923+
# Fallback to "app" for non-remote templates
924+
logging.debug("Flat structure (-dir .): using 'app' as fallback")
925+
agent_dir = "app"
926+
895927
# Validate agent directory is a valid Python identifier
896928
validate_agent_directory_name(agent_dir)
897929

@@ -1239,13 +1271,39 @@ def get_agent_directory(
12391271
f"Preserved base template {preserve_file} as starter_pack_{base_name}{extension}"
12401272
)
12411273

1242-
copy_files(
1243-
remote_template_path,
1244-
generated_project_dir,
1245-
agent_name=agent_name,
1246-
overwrite=True,
1247-
agent_directory=agent_directory,
1274+
# Check if this is a flat structure template
1275+
# Flat structure can be detected via:
1276+
# 1. Auto-detection (is_flat_structure flag in remote_config)
1277+
# 2. source_agent_directory set to "." in config
1278+
# 3. CLI override with -dir . (agent_directory = ".")
1279+
cli_agent_dir = (
1280+
cli_overrides.get("settings", {}).get("agent_directory")
1281+
if cli_overrides
1282+
else None
1283+
)
1284+
is_flat_structure = (cli_agent_dir == ".") or (
1285+
remote_config and remote_config.get("is_flat_structure", False)
12481286
)
1287+
1288+
if is_flat_structure:
1289+
# For flat structures, Python files go to agent_directory
1290+
logging.debug(
1291+
f"Flat structure detected: copying files to {agent_directory}/"
1292+
)
1293+
copy_flat_structure_agent_files(
1294+
remote_template_path,
1295+
generated_project_dir,
1296+
agent_directory,
1297+
)
1298+
else:
1299+
# Standard structure: copy as-is
1300+
copy_files(
1301+
remote_template_path,
1302+
generated_project_dir,
1303+
agent_name=agent_name,
1304+
overwrite=True,
1305+
agent_directory=agent_directory,
1306+
)
12491307
logging.debug("Remote template files copied successfully")
12501308

12511309
# Handle ADK agent compatibility
@@ -1738,3 +1796,54 @@ def copy_deployment_files(
17381796
)
17391797
else:
17401798
logging.warning(f"Deployment target directory not found: {deployment_path}")
1799+
1800+
1801+
def copy_flat_structure_agent_files(
1802+
src: pathlib.Path,
1803+
dst: pathlib.Path,
1804+
agent_directory: str,
1805+
) -> None:
1806+
"""Copy agent files from a flat structure template to the agent directory.
1807+
1808+
For flat structure templates, Python files (*.py) in the root are copied
1809+
to the agent directory, while other files are copied to the project root.
1810+
1811+
Args:
1812+
src: Source path (template root with flat structure)
1813+
dst: Destination path (project root)
1814+
agent_directory: Target agent directory name
1815+
"""
1816+
agent_dst = dst / agent_directory
1817+
agent_dst.mkdir(parents=True, exist_ok=True)
1818+
1819+
# Files that should go to agent directory
1820+
agent_file_extensions = {".py"}
1821+
# Files to skip entirely
1822+
skip_files = {"pyproject.toml", "uv.lock", "README.md", ".gitignore"}
1823+
1824+
for item in src.iterdir():
1825+
if item.name.startswith(".") or item.name in skip_files:
1826+
continue
1827+
if item.name == "__pycache__":
1828+
continue
1829+
1830+
if item.is_file():
1831+
if item.suffix in agent_file_extensions:
1832+
# Python files go to agent directory
1833+
dest_file = agent_dst / item.name
1834+
logging.debug(
1835+
f"Flat structure: copying {item.name} -> {agent_directory}/{item.name}"
1836+
)
1837+
shutil.copy2(item, dest_file)
1838+
else:
1839+
# Other files go to project root
1840+
dest_file = dst / item.name
1841+
logging.debug(f"Flat structure: copying {item.name} -> {item.name}")
1842+
shutil.copy2(item, dest_file)
1843+
elif item.is_dir():
1844+
# Directories are copied to project root (preserving structure)
1845+
dest_dir = dst / item.name
1846+
logging.debug(f"Flat structure: copying directory {item.name}")
1847+
if dest_dir.exists():
1848+
shutil.rmtree(dest_dir)
1849+
shutil.copytree(item, dest_dir)

0 commit comments

Comments
 (0)