reasoning-deployment-service 0.2.8__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.

Files changed (29) hide show
  1. examples/programmatic_usage.py +154 -0
  2. reasoning_deployment_service/__init__.py +25 -0
  3. reasoning_deployment_service/cli_editor/__init__.py +5 -0
  4. reasoning_deployment_service/cli_editor/api_client.py +666 -0
  5. reasoning_deployment_service/cli_editor/cli_runner.py +343 -0
  6. reasoning_deployment_service/cli_editor/config.py +82 -0
  7. reasoning_deployment_service/cli_editor/google_deps.py +29 -0
  8. reasoning_deployment_service/cli_editor/reasoning_engine_creator.py +448 -0
  9. reasoning_deployment_service/gui_editor/__init__.py +5 -0
  10. reasoning_deployment_service/gui_editor/main.py +280 -0
  11. reasoning_deployment_service/gui_editor/requirements_minimal.txt +54 -0
  12. reasoning_deployment_service/gui_editor/run_program.sh +55 -0
  13. reasoning_deployment_service/gui_editor/src/__init__.py +1 -0
  14. reasoning_deployment_service/gui_editor/src/core/__init__.py +1 -0
  15. reasoning_deployment_service/gui_editor/src/core/api_client.py +647 -0
  16. reasoning_deployment_service/gui_editor/src/core/config.py +43 -0
  17. reasoning_deployment_service/gui_editor/src/core/google_deps.py +22 -0
  18. reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py +448 -0
  19. reasoning_deployment_service/gui_editor/src/ui/__init__.py +1 -0
  20. reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +312 -0
  21. reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +280 -0
  22. reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py +354 -0
  23. reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py +204 -0
  24. reasoning_deployment_service/gui_editor/src/ui/ui_components.py +1221 -0
  25. reasoning_deployment_service/reasoning_deployment_service.py +687 -0
  26. reasoning_deployment_service-0.2.8.dist-info/METADATA +177 -0
  27. reasoning_deployment_service-0.2.8.dist-info/RECORD +29 -0
  28. reasoning_deployment_service-0.2.8.dist-info/WHEEL +5 -0
  29. reasoning_deployment_service-0.2.8.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1221 @@
