glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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.
Files changed (127) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +217 -42
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  8. glaip_sdk/cli/commands/agents/_common.py +561 -0
  9. glaip_sdk/cli/commands/agents/create.py +151 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +369 -23
  55. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +87 -0
  60. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  61. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  62. glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +374 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +5 -3
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +50 -8
  78. glaip_sdk/client/hitl.py +136 -0
  79. glaip_sdk/client/main.py +7 -1
  80. glaip_sdk/client/mcps.py +44 -13
  81. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  82. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  83. glaip_sdk/client/payloads/agent/responses.py +43 -0
  84. glaip_sdk/client/run_rendering.py +414 -3
  85. glaip_sdk/client/schedules.py +439 -0
  86. glaip_sdk/client/tools.py +57 -26
  87. glaip_sdk/guardrails/__init__.py +80 -0
  88. glaip_sdk/guardrails/serializer.py +89 -0
  89. glaip_sdk/hitl/__init__.py +48 -0
  90. glaip_sdk/hitl/base.py +64 -0
  91. glaip_sdk/hitl/callback.py +43 -0
  92. glaip_sdk/hitl/local.py +121 -0
  93. glaip_sdk/hitl/remote.py +523 -0
  94. glaip_sdk/models/__init__.py +17 -0
  95. glaip_sdk/models/agent_runs.py +2 -1
  96. glaip_sdk/models/schedule.py +224 -0
  97. glaip_sdk/payload_schemas/agent.py +1 -0
  98. glaip_sdk/payload_schemas/guardrails.py +34 -0
  99. glaip_sdk/registry/tool.py +273 -59
  100. glaip_sdk/runner/__init__.py +20 -3
  101. glaip_sdk/runner/deps.py +5 -8
  102. glaip_sdk/runner/langgraph.py +318 -42
  103. glaip_sdk/runner/logging_config.py +77 -0
  104. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  105. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  106. glaip_sdk/schedules/__init__.py +22 -0
  107. glaip_sdk/schedules/base.py +291 -0
  108. glaip_sdk/tools/base.py +67 -14
  109. glaip_sdk/utils/__init__.py +1 -0
  110. glaip_sdk/utils/bundler.py +138 -2
  111. glaip_sdk/utils/import_resolver.py +43 -11
  112. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  113. glaip_sdk/utils/runtime_config.py +15 -12
  114. glaip_sdk/utils/sync.py +31 -11
  115. glaip_sdk/utils/tool_detection.py +274 -6
  116. glaip_sdk/utils/tool_storage_provider.py +140 -0
  117. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
  118. glaip_sdk-0.7.12.dist-info/RECORD +219 -0
  119. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
  120. glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
  121. glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
  122. glaip_sdk/cli/commands/agents.py +0 -1509
  123. glaip_sdk/cli/commands/mcps.py +0 -1356
  124. glaip_sdk/cli/commands/tools.py +0 -576
  125. glaip_sdk/cli/utils.py +0 -263
  126. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  127. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
@@ -151,79 +151,149 @@ class AccountsController:
151
151
 
152
152
  def _render_textual(self, rows: list[dict[str, str | bool]], store: AccountStore, env_lock: bool) -> None:
153
153
  """Launch the Textual accounts browser."""
154
- callbacks = AccountsTUICallbacks(switch_account=lambda name: self._switch_account(store, name, env_lock))
154
+ active_before = store.get_active_account()
155
+ notified = False
156
+
157
+ def _switch_in_textual(name: str) -> tuple[bool, str]:
158
+ nonlocal notified
159
+ switched, message = self._switch_account(
160
+ store,
161
+ name,
162
+ env_lock,
163
+ emit_console=False,
164
+ invalidate_session=True,
165
+ )
166
+ if switched:
167
+ notified = True
168
+ return switched, message
169
+
170
+ callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
155
171
  active = next((row["name"] for row in rows if row.get("active")), None)
156
- run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
157
- # Exit snapshot: surface a success banner when a switch occurred inside the TUI
158
- active_after = store.get_active_account() or "default"
172
+ try:
173
+ # Inject TUI context for theme support
174
+ tui_ctx = getattr(self.session, "tui_ctx", None)
175
+ run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks, ctx=tui_ctx)
176
+ except Exception as exc: # pragma: no cover - defensive around Textual failures
177
+ self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
178
+
179
+ # Exit snapshot: surface a success banner when a switch occurred inside the TUI.
180
+ # Always notify when the active account changed, even if Textual raised.
181
+ active_after = store.get_active_account()
182
+ if active_after != active_before and not notified:
183
+ self._notify_account_switched(active_after)
159
184
  if active_after != active:
160
185
  host_after = ""
161
- account_after = store.get_account(active_after) if hasattr(store, "get_account") else None
186
+ display_account = active_after or "default"
187
+ account_after = store.get_account(display_account) if hasattr(store, "get_account") else None
162
188
  if account_after:
163
189
  host_after = account_after.get("api_url", "")
164
190
  host_suffix = f" • {host_after}" if host_after else ""
