reasoning-deployment-service 0.4.0__py3-none-any.whl → 0.8.3__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 reasoning-deployment-service might be problematic. Click here for more details.

@@ -14,14 +14,11 @@ try:
14
14
  HAS_GOOGLE, google, GoogleAuthRequest,
15
15
  vertexai, agent_engines
16
16
  )
17
- from reasoning_engine_creator import ReasoningEngineCreator
18
17
  except ImportError as e:
19
18
  from .google_deps import (HAS_GOOGLE, google, GoogleAuthRequest, vertexai, agent_engines)
20
- from .reasoning_engine_creator import ReasoningEngineCreator
21
19
 
22
20
  BASE_URL = "https://discoveryengine.googleapis.com/v1alpha"
23
21
 
24
-
25
22
  # --- helpers for clean packaging ---
26
23
  EXCLUDES = [
27
24
  ".env", ".env.*", ".git", "__pycache__", ".pytest_cache", ".mypy_cache",
@@ -501,17 +498,6 @@ class ApiClient:
501
498
  if name in sys.modules:
502
499
  del sys.modules[name]
503
500
 
504
- def create_reasoning_engine_advanced(self, config: Dict[str, Any]) -> Tuple[str, str, Optional[str]]:
505
- """Create a reasoning engine with advanced configuration options."""
506
- creator = ReasoningEngineCreator(
507
- project_id=self.project_id,
508
- location=self.location,
509
- staging_bucket=self.staging_bucket,
510
- debug=self.debug,
511
- )
512
-
513
- return creator.create_advanced_engine(config)
514
-
515
501
  def delete_reasoning_engine(self) -> Tuple[str, str]:
516
502
  if not self._profile.get("name"):
517
503
  return ("not_found", "No engine")
@@ -290,40 +290,6 @@ class ReasoningEngineView(ttk.Frame):
290
290
  finally:
291
291
  self.engines_menu.grab_release()
292
292
 
293
- def _create_engine_advanced(self):
294
- """Create a new reasoning engine with advanced configuration."""
295
- if not self.api.is_authenticated:
296
- self.log("❌ Authentication required")
297
- return
298
-
299
- # Show advanced create engine dialog
300
- dialog = CreateReasoningEngineAdvancedDialog(self.winfo_toplevel(), self.api)
301
- self.wait_window(dialog)
302
-
303
- if not dialog.result:
304
- return # User cancelled
305
-
306
- config = dialog.result
307
- self.log(f"⚙️ Creating advanced reasoning engine '{config['display_name']}'...")
308
- # self.create_advanced_btn.set_enabled(False, "Creating...")
309
-
310
- def callback(res):
311
- # self.create_advanced_btn.set_enabled(True)
312
- if isinstance(res, Exception):
313
- self.log(f"❌ {res}")
314
- return
315
-
316
- status, msg, resource = res
317
- self.log(f"{status.upper()}: {msg}")
318
- if resource:
319
- self.log(f"resource: {resource}")
320
-
321
- # Refresh engines list to show the new engine
322
- self._refresh_engines()
323
- self._update_button_states()
324
-
325
- async_operation(lambda: self.api.create_reasoning_engine_advanced(config), callback=callback, ui_widget=self)
326
-
327
293
  def update_api(self, api: ApiClient):
328
294
  """Update the API client reference."""
329
295
  self.api = api
@@ -1,4 +1,5 @@
1
1
  import json, os, subprocess, yaml, sys
2
+ import uuid
2
3
  import urllib.parse, vertexai, google.auth
3
4
  import requests as _requests
4
5
  from typing import Dict, Optional, Tuple
@@ -17,7 +18,6 @@ DISCOVERY_ENGINE_URL = "https://discoveryengine.googleapis.com/v1alpha"
17
18
 
18
19
  class ReasoningEngineDeploymentService:
19
20
  def __init__(self, root_agent: BaseAgent, deployment_environment: str="DEV"):
20
- # Setup logging
21
21
  self._setup_logging()
22
22
 
23
23
  self._check_required_files_exist()
@@ -39,6 +39,7 @@ class ReasoningEngineDeploymentService:
39
39
 
40
40
  self._load_agent_definition()
41
41
  self._load_deployment_environment_variables(deployment_environment=deployment_environment)
42
+ self._load_runtime_variables()
42
43
  self._check_requirements_file_present()
43
44
 
44
45
  self._http = _requests.Session()
@@ -97,6 +98,37 @@ class ReasoningEngineDeploymentService:
97
98
  def info(self, message: str):
98
99
  self.logger.info(f"[DEPLOYMENT SERVICE: INFO]: {message}")
99
100
 
101
+ def _generate_authorization_id(self) -> Optional[str]:
102
+ get_deployment_environment = os.getenv('AGENT_DEPLOYMENT_PIPELINE_ID', "LOCAL_RUN")
103
+
104
+ if self._authorization_override:
105
+ return self._authorization_override.lower()
106
+
107
+ if not self._authorization_override:
108
+ return None
109
+
110
+ return f"{get_deployment_environment}-{self._reasoning_engine_name}-{str(uuid.uuid4())}-auth".lower()
111
+
112
+ def _load_runtime_variables(self):
113
+ load_dotenv(dotenv_path=".env", override=True)
114
+ runtime_vars = {}
115
+
116
+ for key in self._specific_dot_env_variables:
117
+ if key in os.environ:
118
+ runtime_vars[key] = os.environ[key]
119
+
120
+ runtime_vars.update(self._runtime_variable_definitions or {})
121
+ local_auth_id = self._generate_authorization_id()
122
+
123
+ if self._use_authorization and local_auth_id:
124
+ runtime_vars.update({'AUTHORIZATION_ID': local_auth_id})
125
+ self._authorization_id = local_auth_id
126
+ runtime_vars.update({f"DEPLOYED_PROJECT_NUMBER": self._project_number})
127
+ runtime_vars.update({f"DEPLOYED_PROJECT_ID": self._project_id})
128
+ runtime_vars.update({f"AGENT_DEPLOYMENT_PIPELINE_ID": self._deployed_environment})
129
+
130
+ self._environment_variables = runtime_vars
131
+
100
132
  def _check_required_files_exist(self):
101
133
  end_run = False
102
134
  if not os.path.exists(".env.agent"):
@@ -185,37 +217,14 @@ class ReasoningEngineDeploymentService:
185
217
  if path.exists() and not overwrite:
186
218
  raise FileExistsError(f"{path} already exists. Pass overwrite=True to replace it.")
187
219
 
188
- template = """#===================== **** DEPLOYMENT PROFILE **** =====================
189
- # Development Profile
190
- DEV_PROJECT_ID=
191
- DEV_PROJECT_NUMBER=
192
- DEV_PROJECT_LOCATION=
193
- DEV_STAGING_BUCKET=
194
- DEV_AGENT_SPACE_ENGINE=
195
- DEV_API_TOKEN=
196
- DEV_OAUTH_CLIENT_ID=
197
- DEV_OAUTH_CLIENT_SECRET=
198
-
199
- # Production Profile
200
- PROD_PROJECT_ID=
201
- PROD_PROJECT_NUMBER=
202
- PROD_PROJECT_LOCATION=
203
- PROD_STAGING_BUCKET=
204
- PROD_AGENT_SPACE_ENGINE=
205
- PROD_API_TOKEN=
206
- PROD_OAUTH_CLIENT_ID=
207
- PROD_OAUTH_CLIENT_SECRET=
208
- #===================== **** DEPLOYMENT PROFILE **** =====================
209
-
210
-
211
- #===================== **** YOUR APP ENV VARIABLES **** =====================
212
- DEVELOPER=dev
213
- #===================== **** YOUR APP ENV VARIABLES **** =====================
214
- """
220
+ template = """
221
+ DEV_PROJECT_ID=
222
+ DEV_PROJECT_NUMBER=
223
+ DEV_PROJECT_LOCATION=
224
+ DEV_OAUTH_CLIENT_ID=
225
+ DEV_OAUTH_CLIENT_SECRET="""
215
226
 
216
227
  path.write_text(template.strip() + "\n")
217
-
218
- # Also update .gitignore to include logs
219
228
  self._update_gitignore()
220
229
 
221
230
  return path
@@ -250,13 +259,6 @@ class ReasoningEngineDeploymentService:
250
259
  def _generate_example_yaml_config(self, path: str | Path = "agent.yaml", overwrite: bool = False) -> Path:
251
260
  """
252
261
  Create an example YAML config matching the requested schema.
253
-
254
- Structure:
255
- defaults:
256
- scopes: [ ... ]
257
- metadata: { ... }
258
- auth: { ... }
259
- environment_variables: [ ... ]
260
262
  """
261
263
  path = Path(path)
262
264
  if path.exists() and not overwrite:
@@ -264,23 +266,27 @@ class ReasoningEngineDeploymentService:
264
266
 
265
267
  config = {
266
268
  "defaults": {
267
- "scopes": [
268
- "https://www.googleapis.com/auth/cloud-platform",
269
- "https://www.googleapis.com/auth/userinfo.email",
270
- ],
271
- "metadata": {
272
- "reasoning_engine_name": "reasoning-engine-dev",
273
- "reasoning_engine_description": "A reasoning engine for development",
274
- "agent_space_name": "Agent Space Dev Numba Three!",
275
- "agent_space_description": "Agent spece description, lets go",
276
- "agent_space_tool_description": "Agent space tool description",
269
+ "reasoning_engine": {
270
+ "name": "reasoning-engine-dev",
271
+ "description": "A reasoning engine for development"
272
+ },
273
+ "gemini_enterprise": {
274
+ "target_deployment_engine_id": "your-gemini-enterprise-engine-id",
275
+ "name": "Agent Name Here",
276
+ "description": "Agent description here",
277
+ "tool_description": "Tool description here",
277
278
  },
278
- "auth": {
279
- "oauth_authorization_id": "test_auth_three",
279
+ "authorization": {
280
+ "enabled": True,
281
+ "scopes": [
282
+ "https://www.googleapis.com/auth/cloud-platform",
283
+ "https://www.googleapis.com/auth/userinfo.email"
284
+ ]
280
285
  },
281
- "environment_variables": [
282
- "DEVELOPER",
283
- ],
286
+ "import_from_dot_env_by_name": ["TEST_ENV_VAR"],
287
+ "runtime_variable_definitions":{
288
+ "EXAMPLE_VAR": "An example environment variable for the agent runtime"
289
+ }
284
290
  }
285
291
  }
286
292
 
@@ -298,31 +304,40 @@ class ReasoningEngineDeploymentService:
298
304
 
299
305
  try:
300
306
  config = config['defaults']
301
- scopes = config['scopes']
302
- metadata = config['metadata']
303
- auth = config['auth']
304
- environment_variables = config['environment_variables']
305
-
306
- reasoning_engine_name = metadata['reasoning_engine_name']
307
- reasoning_engine_description = metadata['reasoning_engine_description']
308
- agent_space_name = metadata['agent_space_name']
309
- agent_space_description = metadata['agent_space_description']
310
- agent_space_tool_description = metadata["agent_space_tool_description"]
311
-
312
- self._required_scopes = scopes
307
+ authorization = config['authorization']
308
+ gemini_enterprise = config['gemini_enterprise']
309
+ reasoning_engine = config['reasoning_engine']
310
+ self._specific_dot_env_variables = config.get('import_from_dot_env_by_name', [])
311
+ self._runtime_variable_definitions = config.get('runtime_variable_definitions', {})
312
+
313
+ reasoning_engine_name = reasoning_engine.get('name')
314
+ reasoning_engine_description = reasoning_engine.get('description')
315
+ reasoning_engine_bypass = reasoning_engine.get('skip_build', False)
316
+
317
+ gemini_enterprise_name = gemini_enterprise.get('name')
318
+ gemini_enterprise_description = gemini_enterprise.get('description')
319
+ gemini_enterprise_tool_description = gemini_enterprise.get('tool_description')
320
+ gemini_enterprise_engine_id = gemini_enterprise.get('target_deployment_engine_id')
321
+ gemini_enterprise_icon_uri = gemini_enterprise.get('icon_uri')
322
+
323
+
324
+ self._reasoning_engine_bypass = reasoning_engine_bypass
325
+ self._icon_uri = gemini_enterprise_icon_uri
326
+ self._agent_space_engine = gemini_enterprise_engine_id or os.getenv(f"{self.deployment_env}_AGENT_SPACE_ENGINE")
327
+ self._required_scopes = authorization.get('scopes', [])
313
328
  self._agent_folder = "agent"
314
329
  self._reasoning_engine_name = reasoning_engine_name
315
330
  self._reasoning_engine_description = reasoning_engine_description
316
- self._agent_space_name = agent_space_name
317
- self._agent_space_description = agent_space_description
318
- self._agent_space_tool_description = agent_space_tool_description
319
- self._authorization_id = auth.get("oauth_authorization_id", None)
320
- self._environment_variables = environment_variables or []
331
+ self._agent_space_name = gemini_enterprise_name
332
+ self._agent_space_description = gemini_enterprise_description
333
+ self._agent_space_tool_description = gemini_enterprise_tool_description
334
+ self._use_authorization = authorization.get('enabled', False)
335
+ self._authorization_override = authorization.get('authorization_id_override', None)
321
336
  except KeyError as e:
322
- raise RuntimeError(f"Missing required key in agent.yaml: {e}")
337
+ raise RuntimeError(f"Missing required key in agent.yaml: {e}. Your agent.yaml file is not valid for this deployment service version.")
323
338
 
324
339
  def _load_deployment_environment_variables(self, deployment_environment: str):
325
- required_vars = ['PROJECT_ID', 'PROJECT_NUMBER', 'PROJECT_LOCATION', 'STAGING_BUCKET', 'AGENT_SPACE_ENGINE']
340
+ required_vars = ['PROJECT_ID', 'PROJECT_NUMBER', 'PROJECT_LOCATION', 'STAGING_BUCKET']
326
341
 
327
342
  for var in required_vars:
328
343
  env_var = f"{deployment_environment}_{var}"
@@ -331,7 +346,10 @@ class ReasoningEngineDeploymentService:
331
346
 
332
347
  setattr(self, f"_{var.lower()}", os.getenv(env_var))
333
348
 
334
- if self._authorization_id:
349
+ if not self._agent_space_engine:
350
+ raise RuntimeError(f"Missing AGENT_SPACE_ENGINE for deployment environment {deployment_environment}.")
351
+
352
+ if self._use_authorization:
335
353
  required_auth_vars = ['OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET']
336
354
 
337
355
  for var in required_auth_vars:
@@ -341,6 +359,8 @@ class ReasoningEngineDeploymentService:
341
359
 
342
360
  setattr(self, f"_{var.lower()}", os.getenv(env_var))
343
361
 
362
+ self._deployed_environment = os.getenv(f"AGENT_DEPLOYMENT_PIPELINE_ID", "unregistered_environment")
363
+
344
364
  def _check_requirements_file_present(self):
345
365
  if not os.path.exists("requirements.txt"):
346
366
  raise RuntimeError("Missing requirements.txt file")
@@ -417,7 +437,10 @@ class ReasoningEngineDeploymentService:
417
437
  },
418
438
  }
419
439
 
420
- if self._authorization_id:
440
+ if self._icon_uri:
441
+ payload["icon"] = {"uri": self._icon_uri}
442
+
443
+ if self._use_authorization and self._authorization_id:
421
444
  payload["adk_agent_definition"]["authorizations"] = [
422
445
  f"projects/{self._project_number}/locations/global/authorizations/{self._authorization_id}"
423
446
  ]
@@ -520,7 +543,7 @@ class ReasoningEngineDeploymentService:
520
543
  def _create_authorization(self) -> dict:
521
544
  read_authorizations = self._read_engine_deployment_record()
522
545
 
523
- if not self._authorization_id:
546
+ if not self._authorization_id or not self._use_authorization:
524
547
  self.warning("No authorization ID provided; skipping authorization creation.")
525
548
 
526
549
  return
@@ -603,8 +626,10 @@ class ReasoningEngineDeploymentService:
603
626
 
604
627
  return True
605
628
 
606
- def _delete_authorization(self):
607
- if not self._authorization_id:
629
+ def _delete_authorization(self, drop_authorization_for_refresh: Optional[str] = None):
630
+ auth_to_drop = drop_authorization_for_refresh or self._authorization_id
631
+
632
+ if not auth_to_drop:
608
633
  self.warning("No authorization ID provided; skipping deletion.")
609
634
  return
610
635
 
@@ -617,18 +642,21 @@ class ReasoningEngineDeploymentService:
617
642
 
618
643
  url = (
619
644
  f"{discovery_engine_url}/projects/{self._project_id}/locations/global/authorizations"
620
- f"?authorizationId={self._authorization_id}"
645
+ f"?authorizationId={auth_to_drop}"
621
646
  )
622
647
 
623
648
  r = self._http.delete(url, headers=headers, timeout=60)
624
649
 
625
650
  if r.status_code < 400:
651
+ if drop_authorization_for_refresh:
652
+ self.info(f"Authorization {drop_authorization_for_refresh} deleted successfully for refresh.")
653
+ return True
654
+
626
655
  self.info("Authorization deleted successfully.")
627
656
  self._authorization_id = None
628
657
  self._update_in_agent_space()
629
658
  return True
630
659
 
631
- # Log API failure details to file only
632
660
  with open(self.log_filename, 'a') as f:
633
661
  timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:-3]
