11"""Pre-deployment validation for agent entrypoints."""
2+
23from __future__ import annotations
34import shutil
45import subprocess
1011
1112class ValidationError (Exception ):
1213 """Raised when agent validation fails."""
14+
1315 pass
1416
1517
1618def validate_agent_entrypoint (
17- source_dir : Path ,
18- entrypoint_file : str ,
19- verbose : bool = False
19+ source_dir : Path , entrypoint_file : str , verbose : bool = False
2020) -> None :
2121 """
2222 Validate that the agent can run successfully in a fresh environment.
23-
23+
2424 This creates a temporary virtual environment, installs dependencies,
2525 and attempts to start the agent to verify it works before deployment.
26-
26+
2727 Args:
2828 source_dir: Directory containing the agent source code
2929 entrypoint_file: Relative path to the entrypoint file (e.g., "main.py")
3030 verbose: Whether to print verbose validation output
31-
31+
3232 Raises:
3333 ValidationError: If validation fails
3434 """
35- print (f"🔍 Validating agent can run before deployment... (skip this step with --skip-validation)" )
35+ print (
36+ f"🔍 Validating agent can run before deployment... (skip this step with --skip-validation)"
37+ )
3638 if verbose :
3739 print (f"🔍 Validating agent before deployment..." )
3840 print (f" Source: { source_dir } " )
3941 print (f" Entrypoint: { entrypoint_file } " )
40-
42+
4143 # Check file exists
4244 entrypoint_path = source_dir / entrypoint_file
4345 if not entrypoint_path .exists ():
4446 raise ValidationError (
4547 f"Entrypoint file not found: { entrypoint_file } \n "
4648 f"Expected at: { entrypoint_path } "
4749 )
48-
50+
4951 # Check if requirements.txt exists
5052 requirements_path = source_dir / "requirements.txt"
5153 if not requirements_path .exists ():
@@ -55,86 +57,98 @@ def validate_agent_entrypoint(
5557 f"Create a requirements.txt with at minimum:\n "
5658 f" gradient-adk\n "
5759 )
58-
60+
5961 # Check for config file
6062 config_path = source_dir / ".gradient" / "agent.yml"
6163 if not config_path .exists ():
6264 raise ValidationError (
6365 f"No agent configuration found at { config_path } \n "
6466 f"Run 'gradient agent configure' first to set up your agent."
6567 )
66-
68+
6769 # Create temporary directory for validation
6870 temp_dir = None
6971 try :
7072 temp_dir = Path (tempfile .mkdtemp (prefix = "gradient_validation_" ))
71-
73+
7274 if verbose :
7375 print (f"\n � Created temporary validation environment: { temp_dir } " )
74-
76+
7577 # Copy source files to temp directory
7678 if verbose :
7779 print (f"📋 Copying source files..." )
78-
80+
7981 # Copy all files except common exclusions
8082 _copy_source_files (source_dir , temp_dir , verbose = verbose )
81-
83+
8284 # Create virtual environment
8385 venv_path = temp_dir / ".venv"
8486 if verbose :
8587 print (f"\n 🔨 Creating virtual environment..." )
86-
88+
8789 result = subprocess .run (
8890 [sys .executable , "-m" , "venv" , str (venv_path )],
8991 capture_output = True ,
9092 text = True ,
91- timeout = 60
93+ timeout = 60 ,
9294 )
93-
95+
9496 if result .returncode != 0 :
9597 raise ValidationError (
9698 f"Failed to create virtual environment:\n { result .stderr } "
9799 )
98-
100+
99101 # Get pip path
100102 if sys .platform == "win32" :
101103 pip_path = venv_path / "Scripts" / "pip"
102104 python_path = venv_path / "Scripts" / "python"
103105 else :
104106 pip_path = venv_path / "bin" / "pip"
105107 python_path = venv_path / "bin" / "python"
106-
108+
107109 # Install requirements
108110 if verbose :
109111 print (f"📦 Installing dependencies from requirements.txt..." )
110-
112+
111113 result = subprocess .run (
112114 [str (pip_path ), "install" , "-r" , "requirements.txt" ],
113115 cwd = temp_dir ,
114116 capture_output = True ,
115117 text = True ,
116- timeout = 300 # 5 minutes for dependency installation
118+ timeout = 300 , # 5 minutes for dependency installation
117119 )
118-
120+
119121 if result .returncode != 0 :
120122 raise ValidationError (
121123 f"Failed to install dependencies:\n { result .stderr } \n \n "
122124 f"Fix your requirements.txt and try again."
123125 )
124-
126+
125127 if verbose :
126128 print (f"✅ Dependencies installed successfully" )
127-
129+
128130 # Try to import the entrypoint and verify decorator
129131 if verbose :
130132 print (f"\n 🔍 Verifying agent entrypoint has @entrypoint decorator..." )
131-
133+
132134 # Check that the entrypoint file has the @entrypoint decorator
135+ # Convert file path to module path (e.g., "agents/my_agent.py" -> "agents.my_agent")
136+ module_path = entrypoint_file .replace (".py" , "" ).replace ("/" , "." )
137+
133138 check_script = f"""
134139import sys
135140import re
141+ import os
136142sys.path.insert(0, '.')
137143
144+ # Load environment variables from .env file if it exists
145+ if os.path.exists('.env'):
146+ try:
147+ from dotenv import load_dotenv
148+ load_dotenv()
149+ except ImportError:
150+ pass
151+
138152# Read the entrypoint file
139153with open('{ entrypoint_file } ', 'r') as f:
140154 content = f.read()
@@ -146,21 +160,23 @@ def validate_agent_entrypoint(
146160
147161# Try to import it
148162try:
149- from { entrypoint_file . replace ( '.py' , '' ) } import *
163+ import { module_path }
150164 print('✅ Entrypoint imported successfully with @entrypoint decorator')
151165except Exception as e:
152166 print(f"ERROR: {{type(e).__name__}}: {{e}}")
167+ import traceback
168+ traceback.print_exc()
153169 sys.exit(1)
154170"""
155-
171+
156172 result = subprocess .run (
157173 [str (python_path ), "-c" , check_script ],
158174 cwd = temp_dir ,
159175 capture_output = True ,
160176 text = True ,
161- timeout = 30
177+ timeout = 30 ,
162178 )
163-
179+
164180 if result .returncode != 0 :
165181 error_output = result .stdout + result .stderr
166182 if "No @entrypoint decorator found" in error_output :
@@ -178,13 +194,13 @@ def validate_agent_entrypoint(
178194 f"Failed to import entrypoint:\n { error_output } \n \n "
179195 f"The agent code has errors or missing dependencies."
180196 )
181-
197+
182198 if verbose :
183199 print (result .stdout .strip ())
184-
200+
185201 if verbose :
186202 print (f"\n ✅ Agent validation passed - ready to deploy!" )
187-
203+
188204 except subprocess .TimeoutExpired :
189205 raise ValidationError (
190206 "Validation timed out - the agent may have infinite loops or be waiting for input.\n "
@@ -212,7 +228,7 @@ def validate_agent_entrypoint(
212228
213229def _copy_source_files (source_dir : Path , dest_dir : Path , verbose : bool = False ) -> None :
214230 """Copy source files to destination, excluding common patterns."""
215-
231+
216232 # Common exclusions
217233 exclude_patterns = {
218234 "__pycache__" ,
@@ -221,15 +237,14 @@ def _copy_source_files(source_dir: Path, dest_dir: Path, verbose: bool = False)
221237 ".venv" ,
222238 "venv" ,
223239 "env" ,
224- ".env" ,
225240 "node_modules" ,
226241 ".pytest_cache" ,
227242 ".mypy_cache" ,
228243 "*.egg-info" ,
229244 "dist" ,
230245 "build" ,
231246 }
232-
247+
233248 def should_exclude (path : Path ) -> bool :
234249 """Check if a path should be excluded."""
235250 for pattern in exclude_patterns :
@@ -239,22 +254,28 @@ def should_exclude(path: Path) -> bool:
239254 elif path .name == pattern :
240255 return True
241256 return False
242-
257+
243258 # Copy files and directories
244259 for item in source_dir .iterdir ():
245260 if should_exclude (item ):
246261 if verbose :
247262 print (f" Skipping: { item .name } " )
248263 continue
249-
264+
250265 dest_path = dest_dir / item .name
251-
266+
252267 try :
253268 if item .is_dir ():
254- shutil .copytree (item , dest_path , ignore = lambda d , files : [f for f in files if should_exclude (Path (d ) / f )])
269+ shutil .copytree (
270+ item ,
271+ dest_path ,
272+ ignore = lambda d , files : [
273+ f for f in files if should_exclude (Path (d ) / f )
274+ ],
275+ )
255276 else :
256277 shutil .copy2 (item , dest_path )
257-
278+
258279 if verbose :
259280 print (f" Copied: { item .name } " )
260281 except Exception as e :
0 commit comments