agent-starter-pack 0.11.1__py3-none-any.whl → 0.12.0__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.
Files changed (72) hide show
  1. {agent_starter_pack-0.11.1.dist-info → agent_starter_pack-0.12.0.dist-info}/METADATA +1 -1
  2. {agent_starter_pack-0.11.1.dist-info → agent_starter_pack-0.12.0.dist-info}/RECORD +45 -70
  3. agents/adk_base/app/__init__.py +17 -0
  4. agents/adk_base/notebooks/adk_app_testing.ipynb +4 -1
  5. agents/adk_base/tests/integration/test_agent.py +1 -1
  6. agents/agentic_rag/app/__init__.py +17 -0
  7. agents/agentic_rag/app/agent.py +2 -2
  8. agents/agentic_rag/notebooks/adk_app_testing.ipynb +4 -1
  9. agents/agentic_rag/tests/integration/test_agent.py +2 -2
  10. agents/crewai_coding_crew/tests/integration/test_agent.py +1 -1
  11. agents/langgraph_base_react/tests/integration/test_agent.py +1 -1
  12. agents/live_api/tests/unit/test_server.py +6 -6
  13. llm.txt +15 -4
  14. src/base_template/Makefile +5 -5
  15. src/base_template/README.md +4 -4
  16. src/base_template/deployment/terraform/dev/variables.tf +3 -2
  17. src/base_template/deployment/terraform/iam.tf +10 -34
  18. src/base_template/deployment/terraform/variables.tf +3 -3
  19. src/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +2 -2
  20. src/base_template/pyproject.toml +2 -2
  21. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +1 -1
  22. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +1 -1
  23. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +2 -2
  24. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +1 -1
  25. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +1 -1
  26. src/cli/commands/create.py +25 -2
  27. src/cli/commands/enhance.py +94 -15
  28. src/cli/commands/list.py +1 -1
  29. src/cli/commands/setup_cicd.py +50 -41
  30. src/cli/utils/remote_template.py +1 -1
  31. src/cli/utils/template.py +120 -41
  32. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +3 -3
  33. src/deployment_targets/agent_engine/{app → {{cookiecutter.agent_directory}}}/agent_engine_app.py +10 -10
  34. src/deployment_targets/cloud_run/Dockerfile +2 -2
  35. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +3 -3
  36. src/deployment_targets/cloud_run/tests/load_test/README.md +1 -1
  37. src/deployment_targets/cloud_run/tests/load_test/load_test.py +2 -2
  38. {agents/live_api/app → src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}}/server.py +186 -7
  39. src/resources/docs/adk-cheatsheet.md +3 -3
  40. src/base_template/app/__init__.py +0 -3
  41. src/deployment_targets/cloud_run/app/server.py +0 -206
  42. src/frontends/adk_gemini_fullstack/frontend/components.json +0 -21
  43. src/frontends/adk_gemini_fullstack/frontend/eslint.config.js +0 -28
  44. src/frontends/adk_gemini_fullstack/frontend/index.html +0 -12
  45. src/frontends/adk_gemini_fullstack/frontend/package-lock.json +0 -6105
  46. src/frontends/adk_gemini_fullstack/frontend/package.json +0 -47
  47. src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +0 -564
  48. src/frontends/adk_gemini_fullstack/frontend/src/components/ActivityTimeline.tsx +0 -244
  49. src/frontends/adk_gemini_fullstack/frontend/src/components/ChatMessagesView.tsx +0 -420
  50. src/frontends/adk_gemini_fullstack/frontend/src/components/InputForm.tsx +0 -60
  51. src/frontends/adk_gemini_fullstack/frontend/src/components/WelcomeScreen.tsx +0 -56
  52. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/badge.tsx +0 -46
  53. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/button.tsx +0 -59
  54. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/card.tsx +0 -92
  55. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/input.tsx +0 -21
  56. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/scroll-area.tsx +0 -56
  57. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/select.tsx +0 -183
  58. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/tabs.tsx +0 -64
  59. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/textarea.tsx +0 -18
  60. src/frontends/adk_gemini_fullstack/frontend/src/global.css +0 -154
  61. src/frontends/adk_gemini_fullstack/frontend/src/main.tsx +0 -13
  62. src/frontends/adk_gemini_fullstack/frontend/src/utils.ts +0 -7
  63. src/frontends/adk_gemini_fullstack/frontend/src/vite-env.d.ts +0 -1
  64. src/frontends/adk_gemini_fullstack/frontend/tsconfig.json +0 -28
  65. src/frontends/adk_gemini_fullstack/frontend/tsconfig.node.json +0 -24
  66. src/frontends/adk_gemini_fullstack/frontend/vite.config.ts +0 -41
  67. {agent_starter_pack-0.11.1.dist-info → agent_starter_pack-0.12.0.dist-info}/WHEEL +0 -0
  68. {agent_starter_pack-0.11.1.dist-info → agent_starter_pack-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {agent_starter_pack-0.11.1.dist-info → agent_starter_pack-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/gcs.py +0 -0
  71. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/tracing.py +0 -0
  72. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/typing.py +0 -0
src/cli/utils/template.py CHANGED
@@ -34,9 +34,6 @@ from .remote_template import (
34
34
  render_and_merge_makefiles,
35
35
  )
36
36
 
37
- ADK_FILES = ["app/__init__.py"]
38
- NON_ADK_FILES: list[str] = []
39
-
40
37
 
41
38
  @dataclass
42
39
  class TemplateConfig:
@@ -72,7 +69,11 @@ class TemplateConfig:
72
69
  raise ValueError(f"Error loading template config: {err}") from err
73
70
 
74
71
 
75
- OVERWRITE_FOLDERS = ["app", "frontend", "tests", "notebooks"]
72
+ def get_overwrite_folders(agent_directory: str) -> list[str]:
73
+ """Get folders to overwrite with configurable agent directory."""
74
+ return [agent_directory, "frontend", "tests", "notebooks"]
75
+
76
+
76
77
  TEMPLATE_CONFIG_FILE = "templateconfig.yaml"
77
78
  DEPLOYMENT_FOLDERS = ["cloud_run", "agent_engine"]
78
79
  DEFAULT_FRONTEND = "streamlit"
@@ -446,6 +447,7 @@ def process_template(
446
447
  remote_template_path: pathlib.Path | None = None,
447
448
  remote_config: dict[str, Any] | None = None,
448
449
  in_folder: bool = False,
450
+ cli_overrides: dict[str, Any] | None = None,
449
451
  ) -> None:
450
452
  """Process the template directory and create a new project.
451
453
 
@@ -462,12 +464,25 @@ def process_template(
462
464
  remote_template_path: Optional path to remote template for overlay
463
465
  remote_config: Optional remote template configuration
464
466
  in_folder: Whether to template directly into the output directory instead of creating a subdirectory
467
+ cli_overrides: Optional CLI override values that should take precedence over template config
465
468
  """
466
469
  logging.debug(f"Processing template from {template_dir}")
467
470
  logging.debug(f"Project name: {project_name}")
468
471
  logging.debug(f"Include pipeline: {datastore}")
469
472
  logging.debug(f"Output directory: {output_dir}")
470
473
 
474
+ def get_agent_directory(
475
+ template_config: dict[str, Any], cli_overrides: dict[str, Any] | None = None
476
+ ) -> str:
477
+ """Get agent directory with CLI override support."""
478
+ if (
479
+ cli_overrides
480
+ and "settings" in cli_overrides
481
+ and "agent_directory" in cli_overrides["settings"]
482
+ ):
483
+ return cli_overrides["settings"]["agent_directory"]
484
+ return template_config.get("settings", {}).get("agent_directory", "app")
485
+
471
486
  # Handle remote vs local templates
472
487
  is_remote = remote_template_path is not None
473
488
 
@@ -519,7 +534,21 @@ def process_template(
519
534
  base_template_path = (
520
535
  pathlib.Path(__file__).parent.parent.parent / "base_template"
521
536
  )
522
- copy_files(base_template_path, project_template, agent_name, overwrite=True)
537
+ # Get agent directory from config early for use in file copying
538
+ # Load config early to get agent_directory
539
+ if is_remote:
540
+ early_config = remote_config or {}
541
+ else:
542
+ template_path = pathlib.Path(template_dir)
543
+ early_config = load_template_config(template_path)
544
+ agent_directory = get_agent_directory(early_config, cli_overrides)
545
+ copy_files(
546
+ base_template_path,
547
+ project_template,
548
+ agent_name,
549
+ overwrite=True,
550
+ agent_directory=agent_directory,
551
+ )
523
552
  logging.debug(f"1. Copied base template from {base_template_path}")
524
553
 
525
554
  # 2. Process deployment target if specified
@@ -535,6 +564,7 @@ def process_template(
535
564
  project_template,
536
565
  agent_name=agent_name,
537
566
  overwrite=True,
567
+ agent_directory=agent_directory,
538
568
  )
539
569
  logging.debug(
540
570
  f"2. Processed deployment files for target: {deployment_target}"
@@ -556,27 +586,11 @@ def process_template(
556
586
  copy_frontend_files(frontend_type, project_template)
557
587
  logging.debug(f"4. Processed frontend files for type: {frontend_type}")
558
588
 
559
- # 5. Copy agent-specific files to override base template
560
- if agent_path.exists():
561
- for folder in OVERWRITE_FOLDERS:
562
- agent_folder = agent_path / folder
563
- project_folder = project_template / folder
564
- if agent_folder.exists():
565
- logging.debug(f"5. Copying agent folder {folder} with override")
566
- copy_files(
567
- agent_folder, project_folder, agent_name, overwrite=True
568
- )
569
-
570
- # 6. Finally, overlay remote template files if present
589
+ # 6. Skip remote template files during cookiecutter processing
590
+ # Remote files will be copied after cookiecutter to avoid Jinja conflicts
571
591
  if is_remote and remote_template_path:
572
592
  logging.debug(
573
- f"6. Overlaying remote template files from {remote_template_path}"
574
- )
575
- copy_files(
576
- remote_template_path,
577
- project_template,
578
- agent_name=agent_name,
579
- overwrite=True,
593
+ "6. Skipping remote template files during cookiecutter processing - will copy after templating"
580
594
  )
581
595
 
582
596
  # Load and validate template config first
@@ -602,6 +616,45 @@ def process_template(
602
616
  # Use the already loaded config
603
617
  template_config = config
604
618
 
619
+ # 5. Copy agent-specific files to override base template (using final config)
620
+ if agent_path.exists():
621
+ agent_directory = get_agent_directory(template_config, cli_overrides)
622
+
623
+ # Get the template's default agent directory (usually "app")
624
+ template_agent_directory = template_config.get("settings", {}).get(
625
+ "agent_directory", "app"
626
+ )
627
+
628
+ # Copy agent directory (always from "app" to target directory)
629
+ source_agent_folder = agent_path / template_agent_directory
630
+ target_agent_folder = project_template / agent_directory
631
+ if source_agent_folder.exists():
632
+ logging.debug(
633
+ f"5. Copying agent folder {template_agent_directory} -> {agent_directory} with override"
634
+ )
635
+ copy_files(
636
+ source_agent_folder,
637
+ target_agent_folder,
638
+ agent_name,
639
+ overwrite=True,
640
+ agent_directory=agent_directory,
641
+ )
642
+
643
+ # Copy other folders (frontend, tests, notebooks)
644
+ other_folders = ["frontend", "tests", "notebooks"]
645
+ for folder in other_folders:
646
+ agent_folder = agent_path / folder
647
+ project_folder = project_template / folder
648
+ if agent_folder.exists():
649
+ logging.debug(f"5. Copying {folder} folder with override")
650
+ copy_files(
651
+ agent_folder,
652
+ project_folder,
653
+ agent_name,
654
+ overwrite=True,
655
+ agent_directory=agent_directory,
656
+ )
657
+
605
658
  # Check if data processing should be included
606
659
  if include_data_ingestion and datastore:
607
660
  logging.debug(
@@ -649,6 +702,7 @@ def process_template(
649
702
  "extra_dependencies": [extra_deps],
650
703
  "data_ingestion": include_data_ingestion,
651
704
  "datastore_type": datastore if datastore else "",
705
+ "agent_directory": get_agent_directory(template_config, cli_overrides),
652
706
  "adk_cheatsheet": adk_cheatsheet_content,
653
707
  "llm_txt": llm_txt_content,
654
708
  "_copy_without_render": [
@@ -664,7 +718,9 @@ def process_template(
664
718
  "*templates.py", # Don't render templates files
665
719
  "Makefile", # Don't render Makefile - handled by render_and_merge_makefiles
666
720
  # Don't render agent.py unless it's agentic_rag
667
- "app/agent.py" if agent_name != "agentic_rag" else "",
721
+ f"{get_agent_directory(template_config, cli_overrides)}/agent.py"
722
+ if agent_name != "agentic_rag"
723
+ else "",
668
724
  ],
669
725
  }
670
726
 
@@ -690,6 +746,21 @@ def process_template(
690
746
  )
691
747
  logging.debug("Template processing completed successfully")
692
748
 
749
+ # Now overlay remote template files if present (after cookiecutter processing)
750
+ if is_remote and remote_template_path:
751
+ generated_project_dir = temp_path / project_name
752
+ logging.debug(
753
+ f"Copying remote template files from {remote_template_path} to {generated_project_dir}"
754
+ )
755
+ copy_files(
756
+ remote_template_path,
757
+ generated_project_dir,
758
+ agent_name=agent_name,
759
+ overwrite=True,
760
+ agent_directory=agent_directory,
761
+ )
762
+ logging.debug("Remote template files copied successfully")
763
+
693
764
  # Move the generated project to the final destination
694
765
  generated_project_dir = temp_path / project_name
695
766
 
@@ -842,15 +913,7 @@ def process_template(
842
913
  )
843
914
 
844
915
  # Delete appropriate files based on ADK tag
845
- if "adk" in tags:
846
- files_to_delete = [final_destination / f for f in NON_ADK_FILES]
847
- else:
848
- files_to_delete = [final_destination / f for f in ADK_FILES]
849
-
850
- for file_path in files_to_delete:
851
- if file_path.exists():
852
- file_path.unlink()
853
- logging.debug(f"Deleted {file_path}")
916
+ agent_directory = get_agent_directory(template_config, cli_overrides)
854
917
 
855
918
  # Clean up unused_* files and directories created by conditional templates
856
919
  import glob
@@ -923,11 +986,15 @@ def process_template(
923
986
  os.chdir(original_dir)
924
987
 
925
988
 
926
- def should_exclude_path(path: pathlib.Path, agent_name: str) -> bool:
989
+ def should_exclude_path(
990
+ path: pathlib.Path, agent_name: str, agent_directory: str = "app"
991
+ ) -> bool:
927
992
  """Determine if a path should be excluded based on the agent type."""
928
993
  if agent_name == "live_api":
929
- # Exclude the unit test utils folder and app/utils folder for live_api
930
- if "tests/unit/test_utils" in str(path) or "app/utils" in str(path):
994
+ # Exclude the unit test utils folder and agent utils folder for live_api
995
+ if "tests/unit/test_utils" in str(path) or f"{agent_directory}/utils" in str(
996
+ path
997
+ ):
931
998
  logging.debug(f"Excluding path for live_api: {path}")
932
999
  return True
933
1000
  return False
@@ -938,6 +1005,7 @@ def copy_files(
938
1005
  dst: pathlib.Path,
939
1006
  agent_name: str | None = None,
940
1007
  overwrite: bool = False,
1008
+ agent_directory: str = "app",
941
1009
  ) -> None:
942
1010
  """
943
1011
  Copy files with configurable behavior for exclusions and overwrites.
@@ -947,6 +1015,7 @@ def copy_files(
947
1015
  dst: Destination path
948
1016
  agent_name: Name of the agent (for agent-specific exclusions)
949
1017
  overwrite: Whether to overwrite existing files (True) or skip them (False)
1018
+ agent_directory: Name of the agent directory (for agent-specific exclusions)
950
1019
  """
951
1020
 
952
1021
  def should_skip(path: pathlib.Path) -> bool:
@@ -957,7 +1026,9 @@ def copy_files(
957
1026
  return True
958
1027
  if ".git" in path.parts:
959
1028
  return True
960
- if agent_name is not None and should_exclude_path(path, agent_name):
1029
+ if agent_name is not None and should_exclude_path(
1030
+ path, agent_name, agent_directory
1031
+ ):
961
1032
  return True
962
1033
  if path.is_dir() and path.name == ".template":
963
1034
  return True
@@ -970,9 +1041,10 @@ def copy_files(
970
1041
  if should_skip(item):
971
1042
  logging.debug(f"Skipping file/directory: {item}")
972
1043
  continue
1044
+
973
1045
  d = dst / item.name
974
1046
  if item.is_dir():
975
- copy_files(item, d, agent_name, overwrite)
1047
+ copy_files(item, d, agent_name, overwrite, agent_directory)
976
1048
  else:
977
1049
  if overwrite or not d.exists():
978
1050
  logging.debug(f"Copying file: {item} -> {d}")
@@ -1012,7 +1084,10 @@ def copy_frontend_files(frontend_type: str, project_template: pathlib.Path) -> N
1012
1084
 
1013
1085
 
1014
1086
  def copy_deployment_files(
1015
- deployment_target: str, agent_name: str, project_template: pathlib.Path
1087
+ deployment_target: str,
1088
+ agent_name: str,
1089
+ project_template: pathlib.Path,
1090
+ agent_directory: str = "app",
1016
1091
  ) -> None:
1017
1092
  """Copy files from the specified deployment target folder."""
1018
1093
  if not deployment_target:
@@ -1028,7 +1103,11 @@ def copy_deployment_files(
1028
1103
  logging.debug(f"Copying deployment files from {deployment_path}")
1029
1104
  # Pass agent_name to respect agent-specific exclusions
1030
1105
  copy_files(
1031
- deployment_path, project_template, agent_name=agent_name, overwrite=True
1106
+ deployment_path,
1107
+ project_template,
1108
+ agent_name=agent_name,
1109
+ overwrite=True,
1110
+ agent_directory=agent_directory,
1032
1111
  )
1033
1112
  else:
1034
1113
  logging.warning(f"Deployment target directory not found: {deployment_path}")
@@ -18,11 +18,11 @@ import pytest
18
18
  {%- if "adk" in cookiecutter.tags %}
19
19
  from google.adk.events.event import Event
20
20
 
21
- from app.agent import root_agent
22
- from app.agent_engine_app import AgentEngineApp
21
+ from {{cookiecutter.agent_directory}}.agent import root_agent
22
+ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
23
23
  {%- else %}
24
24
 
25
- from app.agent_engine_app import AgentEngineApp
25
+ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
26
26
  {%- endif %}
27
27
 
28
28
 
@@ -30,10 +30,10 @@ from opentelemetry.sdk.trace import TracerProvider, export
30
30
  from vertexai import agent_engines
31
31
  from vertexai.preview.reasoning_engines import AdkApp
32
32
 
33
- from app.agent import root_agent
34
- from app.utils.gcs import create_bucket_if_not_exists
35
- from app.utils.tracing import CloudTraceLoggingSpanExporter
36
- from app.utils.typing import Feedback
33
+ from {{cookiecutter.agent_directory}}.agent import root_agent
34
+ from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
35
+ from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
36
+ from {{cookiecutter.agent_directory}}.utils.typing import Feedback
37
37
 
38
38
 
39
39
  class AgentEngineApp(AdkApp):
@@ -95,9 +95,9 @@ from langchain_core.runnables import RunnableConfig
95
95
  from traceloop.sdk import Instruments, Traceloop
96
96
  from vertexai import agent_engines
97
97
 
98
- from app.utils.gcs import create_bucket_if_not_exists
99
- from app.utils.tracing import CloudTraceLoggingSpanExporter
100
- from app.utils.typing import Feedback, InputChat, dumpd, ensure_valid_config
98
+ from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
99
+ from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
100
+ from {{cookiecutter.agent_directory}}.utils.typing import Feedback, InputChat, dumpd, ensure_valid_config
101
101
 
102
102
 
103
103
  class AgentEngineApp:
@@ -110,7 +110,7 @@ class AgentEngineApp:
110
110
  def set_up(self) -> None:
111
111
  """The set_up method is used to define application initialization logic"""
112
112
  # Lazy import agent at setup time to avoid deployment dependencies
113
- from app.agent import agent
113
+ from {{cookiecutter.agent_directory}}.agent import agent
114
114
 
115
115
  logging_client = google_cloud_logging.Client(project=self.project_id)
116
116
  self.logger = logging_client.logger(__name__)
@@ -206,7 +206,7 @@ def deploy_agent_engine_app(
206
206
  location: str,
207
207
  agent_name: str | None = None,
208
208
  requirements_file: str = ".requirements.txt",
209
- extra_packages: list[str] = ["./app"],
209
+ extra_packages: list[str] = ["./{{cookiecutter.agent_directory}}"],
210
210
  env_vars: dict[str, str] = {},
211
211
  service_account: str | None = None,
212
212
  ) -> agent_engines.AgentEngine:
@@ -305,7 +305,7 @@ if __name__ == "__main__":
305
305
  parser.add_argument(
306
306
  "--extra-packages",
307
307
  nargs="+",
308
- default=["./app"],
308
+ default=["./{{cookiecutter.agent_directory}}"],
309
309
  help="Additional packages to include",
310
310
  )
311
311
  parser.add_argument(
@@ -20,7 +20,7 @@ WORKDIR /code
20
20
 
21
21
  COPY ./pyproject.toml ./README.md ./uv.lock* ./
22
22
 
23
- COPY ./app ./app
23
+ COPY ./{{cookiecutter.agent_directory}} ./{{cookiecutter.agent_directory}}
24
24
 
25
25
  RUN uv sync --frozen
26
26
 
@@ -29,4 +29,4 @@ ENV COMMIT_SHA=${COMMIT_SHA}
29
29
 
30
30
  EXPOSE 8080
31
31
 
32
- CMD ["uv", "run", "uvicorn", "app.server:app", "--host", "0.0.0.0", "--port", "8080"]
32
+ CMD ["uv", "run", "uvicorn", "{{cookiecutter.agent_directory}}.server:app", "--host", "0.0.0.0", "--port", "8080"]
@@ -54,7 +54,7 @@ def start_server() -> subprocess.Popen[str]:
54
54
  sys.executable,
55
55
  "-m",
56
56
  "uvicorn",
57
- "app.server:app",
57
+ "{{cookiecutter.agent_directory}}.server:app",
58
58
  "--host",
59
59
  "0.0.0.0",
60
60
  "--port",
@@ -129,7 +129,7 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
129
129
  user_id = "test_user_123"
130
130
  session_data = {"state": {"preferred_language": "English", "visit_count": 1}}
131
131
 
132
- session_url = f"{BASE_URL}/apps/app/users/{user_id}/sessions"
132
+ session_url = f"{BASE_URL}/apps/{{cookiecutter.agent_directory}}/users/{user_id}/sessions"
133
133
  session_response = requests.post(
134
134
  session_url,
135
135
  headers=HEADERS,
@@ -142,7 +142,7 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
142
142
 
143
143
  # Then send chat message
144
144
  data = {
145
- "app_name": "app",
145
+ "app_name": "{{cookiecutter.agent_directory}}",
146
146
  "user_id": user_id,
147
147
  "session_id": session_id,
148
148
  "new_message": {
@@ -11,7 +11,7 @@ Follow these steps to execute load tests on your local machine:
11
11
  Launch the FastAPI server in a separate terminal:
12
12
 
13
13
  ```bash
14
- uv run uvicorn app.server:app --host 0.0.0.0 --port 8000 --reload
14
+ uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload
15
15
  ```
16
16
 
17
17
  **2. (In another tab) Create virtual environment with Locust**
@@ -45,7 +45,7 @@ class ChatStreamUser(HttpUser):
45
45
  user_id = f"user_{uuid.uuid4()}"
46
46
  session_data = {"state": {"preferred_language": "English", "visit_count": 1}}
47
47
 
48
- session_url = f"{self.client.base_url}/apps/app/users/{user_id}/sessions"
48
+ session_url = f"{self.client.base_url}/apps/{{cookiecutter.agent_directory}}/users/{user_id}/sessions"
49
49
  session_response = requests.post(
50
50
  session_url,
51
51
  headers=headers,
@@ -58,7 +58,7 @@ class ChatStreamUser(HttpUser):
58
58
 
59
59
  # Send chat message
60
60
  data = {
61
- "app_name": "app",
61
+ "app_name": "{{cookiecutter.agent_directory}}",
62
62
  "user_id": user_id,
63
63
  "session_id": session_id,
64
64
  "new_message": {
@@ -11,7 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
14
+ {% if cookiecutter.agent_name == "live_api" %}
15
15
  import asyncio
16
16
  import json
17
17
  import logging
@@ -27,7 +27,7 @@ from google.genai.types import LiveServerToolCall
27
27
  from pydantic import BaseModel
28
28
  from websockets.exceptions import ConnectionClosedError
29
29
 
30
- from app.agent import MODEL_ID, genai_client, live_connect_config, tool_functions
30
+ from .agent import MODEL_ID, genai_client, live_connect_config, tool_functions
31
31
 
32
32
  app = FastAPI()
33
33
  app.add_middleware(
@@ -197,16 +197,195 @@ class Feedback(BaseModel):
197
197
  run_id: str
198
198
  user_id: str | None
199
199
  log_type: Literal["feedback"] = "feedback"
200
+ {% elif "adk" in cookiecutter.tags %}
201
+ import os
202
+
203
+ import google.auth
204
+ from fastapi import FastAPI
205
+ from google.adk.cli.fast_api import get_fast_api_app
206
+ from google.cloud import logging as google_cloud_logging
207
+ from opentelemetry import trace
208
+ from opentelemetry.sdk.trace import TracerProvider, export
209
+ {%- if cookiecutter.session_type == "agent_engine" %}
210
+ from vertexai import agent_engines
211
+ {%- endif %}
212
+
213
+ from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
214
+ from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
215
+ from {{cookiecutter.agent_directory}}.utils.typing import Feedback
200
216
 
217
+ _, project_id = google.auth.default()
218
+ logging_client = google_cloud_logging.Client()
219
+ logger = logging_client.logger(__name__)
220
+ allow_origins = (
221
+ os.getenv("ALLOW_ORIGINS", "").split(",") if os.getenv("ALLOW_ORIGINS") else None
222
+ )
223
+
224
+ bucket_name = f"gs://{project_id}-{{cookiecutter.project_name}}-logs-data"
225
+ create_bucket_if_not_exists(
226
+ bucket_name=bucket_name, project=project_id, location="us-central1"
227
+ )
228
+
229
+ provider = TracerProvider()
230
+ processor = export.BatchSpanProcessor(CloudTraceLoggingSpanExporter())
231
+ provider.add_span_processor(processor)
232
+ trace.set_tracer_provider(provider)
233
+
234
+ AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
235
+
236
+ {%- if cookiecutter.session_type == "alloydb" %}
237
+ # AlloyDB session configuration
238
+ db_user = os.environ.get("DB_USER", "postgres")
239
+ db_name = os.environ.get("DB_NAME", "postgres")
240
+ db_pass = os.environ.get("DB_PASS")
241
+ db_host = os.environ.get("DB_HOST")
242
+
243
+ # Set session_service_uri if database credentials are available
244
+ session_service_uri = None
245
+ if db_host and db_pass:
246
+ session_service_uri = f"postgresql://{db_user}:{db_pass}@{db_host}:5432/{db_name}"
247
+ {%- elif cookiecutter.session_type == "agent_engine" %}
248
+ # Agent Engine session configuration
249
+ # Use environment variable for agent name, default to project name
250
+ agent_name = os.environ.get("AGENT_ENGINE_SESSION_NAME", "{{cookiecutter.project_name}}")
251
+
252
+ # Check if an agent with this name already exists
253
+ existing_agents = list(agent_engines.list(filter=f"display_name={agent_name}"))
254
+
255
+ if existing_agents:
256
+ # Use the existing agent
257
+ agent_engine = existing_agents[0]
258
+ else:
259
+ # Create a new agent if none exists
260
+ agent_engine = agent_engines.create(display_name=agent_name)
261
+
262
+ session_service_uri = f"agentengine://{agent_engine.resource_name}"
263
+ {%- else %}
264
+ # In-memory session configuration - no persistent storage
265
+ session_service_uri = None
266
+ {%- endif %}
267
+
268
+ app: FastAPI = get_fast_api_app(
269
+ agents_dir=AGENT_DIR,
270
+ web=True,
271
+ artifact_service_uri=bucket_name,
272
+ allow_origins=allow_origins,
273
+ session_service_uri=session_service_uri,
274
+ )
275
+ app.title = "{{cookiecutter.project_name}}"
276
+ app.description = "API for interacting with the Agent {{cookiecutter.project_name}}"
277
+ {% else %}
278
+ import logging
279
+ import os
280
+ from collections.abc import Generator
281
+
282
+ from fastapi import FastAPI
283
+ from fastapi.responses import RedirectResponse, StreamingResponse
284
+ from google.cloud import logging as google_cloud_logging
285
+ from langchain_core.runnables import RunnableConfig
286
+ from traceloop.sdk import Instruments, Traceloop
287
+
288
+ from {{cookiecutter.agent_directory}}.agent import agent
289
+ from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
290
+ from {{cookiecutter.agent_directory}}.utils.typing import Feedback, InputChat, Request, dumps, ensure_valid_config
291
+
292
+ # Initialize FastAPI app and logging
293
+ app = FastAPI(
294
+ title="{{cookiecutter.project_name}}",
295
+ description="API for interacting with the Agent {{cookiecutter.project_name}}",
296
+ )
297
+ logging_client = google_cloud_logging.Client()
298
+ logger = logging_client.logger(__name__)
299
+
300
+ # Initialize Telemetry
301
+ try:
302
+ Traceloop.init(
303
+ app_name=app.title,
304
+ disable_batch=False,
305
+ exporter=CloudTraceLoggingSpanExporter(),
306
+ instruments={Instruments.LANGCHAIN, Instruments.CREW},
307
+ )
308
+ except Exception as e:
309
+ logging.error("Failed to initialize Telemetry: %s", str(e))
310
+
311
+
312
+ def set_tracing_properties(config: RunnableConfig) -> None:
313
+ """Sets tracing association properties for the current request.
314
+
315
+ Args:
316
+ config: Optional RunnableConfig containing request metadata
317
+ """
318
+ Traceloop.set_association_properties(
319
+ {
320
+ "log_type": "tracing",
321
+ "run_id": str(config.get("run_id", "None")),
322
+ "user_id": config["metadata"].pop("user_id", "None"),
323
+ "session_id": config["metadata"].pop("session_id", "None"),
324
+ "commit_sha": os.environ.get("COMMIT_SHA", "None"),
325
+ }
326
+ )
327
+
328
+
329
+ def stream_messages(
330
+ input: InputChat,
331
+ config: RunnableConfig | None = None,
332
+ ) -> Generator[str, None, None]:
333
+ """Stream events in response to an input chat.
334
+
335
+ Args:
336
+ input: The input chat messages
337
+ config: Optional configuration for the runnable
338
+
339
+ Yields:
340
+ JSON serialized event data
341
+ """
342
+ config = ensure_valid_config(config=config)
343
+ set_tracing_properties(config)
344
+ input_dict = input.model_dump()
345
+
346
+ for data in agent.stream(input_dict, config=config, stream_mode="messages"): # type: ignore[arg-type]
347
+ yield dumps(data) + "\n"
348
+
349
+
350
+ # Routes
351
+ @app.get("/", response_class=RedirectResponse)
352
+ def redirect_root_to_docs() -> RedirectResponse:
353
+ """Redirect the root URL to the API documentation."""
354
+ return RedirectResponse(url="/docs")
355
+
356
+
357
+ @app.post("/stream_messages")
358
+ def stream_chat_events(request: Request) -> StreamingResponse:
359
+ """Stream chat events in response to an input request.
360
+
361
+ Args:
362
+ request: The chat request containing input and config
363
+
364
+ Returns:
365
+ Streaming response of chat events
366
+ """
367
+ return StreamingResponse(
368
+ stream_messages(input=request.input, config=request.config),
369
+ media_type="text/event-stream",
370
+ )
371
+ {% endif %}
201
372
 
202
373
  @app.post("/feedback")
203
- async def collect_feedback(feedback_dict: Feedback) -> None:
204
- """Collect and log feedback."""
205
- feedback_data = feedback_dict.model_dump()
206
- logger.log_struct(feedback_data, severity="INFO")
374
+ def collect_feedback(feedback: Feedback) -> dict[str, str]:
375
+ """Collect and log feedback.
376
+
377
+ Args:
378
+ feedback: The feedback data to log
379
+
380
+ Returns:
381
+ Success message
382
+ """
383
+ logger.log_struct(feedback.model_dump(), severity="INFO")
384
+ return {"status": "success"}
207
385
 
208
386
 
387
+ # Main execution
209
388
  if __name__ == "__main__":
210
389
  import uvicorn
211
390
 
212
- uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug")
391
+ uvicorn.run(app, host="0.0.0.0", port=8000)