634
662
  f.write(f"{timestamp} - ReasoningEngineDeployment - ERROR - Failed to delete authorization with status {r.status_code} {r.reason}\n")
@@ -641,9 +669,8 @@ class ReasoningEngineDeploymentService:
641
669
  except:
642
670
  pass
643
671
 
644
- # Terminal message - simple
672
+
645
673
  self.error("Failed to delete authorization")
646
- # This will also log the record file
647
674
  return False
648
675
 
649
676
  def one_deployment_with_everything_on_it(self, skip_engine_step=False):
@@ -721,7 +748,14 @@ class ReasoningEngineDeploymentService:
721
748
  if r.status_code == 404:
722
749
  return None
723
750
 
724
- r.raise_for_status()
751
+ self.info(r.json())
752
+ self.info(r.text)
753
+
754
+ if r.status_code == 403:
755
+ self.warning("Access denied")
756
+ return None
757
+ else:
758
+ r.raise_for_status()
725
759
 
726
760
  return r.json().get("name", name)
727
761
 
@@ -766,9 +800,121 @@ class ReasoningEngineDeploymentService:
766
800
  )
767
801
 
768
802
  return matches[0]
803
+
804
+ def patch_agent_space_metadata_and_auth(
805
+ self,
806
+ agent_id: str,
807
+ new_display_name: Optional[str] = None,
808
+ new_description: Optional[str] = None,
809
+ new_reasoning_engine: Optional[str] = None,
810
+ new_authorizations: Optional[list[str]] = None,
811
+ icon_uri: Optional[str] = None,
812
+ ) -> dict:
813
+ """
814
+ Safely patch metadata (displayName, description) and linkage fields
815
+ (reasoningEngine, authorizations) for an existing Agent Space agent.
816
+ Preserves all other required adkAgentDefinition fields.
817
+ """
818
+ url = (
819
+ f"{DISCOVERY_ENGINE_URL}/projects/{self._project_id}/locations/global/"
820
+ f"collections/default_collection/engines/{self._agent_space_engine}/"
821
+ f"assistants/default_assistant/agents/{agent_id}"
822
+ )
769
823
 
