reasoning-deployment-service 0.3.5__py3-none-any.whl → 0.3.7__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.

@@ -16,28 +16,44 @@ class AuthorizationView(ttk.Frame):
16
16
  self.api = api
17
17
  self.log = log
18
18
  self._auth_auto_loaded = False # Track if authorizations have been auto-loaded
19
-
20
19
  # Cache authentication state to avoid repeated API calls
21
20
  self._cached_auth_state = None
22
21
  self._last_auth_check = 0
23
22
  self._auth_cache_duration = 30 # 30 seconds
24
-
25
23
  self._setup_ui()
26
24
 
27
25
  def _setup_ui(self):
28
26
  # Control buttons
29
27
  btns = ttk.Frame(self)
30
28
  btns.pack(fill="x", pady=(6, 4))
31
-
29
+
32
30
  self.refresh_btn = StatusButton(btns, text="Refresh Authorizations", command=self.refresh)
33
31
  self.refresh_btn.pack(side="left", padx=4)
34
-
32
+
35
33
  self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
36
34
  self.delete_btn.pack(side="left", padx=8)
37
-
35
+
38
36
  self.status = tk.StringVar(value="Ready.")
39
37
  ttk.Label(btns, textvariable=self.status).pack(side="right")
40
38
 
39
+ # Update scopes UI
40
+ scopes_frame = ttk.LabelFrame(self, text="Update Authorization Scopes")
41
+ scopes_frame.pack(fill="x", padx=4, pady=4)
42
+ ttk.Label(scopes_frame, text="Authorization ID:").grid(row=0, column=0, sticky="w")
43
+ self.auth_id_entry = ttk.Entry(scopes_frame, width=30)
44
+ self.auth_id_entry.grid(row=0, column=1, sticky="w")
45
+ ttk.Label(scopes_frame, text="Scopes:").grid(row=1, column=0, sticky="nw")
46
+ self.scopes_var_list = []
47
+ self.scopes_check_frame = ttk.Frame(scopes_frame)
48
+ self.scopes_check_frame.grid(row=1, column=1, sticky="nw")
49
+ self.add_scope_btn = ttk.Button(scopes_frame, text="Add Scope", command=self._add_scope_dialog)
50
+ self.add_scope_btn.grid(row=2, column=1, sticky="w", pady=(2,0))
51
+ ttk.Label(scopes_frame, text="OAuth Client ID:").grid(row=2, column=0, sticky="w")
52
+ self.client_id_entry = ttk.Entry(scopes_frame, width=40)
53
+ self.client_id_entry.grid(row=2, column=1, sticky="w")
54
+ self.update_scopes_btn = ttk.Button(scopes_frame, text="Update Scopes", command=self.update_scopes)
55
+ self.update_scopes_btn.grid(row=3, column=0, columnspan=2, pady=4)
56
+
41
57
  # Info label
42
58
  info_frame = ttk.Frame(self)
43
59
  info_frame.pack(fill="x", padx=4, pady=4)
@@ -49,40 +65,94 @@ class AuthorizationView(ttk.Frame):
49
65
  wrap.pack(fill="both", expand=True)
50
66
  cols = ("id", "name")
51
67
  self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
52
-
68
+
53
69
  for c, t, w in [
54
70
  ("id", "Authorization ID", 300),
55
71
  ("name", "Full Resource Name", 600),
56
72
  ]:
57
73
  self.tree.heading(c, text=t)
58
74
  self.tree.column(c, width=w, anchor="w")
59
-
75
+
60
76
  self.tree.pack(side="left", fill="both", expand=True)
61
77
  vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
62
78
  self.tree.configure(yscroll=vsb.set)
63
79
  vsb.pack(side="right", fill="y")
64
-
80
+
65
81
  # Event bindings
66
82
  self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
67
83
  self.tree.bind("<Button-3>", self._popup)
68
-
84
+
69
85
  # Context menu
70
86
  self.menu = tk.Menu(self, tearoff=0)
