glaip-sdk 0.6.5b6__py3-none-any.whl → 0.6.6__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.
- glaip_sdk/agents/base.py +7 -43
- glaip_sdk/cli/slash/accounts_controller.py +110 -32
- glaip_sdk/cli/slash/agent_session.py +4 -1
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/session.py +35 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +4 -0
- glaip_sdk/cli/transcript/viewer.py +4 -2
- glaip_sdk/client/tools.py +5 -3
- glaip_sdk/registry/tool.py +11 -4
- glaip_sdk/utils/runtime_config.py +0 -116
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.6.6.dist-info}/METADATA +3 -13
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.6.6.dist-info}/RECORD +14 -28
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.6.6.dist-info}/WHEEL +1 -1
- glaip_sdk/runner/__init__.py +0 -59
- glaip_sdk/runner/base.py +0 -84
- glaip_sdk/runner/deps.py +0 -115
- glaip_sdk/runner/langgraph.py +0 -597
- glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -158
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
- glaip_sdk/runner/tool_adapter/__init__.py +0 -18
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -177
- glaip_sdk/utils/a2a/__init__.py +0 -34
- glaip_sdk/utils/a2a/event_processor.py +0 -188
- glaip_sdk/utils/tool_detection.py +0 -33
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.6.6.dist-info}/entry_points.txt +0 -0
glaip_sdk/agents/base.py
CHANGED
|
@@ -50,11 +50,6 @@ from pathlib import Path
|
|
|
50
50
|
from typing import TYPE_CHECKING, Any
|
|
51
51
|
|
|
52
52
|
from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
|
|
53
|
-
from glaip_sdk.runner import get_default_runner
|
|
54
|
-
from glaip_sdk.runner.deps import (
|
|
55
|
-
check_local_runtime_available,
|
|
56
|
-
get_local_runtime_missing_message,
|
|
57
|
-
)
|
|
58
53
|
from glaip_sdk.utils.discovery import find_agent
|
|
59
54
|
from glaip_sdk.utils.resource_refs import is_uuid
|
|
60
55
|
from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
|
|
@@ -898,20 +893,13 @@ class Agent:
|
|
|
898
893
|
message: str,
|
|
899
894
|
verbose: bool = False,
|
|
900
895
|
runtime_config: dict[str, Any] | None = None,
|
|
901
|
-
chat_history: list[dict[str, str]] | None = None,
|
|
902
896
|
**kwargs: Any,
|
|
903
897
|
) -> str:
|
|
904
898
|
"""Run the agent synchronously with a message.
|
|
905
899
|
|
|
906
|
-
Supports two execution modes:
|
|
907
|
-
- **Server-backed**: When the agent is deployed (has an ID), execution
|
|
908
|
-
happens via the AIP backend server.
|
|
909
|
-
- **Local**: When the agent is not deployed and glaip-sdk[local] is installed,
|
|
910
|
-
execution happens locally via aip-agents (no server required).
|
|
911
|
-
|
|
912
900
|
Args:
|
|
913
901
|
message: The message to send to the agent.
|
|
914
|
-
verbose: If True, print streaming output to console.
|
|
902
|
+
verbose: If True, print streaming output to console.
|
|
915
903
|
runtime_config: Optional runtime configuration for tools, MCPs, and agents.
|
|
916
904
|
Keys can be SDK objects, UUIDs, or names. Example:
|
|
917
905
|
{
|
|
@@ -919,43 +907,19 @@ class Agent:
|
|
|
919
907
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
920
908
|
"agent_config": {"planning": True},
|
|
921
909
|
}
|
|
922
|
-
Defaults to None.
|
|
923
|
-
chat_history: Optional list of prior conversation messages for context.
|
|
924
|
-
Each message is a dict with "role" and "content" keys.
|
|
925
|
-
Defaults to None.
|
|
926
910
|
**kwargs: Additional arguments to pass to the run API.
|
|
927
911
|
|
|
928
912
|
Returns:
|
|
929
913
|
The agent's response as a string.
|
|
930
914
|
|
|
931
915
|
Raises:
|
|
932
|
-
ValueError: If the agent
|
|
933
|
-
RuntimeError: If
|
|
916
|
+
ValueError: If the agent hasn't been deployed yet.
|
|
917
|
+
RuntimeError: If client is not available.
|
|
934
918
|
"""
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
|
|
940
|
-
)
|
|
941
|
-
if chat_history is not None:
|
|
942
|
-
call_kwargs["chat_history"] = chat_history
|
|
943
|
-
return agent_client.run_agent(**call_kwargs)
|
|
944
|
-
|
|
945
|
-
# Local execution path (agent is not deployed)
|
|
946
|
-
if check_local_runtime_available():
|
|
947
|
-
runner = get_default_runner()
|
|
948
|
-
return runner.run(
|
|
949
|
-
agent=self,
|
|
950
|
-
message=message,
|
|
951
|
-
verbose=verbose,
|
|
952
|
-
runtime_config=runtime_config,
|
|
953
|
-
chat_history=chat_history,
|
|
954
|
-
**kwargs,
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
# Neither deployed nor local runtime available - provide actionable error
|
|
958
|
-
raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
|
|
919
|
+
agent_client, call_kwargs = self._prepare_run_kwargs(
|
|
920
|
+
message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
|
|
921
|
+
)
|
|
922
|
+
return agent_client.run_agent(**call_kwargs)
|
|
959
923
|
|
|
960
924
|
async def arun(
|
|
961
925
|
self,
|
|
@@ -151,79 +151,147 @@ 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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
try:
|
|
173
|
+
run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
|
|
174
|
+
except Exception as exc: # pragma: no cover - defensive around Textual failures
|
|
175
|
+
self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
|
|
176
|
+
|
|
177
|
+
# Exit snapshot: surface a success banner when a switch occurred inside the TUI.
|
|
178
|
+
# Always notify when the active account changed, even if Textual raised.
|
|
179
|
+
active_after = store.get_active_account()
|
|
180
|
+
if active_after != active_before and not notified:
|
|
181
|
+
self._notify_account_switched(active_after)
|
|
159
182
|
if active_after != active:
|
|
160
183
|
host_after = ""
|
|
161
|
-
|
|
184
|
+
display_account = active_after or "default"
|
|
185
|
+
account_after = store.get_account(display_account) if hasattr(store, "get_account") else None
|
|
162
186
|
if account_after:
|
|
163
187
|
host_after = account_after.get("api_url", "")
|
|
164
188
|
host_suffix = f" • {host_after}" if host_after else ""
|
|
165
189
|
self.console.print(
|
|
166
190
|
AIPPanel(
|
|
167
|
-
f"[{SUCCESS_STYLE}]Active account ➜ {
|
|
191
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {display_account}[/]{host_suffix}",
|
|
168
192
|
title="✅ Account Switched",
|
|
169
193
|
border_style=SUCCESS_STYLE,
|
|
170
194
|
)
|
|
171
195
|
)
|
|
172
196
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
197
|
+
def _format_connection_error_message(self, error_reason: str, account_name: str, api_url: str) -> str:
|
|
198
|
+
"""Format error message for connection validation failures."""
|
|
199
|
+
code, detail = self._parse_error_reason(error_reason)
|
|
200
|
+
if code == "connection_failed":
|
|
201
|
+
return f"Switch aborted: cannot reach {api_url}. Check URL or network."
|
|
202
|
+
if code == "api_failed":
|
|
203
|
+
return f"Switch aborted: API error for '{account_name}'. Check credentials."
|
|
204
|
+
detail_suffix = f": {detail}" if detail else ""
|
|
205
|
+
return f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
|
|
206
|
+
|
|
207
|
+
def _emit_error_message(self, msg: str, style: str = ERROR_STYLE) -> None:
|
|
208
|
+
"""Emit an error or warning message to the console."""
|
|
209
|
+
self.console.print(f"[{style}]{msg}[/]")
|
|
210
|
+
|
|
211
|
+
def _validate_account_switch(
|
|
212
|
+
self, store: AccountStore, name: str, env_lock: bool, emit_console: bool
|
|
213
|
+
) -> tuple[bool, str, dict[str, str] | None]:
|
|
214
|
+
"""Validate account switch prerequisites; returns (is_valid, error_msg, account_dict)."""
|
|
175
215
|
if env_lock:
|
|
176
216
|
msg = "Env credentials detected (AIP_API_URL/AIP_API_KEY); switching is disabled."
|
|
177
|
-
|
|
178
|
-
|
|
217
|
+
if emit_console:
|
|
218
|
+
self._emit_error_message(msg, WARNING_STYLE)
|
|
219
|
+
return False, msg, None
|
|
179
220
|
|
|
180
221
|
account = store.get_account(name)
|
|
181
222
|
if not account:
|
|
182
223
|
msg = f"Account '{name}' not found."
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
if emit_console:
|
|
225
|
+
self._emit_error_message(msg)
|
|
226
|
+
return False, msg, None
|
|
185
227
|
|
|
186
228
|
api_url = account.get("api_url", "")
|
|
187
229
|
api_key = account.get("api_key", "")
|
|
188
230
|
if not api_url or not api_key:
|
|
189
231
|
edit_cmd = f"aip accounts edit {name}"
|
|
190
232
|
msg = f"Account '{name}' is missing credentials. Use `/login` or `{edit_cmd}`."
|
|
191
|
-
|
|
192
|
-
|
|
233
|
+
if emit_console:
|
|
234
|
+
self._emit_error_message(msg)
|
|
235
|
+
return False, msg, None
|
|
193
236
|
|
|
194
237
|
ok, error_reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
|
|
195
238
|
if not ok:
|
|
196
|
-
|
|
197
|
-
if
|
|
198
|
-
msg
|
|
199
|
-
|
|
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
|
|
239
|
+
msg = self._format_connection_error_message(error_reason, name, api_url)
|
|
240
|
+
if emit_console:
|
|
241
|
+
self._emit_error_message(msg, WARNING_STYLE)
|
|
242
|
+
return False, msg, None
|
|
206
243
|
|
|
244
|
+
return True, "", account
|
|
245
|
+
|
|
246
|
+
def _execute_account_switch(
|
|
247
|
+
self, store: AccountStore, name: str, account: dict[str, str], invalidate_session: bool, emit_console: bool
|
|
248
|
+
) -> tuple[bool, str]:
|
|
249
|
+
"""Execute the account switch and emit success message."""
|
|
207
250
|
try:
|
|
208
251
|
store.set_active_account(name)
|
|
252
|
+
api_url = account.get("api_url", "")
|
|
253
|
+
api_key = account.get("api_key", "")
|
|
209
254
|
masked_key = mask_api_key_display(api_key)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
255
|
+
if invalidate_session:
|
|
256
|
+
self._notify_account_switched(name)
|
|
257
|
+
if emit_console:
|
|
258
|
+
self.console.print(
|
|
259
|
+
AIPPanel(
|
|
260
|
+
f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
|
|
261
|
+
title="✅ Account Switched",
|
|
262
|
+
border_style=SUCCESS_STYLE,
|
|
263
|
+
)
|
|
215
264
|
)
|
|
216
|
-
)
|
|
217
265
|
return True, f"Switched to '{name}'."
|
|
218
266
|
except AccountStoreError as exc:
|
|
219
267
|
msg = f"Failed to set active account: {exc}"
|
|
220
|
-
|
|
268
|
+
if emit_console:
|
|
269
|
+
self._emit_error_message(msg)
|
|
221
270
|
return False, msg
|
|
222
271
|
except Exception as exc: # NOSONAR(S1045) - catch-all needed for unexpected errors
|
|
223
272
|
msg = f"Unexpected error while switching to '{name}': {exc}"
|
|
224
|
-
|
|
273
|
+
if emit_console:
|
|
274
|
+
self._emit_error_message(msg)
|
|
225
275
|
return False, msg
|
|
226
276
|
|
|
277
|
+
def _switch_account(
|
|
278
|
+
self,
|
|
279
|
+
store: AccountStore,
|
|
280
|
+
name: str,
|
|
281
|
+
env_lock: bool,
|
|
282
|
+
*,
|
|
283
|
+
emit_console: bool = True,
|
|
284
|
+
invalidate_session: bool = True,
|
|
285
|
+
) -> tuple[bool, str]:
|
|
286
|
+
"""Validate and switch active account; returns (success, message)."""
|
|
287
|
+
is_valid, error_msg, account = self._validate_account_switch(store, name, env_lock, emit_console)
|
|
288
|
+
if not is_valid:
|
|
289
|
+
return False, error_msg
|
|
290
|
+
|
|
291
|
+
if account is None: # Defensive – should never happen, but avoid crashing in production
|
|
292
|
+
return False, "Unable to locate account after validation."
|
|
293
|
+
return self._execute_account_switch(store, name, account, invalidate_session, emit_console)
|
|
294
|
+
|
|
227
295
|
@staticmethod
|
|
228
296
|
def _parse_error_reason(reason: str | None) -> tuple[str, str]:
|
|
229
297
|
"""Parse error reason into (code, detail) to avoid fragile substring checks."""
|
|
@@ -404,8 +472,18 @@ class AccountsController:
|
|
|
404
472
|
except Exception as exc:
|
|
405
473
|
self.console.print(f"[{WARNING_STYLE}]Account saved but could not set active: {exc}[/]")
|
|
406
474
|
else:
|
|
475
|
+
self._notify_account_switched(name)
|
|
407
476
|
self._announce_active_change(store, name)
|
|
408
477
|
|
|
478
|
+
def _notify_account_switched(self, name: str | None) -> None:
|
|
479
|
+
"""Best-effort notify the hosting session that the active account changed."""
|
|
480
|
+
notify = getattr(self.session, "on_account_switched", None)
|
|
481
|
+
if callable(notify):
|
|
482
|
+
try:
|
|
483
|
+
notify(name)
|
|
484
|
+
except Exception: # pragma: no cover - best-effort callback
|
|
485
|
+
pass
|
|
486
|
+
|
|
409
487
|
def _confirm_delete_prompt(self, name: str) -> bool:
|
|
410
488
|
"""Ask for delete confirmation; return True when confirmed."""
|
|
411
489
|
self.console.print(f"[{WARNING_STYLE}]Type '{name}' to confirm deletion. This cannot be undone.[/]")
|
|
@@ -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 =
|
|
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.",
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -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
|
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -548,7 +548,7 @@ class SlashSession:
|
|
|
548
548
|
try:
|
|
549
549
|
# Use the modern account-aware wizard directly (bypasses legacy config gating)
|
|
550
550
|
_configure_interactive(account_name=None)
|
|
551
|
-
self.
|
|
551
|
+
self.on_account_switched()
|
|
552
552
|
if self._suppress_login_layout:
|
|
553
553
|
self._welcome_rendered = False
|
|
554
554
|
self._default_actions_shown = False
|
|
@@ -1211,6 +1211,33 @@ class SlashSession:
|
|
|
1211
1211
|
self._client = get_client(self.ctx)
|
|
1212
1212
|
return self._client
|
|
1213
1213
|
|
|
1214
|
+
def on_account_switched(self, _account_name: str | None = None) -> None:
|
|
1215
|
+
"""Reset any state that depends on the active account.
|
|
1216
|
+
|
|
1217
|
+
The active account can change via `/accounts` (or other flows that call
|
|
1218
|
+
AccountStore.set_active_account). The slash session caches a configured
|
|
1219
|
+
client instance, so we must invalidate it to avoid leaking the previous
|
|
1220
|
+
account's API URL/key into subsequent commands like `/agents` or `/runs`.
|
|
1221
|
+
|
|
1222
|
+
This method clears:
|
|
1223
|
+
- Client and config cache (account-specific credentials)
|
|
1224
|
+
- Current agent and recent agents (agent data is account-scoped)
|
|
1225
|
+
- Runs pagination state (runs are account-scoped)
|
|
1226
|
+
- Active renderer and transcript ready state (UI state tied to account context)
|
|
1227
|
+
- Contextual commands (may be account-specific)
|
|
1228
|
+
|
|
1229
|
+
These broader resets ensure a clean slate when switching accounts, preventing
|
|
1230
|
+
stale data from the previous account from appearing in the new account's context.
|
|
1231
|
+
"""
|
|
1232
|
+
self._client = None
|
|
1233
|
+
self._config_cache = None
|
|
1234
|
+
self._current_agent = None
|
|
1235
|
+
self.recent_agents = []
|
|
1236
|
+
self._runs_pagination_state.clear()
|
|
1237
|
+
self.clear_active_renderer()
|
|
1238
|
+
self.clear_agent_transcript_ready()
|
|
1239
|
+
self.set_contextual_commands(None)
|
|
1240
|
+
|
|
1214
1241
|
def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
|
|
1215
1242
|
"""Set context-specific commands that should appear in completions."""
|
|
1216
1243
|
self._contextual_commands = dict(commands or {})
|
|
@@ -1340,14 +1367,16 @@ class SlashSession:
|
|
|
1340
1367
|
f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
|
|
1341
1368
|
)
|
|
1342
1369
|
status_line = f"[{SUCCESS_STYLE}]ready[/]"
|
|
1343
|
-
|
|
1370
|
+
if not transcript_status["has_transcript"]:
|
|
1371
|
+
status_line += " · no transcript"
|
|
1372
|
+
elif transcript_status["transcript_ready"]:
|
|
1373
|
+
status_line += " · transcript ready"
|
|
1374
|
+
else:
|
|
1375
|
+
status_line += " · transcript pending"
|
|
1344
1376
|
header_grid.add_row(primary_line, status_line)
|
|
1345
1377
|
|
|
1346
1378
|
if agent_info["description"]:
|
|
1347
|
-
|
|
1348
|
-
if not transcript_status["transcript_ready"]:
|
|
1349
|
-
description = f"{description} (transcript pending)"
|
|
1350
|
-
header_grid.add_row(f"[dim]{description}[/dim]", "")
|
|
1379
|
+
header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
|
|
1351
1380
|
|
|
1352
1381
|
return header_grid
|
|
1353
1382
|
|
|
@@ -684,6 +684,10 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
684
684
|
return
|
|
685
685
|
self.exit()
|
|
686
686
|
|
|
687
|
+
def action_app_exit(self) -> None:
|
|
688
|
+
"""Exit the application regardless of focus state."""
|
|
689
|
+
self.exit()
|
|
690
|
+
|
|
687
691
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
688
692
|
"""Handle filter bar buttons."""
|
|
689
693
|
if event.button.id == "filter-clear":
|
|
@@ -23,6 +23,7 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
23
23
|
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
24
24
|
from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
|
|
25
25
|
from glaip_sdk.utils.rendering.layout.progress import is_delegation_tool
|
|
26
|
+
from glaip_sdk.utils.rendering.layout.transcript import DEFAULT_TRANSCRIPT_THEME
|
|
26
27
|
from glaip_sdk.utils.rendering.viewer import (
|
|
27
28
|
ViewerContext as PresenterViewerContext,
|
|
28
29
|
prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
|
|
@@ -93,8 +94,9 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
93
94
|
if self._view_mode == "default":
|
|
94
95
|
presenter_render_post_run_view(self.console, self.ctx)
|
|
95
96
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
theme = DEFAULT_TRANSCRIPT_THEME
|
|
98
|
+
snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None, theme=theme)
|
|
99
|
+
presenter_render_transcript_view(self.console, snapshot, theme=theme)
|
|
98
100
|
presenter_render_transcript_events(self.console, state.events)
|
|
99
101
|
|
|
100
102
|
# ------------------------------------------------------------------
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -562,12 +562,14 @@ class ToolClient(BaseClient):
|
|
|
562
562
|
) -> Tool:
|
|
563
563
|
"""Find tool by name and update, or create if not found."""
|
|
564
564
|
existing = self.find_tools(name)
|
|
565
|
+
name_lower = name.lower()
|
|
566
|
+
exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
|
|
565
567
|
|
|
566
|
-
if len(
|
|
568
|
+
if len(exact_matches) == 1:
|
|
567
569
|
logger.info("Updating existing tool: %s", name)
|
|
568
|
-
return self._do_tool_upsert_update(
|
|
570
|
+
return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
|
|
569
571
|
|
|
570
|
-
if len(
|
|
572
|
+
if len(exact_matches) > 1:
|
|
571
573
|
raise ValueError(f"Multiple tools found with name '{name}'")
|
|
572
574
|
|
|
573
575
|
# Create new tool - code is required
|
glaip_sdk/registry/tool.py
CHANGED
|
@@ -160,12 +160,19 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
160
160
|
True if ref is a custom tool that needs uploading.
|
|
161
161
|
"""
|
|
162
162
|
try:
|
|
163
|
-
from
|
|
163
|
+
from langchain_core.tools import BaseTool # noqa: PLC0415
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
# LangChain BaseTool class
|
|
166
|
+
if isinstance(ref, type) and issubclass(ref, BaseTool):
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
# LangChain BaseTool instance
|
|
170
|
+
if isinstance(ref, BaseTool):
|
|
171
|
+
return True
|
|
166
172
|
except ImportError:
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
return False
|
|
169
176
|
|
|
170
177
|
def resolve(self, ref: Any) -> Tool:
|
|
171
178
|
"""Resolve a tool reference to a platform Tool object.
|
|
@@ -304,119 +304,3 @@ def normalize_runtime_config_keys(
|
|
|
304
304
|
logger.warning("Unknown field '%s' in runtime_config, ignoring", field)
|
|
305
305
|
|
|
306
306
|
return result
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# =============================================================================
|
|
310
|
-
# LOCAL MODE UTILITIES
|
|
311
|
-
# =============================================================================
|
|
312
|
-
# The functions below are for local execution mode where resources are NOT
|
|
313
|
-
# deployed and have no UUIDs. They resolve keys to names (not IDs).
|
|
314
|
-
# =============================================================================
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _get_name_from_class(cls: type) -> str:
|
|
318
|
-
"""Extract name from a class, handling Pydantic models.
|
|
319
|
-
|
|
320
|
-
Args:
|
|
321
|
-
cls: The class to extract name from.
|
|
322
|
-
|
|
323
|
-
Returns:
|
|
324
|
-
The resolved name string.
|
|
325
|
-
"""
|
|
326
|
-
# Try class-level name attribute first
|
|
327
|
-
class_name = getattr(cls, "name", None)
|
|
328
|
-
if class_name:
|
|
329
|
-
return class_name
|
|
330
|
-
|
|
331
|
-
# For Pydantic models, check model_fields for default value
|
|
332
|
-
model_fields = getattr(cls, "model_fields", None)
|
|
333
|
-
if model_fields and "name" in model_fields:
|
|
334
|
-
field_info = model_fields["name"]
|
|
335
|
-
default = getattr(field_info, "default", None)
|
|
336
|
-
if default and isinstance(default, str):
|
|
337
|
-
return default
|
|
338
|
-
|
|
339
|
-
# Fallback to class __name__
|
|
340
|
-
return cls.__name__
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def get_name_from_key(key: object) -> str | None:
|
|
344
|
-
"""Resolve config key to name for local mode (no registry needed).
|
|
345
|
-
|
|
346
|
-
Supports instances, classes, and string names. UUID strings are not
|
|
347
|
-
supported in local mode and return None with a warning.
|
|
348
|
-
|
|
349
|
-
Args:
|
|
350
|
-
key: Tool, MCP, or Agent instance/class/string.
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
The resolved name string, or None if UUID (not applicable locally).
|
|
354
|
-
|
|
355
|
-
Raises:
|
|
356
|
-
ValueError: If the key cannot be resolved to a valid name.
|
|
357
|
-
"""
|
|
358
|
-
# Instance with name attribute
|
|
359
|
-
if hasattr(key, "name"):
|
|
360
|
-
name = getattr(key, "name", None)
|
|
361
|
-
if name:
|
|
362
|
-
return name
|
|
363
|
-
raise ValueError(f"Unable to resolve config key: {key!r}")
|
|
364
|
-
|
|
365
|
-
# Class type (not instance)
|
|
366
|
-
if isinstance(key, type):
|
|
367
|
-
return _get_name_from_class(key)
|
|
368
|
-
|
|
369
|
-
# String key
|
|
370
|
-
if isinstance(key, str):
|
|
371
|
-
if is_uuid(key):
|
|
372
|
-
logger.warning("UUID '%s' not supported in local mode, skipping", key)
|
|
373
|
-
return None
|
|
374
|
-
return key
|
|
375
|
-
|
|
376
|
-
raise ValueError(f"Unable to resolve config key: {key!r}")
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def normalize_local_config_keys(config: dict[object, object]) -> dict[str, object]:
|
|
380
|
-
"""Normalize all keys in a config dict to names for local mode.
|
|
381
|
-
|
|
382
|
-
Converts instance/class/string keys to string names without using
|
|
383
|
-
registry. UUID keys are skipped with a warning.
|
|
384
|
-
|
|
385
|
-
Args:
|
|
386
|
-
config: Dict with instance/class/string keys and any values.
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
Dict with string name keys only. UUID keys are omitted.
|
|
390
|
-
"""
|
|
391
|
-
if not config:
|
|
392
|
-
return {}
|
|
393
|
-
|
|
394
|
-
result: dict[str, object] = {}
|
|
395
|
-
for key, value in config.items():
|
|
396
|
-
name = get_name_from_key(key)
|
|
397
|
-
if name is not None:
|
|
398
|
-
result[name] = value
|
|
399
|
-
return result
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def merge_configs(*configs: dict | None) -> dict:
|
|
403
|
-
"""Merge multiple config dicts with priority ordering.
|
|
404
|
-
|
|
405
|
-
Later configs override earlier ones for the same key. None configs
|
|
406
|
-
are skipped gracefully.
|
|
407
|
-
|
|
408
|
-
Args:
|
|
409
|
-
*configs: Config dicts in priority order (lowest priority first).
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
Merged config dict with later values overriding earlier ones.
|
|
413
|
-
|
|
414
|
-
Example:
|
|
415
|
-
>>> merge_configs({"a": 1}, {"a": 2, "b": 3})
|
|
416
|
-
{"a": 2, "b": 3}
|
|
417
|
-
"""
|
|
418
|
-
result: dict = {}
|
|
419
|
-
for config in configs:
|
|
420
|
-
if config:
|
|
421
|
-
result.update(config)
|
|
422
|
-
return result
|
|
@@ -1,37 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: glaip-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Raymond Christopher
|
|
7
7
|
Author-email: raymond.christopher@gdplabs.id
|
|
8
|
-
Requires-Python: >=3.11,<3.
|
|
8
|
+
Requires-Python: >=3.11,<3.14
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Provides-Extra: dev
|
|
14
|
-
Requires-Dist: aip-agents-binary (>=0.5.0)
|
|
15
13
|
Requires-Dist: click (>=8.2.0,<8.3.0)
|
|
16
14
|
Requires-Dist: gllm-core-binary (>=0.1.0)
|
|
17
15
|
Requires-Dist: gllm-tools-binary (>=0.1.3)
|
|
18
16
|
Requires-Dist: httpx (>=0.28.1)
|
|
19
17
|
Requires-Dist: langchain-core (>=0.3.0)
|
|
20
18
|
Requires-Dist: packaging (>=23.2)
|
|
21
|
-
Requires-Dist: pre-commit (>=4.3.0) ; extra == "dev"
|
|
22
19
|
Requires-Dist: pydantic (>=2.0.0)
|
|
23
|
-
Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
|
|
24
|
-
Requires-Dist: pytest-asyncio (>=0.23.6) ; extra == "dev"
|
|
25
|
-
Requires-Dist: pytest-cov (>=4.0.0) ; extra == "dev"
|
|
26
|
-
Requires-Dist: pytest-dotenv (>=0.5.2) ; extra == "dev"
|
|
27
|
-
Requires-Dist: pytest-timeout (>=2.3.1) ; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest-xdist (>=3.8.0) ; extra == "dev"
|
|
29
20
|
Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
|
|
30
21
|
Requires-Dist: pyyaml (>=6.0.0)
|
|
31
22
|
Requires-Dist: questionary (>=2.1.0,<3.0.0)
|
|
32
23
|
Requires-Dist: readchar (>=4.2.1,<5.0.0)
|
|
33
24
|
Requires-Dist: rich (>=13.0.0)
|
|
34
|
-
Requires-Dist: ruff (>=0.14.0) ; extra == "dev"
|
|
35
25
|
Requires-Dist: textual (>=0.52.0)
|
|
36
26
|
Description-Content-Type: text/markdown
|
|
37
27
|
|