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.

@@ -30,6 +30,27 @@ EXCLUDES = [
30
30
 
31
31
 
32
32
  class ApiClient:
33
+ def get_authorization_info(self, auth_id: str) -> dict:
34
+ """Get details for a specific authorization by ID."""
35
+ if not self.is_live:
36
+ # Return mock info for testing
37
+ return {
38
+ "id": auth_id,
39
+ "scopes": [
40
+ "https://www.googleapis.com/auth/cloud-platform",
41
+ "https://www.googleapis.com/auth/userinfo.email"
42
+ ],
43
+ "status": "mock"
44
+ }
45
+ headers = {
46
+ "Authorization": f"Bearer {self._access_token()}",
47
+ "Content-Type": "application/json",
48
+ "X-Goog-User-Project": self.project_id,
49
+ }
50
+ url = f"https://discoveryengine.googleapis.com/v1alpha/projects/{self.project_id}/locations/global/authorizations/{auth_id}"
51
+ r = self._http.get(url, headers=headers, timeout=60)
52
+ r.raise_for_status()
53
+ return r.json()
33
54
  """
34
55
  Single responsibility: hold configuration & credentials and expose API calls.
35
56
  This class has both 'live' and 'mock' modes; the public surface is identical.
@@ -559,6 +580,63 @@ class ApiClient:
559
580
  else:
560
581
  return ("failed", f"{r.status_code} {r.text}")
561
582
 
583
+ def get_authorization_info(self, auth_id: str) -> Dict[str, Any]:
584
+ """Get details for a specific authorization by ID."""
585
+ if not self.is_live:
586
+ # Return mock info for testing
587
+ return {
588
+ "id": auth_id,
589
+ "scopes": [
590
+ "https://www.googleapis.com/auth/cloud-platform",
591
+ "https://www.googleapis.com/auth/userinfo.email"
592
+ ],
593
+ "status": "mock"
594
+ }
595
+ headers = {
596
+ "Authorization": f"Bearer {self._access_token()}",
597
+ "Content-Type": "application/json",
598
+ "X-Goog-User-Project": self.project_id,
599
+ }
600
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations/{auth_id}"
601
+ r = self._http.get(url, headers=headers, timeout=60)
602
+ r.raise_for_status()
603
+ return r.json()
604
+
605
+ def update_authorization_scopes(self, auth_id: str, scopes: list) -> Dict[str, Any]:
606
+ """Patch the scopes for a specific authorization by ID, updating the authorizationUri as well."""
607
+ if not self.is_live:
608
+ return {
609
+ "id": auth_id,
610
+ "scopes": scopes,
611
+ "status": "mock-patched"
612
+ }
613
+ headers = {
614
+ "Authorization": f"Bearer {self._access_token()}",
615
+ "Content-Type": "application/json",
616
+ "X-Goog-User-Project": self.project_number,
617
+ }
618
+ url = f"{BASE_URL}/projects/{self.project_number}/locations/global/authorizations/{auth_id}?update_mask=server_side_oauth2.authorization_uri"
619
+ scopes_str = "%20".join(scopes)
620
+ authorization_uri = (
621
+ "https://accounts.google.com/o/oauth2/auth"
622
+ "?response_type=code"
623
+ f"&client_id={self.oauth_client_id or 'your-client-id'}"
624
+ f"&scope={scopes_str}"
625
+ "&access_type=offline&prompt=consent"
626
+ )
627
+
628
+ payload = {
629
+ "serverSideOauth2": {
630
+ "authorizationUri": authorization_uri
631
+ }
632
+ }
633
+ r = self._http.patch(url, headers=headers, json=payload, timeout=60)
634
+ print(r.status_code)
635
+ print(r.text)
636
+ print(r.json())
637
+ r.raise_for_status()
638
+ return r.json()
639
+
562
640
  def _ensure_authorization(self, auth_name: str) -> Tuple[bool, str]:
563
641
  if not self.is_live:
564
642
  time.sleep(0.02)
@@ -663,4 +741,60 @@ class ApiClient:
663
741
  }
664
742
  self._profile["agent_space_agent_id"] = full
665
743
  self._save_profile()
666
- return ("created", "Deployed", item)
744
+ return ("created", "Deployed", item)
745
+
746
+ def remove_authorization_from_agent(self, agent_id: str) -> Dict[str, Any]:
747
+ """Remove all authorizations from an agent by PATCHing with an empty authorizations list."""
748
+ if not self.is_live:
749
+ return {
750
+ "id": agent_id,
751
+ "authorizations": [],
752
+ "status": "mock-removed"
753
+ }
754
+ headers = {
755
+ "Authorization": f"Bearer {self._access_token()}",
756
+ "Content-Type": "application/json",
757
+ "X-Goog-User-Project": self.project_id,
758
+ }
759
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/assistants/default_assistant/agents/{agent_id}"
760
+ payload = {"authorizations": []}
761
+ r = self._http.patch(url, headers=headers, json=payload, timeout=60)
762
+ r.raise_for_status()
763
+ return r.json()
764
+
765
+ def drop_agent_authorizations(self, agent_id: str) -> Dict[str, Any]:
766
+ """Drop all authorizations for an agent space agent by PATCHing with all attributes except authorizations (fully omitted)."""
767
+ if not self.is_live:
768
+ return {
769
+ "id": agent_id,
770
+ "authorizations": [],
771
+ "status": "mock-dropped"
772
+ }
773
+ headers = {
774
+ "Authorization": f"Bearer {self._access_token()}",
775
+ "Content-Type": "application/json",
776
+ "X-Goog-User-Project": self.project_id,
777
+ }
778
+ url = (f"{BASE_URL}/projects/{self.project_id}/locations/global/collections/default_collection/"
779
+ f"engines/{self.engine_name}/assistants/default_assistant/agents/{agent_id}")
780
+ # GET the current agent definition
781
+ get_resp = self._http.get(url, headers=headers, timeout=60)
782
+ get_resp.raise_for_status()
783
+ agent_data = get_resp.json()
784
+ # Build PATCH payload: copy all attributes except authorizations
785
+ patch_payload = {}
786
+ for key in ["displayName", "description"]:
787
+ if key in agent_data:
788
+ patch_payload[key] = agent_data[key]
789
+ adk_def = agent_data.get("adkAgentDefinition", {})
790
+ patch_adk_def = {}
791
+ # Copy all adkAgentDefinition fields except 'authorizations'
792
+ for k, v in adk_def.items():
793
+ if k != "authorizations":
794
+ patch_adk_def[k] = v
795
+ if patch_adk_def:
796
+ patch_payload["adk_agent_definition"] = patch_adk_def
797
+ # PATCH with the new payload
798
+ patch_resp = self._http.patch(url, headers=headers, json=patch_payload, timeout=60)
799
+ patch_resp.raise_for_status()
800
+ return patch_resp.json()
@@ -43,6 +43,14 @@ class CLIRunner:
43
43
  subparsers.add_parser("list-authorizations", help="List all authorizations in the project")
44
44
  delete_auth_parser = subparsers.add_parser("delete-authorization", help="Delete an authorization by list position")
45
45
  delete_auth_parser.add_argument("position", help="Position number from the list (e.g., 1, 2, 3...)")
46
+ # New commands
47
+ get_auth_info_parser = subparsers.add_parser("get-authorization-info", help="Get details for a specific authorization")
48
+ get_auth_info_parser.add_argument("auth_id", help="Authorization ID")
49
+ update_auth_scopes_parser = subparsers.add_parser("update-authorization-scopes", help="Update scopes for a specific authorization")
50
+ update_auth_scopes_parser.add_argument("auth_id", help="Authorization ID")
51
+ update_auth_scopes_parser.add_argument("scopes", help="Comma-separated list of scopes")
52
+ drop_agent_auth_parser = subparsers.add_parser("drop-agent-authorizations", help="Drop all authorizations for an agent space agent")
53
+ drop_agent_auth_parser.add_argument("agent_id", help="Agent ID")
46
54
 
47
55
  def run(self):
48
56
  # Load environment variables from .env.agent file (same as GUI editor)
@@ -113,12 +121,20 @@ class CLIRunner:
113
121
  self._list_authorizations()
114
122
  elif args.command == "delete-authorization":
115
123
  self._delete_authorization(args.position)
124
+ elif args.command == "get-authorization-info":
125
+ self._get_authorization_info(args.auth_id)
126
+ elif args.command == "update-authorization-scopes":
127
+ scopes = [s.strip() for s in args.scopes.split(",") if s.strip()]
128
+ self._update_authorization_scopes(args.auth_id, scopes)
129
+ elif args.command == "drop-agent-authorizations":
130
+ self._drop_agent_authorizations(args.agent_id)
116
131
  else:
117
132
  self.parser.print_help()
118
133
  except SystemExit:
119
134
  # Catch argparse's SystemExit to prevent CLI from closing
120
135
  print("Invalid command. Please try again.")
121
136
  except Exception as e:
137
+ raise
122
138
  print(f"An error occurred: {e}")
123
139
  print("\nAvailable commands:")
124
140
  print(" list-engines List all reasoning engines in the project")
@@ -339,5 +355,35 @@ class CLIRunner:
339
355
  except Exception as e:
340
356
  print(f"❌ Error deleting authorization: {e}")
341
357
 
358
+ def _get_authorization_info(self, auth_id):
359
+ """Get details for a specific authorization."""
360
+ print(f"🔍 Getting info for authorization: {auth_id}")
361
+ try:
362
+ info = self.api_client.get_authorization_info(auth_id)
363
+ from pprint import pprint
364
+ pprint(info)
365
+ except Exception as e:
366
+ print(f"❌ Error getting authorization info: {e}")
367
+
368
+ def _update_authorization_scopes(self, auth_id, scopes):
369
+ """Update scopes for a specific authorization."""
370
+ print(f"🔧 Updating scopes for authorization: {auth_id}")
371
+ print(f"New scopcdscsdes: {scopes}")
372
+
373
+ result = self.api_client.update_authorization_scopes(auth_id, scopes)
374
+ from pprint import pprint
375
+ pprint(result)
376
+
377
+ def _drop_agent_authorizations(self, agent_id):
378
+ """Drop all authorizations for an agent space agent."""
379
+ print(f"🗑️ Dropping all authorizations for agent: {agent_id}")
380
+ try:
381
+ result = self.api_client.drop_agent_authorizations(agent_id)
382
+ from pprint import pprint
383
+ pprint(result)
384
+ except Exception as e:
385
+ raise
386
+ print(f"❌ Error dropping agent authorizations: {e}")
387
+
342
388
  if __name__ == "__main__":
343
389
  CLIRunner().run()
@@ -0,0 +1,48 @@
1
+ """
2
+ Scrollable agent list with checkboxes for mass selection and actions.
3
+ """
4
+ import tkinter as tk
5
+ from tkinter import ttk, messagebox
6
+
7
+ class AgentCheckboxList(tk.Frame):
8
+ def __init__(self, master, agents, on_mass_action):
9
+ super().__init__(master)
10
+ self.agents = agents
11
+ self.on_mass_action = on_mass_action
12
+ self.vars = {}
13
+ self._setup_ui()
14
+
15
+ def _setup_ui(self):
16
+ canvas = tk.Canvas(self)
17
+ scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
18
+ self.scrollable_frame = tk.Frame(canvas)
19
+
20
+ self.scrollable_frame.bind(
21
+ "<Configure>",
22
+ lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
23
+ )
24
+
25
+ canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
26
+ canvas.configure(yscrollcommand=scrollbar.set, height=300)
27
+
28
+ canvas.pack(side="left", fill="both", expand=True)
29
+ scrollbar.pack(side="right", fill="y")
30
+
31
+ for agent in self.agents:
32
+ var = tk.BooleanVar()
33
+ self.vars[agent["id"]] = var
34
+ cb = tk.Checkbutton(self.scrollable_frame, text=f"{agent.get('display_name', agent['id'])}", variable=var)
35
+ cb.pack(anchor="w", padx=4, pady=2)
36
+
37
+ self.action_btn = ttk.Button(self, text="Drop Authorizations for Checked Agents", command=self._do_mass_action)
38
+ self.action_btn.pack(fill="x", pady=8)
39
+
40
+ def get_checked_agent_ids(self):
41
+ return [aid for aid, var in self.vars.items() if var.get()]
42
+
43
+ def _do_mass_action(self):
44
+ checked = self.get_checked_agent_ids()
45
+ if not checked:
46
+ messagebox.showinfo("No agents selected", "Please check agents to drop authorizations.")
47
+ return
48
+ self.on_mass_action(checked)
@@ -25,6 +25,94 @@ EXCLUDES = [
25
25
 
26
26
 
27
27
  class ApiClient:
28
+ def drop_agent_authorizations(self, agent_id: str) -> dict:
29
+ """Drop all authorizations for an agent space agent by PATCHing with all attributes except authorizations (fully omitted)."""
30
+ if not self.is_live:
31
+ return {
32
+ "id": agent_id,
33
+ "authorizations": [],
34
+ "status": "mock-dropped"
35
+ }
36
+ headers = {
37
+ "Authorization": f"Bearer {self._access_token()}",
38
+ "Content-Type": "application/json",
39
+ "X-Goog-User-Project": self.project_id,
40
+ }
41
+ url = (f"{BASE_URL}/projects/{self.project_id}/locations/global/collections/default_collection/"
42
+ f"engines/{self.engine_name}/assistants/default_assistant/agents/{agent_id}")
43
+ # GET the current agent definition
44
+ get_resp = self._http.get(url, headers=headers, timeout=60)
45
+ get_resp.raise_for_status()
46
+ agent_data = get_resp.json()
47
+ # Build PATCH payload: copy all attributes except authorizations
48
+ patch_payload = {}
49
+ for key in ["displayName", "description"]:
50
+ if key in agent_data:
51
+ patch_payload[key] = agent_data[key]
52
+ adk_def = agent_data.get("adkAgentDefinition", {})
53
+ patch_adk_def = {}
54
+ # Copy all adkAgentDefinition fields except 'authorizations'
55
+ for k, v in adk_def.items():
56
+ if k != "authorizations":
57
+ patch_adk_def[k] = v
58
+ if patch_adk_def:
59
+ patch_payload["adk_agent_definition"] = patch_adk_def
60
+ # PATCH with the new payload
61
+ patch_resp = self._http.patch(url, headers=headers, json=patch_payload, timeout=60)
62
+ patch_resp.raise_for_status()
63
+ return patch_resp.json()
64
+ def update_authorization_scopes(self, auth_id: str, scopes: list, oauth_client_id: str) -> dict:
65
+ """Patch the scopes for a specific authorization by ID, updating the authorizationUri as well."""
66
+ if not self.is_live:
67
+ return {
68
+ "id": auth_id,
69
+ "scopes": scopes,
70
+ "status": "mock-patched"
71
+ }
72
+ import requests
73
+ scopes_str = " ".join(scopes)
74
+ authorization_uri = (
75
+ "https://accounts.google.com/o/oauth2/auth"
76
+ "?response_type=code"
77
+ f"&client_id={oauth_client_id}"
78
+ f"&scope={scopes_str}"
79
+ "&access_type=offline&prompt=consent"
80
+ )
81
+ payload = {
82
+ "serverSideOauth2": {
83
+ "authorizationUri": authorization_uri
84
+ }
85
+ }
86
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations/{auth_id}?update_mask=server_side_oauth2.authorization_uri"
87
+ headers = {
88
+ "Authorization": f"Bearer {self._access_token()}",
89
+ "Content-Type": "application/json",
90
+ "X-Goog-User-Project": self.project_id,
91
+ }
92
+ r = self._http.patch(url, headers=headers, json=payload, timeout=60)
93
+ r.raise_for_status()
94
+ return r.json()
95
+ def get_authorization_info(self, auth_id: str) -> dict:
96
+ """Get details for a specific authorization by ID."""
97
+ if not self.is_live:
98
+ # Return mock info for testing
99
+ return {
100
+ "id": auth_id,
101
+ "scopes": [
102
+ "https://www.googleapis.com/auth/cloud-platform",
103
+ "https://www.googleapis.com/auth/userinfo.email"
104
+ ],
105
+ "status": "mock"
106
+ }
107
+ headers = {
108
+ "Authorization": f"Bearer {self._access_token()}",
109
+ "Content-Type": "application/json",
110
+ "X-Goog-User-Project": self.project_id,
111
+ }
112
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations/{auth_id}"
113
+ r = self._http.get(url, headers=headers, timeout=60)
114
+ r.raise_for_status()
115
+ return r.json()
28
116
  """
29
117
  Single responsibility: hold configuration & credentials and expose API calls.
30
118
  This class has both 'live' and 'mock' modes; the public surface is identical.
@@ -6,6 +6,7 @@ from typing import Callable
6
6
 
7
7
  from ..core.api_client import ApiClient
8
8
  from .ui_components import async_operation, StatusButton, AgentDetailsDialog, LoadingDialog
9
+ from reasoning_deployment_service.gui_editor.agent_checkbox_list import AgentCheckboxList
9
10
 
10
11
 
11
12
  class AgentSpaceView(ttk.Frame):
@@ -34,6 +35,9 @@ class AgentSpaceView(ttk.Frame):
34
35
  self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
35
36
  self.delete_btn.pack(side="left", padx=8)
36
37
 
38
+ self.drop_auth_btn = StatusButton(btns, text="Drop Authorizations (Selected)", command=self.drop_selected_authorizations)
39
+ self.drop_auth_btn.pack(side="left", padx=8)
40
+
37
41
  self.details_btn = StatusButton(btns, text="More Agent Details", command=self.show_agent_details)
38
42
  self.details_btn.pack(side="left", padx=8)
39
43
 
@@ -80,6 +84,10 @@ class AgentSpaceView(ttk.Frame):
80
84
  self._cached_selection = None
81
85
  self._selection_is_dirty = True
82
86
 
87
+ # Checkbox agent list (initially empty, filled on refresh)
88
+ self.checkbox_list_frame = ttk.LabelFrame(self, text="Mass Select Agents (Checkboxes)")
89
+ self.checkbox_list_frame.pack(fill="x", padx=10, pady=8)
90
+ self.agent_checkbox_list = None
83
91
  # Initialize button states without triggering immediate API calls
84
92
  self._update_button_states()
85
93
 
@@ -106,29 +114,66 @@ class AgentSpaceView(ttk.Frame):
106
114
 
107
115
  def _update_button_states(self):
108
116
  """Update button states based on current conditions - IMMEDIATE, no timers."""
109
- # Refresh button - always enabled if authenticated (use cached state)
110
117
  is_auth = self._get_cached_auth_state()
111
118
  self.refresh_btn.set_enabled(
112
119
  is_auth,
113
120
  "Authentication required" if not is_auth else ""
114
121
  )
115
-
116
- # Get selection once and cache it
117
122
  selection = self._get_selection()
118
123
  has_selection = bool(selection)
119
124
  single_selection = len(selection) == 1
120
-
121
- # Delete button - enabled only if agents are selected
122
125
  self.delete_btn.set_enabled(
123
126
  has_selection,
124
127
  "Select agents to delete" if not has_selection else ""
125
128
  )
126
-
127
- # Details button - enabled only if a single agent is selected
129
+ self.drop_auth_btn.set_enabled(
130
+ has_selection,
131
+ "Select agents to drop authorizations" if not has_selection else ""
132
+ )
128
133
  self.details_btn.set_enabled(
129
134
  single_selection,
130
135
  "Select a single agent to view details" if not single_selection else ""
131
136
  )
137
+ def drop_selected_authorizations(self):
138
+ """Drop authorizations for selected agents."""
139
+ selection = self._get_selection()
140
+ if not selection:
141
+ messagebox.showinfo("No selection", "Select agents to drop authorizations.")
142
+ return
143
+ count = len(selection)
144
+ if not messagebox.askyesno("Confirm", f"Drop authorizations for {count} agent{'s' if count != 1 else ''}?"):
145
+ return
146
+ agent_ids = []
147
+ for item in selection:
148
+ values = self.tree.item(item, "values")
149
+ if len(values) >= 1:
150
+ agent_ids.append(values[0])
151
+ self.status.set(f"Dropping authorizations for {count} agent{'s' if count != 1 else ''}...")
152
+ def callback(results):
153
+ if isinstance(results, Exception):
154
+ self.status.set(f"Error: {results}")
155
+ self.log(f"❌ Error dropping authorizations: {results}")
156
+ return
157
+ ok = [k for k, v in results.items() if isinstance(v, dict)]
158
+ bad = {k: v for k, v in results.items() if not isinstance(v, dict)}
159
+ if ok:
160
+ self.log(f"✅ Dropped authorizations for {len(ok)} agent(s): {', '.join(ok[:10])}{'…' if len(ok) > 10 else ''}")
161
+ for k, v in list(bad.items())[:10]:
162
+ self.log(f"⚠️ {k}: {v}")
163
+ if len(bad) > 10:
164
+ self.log(f"…and {len(bad)-10} more failures")
165
+ self.status.set(f"Done. Dropped authorizations for {len(ok)} agent(s).")
166
+ self._update_button_states()
167
+ self.refresh()
168
+ def mass_drop():
169
+ results = {}
170
+ for agent_id in agent_ids:
171
+ try:
172
+ results[agent_id] = self.api.drop_agent_authorizations(agent_id)
173
+ except Exception as e:
174
+ results[agent_id] = str(e)
175
+ return results
176
+ async_operation(mass_drop, callback=callback, ui_widget=self)
132
177
 
133
178
  def _on_selection_change(self, event=None):
134
179
  """Handle tree selection changes - IMMEDIATE update, no debouncing."""
@@ -158,7 +203,54 @@ class AgentSpaceView(ttk.Frame):
158
203
  count = len(rows)
159
204
  self.status.set(f"Loaded {count} agent{'s' if count != 1 else ''}.")
160
205
  self.log(f"✅ Loaded {count} agent space agent{'s' if count != 1 else ''}. Data keys: {len(self._agents_data)}")
206
+ # Update checkbox list
207
+ self._update_checkbox_list(list(self._agents_data.values()))
208
+ self._update_button_states()
209
+
210
+ def _update_checkbox_list(self, agents):
211
+ # Remove previous checkbox list if present
212
+ if self.agent_checkbox_list:
213
+ self.agent_checkbox_list.destroy()
214
+ self.agent_checkbox_list = AgentCheckboxList(
215
+ self.checkbox_list_frame,
216
+ agents,
217
+ self._mass_drop_authorizations_from_checkboxes
218
+ )
219
+ self.agent_checkbox_list.pack(fill="x", padx=4, pady=4)
220
+
221
+ def _mass_drop_authorizations_from_checkboxes(self, agent_ids):
222
+ if not agent_ids:
223
+ messagebox.showinfo("No agents selected", "Please check agents to drop authorizations.")
224
+ return
225
+ count = len(agent_ids)
226
+ if not messagebox.askyesno("Confirm", f"Drop authorizations for {count} agent{'s' if count != 1 else ''}?"):
227
+ return
228
+ self.status.set(f"Dropping authorizations for {count} agent{'s' if count != 1 else ''}...")
229
+ def callback(results):
230
+ if isinstance(results, Exception):
231
+ self.status.set(f"Error: {results}")
232
+ self.log(f"❌ Error dropping authorizations: {results}")
233
+ return
234
+ ok = [k for k, v in results.items() if isinstance(v, dict)]
235
+ bad = {k: v for k, v in results.items() if not isinstance(v, dict)}
236
+ if ok:
237
+ self.log(f"✅ Dropped authorizations for {len(ok)} agent(s): {', '.join(ok[:10])}{'…' if len(ok) > 10 else ''}")
238
+ for k, v in list(bad.items())[:10]:
239
+ self.log(f"⚠️ {k}: {v}")
240
+ if len(bad) > 10:
241
+ self.log(f"…and {len(bad)-10} more failures")
242
+ self.status.set(f"Done. Dropped authorizations for {len(ok)} agent(s).")
161
243
  self._update_button_states()
244
+ self.refresh()
245
+ def mass_drop():
246
+ results = {}
247
+ for agent_id in agent_ids:
248
+ try:
249
+ results[agent_id] = self.api.drop_agent_authorizations(agent_id)
250
+ except Exception as e:
251
+ results[agent_id] = str(e)
252
+ return results
253
+ async_operation(mass_drop, callback=callback, ui_widget=self)
162
254
 
163
255
  def refresh(self):
164
256
  """Refresh the agent list from the API."""