71
87
  self.menu.add_command(label="Delete", command=self.delete_selected)
72
-
88
+
73
89
  # Debouncing for button updates
74
90
  self._update_timer = None
75
-
91
+
76
92
  # Store full authorization data
77
93
  self._authorizations_data = {}
78
-
94
+
79
95
  # Cache selection state to avoid redundant tree.selection() calls
80
96
  self._cached_selection = None
81
97
  self._selection_is_dirty = True
82
-
98
+
83
99
  # Initialize button states without immediate API calls
84
100
  self._update_button_states()
85
101
 
102
+ def update_scopes(self):
103
+ """Update scopes for the given authorization."""
104
+ auth_id = self.auth_id_entry.get().strip()
105
+ scopes = [var.get().strip() for var, cb_var in self.scopes_var_list if cb_var.get() and var.get().strip()]
106
+ client_id = self.client_id_entry.get().strip()
107
+ if not auth_id or not scopes or not client_id:
108
+ messagebox.showwarning("Missing info", "Please fill all fields.")
109
+ return
110
+ self.status.set(f"Updating scopes for {auth_id}...")
111
+ def callback(result):
112
+ if isinstance(result, Exception):
113
+ self.status.set(f"Error: {result}")
114
+ self.log(f"❌ Error updating scopes: {result}")
115
+ return
116
+ self.status.set(f"Scopes updated for {auth_id}.")
117
+ self.log(f"✅ Scopes updated for {auth_id}: {result}")
118
+ async_operation(lambda: self.api.update_authorization_scopes(auth_id, scopes, client_id), callback=callback, ui_widget=self)
119
+
120
+ def _add_scope_dialog(self):
121
+ dialog = tk.Toplevel(self)
122
+ dialog.title("Add Scope")
123
+ ttk.Label(dialog, text="Enter new scope:").pack(padx=8, pady=8)
124
+ entry = ttk.Entry(dialog, width=50)
125
+ entry.pack(padx=8, pady=(0,8))
126
+ def on_add():
127
+ scope = entry.get().strip()
128
+ if scope:
129
+ self._add_scope_checkbox(scope, checked=True)
130
+ dialog.destroy()
131
+ ttk.Button(dialog, text="Add", command=on_add).pack(pady=(0,8))
132
+ entry.focus_set()
133
+ dialog.transient(self)
134
+ dialog.grab_set()
135
+ self.wait_window(dialog)
136
+
137
+ def _add_scope_checkbox(self, scope, checked=False):
138
+ var = tk.StringVar(value=scope)
139
+ cb_var = tk.BooleanVar(value=checked)
140
+ frame = ttk.Frame(self.scopes_check_frame)
141
+ cb = ttk.Checkbutton(frame, text=scope, variable=cb_var)
142
+ cb.pack(side="left")
143
+ del_btn = ttk.Button(frame, text="✕", width=2, command=lambda: self._remove_scope_checkbox(frame, var, cb_var))
144
+ del_btn.pack(side="left", padx=(4,0))
145
+ frame.pack(anchor="w", pady=1)
146
+ self.scopes_var_list.append((var, cb_var))
147
+ # Update checkbutton text if scope changes
148
+ def update_text(*_):
149
+ cb.config(text=var.get())
150
+ var.trace_add('write', update_text)
151
+
152
+ def _remove_scope_checkbox(self, frame, var, cb_var):
153
+ frame.destroy()
154
+ self.scopes_var_list = [(v, c) for v, c in self.scopes_var_list if v != var]
155
+
86
156
  def _get_cached_auth_state(self) -> bool:
87
157
  """Get authentication state with local caching to reduce API calls."""
88
158
  now = time.time()
@@ -127,6 +197,50 @@ class AuthorizationView(ttk.Frame):
127
197
  # Immediate update - no timers or delays
128
198
  self._update_button_states()
129
199
 