165
191
  self.console.print(
166
192
  AIPPanel(
167
- f"[{SUCCESS_STYLE}]Active account ➜ {active_after}[/]{host_suffix}",
193
+ f"[{SUCCESS_STYLE}]Active account ➜ {display_account}[/]{host_suffix}",
168
194
  title="✅ Account Switched",
169
195
  border_style=SUCCESS_STYLE,
170
196
  )
171
197
  )
172
198
 
173
- def _switch_account(self, store: AccountStore, name: str, env_lock: bool) -> tuple[bool, str]:
174
- """Validate and switch active account; returns (success, message)."""
199
+ def _format_connection_error_message(self, error_reason: str, account_name: str, api_url: str) -> str:
200
+ """Format error message for connection validation failures."""
201
+ code, detail = self._parse_error_reason(error_reason)
202
+ if code == "connection_failed":
203
+ return f"Switch aborted: cannot reach {api_url}. Check URL or network."
204
+ if code == "api_failed":
205
+ return f"Switch aborted: API error for '{account_name}'. Check credentials."
206
+ detail_suffix = f": {detail}" if detail else ""
207
+ return f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
208
+
209
+ def _emit_error_message(self, msg: str, style: str = ERROR_STYLE) -> None:
210
+ """Emit an error or warning message to the console."""
211
+ self.console.print(f"[{style}]{msg}[/]")
212
+
213
+ def _validate_account_switch(
214
+ self, store: AccountStore, name: str, env_lock: bool, emit_console: bool
215
+ ) -> tuple[bool, str, dict[str, str] | None]:
216
+ """Validate account switch prerequisites; returns (is_valid, error_msg, account_dict)."""
175
217
  if env_lock:
176
218
  msg = "Env credentials detected (AIP_API_URL/AIP_API_KEY); switching is disabled."
177
- self.console.print(f"[{WARNING_STYLE}]{msg}[/]")
178
- return False, msg
219
+ if emit_console:
220
+ self._emit_error_message(msg, WARNING_STYLE)
221
+ return False, msg, None
179
222
 
180
223
  account = store.get_account(name)
181
224
  if not account:
182
225
  msg = f"Account '{name}' not found."
183
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
184
- return False, msg
226
+ if emit_console:
227
+ self._emit_error_message(msg)
228
+ return False, msg, None
185
229
 
186
230
  api_url = account.get("api_url", "")
187
231
  api_key = account.get("api_key", "")
188
232
  if not api_url or not api_key:
189
233
  edit_cmd = f"aip accounts edit {name}"
190
234
  msg = f"Account '{name}' is missing credentials. Use `/login` or `{edit_cmd}`."
191
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
192
- return False, msg
235
+ if emit_console:
236
+ self._emit_error_message(msg)
237
+ return False, msg, None
193
238
 
194
239
  ok, error_reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
195
240
  if not ok:
196
- code, detail = self._parse_error_reason(error_reason)
197
- if code == "connection_failed":
198
- msg = f"Switch aborted: cannot reach {api_url}. Check URL or network."
199
- elif code == "api_failed":
200
- msg = f"Switch aborted: API error for '{name}'. Check credentials."
201
- else:
202
- detail_suffix = f": {detail}" if detail else ""
203
- msg = f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
204
- self.console.print(f"[{WARNING_STYLE}]{msg}[/]")
205
- return False, msg
241
+ msg = self._format_connection_error_message(error_reason, name, api_url)
242
+ if emit_console:
243
+ self._emit_error_message(msg, WARNING_STYLE)
244
+ return False, msg, None
206
245
 
246
+ return True, "", account
247
+
248
+ def _execute_account_switch(
249
+ self, store: AccountStore, name: str, account: dict[str, str], invalidate_session: bool, emit_console: bool
250
+ ) -> tuple[bool, str]:
251
+ """Execute the account switch and emit success message."""
207
252
  try:
208
253
  store.set_active_account(name)
254
+ api_url = account.get("api_url", "")
255
+ api_key = account.get("api_key", "")
209
256
  masked_key = mask_api_key_display(api_key)