1
+ """Common UI components and utilities."""
2
+ import tkinter as tk
3
+ from tkinter import ttk, messagebox
4
+ from tkinter import filedialog
5
+ from tkinter.scrolledtext import ScrolledText
6
+ import threading
7
+ import os
8
+ import json
9
+ from typing import Callable, Optional, Dict, Any
10
+
11
+
12
+ def async_operation(func: Callable, *args, callback: Optional[Callable] = None, ui_widget=None):
13
+ """Execute function asynchronously and call callback with result on UI thread."""
14
+ def run():
15
+ try:
16
+ result = func(*args)
17
+ except Exception as e:
18
+ result = e
19
+ if callback and ui_widget:
20
+ try:
21
+ ui_widget.after(0, lambda: callback(result))
22
+ except RuntimeError:
23
+ # Handle case where widget is destroyed or not in main loop
24
+ pass
25
+
26
+ threading.Thread(target=run, daemon=True).start()
27
+
28
+
29
+ class LogConsole(ttk.Frame):
30
+ """Scrollable text console for logging messages with automatic trimming."""
31
+
32
+ def __init__(self, master, max_lines=1000):
33
+ super().__init__(master)
34
+ self.max_lines = max_lines
35
+ self.text = ScrolledText(self, wrap="word", height=10)
36
+ self.text.pack(fill="both", expand=True)
37
+
38
+ def log(self, msg: str):
39
+ """Add a message to the console with automatic trimming to prevent memory bloat."""
40
+ self.text.insert("end", msg + "\n")
41
+
42
+ # Trim if we exceed max_lines to prevent performance degradation
43
+ current_lines = int(self.text.index("end-1c").split('.')[0])
44
+ if current_lines > self.max_lines:
45
+ # Delete from beginning until we're back to max_lines
46
+ lines_to_delete = current_lines - self.max_lines
47
+ self.text.delete("1.0", f"{lines_to_delete + 1}.0")
48
+
49
+ self.text.see("end")
50
+
51
+ def clear(self):
52
+ """Clear the console."""
53
+ self.text.delete("1.0", "end")
54
+
55
+
56
+ class CreateReasoningEngineDialog(tk.Toplevel):
57
+ """Dialog for creating a new reasoning engine with metadata."""
58
+
59
+ def __init__(self, parent, api_client):
60
+ super().__init__(parent)
61
+ self.title("Create Reasoning Engine")
62
+ self.resizable(False, False)
63
+ self.attributes('-topmost', True)
64
+ self.result = None
65
+ self.api = api_client
66
+
67
+ frm = ttk.Frame(self, padding=15)
68
+ frm.pack(fill="both", expand=True)
69
+
70
+ # Auto-populate from config/profile
71
+ try:
72
+ profile = getattr(self.api, '_profile', {})
73
+ config_display_name = profile.get("display_name", "Your Agent")
74
+ config_description = profile.get("description", "Live Agent")
75
+ config_requirements = profile.get("requirements", [])
76
+ config_extra_packages = profile.get("extra_packages", [])
77
+ config_tool_description = profile.get("tool_description", "Tooling")
78
+ except Exception:
79
+ # Fallback values if profile access fails
80
+ config_display_name = "Your Agent"
81
+ config_description = "Live Agent"
82
+ config_requirements = []
83
+ config_extra_packages = []
84
+ config_tool_description = "Tooling"
85
+
86
+ # Display Name
87
+ ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
88
+ self.display_name_var = tk.StringVar(value=config_display_name)
89
+ ttk.Entry(frm, textvariable=self.display_name_var, width=50).grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
90
+
91
+ # Description
92
+ ttk.Label(frm, text="Description:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
93
+ self.description_var = tk.StringVar(value=config_description)
94
+ ttk.Entry(frm, textvariable=self.description_var, width=50).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
95
+
96
+ # Tool Description
97
+ ttk.Label(frm, text="Tool Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
98
+ self.tool_description_var = tk.StringVar(value=config_tool_description)
99
+ ttk.Entry(frm, textvariable=self.tool_description_var, width=50).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
100
+
101
+ # Requirements (one per line)
102
+ ttk.Label(frm, text="Requirements:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
103
+ requirements_frame = ttk.Frame(frm)
104
+ requirements_frame.grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
105
+
106
+ self.requirements_text = tk.Text(requirements_frame, height=4, width=50, wrap="word")
107
+ self.requirements_text.pack(side="left", fill="both", expand=True)
108
+
109
+ req_scroll = ttk.Scrollbar(requirements_frame, orient="vertical", command=self.requirements_text.yview)
110
+ self.requirements_text.configure(yscrollcommand=req_scroll.set)
111
+ req_scroll.pack(side="right", fill="y")
112
+
113
+ # Pre-populate requirements
114
+ if config_requirements:
115
+ self.requirements_text.insert("1.0", "\n".join(config_requirements))
116
+
117
+ # Extra Packages (one per line)
118
+ ttk.Label(frm, text="Extra Packages:", font=("TkDefaultFont", 10, "bold")).grid(row=4, column=0, sticky="nw", pady=(0, 5))
119
+ packages_frame = ttk.Frame(frm)
120
+ packages_frame.grid(row=4, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
121
+
122
+ self.packages_text = tk.Text(packages_frame, height=4, width=50, wrap="word")
123
+ self.packages_text.pack(side="left", fill="both", expand=True)
124
+
125
+ pkg_scroll = ttk.Scrollbar(packages_frame, orient="vertical", command=self.packages_text.yview)
126
+ self.packages_text.configure(yscrollcommand=pkg_scroll.set)
127
+ pkg_scroll.pack(side="right", fill="y")
128
+
129
+ # Pre-populate extra packages
130
+ if config_extra_packages:
131
+ self.packages_text.insert("1.0", "\n".join(config_extra_packages))
132
+
133
+ # Agent Import (only for live mode)
134
+ agent_import_value = getattr(self.api, 'agent_import', '') or ''
135
+ if self.api.is_live:
136
+ ttk.Label(frm, text="Agent Import:", font=("TkDefaultFont", 10, "bold")).grid(row=5, column=0, sticky="nw", pady=(0, 5))
137
+ self.agent_import_var = tk.StringVar(value=agent_import_value)
138
+ ttk.Entry(frm, textvariable=self.agent_import_var, width=50).grid(row=5, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
139
+
140
+ # Help text for agent import
141
+ help_label = ttk.Label(frm, text="Format: module:attribute (e.g., my_agent:agent)",
142
+ font=("TkDefaultFont", 8), foreground="gray")
143
+ help_label.grid(row=6, column=1, sticky="w", padx=(10, 0), pady=(0, 10))
144
+ current_row = 7
145
+ else:
146
+ current_row = 5
147
+
148
+ # Buttons
149
+ btns = ttk.Frame(frm)
150
+ btns.grid(row=current_row, column=0, columnspan=2, pady=(15, 0), sticky="e")
151
+ ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
152
+ ttk.Button(btns, text="Create Engine", command=self._ok).pack(side="right")
153
+
154
+ frm.grid_columnconfigure(1, weight=1)
155
+
156
+ # Keyboard shortcuts
157
+ self.bind("<Return>", lambda _: self._ok())
158
+ self.bind("<Escape>", lambda _: self._cancel())
159
+
160
+ # Focus the display name field
161
+ self.display_name_var.trace_add("write", lambda *_: None) # Ensure field is editable
162
+ self.after(100, lambda: frm.focus_set())
163
+
164
+ def _ok(self):
165
+ """Validate and save the engine configuration."""
166
+ display_name = self.display_name_var.get().strip()
167
+ description = self.description_var.get().strip()
168
+ tool_description = self.tool_description_var.get().strip()
169
+
170
+ if not display_name:
171
+ messagebox.showerror("Missing Display Name", "Display name is required.", parent=self)
172
+ return
173
+
174
+ if not description:
175
+ messagebox.showerror("Missing Description", "Description is required.", parent=self)
176
+ return
177
+
178
+ # Parse requirements and extra packages
179
+ requirements_text = self.requirements_text.get("1.0", "end-1c").strip()
180
+ requirements = [line.strip() for line in requirements_text.split("\n") if line.strip()]
181
+
182
+ packages_text = self.packages_text.get("1.0", "end-1c").strip()
183
+ extra_packages = [line.strip() for line in packages_text.split("\n") if line.strip()]
184
+
185
+ # Agent import (live mode only)
186
+ agent_import = ""
187
+ if self.api.is_live:
188
+ agent_import = self.agent_import_var.get().strip()
189
+ if not agent_import:
190
+ messagebox.showerror("Missing Agent Import",
191
+ "Agent import is required for live mode (format: module:attribute).",
192
+ parent=self)
193
+ return
194
+
195
+ self.result = {
196
+ "display_name": display_name,
197
+ "description": description,
198
+ "tool_description": tool_description,
199
+ "requirements": requirements,
200
+ "extra_packages": extra_packages,
201
+ "agent_import": agent_import
202
+ }
203
+ self.destroy()
204
+
205
+ def _cancel(self):
206
+ self.result = None
207
+ self.destroy()
208
+
209
+
210
+ class CreateReasoningEngineAdvancedDialog(tk.Toplevel):
211
+ """Advanced dialog for creating reasoning engines with flexible project support."""
212
+
213
+ def __init__(self, parent, api_client):
214
+ super().__init__(parent)
215
+ self.title("Create Reasoning Engine - Advanced")
216
+ self.resizable(True, True)
217
+ self.geometry("750x725") # Increased from 700x800 for better visibility
218
+ self.attributes('-topmost', True)
219
+ self.result = None
220
+ self.api = api_client
221
+
222
+ # Create main scrollable frame
223
+ main_frame = ttk.Frame(self)
224
+ main_frame.pack(fill="both", expand=True, padx=15, pady=15)
225
+
226
+ # Create canvas and scrollbar for scrolling
227
+ canvas = tk.Canvas(main_frame)
228
+ scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
229
+ scrollable_frame = ttk.Frame(canvas)
230
+
231
+ scrollable_frame.bind(
232
+ "<Configure>",
233
+ lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
234
+ )
235
+
236
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
237
+ canvas.configure(yscrollcommand=scrollbar.set)
238
+
239
+ # Pack canvas and scrollbar
240
+ canvas.pack(side="left", fill="both", expand=True)
241
+ scrollbar.pack(side="right", fill="y")
242
+
243
+ # Bind mousewheel to canvas (bind to canvas only, not globally)
244
+ def _on_mousewheel(event):
245
+ try:
246
+ canvas.yview_scroll(int(-1*(event.delta/120)), "units")
247
+ except tk.TclError:
248
+ # Canvas destroyed, ignore
249
+ pass
250
+ canvas.bind("<MouseWheel>", _on_mousewheel)
251
+ self._canvas = canvas # Store reference for cleanup
252
+
253
+ frm = scrollable_frame
254
+ current_row = 0
255
+
256
+ # === ENGINE METADATA ===
257
+ meta_frame = ttk.LabelFrame(frm, text="Engine Configuration", padding=15)
258
+ meta_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
259
+ meta_frame.grid_columnconfigure(1, weight=1)
260
+ current_row += 1
261
+
262
+ # Auto-populate staging bucket from config
263
+ try:
264
+ config_bucket = getattr(self.api, 'staging_bucket', 'gs://my-staging-bucket')
265
+ except Exception:
266
+ config_bucket = 'gs://my-staging-bucket'
267
+
268
+ # Staging Bucket
269
+ ttk.Label(meta_frame, text="Staging Bucket:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
270
+ self.bucket_var = tk.StringVar(value=config_bucket)
271
+ bucket_frame = ttk.Frame(meta_frame)
272
+ bucket_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
273
+ ttk.Entry(bucket_frame, textvariable=self.bucket_var, width=35).pack(side="left", fill="x", expand=True)
274
+ ttk.Label(bucket_frame, text="(gs:// prefix optional)", font=("TkDefaultFont", 8), foreground="gray").pack(side="right", padx=(5, 0))
275
+
276
+ # Display Name
277
+ ttk.Label(meta_frame, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
278
+ self.display_name_var = tk.StringVar(value="Meeting Follow-Up Assistant")
279
+ ttk.Entry(meta_frame, textvariable=self.display_name_var, width=40).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
280
+
281
+ # Description
282
+ ttk.Label(meta_frame, text="Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
283
+ self.description_var = tk.StringVar(value="AI-powered assistant with Gmail + Calendar")
284
+ ttk.Entry(meta_frame, textvariable=self.description_var, width=40).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
285
+
286
+ # === AGENT SOURCE CONFIGURATION ===
287
+ source_frame = ttk.LabelFrame(frm, text="Agent Source Configuration", padding=15)
288
+ source_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
289
+ source_frame.grid_columnconfigure(1, weight=1)
290
+ current_row += 1
291
+
292
+ # Agent File - simplified to just file picker
293
+ ttk.Label(source_frame, text="Agent File:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
294
+ file_frame = ttk.Frame(source_frame)
295
+ file_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
296
+ self.agent_file_var = tk.StringVar(value="/Users/me/projects/my_agent_project/agent.py")
297
+ ttk.Entry(file_frame, textvariable=self.agent_file_var, width=40).pack(side="left", fill="x", expand=True)
298
+ ttk.Button(file_frame, text="Browse", command=self._browse_agent_file).pack(side="right", padx=(5, 0))
299
+
300
+ # Help text
301
+ ttk.Label(source_frame, text="Select the Python file containing your 'root_agent' definition",
302
+ font=("TkDefaultFont", 8), foreground="gray").grid(row=1, column=1, sticky="w", padx=(10, 0))
303
+
304
+ # === REQUIREMENTS CONFIGURATION ===
305
+ req_frame = ttk.LabelFrame(frm, text="Requirements Configuration", padding=15)
306
+ req_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
307
+ req_frame.grid_columnconfigure(1, weight=1)
308
+ current_row += 1
309
+
310
+ # Requirements source (Radio buttons)
311
+ ttk.Label(req_frame, text="Requirements Source:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
312
+
313
+ req_source_frame = ttk.Frame(req_frame)
314
+ req_source_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
315
+
316
+ self.req_source_var = tk.StringVar(value="file")
317
+
318
+ # Requirements file option
319
+ req_file_frame = ttk.Frame(req_source_frame)
320
+ req_file_frame.pack(fill="x", pady=(0, 5))
321
+ ttk.Radiobutton(req_file_frame, text="requirements.txt file:", variable=self.req_source_var,
322
+ value="file", command=self._toggle_req_source).pack(side="left")
323
+ self.req_file_var = tk.StringVar(value="requirements.txt")
324
+ self.req_file_entry = ttk.Entry(req_file_frame, textvariable=self.req_file_var, width=25)
325
+ self.req_file_entry.pack(side="left", padx=(10, 0), fill="x", expand=True)
326
+ self.req_file_button = ttk.Button(req_file_frame, text="Browse", command=self._browse_req_file)
327
+ self.req_file_button.pack(side="right", padx=(5, 0))
328
+
329
+ # Paste requirements option
330
+ req_text_frame = ttk.Frame(req_source_frame)
331
+ req_text_frame.pack(fill="x", pady=(0, 5))
332
+ ttk.Radiobutton(req_text_frame, text="Paste here:", variable=self.req_source_var,
333
+ value="text", command=self._toggle_req_source).pack(side="left")
334
+
335
+ # Requirements text area
336
+ self.req_text_frame = ttk.Frame(req_frame)
337
+ self.req_text_frame.grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(5, 0))
338
+
339
+ self.req_text = tk.Text(self.req_text_frame, height=6, width=50, wrap="word", state="disabled")
340
+ self.req_text.pack(side="left", fill="both", expand=True)
341
+
342
+ req_text_scroll = ttk.Scrollbar(self.req_text_frame, orient="vertical", command=self.req_text.yview)
343
+ self.req_text.configure(yscrollcommand=req_text_scroll.set)
344
+ req_text_scroll.pack(side="right", fill="y")
345
+
346
+ # Default requirements text
347
+ default_reqs = """pydantic>=2.0.0
348
+ google-auth>=2.23.0
349
+ google-adk>=1.0.0
350
+ google-cloud-aiplatform[adk,agent-engines]>=1.93.0
351
+ google-auth-oauthlib>=1.0.0
352
+ google-auth-httplib2>=0.1.0
353
+ google-api-python-client>=2.86.0"""
354
+ self.req_text.config(state="normal")
355
+ self.req_text.insert("1.0", default_reqs)
356
+ self.req_text.config(state="disabled")
357
+
358
+ # === OPTIONS ===
359
+ options_frame = ttk.LabelFrame(frm, text="Deployment Options", padding=15)
360
+ options_frame.grid(row=current_row, column=0, sticky="ew", pady=(0, 15))
361
+ current_row += 1
362
+
363
+ # Exclude dev files checkbox
364
+ self.exclude_dev_var = tk.BooleanVar(value=True)
365
+ ttk.Checkbutton(options_frame, text="Exclude dev files (.env, __pycache__, .git, tests, etc.)",
366
+ variable=self.exclude_dev_var).pack(anchor="w")
367
+
368
+ # Enable tracing checkbox
369
+ self.enable_tracing_var = tk.BooleanVar(value=True)
370
+ ttk.Checkbutton(options_frame, text="Enable tracing for debugging",
371
+ variable=self.enable_tracing_var).pack(anchor="w", pady=(5, 0))
372
+
373
+ # === BUTTONS ===
374
+ button_frame = ttk.Frame(frm)
375
+ button_frame.grid(row=current_row, column=0, sticky="ew", pady=(20, 0))
376
+
377
+ # Left side buttons (utility functions)
378
+ left_buttons = ttk.Frame(button_frame)
379
+ left_buttons.pack(side="left")
380
+ ttk.Button(left_buttons, text="Populate with JSON", command=self._populate_from_json).pack(side="left", padx=(0, 4))
381
+ ttk.Button(left_buttons, text="Export to JSON", command=self._export_to_json).pack(side="left")
382
+ ttk.Button(left_buttons, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
383
+
384
+ # Right side buttons (main actions)
385
+ right_buttons = ttk.Frame(button_frame)
386
+ right_buttons.pack(side="right")
387
+ ttk.Button(right_buttons, text="Finalize Reasoning Engine", command=self._ok).pack(side="right")
388
+
389
+ # Configure column weights
390
+ frm.grid_columnconfigure(0, weight=1)
391
+
392
+ # Initialize UI state
393
+ self._toggle_req_source()
394
+
395
+ # Keyboard shortcuts
396
+ self.bind("<Return>", lambda _: self._ok())
397
+ self.bind("<Escape>", lambda _: self._cancel())
398
+
399
+ # Ensure proper cleanup on window close
400
+ self.protocol("WM_DELETE_WINDOW", self._cancel)
401
+
402
+ # Focus
403
+ self.focus_set()
404
+
405
+ def _cleanup(self):
406
+ """Clean up resources to prevent memory leaks."""
407
+ # Cleanup is handled automatically since we bind to canvas, not globally
408
+ pass
409
+
410
+ def _browse_directory(self):
411
+ """Browse for agent directory."""
412
+ directory = filedialog.askdirectory(title="Select Agent Project Directory")
413
+ if directory:
414
+ self.agent_dir_var.set(directory)
415
+
416
+ def _browse_agent_file(self):
417
+ """Browse for agent Python file and automatically set directory."""
418
+ file_path = filedialog.askopenfilename(
419
+ title="Select Agent Python File (containing root_agent)",
420
+ filetypes=[("Python files", "*.py"), ("All files", "*.*")]
421
+ )
422
+ if file_path:
423
+ self.agent_file_var.set(file_path)
424
+ # Automatically infer and set the directory from the file path
425
+ agent_directory = os.path.dirname(file_path)
426
+ print(f"🔍 Auto-detected agent directory: {agent_directory}")
427
+
428
+ def _browse_req_file(self):
429
+ """Browse for requirements file."""
430
+ file_path = filedialog.askopenfilename(
431
+ title="Select Requirements File",
432
+ filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
433
+ )
434
+ if file_path:
435
+ self.req_file_var.set(file_path)
436
+
437
+ def _toggle_agent_source(self):
438
+ """No longer needed - simplified to just file picker."""
439
+ pass
440
+
441
+ def _toggle_req_source(self):
442
+ """Toggle requirements source input fields."""
443
+ if self.req_source_var.get() == "file":
444
+ self.req_file_entry.config(state="normal")
445
+ self.req_file_button.config(state="normal")
446
+ self.req_text.config(state="disabled")
447
+ else:
448
+ self.req_file_entry.config(state="disabled")
449
+ self.req_file_button.config(state="disabled")
450
+ self.req_text.config(state="normal")
451
+
452
+ def _validate(self):
453
+ """Validate the current configuration."""
454
+ errors = []
455
+
456
+ # Validate staging bucket
457
+ if not self.bucket_var.get().strip():
458
+ errors.append("Staging bucket is required")
459
+
460
+ # Validate agent file
461
+ agent_file_path = self.agent_file_var.get().strip()
462
+ if not agent_file_path:
463
+ errors.append("Agent file path is required")
464
+ elif not os.path.exists(agent_file_path):
465
+ errors.append(f"Agent file does not exist: {agent_file_path}")
466
+
467
+ # Validate requirements source
468
+ if self.req_source_var.get() == "file":
469
+ req_file = self.req_file_var.get().strip()
470
+ if req_file and not os.path.exists(req_file):
471
+ errors.append(f"Requirements file does not exist: {req_file}")
472
+ else:
473
+ req_text = self.req_text.get("1.0", "end-1c").strip()
474
+ if not req_text:
475
+ errors.append("Requirements text is empty")
476
+
477
+ # Validate metadata
478
+ if not self.display_name_var.get().strip():
479
+ errors.append("Display name is required")
480
+
481
+ # Show results
482
+ if errors:
483
+ messagebox.showerror("Validation Errors", "\n".join(f"• {err}" for err in errors), parent=self)
484
+ else:
485
+ messagebox.showinfo("Validation Success", "✅ Configuration is valid!", parent=self)
486
+
487
+ def _populate_from_json(self):
488
+ """Populate dialog fields from a JSON file."""
489
+ file_path = filedialog.askopenfilename(
490
+ title="Select Configuration JSON File",
491
+ filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
492
+ parent=self
493
+ )
494
+
495
+ if not file_path:
496
+ return
497
+
498
+ try:
499
+ with open(file_path, 'r') as f:
500
+ config = json.load(f)
501
+
502
+ # Populate basic fields
503
+ if "staging_bucket" in config:
504
+ self.bucket_var.set(config["staging_bucket"])
505
+ if "display_name" in config:
506
+ self.display_name_var.set(config["display_name"])
507
+ if "description" in config:
508
+ self.description_var.set(config["description"])
509
+
510
+ # Populate agent file path
511
+ if "agent_file_path" in config:
512
+ self.agent_file_var.set(config["agent_file_path"])
513
+
514
+ # Populate requirements
515
+ if "requirements_source_type" in config:
516
+ self.req_source_var.set(config["requirements_source_type"])
517
+ self._toggle_req_source() # Update UI state
518
+
519
+ if config["requirements_source_type"] == "file" and "requirements_file" in config:
520
+ self.req_file_var.set(config["requirements_file"])
521
+ elif config["requirements_source_type"] == "text" and "requirements_text" in config:
522
+ self.req_text.config(state="normal")
523
+ self.req_text.delete("1.0", "end")
524
+ self.req_text.insert("1.0", config["requirements_text"])
525
+ # Note: req_text state is managed by _toggle_req_source
526
+
527
+ # Populate options
528
+ if "exclude_dev_files" in config:
529
+ self.exclude_dev_var.set(bool(config["exclude_dev_files"]))
530
+ if "enable_tracing" in config:
531
+ self.enable_tracing_var.set(bool(config["enable_tracing"]))
532
+
533
+ messagebox.showinfo("Configuration Loaded",
534
+ f"✅ Configuration successfully loaded from:\n{os.path.basename(file_path)}",
535
+ parent=self)
536
+
537
+ except json.JSONDecodeError as e:
538
+ messagebox.showerror("JSON Error",
539
+ f"Invalid JSON file:\n{str(e)}",
540
+ parent=self)
541
+ except Exception as e:
542
+ messagebox.showerror("Load Error",
543
+ f"Failed to load configuration:\n{str(e)}",
544
+ parent=self)
545
+
546
+ def _export_to_json(self):
547
+ """Export current dialog configuration to a JSON file."""
548
+ # Get project info from API client
549
+ try:
550
+ project_id = getattr(self.api, 'project_id', 'unknown-project')
551
+ location = getattr(self.api, 'location', 'us-central1')
552
+ except Exception:
553
+ project_id = 'unknown-project'
554
+ location = 'us-central1'
555
+
556
+ # Collect current configuration
557
+ config = {
558
+ "project_id": project_id,
559
+ "location": location,
560
+ "staging_bucket": self.bucket_var.get().strip(),
561
+ "agent_file_path": self.agent_file_var.get().strip(),
562
+ "requirements_source_type": self.req_source_var.get(),
563
+ "requirements_file": self.req_file_var.get().strip() if self.req_source_var.get() == "file" else None,
564
+ "requirements_text": self.req_text.get("1.0", "end-1c").strip() if self.req_source_var.get() == "text" else None,
565
+ "display_name": self.display_name_var.get().strip(),
566
+ "description": self.description_var.get().strip(),
567
+ "exclude_dev_files": self.exclude_dev_var.get(),
568
+ "enable_tracing": self.enable_tracing_var.get(),
569
+ "_metadata": {
570
+ "exported_from": "AgentSpaceDeploymentService",
571
+ "dialog_version": "1.0",
572
+ "export_timestamp": str(__import__('datetime').datetime.now())
573
+ }
574
+ }
575
+
576
+ # Remove None values for cleaner JSON
577
+ config = {k: v for k, v in config.items() if v is not None}
578
+
579
+ # Default filename based on display name
580
+ default_name = self.display_name_var.get().strip()
581
+ if default_name:
582
+ default_filename = f"{default_name.lower().replace(' ', '_')}_config.json"
583
+ else:
584
+ default_filename = "reasoning_engine_config.json"
585
+
586
+ file_path = filedialog.asksaveasfilename(
587
+ title="Save Configuration as JSON",
588
+ defaultextension=".json",
589
+ initialfile=default_filename,
590
+ filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
591
+ parent=self
592
+ )
593
+
594
+ if not file_path:
595
+ return
596
+
597
+ try:
598
+ with open(file_path, 'w') as f:
599
+ json.dump(config, f, indent=2)
600
+
601
+ messagebox.showinfo("Configuration Exported",
602
+ f"✅ Configuration successfully exported to:\n{os.path.basename(file_path)}",
603
+ parent=self)
604
+
605
+ except Exception as e:
606
+ messagebox.showerror("Export Error",
607
+ f"Failed to export configuration:\n{str(e)}",
608
+ parent=self)
609
+
610
+ def _ok(self):
611
+ import os
612
+ """Validate and save the configuration."""
613
+ # Run validation first
614
+ errors = []
615
+
616
+ # Validate staging bucket
617
+ if not self.bucket_var.get().strip():
618
+ errors.append("Staging bucket is required")
619
+
620
+ # Validate agent file
621
+ agent_file = self.agent_file_var.get().strip()
622
+ if not agent_file:
623
+ errors.append("Agent file is required")
624
+ elif not os.path.exists(agent_file):
625
+ errors.append(f"Agent file does not exist: {agent_file}")
626
+
627
+ # Validate requirements source
628
+ if self.req_source_var.get() == "file":
629
+ req_file = self.req_file_var.get().strip()
630
+ if req_file and not os.path.exists(req_file):
631
+ errors.append(f"Requirements file does not exist: {req_file}")
632
+ else:
633
+ req_text = self.req_text.get("1.0", "end-1c").strip()
634
+ if not req_text:
635
+ errors.append("Requirements text is empty")
636
+
637
+ # Validate metadata
638
+ if not self.display_name_var.get().strip():
639
+ errors.append("Display name is required")
640
+
641
+ # Show validation errors if any
642
+ if errors:
643
+ messagebox.showerror("Validation Errors", "\n".join(f"• {err}" for err in errors), parent=self)
644
+ return
645
+
646
+ # Get project info from API client
647
+ try:
648
+ project_id = getattr(self.api, 'project_id', 'unknown-project')
649
+ location = getattr(self.api, 'location', 'us-central1')
650
+ except Exception:
651
+ project_id = 'unknown-project'
652
+ location = 'us-central1'
653
+
654
+ # Collect all configuration
655
+ import os
656
+ agent_file_path = self.agent_file_var.get().strip()
657
+ agent_directory = os.path.dirname(agent_file_path) if agent_file_path else ""
658
+
659
+ config = {
660
+ "project_id": project_id,
661
+ "location": location,
662
+ "staging_bucket": self.bucket_var.get().strip(),
663
+ "agent_directory": agent_directory, # Auto-inferred from file path
664
+ "agent_file_path": agent_file_path,
665
+ "requirements_source_type": self.req_source_var.get(),
666
+ "requirements_file": self.req_file_var.get().strip() if self.req_source_var.get() == "file" else None,
667
+ "requirements_text": self.req_text.get("1.0", "end-1c").strip() if self.req_source_var.get() == "text" else None,
668
+ "display_name": self.display_name_var.get().strip(),
669
+ "description": self.description_var.get().strip(),
670
+ "exclude_dev_files": self.exclude_dev_var.get(),
671
+ "enable_tracing": self.enable_tracing_var.get()
672
+ }
673
+
674
+ self.result = config
675
+ self._cleanup()
676
+ self.destroy()
677
+
678
+ def _cancel(self):
679
+ """Cancel dialog."""
680
+ self.result = None
681
+ self._cleanup()
682
+ self.destroy()
683
+
684
+
685
+ class DeployToAgentSpaceDialog(tk.Toplevel):
686
+ """Dialog for deploying reasoning engine to Agent Space with full configuration."""
687
+
688
+ def __init__(self, parent, api_client, engine_resource_name: str = ""):
689
+ super().__init__(parent)
690
+ self.title("Deploy to Agent Space")
691
+ self.resizable(False, False)
692
+ self.attributes('-topmost', True)
693
+ self.result = None
694
+ self.api = api_client
695
+ self.engine_resource_name = engine_resource_name
696
+
697
+ frm = ttk.Frame(self, padding=15)
698
+ frm.pack(fill="both", expand=True)
699
+
700
+ # Auto-populate from config/profile
701
+ try:
702
+ profile = getattr(self.api, '_profile', {})
703
+ config_display_name = profile.get("display_name", "Your Agent")
704
+ config_description = profile.get("description", "Live Agent")
705
+ config_tool_description = profile.get("tool_description", "Tooling")
706
+ except Exception:
707
+ # Fallback values if profile access fails
708
+ config_display_name = "Your Agent"
709
+ config_description = "Live Agent"
710
+ config_tool_description = "Tooling"
711
+
712
+ # Engine Info (read-only)
713
+ ttk.Label(frm, text="Reasoning Engine:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
714
+ engine_display = engine_resource_name if engine_resource_name else "Current Engine"
715
+ ttk.Label(frm, text=engine_display, wraplength=400, foreground="gray").grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
716
+
717
+ # Agent Display Name
718
+ ttk.Label(frm, text="Agent Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
719
+ self.agent_display_name_var = tk.StringVar(value=config_display_name)
720
+ ttk.Entry(frm, textvariable=self.agent_display_name_var, width=50).grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
721
+
722
+ # Agent Description
723
+ ttk.Label(frm, text="Agent Description:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
724
+ self.agent_description_var = tk.StringVar(value=config_description)
725
+ ttk.Entry(frm, textvariable=self.agent_description_var, width=50).grid(row=2, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
726
+
727
+ # Tool Description
728
+ ttk.Label(frm, text="Tool Description:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
729
+ self.tool_description_var = tk.StringVar(value=config_tool_description)
730
+ ttk.Entry(frm, textvariable=self.tool_description_var, width=50).grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
731
+
732
+ # Authorization section
733
+ auth_frame = ttk.LabelFrame(frm, text="Authorization Settings", padding=10)
734
+ auth_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(15, 0))
735
+ auth_frame.grid_columnconfigure(1, weight=1)
736
+
737
+ # Enable Authorization checkbox
738
+ self.with_auth_var = tk.BooleanVar(value=True)
739
+ ttk.Checkbutton(auth_frame, text="Configure OAuth Authorization",
740
+ variable=self.with_auth_var, command=self._toggle_auth).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 5))
741
+
742
+ # Authorization Name
743
+ ttk.Label(auth_frame, text="Authorization Name:").grid(row=1, column=0, sticky="nw", pady=(0, 5))
744
+ self.auth_name_var = tk.StringVar(value="default-auth")
745
+ self.auth_entry = ttk.Entry(auth_frame, textvariable=self.auth_name_var, width=40)
746
+ self.auth_entry.grid(row=1, column=1, sticky="ew", padx=(10, 0), pady=(0, 5))
747
+
748
+ # Help text for authorization
749
+ self.auth_help = ttk.Label(auth_frame, text="Authorization will be created/attached for OAuth access",
750
+ font=("TkDefaultFont", 8), foreground="gray")
751
+ self.auth_help.grid(row=2, column=0, columnspan=2, sticky="w", pady=(0, 5))
752
+
753
+ # Advanced section (collapsible)
754
+ self.show_advanced_var = tk.BooleanVar(value=False)
755
+ advanced_check = ttk.Checkbutton(frm, text="Show Advanced Settings",
756
+ variable=self.show_advanced_var, command=self._toggle_advanced)
757
+ advanced_check.grid(row=5, column=0, columnspan=2, sticky="w", pady=(15, 5))
758
+
759
+ # Advanced frame (initially hidden)
760
+ self.advanced_frame = ttk.LabelFrame(frm, text="Advanced Settings", padding=10)
761
+
762
+ # Project details (read-only, for reference)
763
+ ttk.Label(self.advanced_frame, text="Project ID:", font=("TkDefaultFont", 9)).grid(row=0, column=0, sticky="nw", pady=(0, 3))
764
+ ttk.Label(self.advanced_frame, text=getattr(self.api, 'project_id', 'N/A'),
765
+ font=("TkDefaultFont", 9), foreground="gray").grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
766
+
767
+ ttk.Label(self.advanced_frame, text="Project Number:", font=("TkDefaultFont", 9)).grid(row=1, column=0, sticky="nw", pady=(0, 3))
768
+ ttk.Label(self.advanced_frame, text=getattr(self.api, 'project_number', 'N/A'),
769
+ font=("TkDefaultFont", 9), foreground="gray").grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
770
+
771
+ ttk.Label(self.advanced_frame, text="Location:", font=("TkDefaultFont", 9)).grid(row=2, column=0, sticky="nw", pady=(0, 3))
772
+ ttk.Label(self.advanced_frame, text=getattr(self.api, 'location', 'N/A'),
773
+ font=("TkDefaultFont", 9), foreground="gray").grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
774
+
775
+ ttk.Label(self.advanced_frame, text="Engine Name:", font=("TkDefaultFont", 9)).grid(row=3, column=0, sticky="nw", pady=(0, 3))
776
+ ttk.Label(self.advanced_frame, text=getattr(self.api, 'engine_name', 'N/A'),
777
+ font=("TkDefaultFont", 9), foreground="gray").grid(row=3, column=1, sticky="nw", padx=(10, 0), pady=(0, 3))
778
+
779
+ self.advanced_frame.grid_columnconfigure(1, weight=1)
780
+
781
+ # Buttons
782
+ btns = ttk.Frame(frm)
783
+ btns.grid(row=7, column=0, columnspan=2, pady=(20, 0), sticky="e")
784
+ ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=(4, 0))
785
+ ttk.Button(btns, text="Deploy to Agent Space", command=self._ok).pack(side="right")
786
+
787
+ frm.grid_columnconfigure(1, weight=1)
788
+
789
+ # Keyboard shortcuts
790
+ self.bind("<Return>", lambda _: self._ok())
791
+ self.bind("<Escape>", lambda _: self._cancel())
792
+
793
+ # Initialize UI state
794
+ self._toggle_auth()
795
+ self._toggle_advanced()
796
+
797
+ # Focus the agent display name field
798
+ self.after(100, lambda: self.agent_display_name_var.get() and None)
799
+
800
+ def _toggle_auth(self):
801
+ """Toggle authorization fields based on checkbox."""
802
+ enabled = self.with_auth_var.get()
803
+ state = "normal" if enabled else "disabled"
804
+ self.auth_entry.config(state=state)
805
+ self.auth_help.config(foreground="black" if enabled else "gray")
806
+
807
+ def _toggle_advanced(self):
808
+ """Toggle advanced settings visibility."""
809
+ if self.show_advanced_var.get():
810
+ self.advanced_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
811
+ else:
812
+ self.advanced_frame.grid_remove()
813
+
814
+ def _ok(self):
815
+ """Validate and save the deployment configuration."""
816
+ agent_display_name = self.agent_display_name_var.get().strip()
817
+ agent_description = self.agent_description_var.get().strip()
818
+ tool_description = self.tool_description_var.get().strip()
819
+ with_authorization = self.with_auth_var.get()
820
+ auth_name = self.auth_name_var.get().strip()
821
+
822
+ if not agent_display_name:
823
+ messagebox.showerror("Missing Agent Display Name", "Agent display name is required.", parent=self)
824
+ return
825
+
826
+ if not agent_description:
827
+ messagebox.showerror("Missing Agent Description", "Agent description is required.", parent=self)
828
+ return
829
+
830
+ if with_authorization and not auth_name:
831
+ messagebox.showerror("Missing Authorization Name",
832
+ "Authorization name is required when OAuth is enabled.", parent=self)
833
+ return
834
+
835
+ self.result = {
836
+ "agent_display_name": agent_display_name,
837
+ "agent_description": agent_description,
838
+ "tool_description": tool_description,
839
+ "with_authorization": with_authorization,
840
+ "authorization_name": auth_name
841
+ }
842
+ self.destroy()
843
+
844
+ def _cancel(self):
845
+ self.result = None
846
+ self.destroy()
847
+
848
+
849
+ class DeployDialog(tk.Toplevel):
850
+ """Dialog for configuring deployment to Agent Space."""
851
+
852
+ def __init__(self, parent, default_auth_name: str = "", default_with_auth: bool = True):
853
+ super().__init__(parent)
854
+ self.title("Deploy to Agent Space")
855
+ self.resizable(False, False)
856
+ self.attributes('-topmost', True)
857
+ self.result = None
858
+
859
+ frm = ttk.Frame(self, padding=10)
860
+ frm.pack(fill="both", expand=True)
861
+
862
+ ttk.Label(frm, text="Authorization name").grid(row=0, column=0, sticky="w")
863
+ self.auth_var = tk.StringVar(value=default_auth_name)
864
+ ttk.Entry(frm, textvariable=self.auth_var, width=40).grid(row=0, column=1, padx=(8, 0), sticky="we")
865
+
866
+ self.chk_var = tk.BooleanVar(value=default_with_auth)
867
+ ttk.Checkbutton(frm, text="Configure OAuth (create/attach authorization)", variable=self.chk_var)\
868
+ .grid(row=1, column=0, columnspan=2, pady=(8, 0), sticky="w")
869
+
870
+ btns = ttk.Frame(frm)
871
+ btns.grid(row=2, column=0, columnspan=2, pady=(12, 0), sticky="e")
872
+ ttk.Button(btns, text="Cancel", command=self._cancel).pack(side="right", padx=4)
873
+ ttk.Button(btns, text="Deploy", command=self._ok).pack(side="right")
874
+
875
+ frm.grid_columnconfigure(1, weight=1)
876
+ self.bind("<Return>", lambda _: self._ok())
877
+ self.bind("<Escape>", lambda _: self._cancel())
878
+
879
+ def _ok(self):
880
+ name = self.auth_var.get().strip()
881
+ with_auth = self.chk_var.get()
882
+ if with_auth and not name:
883
+ messagebox.showerror("Missing authorization name", "Provide an authorization name or uncheck OAuth.")
884
+ return
885
+ self.result = {"authorization_name": name, "with_authorization": with_auth}
886
+ self.destroy()
887
+
888
+ def _cancel(self):
889
+ self.result = None
890
+ self.destroy()
891
+
892
+
893
+ class AgentDetailsDialog(tk.Toplevel):
894
+ """Dialog for displaying detailed agent information."""
895
+
896
+ def __init__(self, parent, agent_data: dict):
897
+ super().__init__(parent)
898
+ self.title(f"Agent Details - {agent_data.get('display_name', 'Unknown')}")
899
+ self.resizable(True, True)
900
+ self.geometry("700x450")
901
+
902
+ # Improved dialog setup for reliability
903
+ self.transient(parent)
904
+
905
+ # Defer grab_set to avoid blocking issues
906
+ self.after(1, self._setup_modal)
907
+
908
+ # Store variables as instance attributes to avoid garbage collection
909
+ self.resource_var = tk.StringVar(value=agent_data.get("full_name", "N/A"))
910
+ auth_full = agent_data.get("authorization_full", "")
911
+ engine_full = agent_data.get("engine_full", "")
912
+
913
+ if auth_full and auth_full != "N/A":
914
+ self.auth_var = tk.StringVar(value=auth_full)
915
+ else:
916
+ self.auth_var = None
917
+
918
+ if engine_full and engine_full != "N/A":
919
+ self.engine_var = tk.StringVar(value=engine_full)
920
+ else:
921
+ self.engine_var = None
922
+
923
+ frm = ttk.Frame(self, padding=15)
924
+ frm.pack(fill="both", expand=True)
925
+
926
+ # Agent details
927
+ ttk.Label(frm, text="Agent ID:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
928
+ ttk.Label(frm, text=agent_data.get("id", "N/A"), wraplength=400).grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
929
+
930
+ ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
931
+ ttk.Label(frm, text=agent_data.get("display_name", "N/A"), wraplength=400).grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
932
+
933
+ # Authorization ID (short name)
934
+ ttk.Label(frm, text="Authorization ID:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
935
+ auth_id = agent_data.get("authorization_id", "N/A")
936
+ ttk.Label(frm, text=auth_id, wraplength=400).grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
937
+
938
+ # Engine ID (short name)
939
+ ttk.Label(frm, text="Engine ID:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 5))
940
+ engine_id = agent_data.get("engine_id", "N/A")
941
+ ttk.Label(frm, text=engine_id, wraplength=400).grid(row=3, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
942
+
943
+ # Full Resource Name with copy button
944
+ ttk.Label(frm, text="Full Resource Name:", font=("TkDefaultFont", 10, "bold")).grid(row=4, column=0, sticky="nw", pady=(0, 10))
945
+
946
+ resource_frame = ttk.Frame(frm)
947
+ resource_frame.grid(row=4, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
948
+
949
+ resource_entry = ttk.Entry(resource_frame, textvariable=self.resource_var, state="readonly", width=50)
950
+ resource_entry.pack(side="left", fill="x", expand=True)
951
+
952
+ copy_resource_btn = ttk.Button(resource_frame, text="Copy", command=self._copy_resource_name)
953
+ copy_resource_btn.pack(side="right", padx=(5, 0))
954
+
955
+ current_row = 5
956
+
957
+ # Full Authorization Path with copy button (if available)
958
+ if self.auth_var:
959
+ ttk.Label(frm, text="Full Authorization Path:", font=("TkDefaultFont", 10, "bold")).grid(row=current_row, column=0, sticky="nw", pady=(0, 10))
960
+
961
+ auth_frame = ttk.Frame(frm)
962
+ auth_frame.grid(row=current_row, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
963
+
964
+ auth_entry = ttk.Entry(auth_frame, textvariable=self.auth_var, state="readonly", width=50)
965
+ auth_entry.pack(side="left", fill="x", expand=True)
966
+
967
+ copy_auth_btn = ttk.Button(auth_frame, text="Copy", command=self._copy_auth_path)
968
+ copy_auth_btn.pack(side="right", padx=(5, 0))
969
+ current_row += 1
970
+
971
+ # Full Engine Path with copy button (if available)
972
+ if self.engine_var:
973
+ ttk.Label(frm, text="Full Engine Path:", font=("TkDefaultFont", 10, "bold")).grid(row=current_row, column=0, sticky="nw", pady=(0, 10))
974
+
975
+ engine_frame = ttk.Frame(frm)
976
+ engine_frame.grid(row=current_row, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
977
+
978
+ engine_entry = ttk.Entry(engine_frame, textvariable=self.engine_var, state="readonly", width=50)
979
+ engine_entry.pack(side="left", fill="x", expand=True)
980
+
981
+ copy_engine_btn = ttk.Button(engine_frame, text="Copy", command=self._copy_engine_path)
982
+ copy_engine_btn.pack(side="right", padx=(5, 0))
983
+ current_row += 1
984
+
985
+ # Close button
986
+ close_btn = ttk.Button(frm, text="Close", command=self._close_dialog)
987
+ close_btn.grid(row=current_row, column=0, columnspan=2, pady=(20, 0))
988
+
989
+ frm.grid_columnconfigure(1, weight=1)
990
+ self.bind("<Escape>", lambda _: self._close_dialog())
991
+
992
+ # Ensure dialog is visible and focused
993
+ self.lift()
994
+ self.focus_set()
995
+
996
+ def _setup_modal(self):
997
+ """Set up modal behavior after dialog is fully created."""
998
+ try:
999
+ self.attributes('-topmost', True)
1000
+ except tk.TclError:
1001
+ # If attributes fails, dialog is still functional
1002
+ pass
1003
+
1004
+ def _close_dialog(self):
1005
+ """Properly close the dialog."""
1006
+ self.destroy()
1007
+
1008
+ def _copy_resource_name(self):
1009
+ """Copy resource name to clipboard."""
1010
+ try:
1011
+ self.clipboard_clear()
1012
+ self.clipboard_append(self.resource_var.get())
1013
+ messagebox.showinfo("Copied", "Resource name copied to clipboard!", parent=self)
1014
+ except Exception:
1015
+ messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
1016
+
1017
+ def _copy_auth_path(self):
1018
+ """Copy authorization path to clipboard."""
1019
+ if not self.auth_var:
1020
+ return
1021
+ try:
1022
+ self.clipboard_clear()
1023
+ self.clipboard_append(self.auth_var.get())
1024
+ messagebox.showinfo("Copied", "Authorization path copied to clipboard!", parent=self)
1025
+ except Exception:
1026
+ messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
1027
+
1028
+ def _copy_engine_path(self):
1029
+ """Copy engine path to clipboard."""
1030
+ if not self.engine_var:
1031
+ return
1032
+ try:
1033
+ self.clipboard_clear()
1034
+ self.clipboard_append(self.engine_var.get())
1035
+ messagebox.showinfo("Copied", "Engine path copied to clipboard!", parent=self)
1036
+ except Exception:
1037
+ messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
1038
+
1039
+
1040
+ class EngineDetailsDialog(tk.Toplevel):
1041
+ """Dialog for displaying detailed engine information."""
1042
+
1043
+ def __init__(self, parent, engine_data: dict):
1044
+ super().__init__(parent)
1045
+ self.title(f"Engine Details - {engine_data.get('display_name', 'Unknown')}")
1046
+ self.resizable(True, True)
1047
+ self.geometry("600x400")
1048
+
1049
+ # Center the dialog
1050
+ self.transient(parent)
1051
+ self.attributes('-topmost', True)
1052
+
1053
+ # Store variable as instance attribute to avoid garbage collection
1054
+ self.resource_var = tk.StringVar(value=engine_data.get("resource_name", "N/A"))
1055
+
1056
+ frm = ttk.Frame(self, padding=15)
1057
+ frm.pack(fill="both", expand=True)
1058
+
1059
+ # Engine details
1060
+ ttk.Label(frm, text="Engine ID:", font=("TkDefaultFont", 10, "bold")).grid(row=0, column=0, sticky="nw", pady=(0, 5))
1061
+ ttk.Label(frm, text=engine_data.get("id", "N/A"), wraplength=400).grid(row=0, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
1062
+
1063
+ ttk.Label(frm, text="Display Name:", font=("TkDefaultFont", 10, "bold")).grid(row=1, column=0, sticky="nw", pady=(0, 5))
1064
+ ttk.Label(frm, text=engine_data.get("display_name", "N/A"), wraplength=400).grid(row=1, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
1065
+
1066
+ ttk.Label(frm, text="Created:", font=("TkDefaultFont", 10, "bold")).grid(row=2, column=0, sticky="nw", pady=(0, 5))
1067
+ ttk.Label(frm, text=engine_data.get("create_time", "N/A"), wraplength=400).grid(row=2, column=1, sticky="nw", padx=(10, 0), pady=(0, 5))
1068
+
1069
+ ttk.Label(frm, text="Resource Name:", font=("TkDefaultFont", 10, "bold")).grid(row=3, column=0, sticky="nw", pady=(0, 10))
1070
+
1071
+ # Resource name with copy button
1072
+ resource_frame = ttk.Frame(frm)
1073
+ resource_frame.grid(row=3, column=1, sticky="ew", padx=(10, 0), pady=(0, 10))
1074
+
1075
+ resource_entry = ttk.Entry(resource_frame, textvariable=self.resource_var, state="readonly", width=50)
1076
+ resource_entry.pack(side="left", fill="x", expand=True)
1077
+
1078
+ copy_btn = ttk.Button(resource_frame, text="Copy", command=self._copy_resource_name)
1079
+ copy_btn.pack(side="right", padx=(5, 0))
1080
+
1081
+ # Close button
1082
+ close_btn = ttk.Button(frm, text="Close", command=self._close_dialog)
1083
+ close_btn.grid(row=4, column=0, columnspan=2, pady=(20, 0))
1084
+
1085
+ frm.grid_columnconfigure(1, weight=1)
1086
+ self.bind("<Escape>", lambda _: self._close_dialog())
1087
+
1088
+ # Focus the dialog
1089
+ self.focus_set()
1090
+
1091
+ def _close_dialog(self):
1092
+ """Properly close the dialog."""
1093
+ self.destroy()
1094
+
1095
+ def _copy_resource_name(self):
1096
+ """Copy resource name to clipboard."""
1097
+ try:
1098
+ self.clipboard_clear()
1099
+ self.clipboard_append(self.resource_var.get())
1100
+ messagebox.showinfo("Copied", "Resource name copied to clipboard!", parent=self)
1101
+ except Exception:
1102
+ messagebox.showerror("Error", "Failed to copy to clipboard.", parent=self)
1103
+
1104
+
1105
+ class LoadingDialog(tk.Toplevel):
1106
+ """Loading dialog with spinner animation."""
1107
+
1108
+ def __init__(self, parent, message="Loading..."):
1109
+ super().__init__(parent)
1110
+
1111
+ # Initialize animation control first
1112
+ self._animation_active = True
1113
+
1114
+ self.title("Loading")
1115
+ self.resizable(False, False)
1116
+ self.geometry("300x120")
1117
+
1118
+ # Center the dialog
1119
+ self.transient(parent)
1120
+ self.attributes('-topmost', True)
1121
+
1122
+ # Try to remove window decorations, but handle errors gracefully
1123
+ try:
1124
+ self.overrideredirect(True)
1125
+ except tk.TclError:
1126
+ # Fall back to normal window if overrideredirect fails
1127
+ pass
1128
+
1129
+ # Center on parent (deferred to avoid blocking)
1130
+ self.after(0, self._center_on_parent, parent)
1131
+
1132
+ # Create frame with border
1133
+ main_frame = ttk.Frame(self, relief="solid", borderwidth=2, padding=20)
1134
+ main_frame.pack(fill="both", expand=True)
1135
+
1136
+ # Loading message
1137
+ ttk.Label(main_frame, text=message, font=("TkDefaultFont", 11)).pack(pady=(0, 10))
1138
+
1139
+ # Simple text-based spinner instead of progress bar (macOS compatibility)
1140
+ self.spinner_label = ttk.Label(main_frame, text="●●●", font=("TkDefaultFont", 12))
1141
+ self.spinner_label.pack(pady=(0, 10))
1142
+
1143
+ # Start text animation
1144
+ self.spinner_states = ["● ", " ● ", " ●", " ● "]
1145
+ self.spinner_index = 0
1146
+ self._animate_spinner()
1147
+
1148
+ # Cancel button (optional)
1149
+ ttk.Button(main_frame, text="Cancel", command=self.destroy).pack()
1150
+
1151
+ # Make sure it's on top
1152
+ self.focus_set()
1153
+ self.lift()
1154
+
1155
+ def _center_on_parent(self, parent):
1156
+ """Center dialog on parent without blocking the UI."""
1157
+ try:
1158
+ x = parent.winfo_x() + (parent.winfo_width() // 2) - 150
1159
+ y = parent.winfo_y() + (parent.winfo_height() // 2) - 60
1160
+ self.geometry(f"300x120+{x}+{y}")
1161
+ except tk.TclError:
1162
+ # Fall back to default position if centering fails
1163
+ pass
1164
+
1165
+ def _animate_spinner(self):
1166
+ """Animate the text spinner."""
1167
+ if not self._animation_active:
1168
+ return
1169
+
1170
+ try:
1171
+ self.spinner_label.config(text=self.spinner_states[self.spinner_index])
1172
+ self.spinner_index = (self.spinner_index + 1) % len(self.spinner_states)
1173
+ self.after(300, self._animate_spinner) # Update every 300ms
1174
+ except tk.TclError:
1175
+ # Widget destroyed, stop animation
1176
+ self._animation_active = False
1177
+
1178
+ def close(self):
1179
+ """Properly close the loading dialog."""
1180
+ self._animation_active = False
1181
+ self.destroy()
1182
+
1183
+
1184
+ class StatusButton(ttk.Button):
1185
+ """Button that can be enabled/disabled based on conditions with status text."""
1186
+
1187
+ def __init__(self, master, text: str, command: Callable, **kwargs):
1188
+ super().__init__(master, text=text, command=self._wrapped_command, **kwargs)
1189
+ self._original_text = text
1190
+ self._original_command = command
1191
+ self._is_enabled = True
1192
+
1193
+ def _wrapped_command(self):
1194
+ """Wrapped command that provides immediate visual feedback."""
1195
+ if self._is_enabled and self._original_command:
1196
+ # Provide immediate visual feedback without blocking UI
1197
+ self.config(state="active")
1198
+ # Remove blocking update_idletasks() call
1199
+
1200
+ # Execute the actual command
1201
+ try:
1202
+ self._original_command()
1203
+ finally:
1204
+ # Restore normal state with ultra-minimal delay (5ms for immediate feel)
1205
+ self.after(5, lambda: self.config(state="normal" if self._is_enabled else "disabled"))
1206
+
1207
+ def set_enabled(self, enabled: bool, reason: str = ""):
1208
+ """Enable/disable button with optional reason tooltip."""
1209
+ self._is_enabled = enabled
1210
+ if enabled:
1211
+ self.config(state="normal")
1212
+ self.config(text=self._original_text)
1213
+ else:
1214
+ self.config(state="disabled")
1215
+ if reason:
1216
+ # You could add tooltip here if needed
1217
+ pass
1218
+
1219
+ @property
1220
+ def is_enabled(self) -> bool:
1221
+ return self._is_enabled