200
+ # Auto-populate update scopes fields if a single authorization is selected
201
+ sel = self._get_selection()
202
+ if len(sel) == 1:
203
+ item_id = sel[0]
204
+ auth_data = self._authorizations_data.get(item_id)
205
+ if auth_data:
206
+ self.auth_id_entry.delete(0, tk.END)
207
+ self.auth_id_entry.insert(0, auth_data.get("id", ""))
208
+ # Try to fetch scopes and client id from API if possible
209
+ try:
210
+ info = self.api.get_authorization_info(auth_data.get("id", ""))
211
+ import pprint
212
+ print("[DEBUG] get_authorization_info response:")
213
+ pprint.pprint(info)
214
+ scopes = []
215
+ sso = info.get("serverSideOauth2", {})
216
+ # Prefer parsing scopes from authorizationUri if present
217
+ if "authorizationUri" in sso and isinstance(sso["authorizationUri"], str):
218
+ import urllib.parse
219
+ parsed = urllib.parse.parse_qs(urllib.parse.urlparse(sso["authorizationUri"]).query)
220
+ scope_str = parsed.get("scope", [""])[0]
221
+ if scope_str:
222
+ # Split scopes by space, comma, or plus, and decode each
223
+ import re
224
+ raw_scopes = re.split(r"[ ,+]+", scope_str)
225
+ scopes = [urllib.parse.unquote_plus(s) for s in raw_scopes if s.strip()]
226
+ # Fallback to scopes field if present
227
+ if not scopes and "scopes" in info:
228
+ scopes = info["scopes"]
229
+ # Clear previous checkboxes
230
+ for child in self.scopes_check_frame.winfo_children():
231
+ child.destroy()
232
+ self.scopes_var_list.clear()
233
+ # Add one checkbox per parsed scope
234
+ for scope in scopes:
235
+ self._add_scope_checkbox(scope, checked=True)
236
+ client_id = sso.get("clientId", "")
237
+ if client_id:
238
+ self.client_id_entry.delete(0, tk.END)
239
+ self.client_id_entry.insert(0, client_id)
240
+ except Exception as e:
241
+ print(f"[ERROR] Exception in _on_selection_change: {e}")
242
+ pass
243
+
130
244
  def refresh(self):
131
245
  """Refresh the list of authorizations."""
132
246
  # Update button states immediately on click
@@ -1,15 +1,17 @@
1
1
  import json, os, subprocess, yaml, sys
2
2
  import urllib.parse, vertexai, google.auth
3
3
  import requests as _requests
4
- from typing import Tuple
4
+ from typing import Dict, Optional, Tuple
5
5
  from pathlib import Path
6
6
  from dotenv import load_dotenv
7
7
  from vertexai import agent_engines
8
+ from vertexai.agent_engines import AgentEngine
8
9
  from google.adk.agents import BaseAgent
9
10
  from google.auth.transport.requests import Request as GoogleAuthRequest
10
11
  from google.api_core.exceptions import NotFound
11
12
  import logging
12
13
  from datetime import datetime
14
+ from urllib.parse import urlparse, parse_qs
13
15
 
14
16
  DISCOVERY_ENGINE_URL = "https://discoveryengine.googleapis.com/v1alpha"
15
17
 
@@ -101,9 +103,9 @@ class ReasoningEngineDeploymentService:
101
103
  self._generate_env_agent()
102
104
  end_run = True
103
105
 
104
- if not os.path.exists("aix_agent.yaml"):
106
+ if not os.path.exists("agent.yaml"):
105
107
  self._generate_example_yaml_config()
106
- self.warning("Creating aix_agent.yaml file ... done")
108
+ self.warning("Creating agent.yaml file ... done")
107
109
  end_run = True
108
110
 
109
111
  self.warning("Please fill in the required values in the generated files and re-run the deployment.")
@@ -246,7 +248,7 @@ class ReasoningEngineDeploymentService:
246
248
  for pattern in new_patterns:
247
249
  f.write(f"{pattern}\n")
248
250
 