210
- self.console.print(
211
- AIPPanel(
212
- f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
213
- title="✅ Account Switched",
214
- border_style=SUCCESS_STYLE,
257
+ if invalidate_session:
258
+ self._notify_account_switched(name)
259
+ if emit_console:
260
+ self.console.print(
261
+ AIPPanel(
262
+ f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
263
+ title="✅ Account Switched",
264
+ border_style=SUCCESS_STYLE,
265
+ )
215
266
  )
216
- )
217
267
  return True, f"Switched to '{name}'."
218
268
  except AccountStoreError as exc:
219
269
  msg = f"Failed to set active account: {exc}"
220
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
270
+ if emit_console:
271
+ self._emit_error_message(msg)
221
272
  return False, msg
222
273
  except Exception as exc: # NOSONAR(S1045) - catch-all needed for unexpected errors
223
274
  msg = f"Unexpected error while switching to '{name}': {exc}"
224
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
275
+ if emit_console:
276
+ self._emit_error_message(msg)
225
277
  return False, msg
226
278
 
279
+ def _switch_account(
280
+ self,
281
+ store: AccountStore,
282
+ name: str,
283
+ env_lock: bool,
284
+ *,
285
+ emit_console: bool = True,
286
+ invalidate_session: bool = True,
287
+ ) -> tuple[bool, str]:
288
+ """Validate and switch active account; returns (success, message)."""
289
+ is_valid, error_msg, account = self._validate_account_switch(store, name, env_lock, emit_console)
290
+ if not is_valid:
291
+ return False, error_msg
292
+
293
+ if account is None: # Defensive – should never happen, but avoid crashing in production
294
+ return False, "Unable to locate account after validation."
295
+ return self._execute_account_switch(store, name, account, invalidate_session, emit_console)
296
+
227
297
  @staticmethod
228
298
  def _parse_error_reason(reason: str | None) -> tuple[str, str]:
229
299
  """Parse error reason into (code, detail) to avoid fragile substring checks."""
@@ -404,8 +474,18 @@ class AccountsController:
404
474
  except Exception as exc:
405
475
  self.console.print(f"[{WARNING_STYLE}]Account saved but could not set active: {exc}[/]")
406
476
  else:
477
+ self._notify_account_switched(name)
407
478
  self._announce_active_change(store, name)
408
479
 
480
+ def _notify_account_switched(self, name: str | None) -> None:
481
+ """Best-effort notify the hosting session that the active account changed."""
482
+ notify = getattr(self.session, "on_account_switched", None)
483
+ if callable(notify):
484
+ try:
485
+ notify(name)
486
+ except Exception: # pragma: no cover - best-effort callback
487
+ pass
488
+
409
489
  def _confirm_delete_prompt(self, name: str) -> bool:
410
490
  """Ask for delete confirmation; return True when confirmed."""
411
491
  self.console.print(f"[{WARNING_STYLE}]Type '{name}' to confirm deletion. This cannot be undone.[/]")
@@ -17,7 +17,7 @@ from glaip_sdk.cli.commands.agents import run as agents_run_command
17
17
  from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
18
18
  from glaip_sdk.cli.hints import format_command_hint
19
19
  from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
20
- from glaip_sdk.cli.utils import bind_slash_session_context
20
+ from glaip_sdk.cli.core.context import bind_slash_session_context
21
21
 
22
22
  if TYPE_CHECKING: # pragma: no cover - type checking only
23
23
  from glaip_sdk.cli.slash.session import SlashSession
@@ -38,7 +38,10 @@ class AgentRunSession:
38
38
  self.console = session.console
39
39
  self._agent_id = str(getattr(agent, "id", ""))
40
40
  self._agent_name = getattr(agent, "name", "") or self._agent_id
41
- self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
41
+ self._prompt_placeholder: str = (
42
+ "Chat with this agent here; use / for shortcuts. "
43
+ "Alt+Enter inserts a newline. Ctrl+T opens the last transcript."
44
+ )
42
45
  self._contextual_completion_help: dict[str, str] = {
43
46
  "details": "Show this agent's configuration (+ expands prompt).",
44
47
  "help": "Display this context-aware menu.",
@@ -162,6 +162,17 @@ def _create_key_bindings(_session: SlashSession) -> Any:
162
162
  if buffer.complete_state is not None:
163
163
  buffer.cancel_completion()
164
164
 
165
+ @bindings.add("c-t") # type: ignore[misc]
166
+ def _handle_ctrl_t_key(event: Any) -> None: # vulture: ignore
167
+ """Handle Ctrl+T key - open the transcript viewer (when available)."""
168
+ buffer = event.app.current_buffer
169
+ if buffer.complete_state is not None:
170
+ buffer.cancel_completion()
171
+
172
+ open_viewer = getattr(_session, "open_transcript_viewer", None)
173
+ if callable(open_viewer):
174
+ open_viewer(announce=True)
175
+
165
176
  return bindings
166
177
 
167
178
 
@@ -30,7 +30,7 @@ from glaip_sdk.branding import (
30
30
  )
31
31
  from glaip_sdk.cli.constants import DEFAULT_REMOTE_RUNS_PAGE_LIMIT
32
32
  from glaip_sdk.cli.slash.tui.remote_runs_app import RemoteRunsTUICallbacks, run_remote_runs_textual
33
- from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
33
+ from glaip_sdk.cli.core.prompting import prompt_export_choice_questionary, questionary_safe_ask
34
34
  from glaip_sdk.exceptions import (
35
35
  AuthenticationError,
36
36
  ForbiddenError,
@@ -294,12 +294,14 @@ class RemoteRunsController:
294
294
  fetch_detail=fetch_detail,
295
295
  export_run=export_run,
296
296
  )
297
+ tui_ctx = getattr(self.session, "tui_ctx", None)
297
298
  page, limit, cursor = run_remote_runs_textual(
298
299
  runs_page,
299
300
  state.get("cursor", 0),
300
301
  callbacks,
301
302
  agent_name=agent_name,
302
303
  agent_id=agent_id,
304
+ ctx=tui_ctx,
303
305
  )
304
306
  state["page"] = page
305
307
  state["limit"] = limit