824
+ agent_updates_body = {
825
+ "displayName": new_display_name,
826
+ "description": new_description,
827
+ "adk_agent_definition": {
828
+ "tool_settings": {
829
+ "tool_description": new_description
830
+ },
831
+ "provisioned_reasoning_engine":{
832
+ "reasoning_engine": new_reasoning_engine
833
+ },
834
+ "authorizations": new_authorizations
835
+ }
836
+ }
837
+
838
+ if icon_uri:
839
+ agent_updates_body["icon"] = {"uri": icon_uri}
840
+
841
+ self.info(agent_updates_body)
770
842
 
771
- def one_github_deployment_to_go(self):
843
+ headers = self._get_headers()
844
+
845
+ update_mask = ["displayName", "description", "adk_agent_definition.tool_settings.tool_description",
846
+ "adk_agent_definition.provisioned_reasoning_engine.reasoning_engine", "adk_agent_definition.authorizations", 'icon.uri']
847
+ params = {"update_mask": ",".join(update_mask)}
848
+ resp = self._http.patch(url, headers=headers, params=params, json=agent_updates_body, timeout=60)
849
+
850
+ return resp.json()
851
+
852
+
853
+ def one_githhub_deployment_to_go_with_skip(self):
854
+ return self.one_github_deployment_to_go(skip_engine=True)
855
+
856
+ def update_authorization_scopes(self, auth_id: str, scopes: list, oauth_client_id: str) -> dict:
857
+ """Patch the scopes for a specific authorization by ID, updating the authorizationUri as well."""
858
+ scopes_str = " ".join(scopes)
859
+ authorization_uri = (
860
+ "https://accounts.google.com/o/oauth2/auth"
861
+ "?response_type=code"
862
+ f"&client_id={oauth_client_id}"
863
+ f"&scope={scopes_str}"
864
+ "&access_type=offline&prompt=consent"
865
+ )
866
+ payload = {
867
+ "serverSideOauth2": {
868
+ "authorizationUri": authorization_uri
869
+ }
870
+ }
871
+ url = f"{DISCOVERY_ENGINE_URL}/projects/{self._project_id}/locations/global/authorizations/{auth_id}?update_mask=server_side_oauth2.authorization_uri"
872
+ headers = {
873
+ "Authorization": f"Bearer {self._access_token()}",
874
+ "Content-Type": "application/json",
875
+ "X-Goog-User-Project": self._project_number,
876
+ }
877
+ r = self._http.patch(url, headers=headers, json=payload, timeout=60)
878
+ r.raise_for_status()
879
+ return r.json()
880
+
881
+ def detect_scope_change(self, auth_full_name, want_scopes) -> Optional[bool]:
882
+ auth_url = f"{DISCOVERY_ENGINE_URL}/{auth_full_name}"
883
+ hdrs = self._get_headers().copy()
884
+
885
+ if "Authorization" in hdrs:
886
+ hdrs["Authorization"] = "Bearer ***"
887
+
888
+ self.info(f"[AUTH] GET {auth_url} headers={json.dumps(hdrs)}")
889
+ r = self._http.get(auth_url, headers=self._get_headers(), timeout=60)
890
+ self.info(f"[AUTH] GET status={r.status_code} ct={r.headers.get('content-type','')}")
891
+
892
+ try:
893
+ self.info(f"[AUTH] GET body={json.dumps(r.json(), indent=2)[:4000]}")
894
+ except Exception:
895
+ self.info(f"[AUTH] GET text={(r.text or '')[:1000]}")
896
+ r.raise_for_status()
897
+
898
+ data = r.json() or {}
899
+ existing_uri = (((data.get("serverSideOauth2") or {}).get("authorizationUri")) or "")
900
+ self.info(f"[AUTH] existing authorizationUri={existing_uri!r}")
901
+ existing_scopes = set()
902
+
903
+ if existing_uri:
904
+ parsed = urlparse(existing_uri)
905
+ qs = parse_qs(parsed.query)
906
+ scope_str = (qs.get("scope", [""])[0] or "")
907
+ existing_scopes = set(scope_str.split())
908
+
909
+ self.info(
910
+ f"[AUTH] scopes existing={sorted(existing_scopes)} want={sorted(want_scopes)} "
911
+ f"missing={sorted(want_scopes - existing_scopes)} extra={sorted(existing_scopes - want_scopes)}"
912
+ )
913
+
914
+ if existing_scopes != want_scopes:
915
+ return True
916
+
917
+ def one_github_deployment_to_go(self, skip_engine=False):
772
918
  """
773
919
  CI-friendly deploy:
774
920
  - Engine: create or update by display_name.
@@ -776,110 +922,110 @@ class ReasoningEngineDeploymentService:
776
922
  - Agent Space: create if missing; patch if found (by displayName under engine).
777
923
  """
