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,666 @@
1
+ """API client for Google Cloud Agent Space and Reasoning Engine operations."""
2
+ import json
3
+ import uuid
4
+ import time
5
+ import subprocess
6
+ import sys, importlib, importlib.util
7
+ from typing import Optional, Dict, Any, List, Tuple
8
+ from pprint import pprint
9
+ from pathlib import Path
10
+ import requests
11
+
12
+ try:
13
+ from google_deps import (
14
+ HAS_GOOGLE, google, GoogleAuthRequest,
15
+ vertexai, agent_engines
16
+ )
17
+ from reasoning_engine_creator import ReasoningEngineCreator
18
+ except ImportError as e:
19
+ from .google_deps import (HAS_GOOGLE, google, GoogleAuthRequest, vertexai, agent_engines)
20
+ from .reasoning_engine_creator import ReasoningEngineCreator
21
+
22
+ BASE_URL = "https://discoveryengine.googleapis.com/v1alpha"
23
+
24
+
25
+ # --- helpers for clean packaging ---
26
+ EXCLUDES = [
27
+ ".env", ".env.*", ".git", "__pycache__", ".pytest_cache", ".mypy_cache",
28
+ ".DS_Store", "*.pyc", "*.pyo", "*.pyd", ".venv", "venv", "tests", "docs"
29
+ ]
30
+
31
+
32
+ class ApiClient:
33
+ """
34
+ Single responsibility: hold configuration & credentials and expose API calls.
35
+ This class has both 'live' and 'mock' modes; the public surface is identical.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ project_id: str,
41
+ project_number: str,
42
+ location: str,
43
+ engine_name: str,
44
+ staging_bucket: str = "",
45
+ oauth_client_id: str = "",
46
+ oauth_client_secret: str = "",
47
+ agent_import: Optional[str] = None,
48
+ mode: str = "mock", # "live" or "mock"
49
+ profile_path: str = "agent_profile.json",
50
+ ):
51
+ self.project_id = project_id
52
+ self.project_number = project_number
53
+ self.location = location
54
+ self.engine_name = engine_name
55
+ self.staging_bucket = staging_bucket
56
+ self.oauth_client_id = oauth_client_id
57
+ self.oauth_client_secret = oauth_client_secret
58
+ self.agent_import = agent_import
59
+ self.mode = mode
60
+ self.profile_path = profile_path
61
+
62
+ # Local state persisted between runs
63
+ self._profile: Dict[str, Any] = {
64
+ "display_name": "Demo Agent" if mode == "mock" else "Your Agent",
65
+ "description": "Prototype only" if mode == "mock" else "Live Agent",
66
+ "name": None, # reasoning engine resource
67
+ "agent_space_agent_id": None,
68
+ "requirements": [],
69
+ "extra_packages": [],
70
+ "tool_description": "Tooling",
71
+ }
72
+ self._agents_cache: List[Dict[str, str]] = [] # mock-only
73
+ self._loaded_agent = None # live-only
74
+
75
+ # Authentication caching for performance ("live enough")
76
+ self._auth_cache = None
77
+ self._auth_cache_time = 0
78
+ self._auth_cache_duration = 30 # Cache for 30 seconds
79
+
80
+ # Performance optimizations
81
+ self._vertex_inited = False # Cache Vertex AI initialization
82
+ self.debug = False # Set True only when debugging needed
83
+
84
+ # Reuse HTTP session to avoid repeated TLS handshakes
85
+ import requests as _requests
86
+ self._http = _requests.Session()
87
+ self._http.headers.update({"Content-Type": "application/json"})
88
+
89
+ self._load_profile()
90
+
91
+ if self.is_live:
92
+ if not HAS_GOOGLE:
93
+ raise RuntimeError("Live mode requested but Google libs not installed.")
94
+ # Lazy-load actual agent code if provided
95
+ self._loaded_agent = self._maybe_import_agent(self.agent_import)
96
+
97
+ # ---------------- Properties ----------------
98
+ @property
99
+ def is_live(self) -> bool:
100
+ return self.mode == "live"
101
+
102
+ @property
103
+ def profile(self) -> Dict[str, Any]:
104
+ return self._profile
105
+
106
+ @property
107
+ def is_authenticated(self) -> bool:
108
+ """Check if we have valid authentication (cached for performance)."""
109
+ if not self.is_live:
110
+ return True
111
+
112
+ # Use cached result if still fresh (30 seconds)
113
+ now = time.time()
114
+ if (self._auth_cache is not None and
115
+ (now - self._auth_cache_time) < self._auth_cache_duration):
116
+ return self._auth_cache
117
+
118
+ # Check authentication and cache result
119
+ try:
120
+ _ = self._access_token()
121
+ self._auth_cache = True
122
+ except Exception:
123
+ self._auth_cache = False
124
+
125
+ self._auth_cache_time = now
126
+ return self._auth_cache
127
+
128
+ def refresh_auth_cache(self):
129
+ """Force refresh of authentication cache."""
130
+ self._auth_cache = None
131
+ self._auth_cache_time = 0
132
+
133
+ def _ensure_vertex_inited(self):
134
+ """Initialize Vertex AI once and reuse to avoid repeated heavy init calls."""
135
+ if not self._vertex_inited:
136
+ vertexai.init(project=self.project_id, location=self.location, staging_bucket=self.staging_bucket)
137
+ self._vertex_inited = True
138
+
139
+ @property
140
+ def has_engine(self) -> bool:
141
+ """Check if we have a reasoning engine."""
142
+ return bool(self._profile.get("name"))
143
+
144
+ @property
145
+ def has_deployed_agent(self) -> bool:
146
+ """Check if we have a deployed agent."""
147
+ return bool(self._profile.get("agent_space_agent_id"))
148
+
149
+ # ---------------- Profile Management ----------------
150
+ def set_auth_name(self, name: str):
151
+ self._profile["working_auth_name"] = name
152
+ self._save_profile()
153
+
154
+ def _save_profile(self):
155
+ try:
156
+ with open(self.profile_path, "w") as f:
157
+ json.dump(self._profile, f, indent=2)
158
+ except Exception:
159
+ pass
160
+
161
+ def _load_profile(self):
162
+ try:
163
+ with open(self.profile_path, "r") as f:
164
+ saved = json.load(f)
165
+ # shallow update only known keys
166
+ for k in self._profile.keys():
167
+ if k in saved:
168
+ self._profile[k] = saved[k]
169
+ # also pick up working_auth_name if present
170
+ if "working_auth_name" in saved:
171
+ self._profile["working_auth_name"] = saved["working_auth_name"]
172
+ except Exception:
173
+ pass
174
+
175
+ def _maybe_import_agent(self, mod_attr: Optional[str]):
176
+ if not mod_attr:
177
+ return None
178
+ parts = mod_attr.split(":")
179
+ if len(parts) != 2:
180
+ return None
181
+ mod, attr = parts
182
+ try:
183
+ imported = __import__(mod, fromlist=[attr])
184
+ return getattr(imported, attr, None)
185
+ except Exception:
186
+ return None
187
+
188
+ # ---------------- Authentication ----------------
189
+ def _access_token(self) -> str:
190
+ """Live: fetch ADC access token; raises if not available."""
191
+ creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
192
+ # Only refresh if needed - avoid network hit on every call
193
+ if not creds.valid or (creds.expired and creds.refresh_token):
194
+ creds.refresh(GoogleAuthRequest())
195
+ return creds.token
196
+
197
+ def authenticate(self) -> bool:
198
+ """
199
+ For Live: ensure ADC is configured (runs gcloud flow if needed).
200
+ For Mock: fast True.
201
+ """
202
+ if not self.is_live:
203
+ # Minimal delay to simulate network without causing UI lag
204
+ time.sleep(0.02)
205
+ return True
206
+
207
+ # If token works, we're good
208
+ try:
209
+ _ = self._access_token()
210
+ return True
211
+ except Exception:
212
+ pass
213
+
214
+ # Launch gcloud browser flow
215
+ try:
216
+ subprocess.run(["gcloud", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
217
+ except Exception:
218
+ raise RuntimeError("'gcloud' not found on PATH. Install Google Cloud SDK.")
219
+
220
+ proc = subprocess.run(
221
+ ["gcloud", "auth", "application-default", "login"],
222
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
223
+ )
224
+ if proc.returncode != 0:
225
+ raise RuntimeError(f"ADC auth failed:\n{proc.stdout}")
226
+
227
+ # Validate we can fetch a token now
228
+ _ = self._access_token()
229
+ return True
230
+
231
+ # ---------------- Agent Space APIs ----------------
232
+ def list_agent_space_agents(self) -> List[Dict[str, str]]:
233
+ if not self.is_live:
234
+ time.sleep(0.02)
235
+ return list(self._agents_cache) # Return empty list initially
236
+
237
+ headers = {
238
+ "Authorization": f"Bearer {self._access_token()}",
239
+ "Content-Type": "application/json",
240
+ "X-Goog-User-Project": self.project_id,
241
+ }
242
+ url = (f"{BASE_URL}/projects/{self.project_id}/locations/global/collections/default_collection/"
243
+ f"engines/{self.engine_name}/assistants/default_assistant/agents")
244
+ try:
245
+ r = self._http.get(url, headers=headers, timeout=60)
246
+ r.raise_for_status()
247
+ data = r.json()
248
+ out = []
249
+
250
+ for a in data.get("agents", []):
251
+ try:
252
+ authorization_full = a.get('adkAgentDefinition', {}).get('authorizations', [])
253
+ authorization_id = "N/A"
254
+ authorization_path = "N/A"
255
+
256
+ if authorization_full and len(authorization_full) > 0:
257
+ authorization_path = authorization_full[0]
258
+ # Extract just the authorization ID from the full path
259
+ if "/" in authorization_path:
260
+ authorization_id = authorization_path.split("/")[-1]
261
+ else:
262
+ authorization_id = authorization_path
263
+
264
+ # Extract engine ID from reasoning engine path
265
+ engine_full = a.get('adkAgentDefinition', {}).get('provisionedReasoningEngine', {}).get('reasoningEngine', '')
266
+ engine_id = "N/A"
267
+
268
+ if engine_full and "/" in engine_full:
269
+ engine_id = engine_full.split("/")[-1]
270
+ elif engine_full:
271
+ engine_id = engine_full
272
+
273
+ except Exception:
274
+ authorization_id = "N/A"
275
+ authorization_path = "N/A"
276
+ engine_id = "N/A"
277
+ engine_full = "N/A"
278
+
279
+ full = a.get("name", "")
280
+ out.append({
281
+ "id": full.split("/")[-1] if full else "",
282
+ "display_name": a.get("displayName", "N/A"),
283
+ "authorization_id": authorization_id,
284
+ "engine_id": engine_id,
285
+ "full_name": full,
286
+ # Store full paths for popup
287
+ "authorization_full": authorization_path,
288
+ "engine_full": engine_full,
289
+ })
290
+ return out
291
+ except requests.exceptions.HTTPError as e:
292
+ print(f"HTTPError: {e}")
293
+ print("Failed to fetch agent space agents. Please check your configuration or permissions.")
294
+ return []
295
+ except Exception as e:
296
+ print(f"Unexpected error: {e}")
297
+ return []
298
+
299
+ def delete_agent_from_space(self, full_name: str) -> Tuple[str, str]:
300
+ if not self.is_live:
301
+ time.sleep(0.02)
302
+ before = len(self._agents_cache)
303
+ self._agents_cache = [a for a in self._agents_cache if a["full_name"] != full_name]
304
+ if before != len(self._agents_cache) and self._profile.get("agent_space_agent_id") == full_name:
305
+ self._profile["agent_space_agent_id"] = None
306
+ return ("deleted", "Removed (mock)")
307
+
308
+ headers = {
309
+ "Authorization": f"Bearer {self._access_token()}",
310
+ "Content-Type": "application/json",
311
+ "X-Goog-User-Project": self.project_id,
312
+ }
313
+ url = f"{BASE_URL}/{full_name}"
314
+ r = self._http.delete(url, headers=headers, timeout=60)
315
+ # Treat any 2xx status as success (many APIs return 204 No Content)
316
+ if 200 <= r.status_code < 300:
317
+ if self._profile.get("agent_space_agent_id") == full_name:
318
+ self._profile["agent_space_agent_id"] = None
319
+ self._save_profile()
320
+ return ("deleted", "Deleted")
321
+ elif r.status_code == 404:
322
+ return ("not_found", "Not found")
323
+ else:
324
+ return ("failed", f"{r.status_code} {r.text}")
325
+
326
+ # ---------------- Reasoning Engine APIs ----------------
327
+ def list_reasoning_engines(self) -> List[Dict[str, str]]:
328
+ """List all reasoning engines in the project."""
329
+ if not self.is_live:
330
+ time.sleep(0.02)
331
+ # Return empty list initially - no mock data by default
332
+ return []
333
+
334
+ # Use the Vertex AI SDK to list reasoning engines
335
+ try:
336
+ self._ensure_vertex_inited() # Use cached initialization
337
+
338
+ # Suppress API registration warnings during engine listing
339
+ import warnings
340
+ with warnings.catch_warnings():
341
+ warnings.filterwarnings("ignore", message=".*Failed to register API methods.*")
342
+ warnings.filterwarnings("ignore", message=".*api_mode.*")
343
+ engines = agent_engines.list()
344
+
345
+ out = []
346
+ for engine in engines:
347
+ try:
348
+ resource_name = str(engine.resource_name) if engine.resource_name else ""
349
+ engine_id = resource_name.split("/")[-1] if resource_name else ""
350
+
351
+ # Handle datetime objects safely
352
+ create_time = "Unknown"
353
+ if hasattr(engine, 'create_time') and engine.create_time:
354
+ try:
355
+ # Convert datetime to string
356
+ create_time = str(engine.create_time)
357
+ except Exception:
358
+ create_time = "Unknown"
359
+
360
+ # Safely get display name
361
+ display_name = str(engine.display_name) if hasattr(engine, 'display_name') and engine.display_name else "Unnamed Engine"
362
+
363
+ out.append({
364
+ "id": engine_id,
365
+ "display_name": display_name,
366
+ "resource_name": resource_name,
367
+ "create_time": create_time
368
+ })
369
+ except Exception as e:
370
+ # Skip engines that cause issues but continue processing
371
+ if self.debug:
372
+ print(f"⚠️ Skipped engine due to error: {str(e)}")
373
+ continue
374
+
375
+ return out
376
+ except Exception as e:
377
+ # Handle API registration and other Vertex AI errors gracefully
378
+ error_msg = str(e)
379
+ if "api_mode" in error_msg or "Failed to register API methods" in error_msg:
380
+ # Return empty list but don't crash the app
381
+ return []
382
+ else:
383
+ raise RuntimeError(f"Failed to list reasoning engines: {error_msg}")
384
+
385
+ def delete_reasoning_engine_by_id(self, resource_name: str) -> Tuple[str, str]:
386
+ """Delete a reasoning engine by resource name."""
387
+ if not self.is_live:
388
+ time.sleep(0.02)
389
+ return ("deleted", f"Engine {resource_name} deleted (mock)")
390
+
391
+ try:
392
+ engine = agent_engines.get(resource_name)
393
+ engine.delete(force=True)
394
+ return ("deleted", "Reasoning engine deleted")
395
+ except Exception as e:
396
+ return ("failed", f"Delete failed: {str(e)}")
397
+
398
+ def _load_agent_from_file(self, agent_file_path: str):
399
+ """Load root_agent from a Python file, handling relative imports properly."""
400
+ agent_file = Path(agent_file_path).resolve()
401
+ if not agent_file.exists():
402
+ raise RuntimeError(f"Agent file not found: {agent_file}")
403
+
404
+ agent_dir = agent_file.parent
405
+ package_name = agent_dir.name
406
+ module_name = f"{package_name}.{agent_file.stem}"
407
+
408
+ # Define paths before using them in print statements
409
+ parent_dir = str(agent_dir.parent)
410
+ agent_dir_str = str(agent_dir)
411
+
412
+ print(f"🤖 Loading {agent_file.stem} from: {agent_file}")
413
+ print(f"📁 Agent directory: {agent_dir}")
414
+ print(f"📦 Package name: {package_name}")
415
+ print(f"🔧 Module name: {module_name}")
416
+ if self.debug:
417
+ print(f"🛤️ Adding to sys.path: {parent_dir} (for package imports)")
418
+ print(f"🛤️ Adding to sys.path: {agent_dir_str} (for absolute imports like 'tools')")
419
+
420
+ # Add both the parent directory (for package imports) and the agent directory (for absolute imports like 'tools')
421
+ paths_added = []
422
+
423
+ # Add parent directory for package imports (e.g., 'executive_summary_builder.agent')
424
+ if parent_dir not in sys.path:
425
+ sys.path.insert(0, parent_dir)
426
+ paths_added.append(parent_dir)
427
+
428
+ # Add agent directory for absolute imports within the package (e.g., 'tools.gmail_search_supporter')
429
+ if agent_dir_str not in sys.path:
430
+ sys.path.insert(0, agent_dir_str)
431
+ paths_added.append(agent_dir_str)
432
+
433
+ try:
434
+ # Create package spec for proper relative import handling
435
+ package_spec = importlib.util.spec_from_file_location(
436
+ package_name,
437
+ agent_dir / "__init__.py" if (agent_dir / "__init__.py").exists() else None
438
+ )
439
+
440
+ if package_spec:
441
+ # Load the package first
442
+ package_module = importlib.util.module_from_spec(package_spec)
443
+ sys.modules[package_name] = package_module
444
+ if package_spec.loader and (agent_dir / "__init__.py").exists():
445
+ package_spec.loader.exec_module(package_module)
446
+
447
+ # Now load the agent module as part of the package
448
+ spec = importlib.util.spec_from_file_location(module_name, agent_file)
449
+ if spec is None or spec.loader is None:
450
+ raise RuntimeError(f"Could not load module spec from {agent_file}")
451
+
452
+ module = importlib.util.module_from_spec(spec)
453
+ # Set the package for relative imports
454
+ module.__package__ = package_name
455
+
456
+ # Add to sys.modules for relative imports to work
457
+ sys.modules[module_name] = module
458
+
459
+ spec.loader.exec_module(module)
460
+
461
+ if not hasattr(module, "root_agent"):
462
+ raise RuntimeError(f"Module '{agent_file}' does not define `root_agent`.")
463
+
464
+ print(f"✅ Successfully loaded root_agent from {agent_file}")
465
+ return getattr(module, "root_agent")
466
+
467
+ except Exception as e:
468
+ print(f"❌ Failed to load agent: {e}")
469
+ raise RuntimeError(f"Failed to execute agent module {agent_file}: {e}") from e
470
+ finally:
471
+ # Clean up sys.path - remove all paths we added
472
+ for path in reversed(paths_added): # Remove in reverse order
473
+ while path in sys.path:
474
+ sys.path.remove(path)
475
+
476
+ # Clean up sys.modules
477
+ modules_to_remove = [name for name in sys.modules.keys()
478
+ if name.startswith(package_name)]
479
+ for name in modules_to_remove:
480
+ if name in sys.modules:
481
+ del sys.modules[name]
482
+
483
+ def create_reasoning_engine_advanced(self, config: Dict[str, Any]) -> Tuple[str, str, Optional[str]]:
484
+ """Create a reasoning engine with advanced configuration options."""
485
+ creator = ReasoningEngineCreator(
486
+ project_id=self.project_id,
487
+ location=self.location,
488
+ staging_bucket=self.staging_bucket,
489
+ debug=self.debug,
490
+ )
491
+
492
+ return creator.create_advanced_engine(config)
493
+
494
+ def delete_reasoning_engine(self) -> Tuple[str, str]:
495
+ if not self._profile.get("name"):
496
+ return ("not_found", "No engine")
497
+ if not self.is_live:
498
+ time.sleep(0.02)
499
+ self._profile["name"] = None
500
+ self._save_profile()
501
+ return ("deleted", "Engine deleted (mock)")
502
+ try:
503
+ agent_engines.delete(self._profile["name"])
504
+ self._profile["name"] = None
505
+ self._save_profile()
506
+ return ("deleted", "Engine deleted")
507
+ except Exception as e:
508
+ return ("failed", str(e))
509
+
510
+ # ---------------- Deploy APIs ----------------
511
+ def list_authorizations(self) -> List[Dict[str, str]]:
512
+ """List all authorizations in the project."""
513
+ if not self.is_live:
514
+ time.sleep(0.02)
515
+ # Mock some authorizations for testing
516
+ return [
517
+ {"id": "demo-auth", "name": f"projects/{self.project_id}/locations/global/authorizations/demo-auth"},
518
+ {"id": "google-drive-auth", "name": f"projects/{self.project_id}/locations/global/authorizations/google-drive-auth"}
519
+ ]
520
+
521
+ headers = {
522
+ "Authorization": f"Bearer {self._access_token()}",
523
+ "Content-Type": "application/json",
524
+ "X-Goog-User-Project": self.project_id,
525
+ }
526
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations"
527
+ r = self._http.get(url, headers=headers, timeout=60)
528
+ r.raise_for_status()
529
+ data = r.json()
530
+
531
+ out = []
532
+ for auth in data.get("authorizations", []):
533
+ full_name = auth.get("name", "")
534
+ out.append({
535
+ "id": full_name.split("/")[-1] if full_name else "",
536
+ "name": full_name,
537
+ })
538
+ return out
539
+
540
+ def delete_authorization(self, auth_id: str) -> Tuple[str, str]:
541
+ """Delete an authorization by ID."""
542
+ if not self.is_live:
543
+ time.sleep(0.02)
544
+ return ("deleted", f"Authorization {auth_id} deleted (mock)")
545
+
546
+ headers = {
547
+ "Authorization": f"Bearer {self._access_token()}",
548
+ "Content-Type": "application/json",
549
+ "X-Goog-User-Project": self.project_id,
550
+ }
551
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations/{auth_id}"
552
+ r = self._http.delete(url, headers=headers, timeout=60)
553
+
554
+ # Treat any 2xx status as success (many APIs return 204 No Content)
555
+ if 200 <= r.status_code < 300:
556
+ return ("deleted", "Authorization deleted")
557
+ elif r.status_code == 404:
558
+ return ("not_found", "Authorization not found")
559
+ else:
560
+ return ("failed", f"{r.status_code} {r.text}")
561
+
562
+ def _ensure_authorization(self, auth_name: str) -> Tuple[bool, str]:
563
+ if not self.is_live:
564
+ time.sleep(0.02)
565
+ return True, "mock"
566
+
567
+ headers = {
568
+ "Authorization": f"Bearer {self._access_token()}",
569
+ "Content-Type": "application/json",
570
+ "X-Goog-User-Project": self.project_number,
571
+ }
572
+ payload = {
573
+ "name": f"projects/{self.project_number}/locations/global/authorizations/{auth_name}",
574
+ "serverSideOauth2": {
575
+ "clientId": self.oauth_client_id or "your-client-id",
576
+ "clientSecret": self.oauth_client_secret or "your-client-secret",
577
+ "authorizationUri": (
578
+ "https://accounts.google.com/o/oauth2/auth"
579
+ "?response_type=code"
580
+ f"&client_id={(self.oauth_client_id or 'your-client-id')}"
581
+ "&scope=openid"
582
+ "%20https://www.googleapis.com/auth/userinfo.email"
583
+ "%20https://www.googleapis.com/auth/calendar"
584
+ "%20https://www.googleapis.com/auth/gmail.send"
585
+ "%20https://www.googleapis.com/auth/gmail.compose"
586
+ "%20https://www.googleapis.com/auth/drive"
587
+ "%20https://www.googleapis.com/auth/presentations"
588
+ "%20https://www.googleapis.com/auth/cloud-platform"
589
+ "%20https://mail.google.com/"
590
+ "&access_type=offline&prompt=consent"
591
+ ),
592
+ "tokenUri": "https://oauth2.googleapis.com/token"
593
+ }
594
+ }
595
+ url = f"{BASE_URL}/projects/{self.project_id}/locations/global/authorizations?authorizationId={auth_name}"
596
+ r = self._http.post(url, headers=headers, json=payload, timeout=60)
597
+
598
+ if self.debug:
599
+ from pprint import pprint
600
+ pprint(r.json()) # Debugging output
601
+ if r.status_code < 400:
602
+ return True, "created"
603
+ if r.status_code == 409:
604
+ return True, "exists"
605
+ return False, f"{r.status_code} {r.text}"
606
+
607
+ def deploy_to_agent_space(self, with_authorization: bool, auth_name: str) -> Tuple[str, str, Optional[Dict[str, str]]]:
608
+ if not self._profile.get("name"):
609
+ return ("failed", "Reasoning engine required before deploy", None)
610
+
611
+ if not self.is_live:
612
+ time.sleep(0.02)
613
+ aid = f"agent_{uuid.uuid4().hex[:6]}"
614
+ full = (f"projects/{self.project_id}/locations/global/collections/default_collection/"
615
+ f"engines/{self.engine_name}/assistants/default_assistant/agents/{aid}")
616
+ item = {"id": aid, "display_name": self._profile.get("display_name", "Demo Agent"), "full_name": full}
617
+ self._agents_cache.append(item)
618
+ self._profile["agent_space_agent_id"] = full
619
+ self._save_profile()
620
+ return ("created", f"Deployed (mock, oauth={with_authorization})", item)
621
+
622
+ if with_authorization:
623
+ ok, msg = self._ensure_authorization(auth_name)
624
+ if not ok:
625
+ return ("failed", f"Authorization failed: {msg}", None)
626
+
627
+ headers = {
628
+ "Authorization": f"Bearer {self._access_token()}",
629
+ "Content-Type": "application/json",
630
+ "X-Goog-User-Project": self.project_number,
631
+ }
632
+ payload = {
633
+ "displayName": self._profile.get("display_name", "Live Agent"),
634
+ "description": self._profile.get("description", "Live Agent"),
635
+ "adk_agent_definition": {
636
+ "tool_settings": {"tool_description": self._profile.get("tool_description", "Tooling")},
637
+ "provisioned_reasoning_engine": {"reasoning_engine": self._profile["name"]},
638
+ },
639
+ }
640
+ if with_authorization:
641
+ payload["adk_agent_definition"]["authorizations"] = [
642
+ f"projects/{self.project_number}/locations/global/authorizations/{auth_name}"
643
+ ]
644
+
645
+ url = (f"{BASE_URL}/projects/{self.project_number}/locations/global/collections/default_collection/"
646
+ f"engines/{self.engine_name}/assistants/default_assistant/agents")
647
+ if self.debug:
648
+ pprint(payload)
649
+ pprint(url)
650
+ r = self._http.post(url, headers=headers, json=payload, timeout=90)
651
+
652
+ if self.debug:
653
+ pprint(r.json()) # Debugging output
654
+ if not r.ok:
655
+ return ("failed", f"Deploy failed: {r.status_code} {r.text}", None)
656
+
657
+ info = r.json()
658
+ full = info.get("name", "")
659
+ item = {
660
+ "id": full.split("/")[-1] if full else "",
661
+ "display_name": info.get("displayName", self._profile.get("display_name", "")),
662
+ "full_name": full,
663
+ }
664
+ self._profile["agent_space_agent_id"] = full
665
+ self._save_profile()
666
+ return ("created", "Deployed", item)