249
- def _generate_example_yaml_config(self, path: str | Path = "aix_agent.yaml", overwrite: bool = False) -> Path:
251
+ def _generate_example_yaml_config(self, path: str | Path = "agent.yaml", overwrite: bool = False) -> Path:
250
252
  """
251
253
  Create an example YAML config matching the requested schema.
252
254
 
@@ -289,11 +291,11 @@ class ReasoningEngineDeploymentService:
289
291
 
290
292
  def _load_agent_definition(self):
291
293
  try:
292
- with open("aix_agent.yaml", "r") as f:
294
+ with open("agent.yaml", "r") as f:
293
295
  config = yaml.safe_load(f)
294
296
  except FileNotFoundError:
295
297
  self._generate_example_yaml_config()
296
- self.error("Could not locate a valid aix_agent.yaml file. Generating example file in your directory.")
298
+ self.error("Could not locate a valid agent.yaml file. Generating example file in your directory.")
297
299
  sys.exit(1)
298
300
 
299
301
  try:
@@ -320,7 +322,7 @@ class ReasoningEngineDeploymentService:
320
322
  self._authorization_id = auth.get("oauth_authorization_id", None)
321
323
  self._environment_variables = environment_variables or []
322
324
  except KeyError as e:
323
- raise RuntimeError(f"Missing required key in aix_agent.yaml: {e}")
325
+ raise RuntimeError(f"Missing required key in agent.yaml: {e}")
324
326
 
325
327
  def _load_deployment_environment_variables(self, deployment_environment: str):
326
328
  required_vars = ['PROJECT_ID', 'PROJECT_NUMBER', 'PROJECT_LOCATION', 'STAGING_BUCKET', 'AGENT_SPACE_ENGINE']
@@ -684,4 +686,202 @@ class ReasoningEngineDeploymentService:
684
686
  self.info("Creating agent space ... ")
685
687
  self._deploy_to_agent_space()
686
688
  else:
687
- self._update_in_agent_space()
689
+ self._update_in_agent_space()
690
+
691
+
692
+ def _ensure_vertex_inited(self):
693
+ """Initialize Vertex AI once and reuse to avoid repeated heavy init calls."""
694
+ if not self._vertex_inited:
695
+ vertexai.init()
696
+ self._vertex_inited = True
697
+
698
+
699
+ def find_engine_by_name(self, engine_name: str) -> Optional[str]:
700
+ engines = list(agent_engines.list(filter=f'display_name="{engine_name}"'))
701
+ if not engines:
702
+ return None
703
+ if len(engines) > 1:
704
+ raise RuntimeError(f"Multiple engines found with display_name='{engine_name}'. Use unique names/labels.")
705
+ eng = engines[0]
706
+ if not isinstance(eng, AgentEngine) or not hasattr(eng, "resource_name"):
707
+ raise RuntimeError("Unexpected engine object; missing AgentEngine/resource_name.")
708
+ return eng.resource_name
709
+
710
+
711
+ def _get_headers(self) -> dict:
712
+ return {
713
+ "Authorization": f"Bearer {self._access_token()}",
714
+ "Content-Type": "application/json",
715
+ "Accept": "application/json",
716
+ "X-Goog-User-Project": str(self._project_number),
717
+ }
718
+
719
+ def find_authorization_by_id(self, authorization_id: str) -> Optional[str]:
720
+ name = f"projects/{self._project_id}/locations/global/authorizations/{authorization_id}"
721
+ url = f"{DISCOVERY_ENGINE_URL}/{name}"
722
+ r = self._http.get(url, headers=self._get_headers(), timeout=60)
723
+
724
+ if r.status_code == 404:
725
+ return None
726
+
727
+ r.raise_for_status()
728
+
729
+ return r.json().get("name", name)
730
+
731
+ def find_agent_space_agents_by_display(self, display_name: str) -> Optional[dict]:
732
+ base = (
733
+ f"{DISCOVERY_ENGINE_URL}/projects/{self._project_id}/locations/global/"
734
+ f"collections/default_collection/engines/{self._agent_space_engine}/"
735
+ f"assistants/default_assistant/agents"
736
+ )
737
+
738
+ headers = self._get_headers()
739
+ page = None
740
+ matches =[]
741
+
742
+ while True:
743
+ url = base + (f"?pageToken={page}" if page else "")
744
+ r = self._http.get(url, headers=headers, timeout=60)
745
+ r.raise_for_status()
746
+ data = r.json()
747
+
748
+ for a in data.get("agents", []):
749
+ if a.get("displayName") != display_name:
750
+ continue
751
+ full = a.get("name", "")
752
+ matches.append({
753
+ "id": full.split("/")[-1] if full else "",
754
+ "display_name": a.get("displayName", ""),
755
+ "full_name": full,
756
+ "labels": a.get("labels", {}),
757
+ })
758
+
759
+ page = data.get("nextPageToken")
760
+ if not page:
761
+ break
762
+
763
+ if not matches:
764
+ return None
765
+ if len(matches) > 1:
766
+ raise RuntimeError(
767
+ f"Found {len(matches)} agents with displayName='{display_name}'. "
768
+ "Provide an ID or labels (e.g., env/app/agent) to disambiguate."
769
+ )
770
+
771
+ return matches[0]
772
+
773
+
774
+ def one_github_deployment_to_go(self):
775
+ """
776
+ CI-friendly deploy:
777
+ - Engine: create or update by display_name.
778
+ - Authorization: create if missing; patch if scopes changed.
779
+ - Agent Space: create if missing; patch if found (by displayName under engine).
780
+ """
781
+ self.info("Starting GitHub deployment...")
782
+
783
+ # Ensure Vertex SDK calls have context for list/update
784
+ vertexai.init(
785
+ project=self._project_id,
786
+ location=self._project_location,
787
+ staging_bucket=self._staging_bucket,
788
+ )
789
+
790
+ # -----------------------------
791
+ # 1) Reasoning Engine (create or update)
792
+ # -----------------------------
793
+ engine_rn = self.find_engine_by_name(self._reasoning_engine_name)
794
+ if not engine_rn:
795
+ self.info(f"Engine '{self._reasoning_engine_name}' not found. Creating...")
796
+ self.create_reasoning_engine()
797
+ # read back the created id (or re-resolve by name as fallback)
798
+ engine_rn = self._read_engine_deployment_record().get("reasoning_engine_id") or \
799
+ self.find_engine_by_name(self._reasoning_engine_name)
800
+ if not engine_rn:
801
+ self.error("Engine creation did not yield a resource name.")
802
+ raise RuntimeError("Engine creation failed.")
803
+ else:
804
+ self.info(f"Engine '{self._reasoning_engine_name}' exists. Updating...")
805
+ self.update_reasoning_engine(engine_rn)
806
+
807
+ # -----------------------------
808
+ # 2) Authorization (create if missing; update scopes if changed)
809
+ # -----------------------------
810
+ if self._authorization_id:
811
+ want_scopes = set(self._required_scopes or [])
812
+ auth_full_name = self.find_authorization_by_id(self._authorization_id)
813
+
814
+ if not auth_full_name:
815
+ self.info(f"Authorization '{self._authorization_id}' not found. Creating...")
816
+ ok = self._create_authorization()
817
+ if not ok:
818
+ self.error("Authorization creation failed.")
819
+ raise RuntimeError("Authorization creation failed.")
820
+ auth_full_name = self.find_authorization_by_id(self._authorization_id)
821
+ if not auth_full_name:
822
+ self.error("Authorization creation did not resolve to a resource.")
823
+ raise RuntimeError("Authorization creation failed to resolve.")
824
+ else:
825
+ # Compare scopes; patch if different
826
+ r = self._http.get(f"{DISCOVERY_ENGINE_URL}/{auth_full_name}",
827
+ headers=self._get_headers(), timeout=60)
828
+ r.raise_for_status()
829
+ data = r.json() or {}
830
+ existing_uri = (((data.get("serverSideOauth2") or {}).get("authorizationUri")) or "")
831
+ existing_scopes = set()
832
+ if existing_uri:
833
+ parsed = urlparse(existing_uri)
834
+ qs = parse_qs(parsed.query)
835
+ scope_str = (qs.get("scope", [""])[0] or "")
836
+ existing_scopes = set(scope_str.split())
837
+
838
+ if existing_scopes != want_scopes:
839
+ self.info("Authorization scopes changed. Patching authorization...")
840
+ new_auth_uri = self._build_authorization_uri(self._oauth_client_id, list(want_scopes))
841
+ patch_payload = {
842
+ "serverSideOauth2": {
843
+ "clientId": self._oauth_client_id,
844
+ "clientSecret": self._oauth_client_secret,
845
+ "authorizationUri": new_auth_uri,
846
+ "tokenUri": "https://oauth2.googleapis.com/token",
847
+ }
848
+ }
849
+ pr = self._http.patch(f"{DISCOVERY_ENGINE_URL}/{auth_full_name}",
850
+ headers=self._get_headers(), json=patch_payload, timeout=60)
851
+ pr.raise_for_status()
852
+ self.info("Authorization updated.")
853
+ else:
854
+ self.info("Authorization scopes unchanged; no update needed.")
855
+ else:
856
+ self.info("No authorization_id configured; skipping authorization step.")
857
+
858
+ # -----------------------------
859
+ # 3) Agent Space Agent (create or update by displayName under engine)
860
+ # -----------------------------
861
+ existing_agent = self.find_agent_space_agents_by_display(self._agent_space_name)
862
+ headers, payload = self._get_agent_space_payload(engine_rn)
863
+
864
+ if not existing_agent:
865
+ self.info(f"Agent Space agent '{self._agent_space_name}' not found. Creating...")
866
+ create_url = self._get_agent_space_agent_url_new()
867
+ cr = self._http.post(create_url, headers=headers, json=payload, timeout=90)
868
+ if cr.status_code >= 400:
869
+ # File-only details already handled elsewhere; surface concise error here
870
+ self.error(f"Agent creation failed [{cr.status_code}].")
871
+ cr.raise_for_status()
872
+ agent_name = (cr.json() or {}).get("name")
873
+ if agent_name:
874
+ self._write_engine_deployment({"agent_space_id": agent_name})
875
+ self.info(f"Agent Space agent created: {agent_name}")
876
+ else:
877
+ self.warning("Agent created but response missing name. Verify in console.")
878
+ else:
879
+ self.info(f"Agent Space agent '{self._agent_space_name}' exists. Updating...")
880
+ patch_url = f"{DISCOVERY_ENGINE_URL}/{existing_agent['full_name']}"
881
+ ur = self._http.patch(patch_url, headers=headers, json=payload, timeout=90)
882
+ if ur.status_code >= 400:
883
+ self.error(f"Agent update failed [{ur.status_code}].")
884
+ ur.raise_for_status()
885
+ self.info("Agent Space agent updated.")
886
+
887
+ self.info("GitHub deployment completed successfully.")
@@ -29,8 +29,8 @@ class Runner:
29
29
  root_agent = Runner._load_agent(args.agent_path)
30
30
 
31
31
  # --- Require config files ---
32
- if not Path(".env.agent").exists() or not Path("aix_agent.yaml").exists():
33
- print("Missing .env.agent or aix_agent.yaml.")
32
+ if not Path(".env.agent").exists() or not Path("agent.yaml").exists():
33
+ print("Missing .env.agent or agent.yaml.")
34
34
  print("Options:\n 5) Generate placeholder config\n q) Quit")
35
35
  choice = input("Enter choice: ").strip()
36
36
  if choice == "5":
@@ -132,6 +132,8 @@ class Runner:
132
132
  GUIEditor().run()
133
133
  elif mode == "populate_files":
134
134
  svc._check_required_files_exist()
135
+ elif mode == "github_deployment":
136
+ svc.one_github_deployment_to_go()
135
137
 
136
138
  @staticmethod
137
139
  def _menu(root_agent):