778
924
  self.info("Starting GitHub deployment...")
925
+ self.info(
926
+ f"[CFG] project_id={self._project_id} project_number={self._project_number} "
927
+ f"location={self._project_location} engine_name={self._reasoning_engine_name} "
928
+ f"agent_space_engine={self._agent_space_engine} auth_id={self._authorization_id} "
929
+ f"scopes={self._required_scopes} staging_bucket={self._staging_bucket}"
930
+ )
779
931
 
780
- # Ensure Vertex SDK calls have context for list/update
781
932
  self._cicd_deploy = True
933
+ delete_old_authorization = False
934
+ self.info(f"[INIT] vertexai.init(project={self._project_id}, location={self._project_location}, staging_bucket={self._staging_bucket})")
782
935
  vertexai.init(
783
936
  project=self._project_id,
784
937
  location=self._project_location,
785
938
  staging_bucket=self._staging_bucket,
786
939
  )
787
940
 
788
- # -----------------------------
789
- # 1) Reasoning Engine (create or update)
790
- # -----------------------------
941
+ self.info(f"[ENGINE] Resolving by display_name={self._reasoning_engine_name}")
791
942
  engine_rn = self.find_engine_by_name(self._reasoning_engine_name)
792
- if not engine_rn:
793
- self.info(f"Engine '{self._reasoning_engine_name}' not found. Creating...")
794
- self.create_reasoning_engine()
795
- # read back the created id (or re-resolve by name as fallback)
796
- engine_rn = self._read_engine_deployment_record().get("reasoning_engine_id") or \
797
- self.find_engine_by_name(self._reasoning_engine_name)
943
+ self.info(f"[ENGINE] find_engine_by_name -> {engine_rn}")
944
+
945
+ if not skip_engine and not self._reasoning_engine_bypass:
798
946
  if not engine_rn:
799
- self.error("Engine creation did not yield a resource name.")
800
- raise RuntimeError("Engine creation failed.")
801
- else:
802
- self.info(f"Engine '{self._reasoning_engine_name}' exists. Updating...")
803
- self.update_reasoning_engine(engine_rn)
947
+ self.info(f"[ENGINE] '{self._reasoning_engine_name}' not found. Creating...")
948
+ self.create_reasoning_engine()
949
+ rec_after_create = self._read_engine_deployment_record()
950
+ self.info(f"[ENGINE] record after create -> {json.dumps(rec_after_create, indent=2)}")
951
+ engine_rn = rec_after_create.get("reasoning_engine_id") or self.find_engine_by_name(self._reasoning_engine_name)
952
+ self.info(f"[ENGINE] post-create resolution -> {engine_rn}")
953
+ if not engine_rn:
954
+ self.error("[ENGINE] Creation did not yield a resource name.")
955
+ raise RuntimeError("Engine creation failed.")
956
+ else:
957
+ self.info(f"[ENGINE] '{self._reasoning_engine_name}' exists. Updating...")
958
+ self.update_reasoning_engine(engine_rn)
804
959
 
805
- # -----------------------------
806
- # 2) Authorization (create if missing; update scopes if changed)
807
- # -----------------------------
808
- if self._authorization_id:
960
+ self.info(f"[ENGINE] final engine_rn={engine_rn}")
961
+
962
+ if not engine_rn:
963
+ self.error("[ENGINE] Reasoning engine required for Agent Space deployment.")
964
+ raise RuntimeError("Reasoning engine resolution failed.")
965
+
966
+ auth_full_name = None
967
+ if self._authorization_id and self._use_authorization:
809
968
  want_scopes = set(self._required_scopes or [])
969
+ self.info(f"[AUTH] id={self._authorization_id} want_scopes={sorted(want_scopes)}")
810
970
  auth_full_name = self.find_authorization_by_id(self._authorization_id)
971
+ self.info(f"[AUTH] find_authorization_by_id -> {auth_full_name}")
972
+
973
+ if auth_full_name and self.detect_scope_change(auth_full_name, want_scopes):
974
+ self.info(f"[AUTH] Scopes changed; patching authorization {self._authorization_id}...")
975
+ delete_old_authorization = auth_full_name
976
+ self._authorization_id = self._generate_authorization_id()
977
+ auth_full_name = None
811
978
 
812
979
  if not auth_full_name:
813
- self.info(f"Authorization '{self._authorization_id}' not found. Creating...")
980
+ self.info(f"[AUTH] '{self._authorization_id}' not found. Creating...")
814
981
  ok = self._create_authorization()
815
- if not ok:
816
- self.error("Authorization creation failed.")
817
- raise RuntimeError("Authorization creation failed.")
982
+ self.info(f"[AUTH] _create_authorization -> {ok}")
818
983
  auth_full_name = self.find_authorization_by_id(self._authorization_id)
819
- if not auth_full_name:
820
- self.error("Authorization creation did not resolve to a resource.")
821
- raise RuntimeError("Authorization creation failed to resolve.")
822
- else:
823
- # Compare scopes; patch if different
824
- r = self._http.get(f"{DISCOVERY_ENGINE_URL}/{auth_full_name}",
825
- headers=self._get_headers(), timeout=60)
826
- r.raise_for_status()
827
- data = r.json() or {}
828
- existing_uri = (((data.get("serverSideOauth2") or {}).get("authorizationUri")) or "")
829
- existing_scopes = set()
830
- if existing_uri:
831
- parsed = urlparse(existing_uri)
832
- qs = parse_qs(parsed.query)
833
- scope_str = (qs.get("scope", [""])[0] or "")
834
- existing_scopes = set(scope_str.split())
835
-
836
- if existing_scopes != want_scopes:
837
- self.info("Authorization scopes changed. Patching authorization...")
838
- new_auth_uri = self._build_authorization_uri(self._oauth_client_id, list(want_scopes))
839
- patch_payload = {
840
- "serverSideOauth2": {
841
- "clientId": self._oauth_client_id,
842
- "clientSecret": self._oauth_client_secret,
843
- "authorizationUri": new_auth_uri,
844
- "tokenUri": "https://oauth2.googleapis.com/token",
845
- }
846
- }
847
- pr = self._http.patch(f"{DISCOVERY_ENGINE_URL}/{auth_full_name}",
848
- headers=self._get_headers(), json=patch_payload, timeout=60)
849
- pr.raise_for_status()
850
- self.info("Authorization updated.")
851
- else:
852
- self.info("Authorization scopes unchanged; no update needed.")
984
+ self.info(f"[AUTH] post-create resolve -> {auth_full_name}")
985
+
986
+ if not ok or not auth_full_name:
987
+ self.error("[AUTH] Creation failed or did not resolve.")
988
+
989
+ raise RuntimeError("Authorization creation failed.")
853
990
  else:
854
- self.info("No authorization_id configured; skipping authorization step.")
991
+ self.info("[AUTH] No authorization_id configured; skipping authorization step.")
855
992
 
856
- # -----------------------------
857
- # 3) Agent Space Agent (create or update by displayName under engine)
858
- # -----------------------------
993
+ self.info(f"[AGENT] Resolving by display_name={self._agent_space_name}")
859
994
  existing_agent = self.find_agent_space_agents_by_display(self._agent_space_name)
860
- headers, payload = self._get_agent_space_payload(engine_rn)
995
+ self.info(f"[AGENT] find_agent_space_agents_by_display -> {json.dumps(existing_agent, indent=2)}")
861
996
 
862
997
  if not existing_agent:
863
- self.info(f"Agent Space agent '{self._agent_space_name}' not found. Creating...")
998
+ headers, payload = self._get_agent_space_payload(engine_rn)
864
999
  create_url = self._get_agent_space_agent_url_new()
1000
+ self.info(f"[AGENT] POST {create_url}")
865
1001
  cr = self._http.post(create_url, headers=headers, json=payload, timeout=90)
866
- if cr.status_code >= 400:
867
- # File-only details already handled elsewhere; surface concise error here
868
- self.error(f"Agent creation failed [{cr.status_code}].")
869
- cr.raise_for_status()
1002
+ self.info(f"[AGENT] POST status={cr.status_code}")
1003
+ self.info(f"[AGENT] POST ct={cr.headers.get('content-type','')}")
1004
+ self.info(f"[AGENT] POST body={(cr.text or '')[:4000]}")
1005
+ cr.raise_for_status()
870
1006
  agent_name = (cr.json() or {}).get("name")
871
1007
  if agent_name:
872
1008
  self._write_engine_deployment({"agent_space_id": agent_name})
873
- self.info(f"Agent Space agent created: {agent_name}")
1009
+ self.info(f"[AGENT] Created: {agent_name}")
874
1010
  else:
875
- self.warning("Agent created but response missing name. Verify in console.")
1011
+ self.warning("[AGENT] Created but response missing name. Verify in console.")
876
1012
  else:
877
- self.info(f"Agent Space agent '{self._agent_space_name}' exists. Updating...")
878
- patch_url = f"{DISCOVERY_ENGINE_URL}/{existing_agent['full_name']}"
879
- ur = self._http.patch(patch_url, headers=headers, json=payload, timeout=90)
880
- if ur.status_code >= 400:
881
- self.error(f"Agent update failed [{ur.status_code}].")
882
- ur.raise_for_status()
883
- self.info("Agent Space agent updated.")
1013
+ self.info(f"[AGENT] '{self._agent_space_name}' exists. Patching metadata and auth only...")
1014
+ patched = self.patch_agent_space_metadata_and_auth(
1015
+ agent_id=existing_agent["id"],
1016
+ new_display_name=self._agent_space_name,
1017
+ new_description=self._agent_space_description,
1018
+ new_reasoning_engine=engine_rn,
1019
+ new_authorizations=[
1020
+ f"projects/{self._project_number}/locations/global/authorizations/{self._authorization_id}"
1021
+ ] if self._authorization_id else None,
1022
+ icon_uri=self._icon_uri,
1023
+ )
1024
+ self.info(f"[AGENT] PATCH result -> {json.dumps(patched, indent=2)[:2000]}")
1025
+
1026
+
1027
+ if delete_old_authorization:
1028
+ self.info(f"[AUTH] Deleting old authorization {delete_old_authorization}...")
1029
+ self._delete_authorization(delete_old_authorization)
884
1030
 
885
1031
  self.info("GitHub deployment completed successfully.")
@@ -13,7 +13,7 @@ class Runner:
13
13
  parser = argparse.ArgumentParser(description="Reasoning Engine Deployment Runner")
14
14
  parser.add_argument(
15
15
  "--mode",
16
- choices=["create", "auth", "cli", "gui", "populate_files", "github_deployment"],
16
+ choices=["create", "auth", "cli", "gui", "populate_files", "github_deployment", "github_deployment_without_engine"],
17
17
  help="Operation mode to run",
18
18
  )
