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.
- reasoning_deployment_service/cli_editor/api_client.py +135 -1
- reasoning_deployment_service/cli_editor/cli_runner.py +46 -0
- reasoning_deployment_service/gui_editor/agent_checkbox_list.py +48 -0
- reasoning_deployment_service/gui_editor/src/core/api_client.py +88 -0
- reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +99 -7
- reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +127 -13
- reasoning_deployment_service/reasoning_deployment_service.py +208 -8
- reasoning_deployment_service/runner.py +4 -2
- reasoning_deployment_service-0.3.7.dist-info/METADATA +211 -0
- {reasoning_deployment_service-0.3.5.dist-info → reasoning_deployment_service-0.3.7.dist-info}/RECORD +13 -12
- reasoning_deployment_service-0.3.5.dist-info/METADATA +0 -183
- {reasoning_deployment_service-0.3.5.dist-info → reasoning_deployment_service-0.3.7.dist-info}/WHEEL +0 -0
- {reasoning_deployment_service-0.3.5.dist-info → reasoning_deployment_service-0.3.7.dist-info}/entry_points.txt +0 -0
- {reasoning_deployment_service-0.3.5.dist-info → reasoning_deployment_service-0.3.7.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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."""
|