19
19
  parser.add_argument(
@@ -105,7 +105,7 @@ class Runner:
105
105
  @staticmethod
106
106
  def _check_proper_configuration():
107
107
  """Ensure required environment variables are set in .env.agent."""
108
- required_vars = ['PROJECT_ID', 'PROJECT_NUMBER', 'PROJECT_LOCATION', 'AGENT_SPACE_ENGINE']
108
+ required_vars = ['PROJECT_ID', 'PROJECT_NUMBER', 'PROJECT_LOCATION']
109
109
  load_dotenv(dotenv_path=".env.agent")
110
110
 
111
111
  ok = True
@@ -133,12 +133,14 @@ class Runner:
133
133
  svc._check_required_files_exist()
134
134
  elif mode == "github_deployment":
135
135
  svc.one_github_deployment_to_go()
136
+ elif mode == "github_deployment_without_engine":
137
+ svc.one_githhub_deployment_to_go_with_skip()
136
138
 
137
139
  @staticmethod
138
140
  def _menu(root_agent):
139
141
  print("Choose an operation:\n1) Create/Update\n2) Auth only\n3) CLI\n4) GUI\nq) Quit")
140
142
  choice = input("Enter choice: ").strip().lower()
141
- mapping = {"1": "create", "2": "auth", "3": "cli", "4": "gui", "5": "github_deployment"}
143
+ mapping = {"1": "create", "2": "auth", "3": "cli", "4": "gui", "5": "github_deployment", "6": "github_deployment_without_engine"}
142
144
  if choice == "q":
143
145
  sys.exit(0)
144
146
  Runner._dispatch(mapping.get(choice, ""), root_agent)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reasoning-deployment-service
3
- Version: 0.4.0
3
+ Version: 0.8.3
4
4
  Summary: Deployment helper for Vertex AI Reasoning Engines & Agent Spaces
5
5
  Author-email: Sergio Estrada <sergio.estrada@accenture.com>
6
6
  License: Apache-2.0
@@ -1,12 +1,11 @@
1
1
  reasoning_deployment_service/__init__.py,sha256=xDuKt9gGviQiTV6vXBdkBvygnlAOIrwnUjVaMGZy0L4,670
2
- reasoning_deployment_service/reasoning_deployment_service.py,sha256=7slzcV6wKunE4Kj3dV1Fyap5D6uf1O0C-e1uhinOVSk,38211
3
- reasoning_deployment_service/runner.py,sha256=Y_SPGp26eoXRr8Ql6ugRF7jNYq3OwaytFIivuym6ICA,5428
2
+ reasoning_deployment_service/reasoning_deployment_service.py,sha256=8CLjzDYFKAwKr6Avd97YlJ1b43T8_Oa3Pqi0Xfim5C8,45298
3
+ reasoning_deployment_service/runner.py,sha256=A88IhRPPAWtqedWPWOVne3Lg16uXhXuqARKGD5ORsWc,5597
4
4
  reasoning_deployment_service/cli_editor/__init__.py,sha256=bN8NPkw8riB92pj2lAwJZuEMOQIO_RRuge0ehnJTW1I,118
5
- reasoning_deployment_service/cli_editor/api_client.py,sha256=Kzx5iYp0MmowggrSmPLE7I2kt1-8xvdGBAgde9a1gCY,33681
5
+ reasoning_deployment_service/cli_editor/api_client.py,sha256=bcuV0kEHxyNobqJ1k2Iwp73EaFjuOWa4XJ77MRrWQr0,33106
6
6
  reasoning_deployment_service/cli_editor/cli_runner.py,sha256=1KkHtgAhVZ7VHQj7o76JibLHnr7NMUB-tieDX_KrAcY,18239
7
7
  reasoning_deployment_service/cli_editor/config.py,sha256=lZ8Ng007NVdN1n5spJ0OFC72TOPFWKvPRxa9eKE-FDY,3573
8
8
  reasoning_deployment_service/cli_editor/google_deps.py,sha256=PhGwdKEC96GdlFHkQrtSJrg_-w1JoUPes3zvaz22rd0,771
9
- reasoning_deployment_service/cli_editor/reasoning_engine_creator.py,sha256=6QC8Y9yZAT8SYNkT_R00g_SSOYuwEkIxAN9lBG3br2k,19564
10
9
  reasoning_deployment_service/gui_editor/__init__.py,sha256=e5e88iNTk1GC243DRsQFi5E7PqMaT2SXmqOez9FbYzo,128
11
10
  reasoning_deployment_service/gui_editor/agent_checkbox_list.py,sha256=ElxFqSgT3iUqDv2U9eR4eV-MfLUHqOXbDz6DqEEevOk,1783
12
11
  reasoning_deployment_service/gui_editor/main.py,sha256=4UzgGUga_xIYIWRVo-80PzhJ1Dlou8PaUXoRiLcLhp8,10914
@@ -19,11 +18,11 @@ reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py,sha
19
18
  reasoning_deployment_service/gui_editor/src/ui/__init__.py,sha256=262ZiXO6Luk8vZnhCIoYxOtGiny0bXK-BTKjxUNBx-w,43
20
19
  reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py,sha256=UTUMRFEzpUuRONl3K7bsCPRjZ_hiVE1s9fTsIHTZtSs,17130
21
20
  reasoning_deployment_service/gui_editor/src/ui/authorization_view.py,sha256=BoNcGRFZ-Rb2pnOAAZxraP7yDdbwMJNvIrBrjMc_hbw,16970
22
- reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py,sha256=tCvSPEf4dW0NRdAqfs3yT5Pa873gYeLzCMMIt2r2T4o,14644
21
+ reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py,sha256=T_kBop74wHv8W7tk9aY17ty44rLu8Dc-vRZdRvhmeH0,13317
23
22
  reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py,sha256=IRjFlBbY98usAZa0roOonjvWQOsF6NBW4bBg_k8KnKI,7860
24
23
  reasoning_deployment_service/gui_editor/src/ui/ui_components.py,sha256=HdQHy-oSZ3GobQ3FNdH7y_w3ANbFiuf2rMoflAmff0A,55366
25
- reasoning_deployment_service-0.4.0.dist-info/METADATA,sha256=dwROlsOFaaHXoFeA82APxw0ZunSfFiVAYYROMqXMJWQ,5302
26
- reasoning_deployment_service-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- reasoning_deployment_service-0.4.0.dist-info/entry_points.txt,sha256=onGKjR5ONTtRv3aqEtK863iw9Ty1kLcjfZlsplkRZrA,84
28
- reasoning_deployment_service-0.4.0.dist-info/top_level.txt,sha256=GKuQS1xHUYLZbatw9DmcYdBxxLhWhhGkV4FmFxgKdp0,29
29
- reasoning_deployment_service-0.4.0.dist-info/RECORD,,
24
+ reasoning_deployment_service-0.8.3.dist-info/METADATA,sha256=hNyOtztxXHUY45RW7jQFn_n_apls8D61leAm-88wCJ0,5302
25
+ reasoning_deployment_service-0.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ reasoning_deployment_service-0.8.3.dist-info/entry_points.txt,sha256=onGKjR5ONTtRv3aqEtK863iw9Ty1kLcjfZlsplkRZrA,84
27
+ reasoning_deployment_service-0.8.3.dist-info/top_level.txt,sha256=GKuQS1xHUYLZbatw9DmcYdBxxLhWhhGkV4FmFxgKdp0,29
28
+ reasoning_deployment_service-0.8.3.dist-info/RECORD,,
@@ -1,448 +0,0 @@
1
- """Reasoning Engine creation with robust venv lifecycle and safe imports.
2
-
3
- This module provides a `ReasoningEngineCreator` that:
4
- - Creates an isolated virtual environment per engine build
5
- - Installs dependencies using the *venv's* interpreter (`python -m pip`)
6
- - Temporarily exposes the venv's site-packages to the current process for imports
7
- - Emulates activation for subprocesses via PATH/VIRTUAL_ENV
8
- - Stages a clean copy of the agent directory as an extra package
9
- - Cleans up the venv after completion
10
-
11
- Pass a config dict to `create_advanced_engine` with keys:
12
- - display_name (str)
13
- - description (str, optional)
14
- - enable_tracing (bool, optional)
15
- - requirements_source_type ("file" | "text")
16
- - requirements_file (str, optional when source_type == "file")
17
- - requirements_text (str, optional when source_type == "text")
18
- - agent_file_path (str, path to the python file exporting `root_agent`)
19
- - project_id, location, staging_bucket should be supplied to the constructor
20
- """
21
- from __future__ import annotations
22
-
23
- import importlib
24
- import importlib.util
25
- import json
26
- import os
27
- import platform
28
- import shutil
29
- import subprocess
30
- import sys
31
- import tempfile
32
- import venv
33
- from datetime import datetime
34
- from pathlib import Path
35
- from typing import Any, Dict, List, Optional, Tuple
36
-
37
- from vertexai import init as vertexai_init
38
- from vertexai.preview.reasoning_engines import AdkApp
39
- from vertexai import agent_engines
40
-
41
- # --- helpers for clean packaging ---
42
- EXCLUDES = [
43
- ".env",
44
- ".env.*",
45
- ".git",
46
- "__pycache__",
47
- ".pytest_cache",
48
- ".mypy_cache",
49
- ".DS_Store",
50
- "*.pyc",
51
- "*.pyo",
52
- "*.pyd",
53
- ".venv",
54
- "venv",
55
- "tests",
56
- "docs",
57
- ]
58
-
59
-
60
- class ReasoningEngineCreator:
61
- """
62
- Dedicated class for creating reasoning engines with advanced virtual environment management.
63
- Handles all the complex logic for venv creation, dependency installation, and deployment.
64
- """
65
-
66
- def __init__(self, project_id: str, location: str, staging_bucket: str, debug: bool = False):
67
- self.project_id = project_id
68
- self.location = location
69
- self.staging_bucket = staging_bucket
70
- self.debug = debug
71
-
72
- # Ensure staging bucket has gs:// prefix
73
- if not self.staging_bucket.startswith("gs://"):
74
- self.staging_bucket = f"gs://{self.staging_bucket}"
75
-
76
- # ---------------- Vertex init ----------------
77
- def _ensure_vertex_inited(self) -> None:
78
- """Initialize Vertex AI once and reuse."""
79
- if not getattr(self, "_vertex_inited", False):
80
- vertexai_init(project=self.project_id, location=self.location, staging_bucket=self.staging_bucket)
81
- self._vertex_inited = True
82
-
83
- # ---------------- Virtual Environment Management ----------------
84
- def _create_venv_name(self, engine_name: str) -> str:
85
- """Generate a unique virtual environment name with timestamp."""
86
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
87
- # Clean engine name for filesystem safety
88
- clean_name = "".join(c for c in engine_name if c.isalnum() or c in "_-").lower()
89
- return f"venv_{timestamp}_{clean_name}"
90
-
91
- def _deactivate_current_venv(self) -> bool:
92
- """Deactivate any currently active virtual environment."""
93
- if not os.environ.get("VIRTUAL_ENV"):
94
- if self.debug:
95
- print("📍 No virtual environment currently active.")
96
- return True
97
- if self.debug:
98
- print(f"📍 Deactivating current virtual environment: {os.environ.get('VIRTUAL_ENV')}")
99
- # No-op for current process; we'll spawn new processes with the target venv
100
- return True
101
-
102
- def _create_and_activate_venv(self, venv_name: str, project_dir: str) -> Tuple[bool, str, str]:
103
- """
104
- Create a new virtual environment.
105
- Returns: (success, venv_path, python_executable)
106
- """
107
- try:
108
- venv_base = os.path.join(os.path.expanduser("~"), ".agent_venvs")
109
- os.makedirs(venv_base, exist_ok=True)
110
- venv_path = os.path.join(venv_base, venv_name)
111
-
112
- print(f"🔧 Creating virtual environment: {venv_path}")
113
- venv.create(venv_path, with_pip=True, clear=True)
114
-
115
- if platform.system() == "Windows":
116
- python_exe = os.path.join(venv_path, "Scripts", "python.exe")
117
- else:
118
- python_exe = os.path.join(venv_path, "bin", "python")
119
-
120
- if not os.path.exists(python_exe):
121
- raise RuntimeError(f"Python executable not found at: {python_exe}")
122
-
123
- print("✅ Virtual environment created successfully")
124
- print(f"📍 Python executable: {python_exe}")
125
- return True, venv_path, python_exe
126
- except Exception as e:
127
- print(f"❌ Failed to create virtual environment: {e}")
128
- return False, "", ""
129
-
130
- def _install_requirements_in_venv(self, python_exe: str, requirements: List[str]) -> bool:
131
- """Install requirements in the specified virtual environment using interpreter-correct pip."""
132
- if not requirements:
133
- print("📍 No requirements to install.")
134
- return True
135
- try:
136
- print(f"📦 Installing {len(requirements)} requirements...")
137
- for req in requirements:
138
- print(f" 📦 Installing: {req}")
139
- cmd = [python_exe, "-m", "pip", "install", req]
140
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
141
- if result.returncode != 0:
142
- print(f"❌ Failed to install {req}\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
143
- return False
144
- print("✅ All requirements installed successfully!")
145
- return True
146
- except subprocess.TimeoutExpired:
147
- print("❌ Package installation timed out")
148
- return False
149
- except Exception as e:
150
- print(f"❌ Error installing requirements: {e}")
151
- return False
152
-
153
- def _cleanup_venv(self, venv_path: str) -> bool:
154
- """Remove the virtual environment directory."""
155
- if not venv_path or not os.path.exists(venv_path):
156
- if self.debug:
157
- print("📍 Virtual environment path doesn't exist, nothing to clean up.")
158
- return True
159
- try:
160
- print(f"🧹 Cleaning up virtual environment: {venv_path}")
161
- shutil.rmtree(venv_path)
162
- print("✅ Virtual environment cleaned up successfully!")
163
- return True
164
- except Exception as e:
165
- print(f"⚠️ Warning: Failed to clean up virtual environment: {e}")
166
- return False
167
-
168
- def _add_venv_to_sys_path(self, python_exe: str) -> Optional[str]:
169
- """Add the virtual environment's site-packages to sys.path for imports."""
170
- try:
171
- code = "import sysconfig, json; print(json.dumps(sysconfig.get_paths()))"
172
- result = subprocess.check_output([python_exe, "-c", code], text=True)
173
- paths = json.loads(result.strip())
174
- site_pkgs = paths.get("purelib") or paths.get("platlib")
175
- if site_pkgs and site_pkgs not in sys.path:
176
- print(f"📍 Adding venv site-packages to sys.path: {site_pkgs}")
177
- sys.path.insert(0, site_pkgs)
178
- return site_pkgs
179
- return site_pkgs
180
- except Exception as e:
181
- print(f"⚠️ Warning: Could not add venv to sys.path: {e}")
182
- return None
183
-
184
- def _remove_venv_from_sys_path(self, site_pkgs_path: Optional[str]) -> None:
185
- """Remove the virtual environment's site-packages from sys.path."""
186
- if site_pkgs_path and site_pkgs_path in sys.path:
187
- sys.path.remove(site_pkgs_path)
188
- print(f"📍 Removed venv site-packages from sys.path: {site_pkgs_path}")
189
-
190
- def _push_venv_envvars(self, venv_path: str) -> None:
191
- """Temporarily emulate activation for subprocesses/tools."""
192
- self._old_env = {
193
- "PATH": os.environ.get("PATH", ""),
194
- "VIRTUAL_ENV": os.environ.get("VIRTUAL_ENV"),
195
- }
196
- bin_dir = os.path.join(venv_path, "Scripts" if platform.system() == "Windows" else "bin")
197
- os.environ["VIRTUAL_ENV"] = venv_path
198
- os.environ["PATH"] = bin_dir + os.pathsep + self._old_env["PATH"]
199
-
200
- def _pop_venv_envvars(self) -> None:
201
- if hasattr(self, "_old_env"):
202
- os.environ["PATH"] = self._old_env["PATH"]
203
- if self._old_env["VIRTUAL_ENV"] is None:
204
- os.environ.pop("VIRTUAL_ENV", None)
205
- else:
206
- os.environ["VIRTUAL_ENV"] = self._old_env["VIRTUAL_ENV"]
207
- del self._old_env
208
-
209
- # ---------------- Validation ----------------
210
- def _assert_no_google_shadow(self, agent_dir: str) -> None:
211
- """Ensure no local package named 'google' shadows site-packages."""
212
- local_google = os.path.join(agent_dir, "google")
213
- if os.path.isdir(local_google) or os.path.isfile(local_google + ".py"):
214
- raise RuntimeError(
215
- f"Found local '{local_google}'. This will shadow 'google.adk'. "
216
- "Rename/remove it or move agent code under a different package."
217
- )
218
-
219
- # ---------------- Agent Loading and Staging ----------------
220
- def _stage_clean_copy(self, src_dir: str) -> str:
221
- """Copy agent directory to temp dir, excluding dev files and secrets."""
222
- src = Path(src_dir).resolve()
223
- dst_root = Path(tempfile.mkdtemp(prefix="agent_stage_"))
224
- dst = dst_root / src.name
225
-
226
- if self.debug:
227
- print("📦 Staging agent directory...")
228
- print(f"📁 Source: {src}")
229
- print(f"📁 Destination: {dst}")
230
- print(f"🚫 Excluding: {EXCLUDES}")
231
- if src.exists():
232
- print("📋 Source contents:")
233
- for item in sorted(src.iterdir()):
234
- print(f" {'📁' if item.is_dir() else '📄'} {item.name}{'/' if item.is_dir() else ''}")
235
-
236
- shutil.copytree(src, dst, ignore=shutil.ignore_patterns(*EXCLUDES), dirs_exist_ok=True)
237
-
238
- if self.debug:
239
- print("📋 Staged contents:")
240
- for item in sorted(dst.iterdir()):
241
- print(f" {'📁' if item.is_dir() else '📄'} {item.name}{'/' if item.is_dir() else ''}")
242
-
243
- # Clean up .env files
244
- for p in dst.rglob(".env*"):
245
- try:
246
- p.unlink()
247
- if self.debug:
248
- print(f"🗑️ Removed: {p}")
249
- except Exception:
250
- pass
251
-
252
- if self.debug:
253
- print(f"✅ Staging complete: {dst}")
254
- return str(dst)
255
-
256
- def _load_agent_from_file(self, agent_file_path: str):
257
- """Load root_agent from a Python file, handling relative imports properly."""
258
- agent_file = Path(agent_file_path).resolve()
259
- if not agent_file.exists():
260
- raise RuntimeError(f"Agent file not found: {agent_file}")
261
-
262
- agent_dir = agent_file.parent
263
- package_name = agent_dir.name
264
- module_name = f"{package_name}.{agent_file.stem}"
265
-
266
- parent_dir = str(agent_dir.parent)
267
- agent_dir_str = str(agent_dir)
268
-
269
- print(f"🤖 Loading {agent_file.stem} from: {agent_file}")
270
- print(f"📁 Agent directory: {agent_dir}")
271
- print(f"📦 Package name: {package_name}")
272
- print(f"🔧 Module name: {module_name}")
273
- if self.debug:
274
- print(f"🛤️ Adding to sys.path: {parent_dir} (for package imports)")
275
- print(f"🛤️ Adding to sys.path: {agent_dir_str} (for absolute imports like 'tools')")
276
-
277
- # Add both parent directory and agent directory
278
- paths_added: List[str] = []
279
- if parent_dir not in sys.path:
280
- sys.path.insert(0, parent_dir)
281
- paths_added.append(parent_dir)
282
- if agent_dir_str not in sys.path:
283
- sys.path.insert(0, agent_dir_str)
284
- paths_added.append(agent_dir_str)
285
-
286
- try:
287
- # Optionally create a package for proper relative import resolution
288
- init_py = agent_dir / "__init__.py"
289
- package_spec = importlib.util.spec_from_file_location(package_name, init_py if init_py.exists() else None)
290
- if package_spec:
291
- package_module = importlib.util.module_from_spec(package_spec)
292
- sys.modules[package_name] = package_module
293
- if package_spec.loader and init_py.exists():
294
- package_spec.loader.exec_module(package_module)
295
-
296
- spec = importlib.util.spec_from_file_location(module_name, agent_file)
297
- if spec is None or spec.loader is None:
298
- raise RuntimeError(f"Could not load module spec from {agent_file}")
299
-
300
- module = importlib.util.module_from_spec(spec)
301
- module.__package__ = package_name # help relative imports
302
- sys.modules[module_name] = module
303
- spec.loader.exec_module(module)
304
-
305
- if not hasattr(module, "root_agent"):
306
- raise RuntimeError(f"Module '{agent_file}' does not define `root_agent`.")
307
- print(f"✅ Successfully loaded root_agent from {agent_file}")
308
- return getattr(module, "root_agent")
309
- except Exception as e:
310
- print(f"❌ Failed to load agent: {e}")
311
- raise RuntimeError(f"Failed to execute agent module {agent_file}: {e}") from e
312
- finally:
313
- for path in reversed(paths_added):
314
- while path in sys.path:
315
- sys.path.remove(path)
316
- # clean any submodules from this package
317
- mods_to_remove = [name for name in list(sys.modules.keys()) if name.startswith(package_name)]
318
- for name in mods_to_remove:
319
- sys.modules.pop(name, None)
320
-
321
- # ---------------- Utilities ----------------
322
- @staticmethod
323
- def _merge_requirements(baseline: List[str], user: List[str]) -> List[str]:
324
- seen = set()
325
- out: List[str] = []
326
- for seq in (baseline, user):
327
- for item in seq:
328
- key = item.strip().lower()
329
- if not key or key in seen:
330
- continue
331
- seen.add(key)
332
- out.append(item.strip())
333
- return out
334
-
335
- # ---------------- Main Creation Method ----------------
336
- def create_advanced_engine(self, config: Dict[str, Any]) -> Tuple[str, str, Optional[str]]:
337
- """Create a reasoning engine with advanced configuration options."""
338
- print("🚀 Starting advanced reasoning engine creation...")
339
- if self.debug:
340
- print("📋 Configuration:")
341
- try:
342
- print(json.dumps(config, indent=2))
343
- except Exception:
344
- print(str(config))
345
-
346
- try:
347
- display_name = config["display_name"]
348
- description = config.get("description", "")
349
- enable_tracing = config.get("enable_tracing", True)
350
-
351
- # Requirements
352
- requirements: List[str] = []
353
- if config["requirements_source_type"] == "file":
354
- req_file = config.get("requirements_file")
355
- if req_file and os.path.exists(req_file):
356
- with open(req_file, "r", encoding="utf-8") as f:
357
- requirements = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
358
- elif config["requirements_source_type"] == "text":
359
- requirements_text = config.get("requirements_text", "").strip()
360
- requirements = [line.strip() for line in requirements_text.splitlines() if line.strip() and not line.strip().startswith("#")]
361
-
362
- # Ensure baseline ADK deps exist in the build venv (idempotent)
363
- baseline = [
364
- "google-adk>=1.0.0",
365
- "google-cloud-aiplatform[agent_engines]>=1.93.0,<2.0.0",
366
- "google-genai>=1.16.1,<2.0.0",
367
- ]
368
- requirements = self._merge_requirements(baseline, requirements)
369
-
370
- # Paths
371
- agent_file_path = config["agent_file_path"]
372
- agent_dir = os.path.dirname(agent_file_path)
373
-
374
- # venv lifecycle
375
- print("🌐 Setting up isolated virtual environment...")
376
- self._deactivate_current_venv()
377
- venv_name = self._create_venv_name(display_name)
378
- v_success, venv_path, python_exe = self._create_and_activate_venv(venv_name, agent_dir)
379
- if not v_success:
380
- raise RuntimeError("Failed to create virtual environment")
381
-
382
- # installs
383
- if requirements:
384
- if not self._install_requirements_in_venv(python_exe, requirements):
385
- self._cleanup_venv(venv_path)
386
- raise RuntimeError("Failed to install requirements in virtual environment")
387
-
388
- # make imports & subprocesses behave like activated
389
- self._push_venv_envvars(venv_path)
390
- venv_site_pkgs = self._add_venv_to_sys_path(python_exe)
391
-
392
- try:
393
- # quick guard against local google/ package
394
- self._assert_no_google_shadow(agent_dir)
395
-
396
- print("🔍 Checking agent directory structure...")
397
- agent_path = Path(agent_dir)
398
- tools_path = agent_path / "tools"
399
- if tools_path.exists() and self.debug:
400
- tool_files = [p.name for p in tools_path.glob("*.py")]
401
- print(f"✅ Found tools directory with files: {tool_files}")
402
- elif not tools_path.exists():
403
- print(f"❌ WARNING: tools directory not found at {tools_path}")
404
-
405
- staged_dir = self._stage_clean_copy(agent_dir)
406
- staged_tools = Path(staged_dir) / "tools"
407
- if not staged_tools.exists():
408
- print("❌ ERROR: Tools directory missing from staged copy!")
409
- raise RuntimeError("Tools directory was not properly staged")
410
-
411
- # load agent
412
- print(f"🤖 Loading root_agent from: {agent_file_path}")
413
- root_agent = self._load_agent_from_file(agent_file_path)
414
-
415
- # vertex init + create
416
- self._ensure_vertex_inited()
417
- print("🚀 Creating reasoning engine with venv dependencies…")
418
- app = AdkApp(agent=root_agent, enable_tracing=enable_tracing)
419
- remote = agent_engines.create(
420
- app,
421
- display_name=display_name,
422
- description=description,
423
- requirements=requirements,
424
- extra_packages=[staged_dir],
425
- )
426
-
427
- print("✅ Engine creation successful!")
428
- return (
429
- "created",
430
- f"Advanced engine '{display_name}' created successfully",
431
- remote.resource_name,
432
- )
433
-
434
- except Exception as e:
435
- print(f"❌ Deployment failed: {e}")
436
- raise
437
- finally:
438
- # undo import/env tweaks, then remove the venv
439
- if venv_site_pkgs:
440
- self._remove_venv_from_sys_path(venv_site_pkgs)
441
- self._pop_venv_envvars()
442
- print("🧹 Cleaning up virtual environment...")
443
- self._cleanup_venv(venv_path)
444
-
445
- except Exception as e:
446
- import traceback
447
- traceback.print_exc()
448
- return ("failed", f"Creation failed: {str(e)}", None)