glaip-sdk 0.0.20__py3-none-any.whl → 0.1.3__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/_version.py +1 -3
- glaip_sdk/branding.py +2 -6
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +11 -30
- glaip_sdk/cli/commands/agents.py +64 -107
- glaip_sdk/cli/commands/configure.py +12 -36
- glaip_sdk/cli/commands/mcps.py +25 -63
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/tools.py +22 -35
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +1 -3
- glaip_sdk/cli/display.py +10 -13
- glaip_sdk/cli/io.py +8 -14
- glaip_sdk/cli/main.py +10 -30
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +3 -9
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/agent_session.py +5 -10
- glaip_sdk/cli/slash/prompt.py +3 -10
- glaip_sdk/cli/slash/session.py +46 -98
- glaip_sdk/cli/transcript/cache.py +6 -19
- glaip_sdk/cli/transcript/capture.py +45 -20
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +224 -47
- glaip_sdk/cli/update_notifier.py +165 -21
- glaip_sdk/cli/utils.py +33 -91
- glaip_sdk/cli/validators.py +11 -12
- glaip_sdk/client/_agent_payloads.py +10 -30
- glaip_sdk/client/agents.py +33 -63
- glaip_sdk/client/base.py +77 -35
- glaip_sdk/client/mcps.py +1 -3
- glaip_sdk/client/run_rendering.py +121 -26
- glaip_sdk/client/tools.py +8 -24
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/icons.py +9 -3
- glaip_sdk/models.py +14 -33
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/client_utils.py +7 -21
- glaip_sdk/utils/display.py +2 -6
- glaip_sdk/utils/general.py +1 -3
- glaip_sdk/utils/import_export.py +3 -9
- glaip_sdk/utils/rendering/formatting.py +52 -12
- glaip_sdk/utils/rendering/models.py +17 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +1 -5
- glaip_sdk/utils/rendering/renderer/base.py +1181 -328
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +4 -14
- glaip_sdk/utils/rendering/renderer/panels.py +1 -3
- glaip_sdk/utils/rendering/renderer/progress.py +3 -11
- glaip_sdk/utils/rendering/renderer/stream.py +9 -42
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps.py +899 -25
- glaip_sdk/utils/resource_refs.py +4 -13
- glaip_sdk/utils/serialization.py +14 -46
- glaip_sdk/utils/validation.py +4 -4
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/METADATA +12 -1
- glaip_sdk-0.1.3.dist-info/RECORD +83 -0
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/entry_points.txt +0 -0
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -90,9 +90,7 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
|
|
|
90
90
|
return result if result else None
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def create_model_instances(
|
|
94
|
-
data: list[dict] | None, model_class: type, client: Any
|
|
95
|
-
) -> list[Any]:
|
|
93
|
+
def create_model_instances(data: list[dict] | None, model_class: type, client: Any) -> list[Any]:
|
|
96
94
|
"""Create model instances from API data with client association.
|
|
97
95
|
|
|
98
96
|
This is a common pattern used across different clients (agents, tools, mcps)
|
|
@@ -112,9 +110,7 @@ def create_model_instances(
|
|
|
112
110
|
return [model_class(**item_data)._set_client(client) for item_data in data]
|
|
113
111
|
|
|
114
112
|
|
|
115
|
-
def find_by_name(
|
|
116
|
-
items: list[Any], name: str, case_sensitive: bool = False
|
|
117
|
-
) -> list[Any]:
|
|
113
|
+
def find_by_name(items: list[Any], name: str, case_sensitive: bool = False) -> list[Any]:
|
|
118
114
|
"""Filter items by name with optional case sensitivity.
|
|
119
115
|
|
|
120
116
|
This is a common pattern used across different clients for client-side
|
|
@@ -234,9 +230,7 @@ def _handle_streaming_error(
|
|
|
234
230
|
if isinstance(e, httpx.ReadTimeout):
|
|
235
231
|
logger.error(f"Read timeout during streaming: {e}")
|
|
236
232
|
logger.error("This usually indicates the backend is taking too long to respond")
|
|
237
|
-
logger.error(
|
|
238
|
-
"Consider increasing the timeout value or checking backend performance"
|
|
239
|
-
)
|
|
233
|
+
logger.error("Consider increasing the timeout value or checking backend performance")
|
|
240
234
|
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
241
235
|
|
|
242
236
|
elif isinstance(e, httpx.TimeoutException):
|
|
@@ -275,9 +269,7 @@ def _yield_event_data(event_data: dict[str, Any] | None) -> Iterator[dict[str, A
|
|
|
275
269
|
yield event_data
|
|
276
270
|
|
|
277
271
|
|
|
278
|
-
def _flush_remaining_buffer(
|
|
279
|
-
buf: list[str], event_type: str | None, event_id: str | None
|
|
280
|
-
) -> Iterator[dict[str, Any]]:
|
|
272
|
+
def _flush_remaining_buffer(buf: list[str], event_type: str | None, event_id: str | None) -> Iterator[dict[str, Any]]:
|
|
281
273
|
"""Flush any remaining data in buffer."""
|
|
282
274
|
if buf:
|
|
283
275
|
yield {
|
|
@@ -317,9 +309,7 @@ def iter_sse_events(
|
|
|
317
309
|
if line is None:
|
|
318
310
|
continue
|
|
319
311
|
|
|
320
|
-
buf, event_type, event_id, event_data, completed = _process_sse_line(
|
|
321
|
-
line, buf, event_type, event_id
|
|
322
|
-
)
|
|
312
|
+
buf, event_type, event_id, event_data, completed = _process_sse_line(line, buf, event_type, event_id)
|
|
323
313
|
|
|
324
314
|
yield from _yield_event_data(event_data)
|
|
325
315
|
if completed:
|
|
@@ -385,9 +375,7 @@ def _create_form_data(message: str) -> dict[str, Any]:
|
|
|
385
375
|
return {"input": message, "message": message, "stream": True}
|
|
386
376
|
|
|
387
377
|
|
|
388
|
-
def _prepare_file_entry(
|
|
389
|
-
item: str | BinaryIO, stack: ExitStack
|
|
390
|
-
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
378
|
+
def _prepare_file_entry(item: str | BinaryIO, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
391
379
|
"""Prepare a single file entry for multipart data."""
|
|
392
380
|
if isinstance(item, str):
|
|
393
381
|
return _prepare_path_entry(item, stack)
|
|
@@ -395,9 +383,7 @@ def _prepare_file_entry(
|
|
|
395
383
|
return _prepare_stream_entry(item)
|
|
396
384
|
|
|
397
385
|
|
|
398
|
-
def _prepare_path_entry(
|
|
399
|
-
path_str: str, stack: ExitStack
|
|
400
|
-
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
386
|
+
def _prepare_path_entry(path_str: str, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
401
387
|
"""Prepare a file path entry."""
|
|
402
388
|
file_path = Path(path_str)
|
|
403
389
|
if not file_path.exists():
|
glaip_sdk/utils/display.py
CHANGED
|
@@ -109,9 +109,7 @@ def print_agent_updated(agent: Any) -> None:
|
|
|
109
109
|
"""
|
|
110
110
|
if RICH_AVAILABLE:
|
|
111
111
|
console = _create_console()
|
|
112
|
-
console.print(
|
|
113
|
-
f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]"
|
|
114
|
-
)
|
|
112
|
+
console.print(f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]")
|
|
115
113
|
else:
|
|
116
114
|
print(f"✅ Agent '{agent.name}' updated successfully")
|
|
117
115
|
|
|
@@ -124,8 +122,6 @@ def print_agent_deleted(agent_id: str) -> None:
|
|
|
124
122
|
"""
|
|
125
123
|
if RICH_AVAILABLE:
|
|
126
124
|
console = _create_console()
|
|
127
|
-
console.print(
|
|
128
|
-
f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]"
|
|
129
|
-
)
|
|
125
|
+
console.print(f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]")
|
|
130
126
|
else:
|
|
131
127
|
print(f"✅ Agent deleted successfully (ID: {agent_id})")
|
glaip_sdk/utils/general.py
CHANGED
|
@@ -76,9 +76,7 @@ def format_datetime(dt: datetime | str | None) -> str:
|
|
|
76
76
|
return str(dt)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def progress_bar(
|
|
80
|
-
iterable: Iterable[Any], description: str = "Processing"
|
|
81
|
-
) -> Iterator[Any]:
|
|
79
|
+
def progress_bar(iterable: Iterable[Any], description: str = "Processing") -> Iterator[Any]:
|
|
82
80
|
"""Simple progress bar using click.
|
|
83
81
|
|
|
84
82
|
Args:
|
glaip_sdk/utils/import_export.py
CHANGED
|
@@ -71,14 +71,10 @@ def _get_default_array_fields() -> list[str]:
|
|
|
71
71
|
|
|
72
72
|
def _should_use_cli_value(cli_value: Any) -> bool:
|
|
73
73
|
"""Check if CLI value should be used."""
|
|
74
|
-
return cli_value is not None and (
|
|
75
|
-
not isinstance(cli_value, list | tuple) or len(cli_value) > 0
|
|
76
|
-
)
|
|
74
|
+
return cli_value is not None and (not isinstance(cli_value, (list, tuple)) or len(cli_value) > 0)
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
def _handle_array_field_merge(
|
|
80
|
-
key: str, cli_value: Any, import_data: dict[str, Any]
|
|
81
|
-
) -> Any:
|
|
77
|
+
def _handle_array_field_merge(key: str, cli_value: Any, import_data: dict[str, Any]) -> Any:
|
|
82
78
|
"""Handle merging of array fields."""
|
|
83
79
|
import_value = import_data[key]
|
|
84
80
|
if isinstance(import_value, list):
|
|
@@ -107,9 +103,7 @@ def _merge_cli_values_with_import(
|
|
|
107
103
|
merged[key] = import_data[key]
|
|
108
104
|
|
|
109
105
|
|
|
110
|
-
def _add_import_only_fields(
|
|
111
|
-
merged: dict[str, Any], import_data: dict[str, Any]
|
|
112
|
-
) -> None:
|
|
106
|
+
def _add_import_only_fields(merged: dict[str, Any], import_data: dict[str, Any]) -> None:
|
|
113
107
|
"""Add fields that exist only in import data."""
|
|
114
108
|
for key, import_value in import_data.items():
|
|
115
109
|
if key not in merged:
|
|
@@ -12,7 +12,14 @@ import time
|
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from glaip_sdk.icons import
|
|
15
|
+
from glaip_sdk.icons import (
|
|
16
|
+
ICON_AGENT_STEP,
|
|
17
|
+
ICON_DELEGATE,
|
|
18
|
+
ICON_STATUS_FAILED,
|
|
19
|
+
ICON_STATUS_SUCCESS,
|
|
20
|
+
ICON_STATUS_WARNING,
|
|
21
|
+
ICON_TOOL_STEP,
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
# Constants for argument formatting
|
|
18
25
|
DEFAULT_ARGS_MAX_LEN = 100
|
|
@@ -37,9 +44,20 @@ SECRET_VALUE_PATTERNS = [
|
|
|
37
44
|
re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
|
|
38
45
|
]
|
|
39
46
|
SENSITIVE_PATTERNS = re.compile(
|
|
40
|
-
r"password
|
|
47
|
+
r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
|
|
41
48
|
re.IGNORECASE,
|
|
42
49
|
)
|
|
50
|
+
CONNECTOR_VERTICAL = "│ "
|
|
51
|
+
CONNECTOR_EMPTY = " "
|
|
52
|
+
CONNECTOR_BRANCH = "├─ "
|
|
53
|
+
CONNECTOR_LAST = "└─ "
|
|
54
|
+
ROOT_MARKER = ""
|
|
55
|
+
SECRET_MASK = "••••••"
|
|
56
|
+
STATUS_GLYPHS = {
|
|
57
|
+
"success": ICON_STATUS_SUCCESS,
|
|
58
|
+
"failed": ICON_STATUS_FAILED,
|
|
59
|
+
"warning": ICON_STATUS_WARNING,
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
|
|
45
63
|
def _truncate_string(s: str, max_len: int) -> str:
|
|
@@ -53,7 +71,7 @@ def mask_secrets_in_string(text: str) -> str:
|
|
|
53
71
|
"""Mask sensitive information in a string."""
|
|
54
72
|
result = text
|
|
55
73
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
56
|
-
result = re.sub(pattern,
|
|
74
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
57
75
|
return result
|
|
58
76
|
|
|
59
77
|
|
|
@@ -74,7 +92,7 @@ def _redact_dict_values(text: dict) -> dict:
|
|
|
74
92
|
result = {}
|
|
75
93
|
for key, value in text.items():
|
|
76
94
|
if _is_sensitive_key(key):
|
|
77
|
-
result[key] =
|
|
95
|
+
result[key] = SECRET_MASK
|
|
78
96
|
elif _should_recurse_redaction(value):
|
|
79
97
|
result[key] = redact_sensitive(value)
|
|
80
98
|
else:
|
|
@@ -92,11 +110,11 @@ def _redact_string_content(text: str) -> str:
|
|
|
92
110
|
result = text
|
|
93
111
|
# First mask secrets
|
|
94
112
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
95
|
-
result = re.sub(pattern,
|
|
113
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
96
114
|
# Then redact sensitive patterns
|
|
97
115
|
result = re.sub(
|
|
98
116
|
SENSITIVE_PATTERNS,
|
|
99
|
-
lambda m: m.group(0).split("=")[0] + "
|
|
117
|
+
lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
|
|
100
118
|
result,
|
|
101
119
|
)
|
|
102
120
|
return result
|
|
@@ -105,15 +123,37 @@ def _redact_string_content(text: str) -> str:
|
|
|
105
123
|
def _is_sensitive_key(key: str) -> bool:
|
|
106
124
|
"""Check if a key contains sensitive information."""
|
|
107
125
|
key_lower = key.lower()
|
|
108
|
-
return any(
|
|
109
|
-
sensitive in key_lower
|
|
110
|
-
for sensitive in ["password", "secret", "token", "key", "api_key"]
|
|
111
|
-
)
|
|
126
|
+
return any(sensitive in key_lower for sensitive in ["password", "secret", "token", "key", "api_key"])
|
|
112
127
|
|
|
113
128
|
|
|
114
129
|
def _should_recurse_redaction(value: Any) -> bool:
|
|
115
130
|
"""Check if a value should be recursively processed."""
|
|
116
|
-
return isinstance(value, dict
|
|
131
|
+
return isinstance(value, (dict, list)) or isinstance(value, str)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def glyph_for_status(icon_key: str | None) -> str | None:
|
|
135
|
+
"""Return glyph representing a step status icon key."""
|
|
136
|
+
if not icon_key:
|
|
137
|
+
return None
|
|
138
|
+
return STATUS_GLYPHS.get(icon_key)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def normalise_display_label(label: str | None) -> str:
|
|
142
|
+
"""Return a user facing label or the Unknown fallback."""
|
|
143
|
+
label = (label or "").strip()
|
|
144
|
+
return label or "Unknown step detail"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
|
|
148
|
+
"""Build connector prefix for a tree line based on ancestry state."""
|
|
149
|
+
if not branch_state:
|
|
150
|
+
return ROOT_MARKER
|
|
151
|
+
|
|
152
|
+
parts: list[str] = []
|
|
153
|
+
for ancestor_is_last in branch_state[:-1]:
|
|
154
|
+
parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
|
|
155
|
+
parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
|
|
156
|
+
return "".join(parts)
|
|
117
157
|
|
|
118
158
|
|
|
119
159
|
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
@@ -132,7 +172,7 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
|
132
172
|
try:
|
|
133
173
|
args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
|
|
134
174
|
return _truncate_string(args_str, max_len)
|
|
135
|
-
except
|
|
175
|
+
except Exception:
|
|
136
176
|
# Fallback to string representation if JSON serialization fails
|
|
137
177
|
args_str = str(masked_args)
|
|
138
178
|
return _truncate_string(args_str, max_len)
|
|
@@ -30,19 +30,32 @@ class Step:
|
|
|
30
30
|
context_id: str | None = None
|
|
31
31
|
started_at: float = field(default_factory=monotonic)
|
|
32
32
|
duration_ms: int | None = None
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
duration_source: str | None = None
|
|
34
|
+
display_label: str | None = None
|
|
35
|
+
status_icon: str | None = None
|
|
36
|
+
failure_reason: str | None = None
|
|
37
|
+
branch_failed: bool = False
|
|
38
|
+
is_parallel: bool = False
|
|
39
|
+
server_started_at: float | None = None
|
|
40
|
+
server_finished_at: float | None = None
|
|
41
|
+
duration_unknown: bool = False
|
|
42
|
+
|
|
43
|
+
def finish(self, duration_raw: float | None, *, source: str | None = None) -> None:
|
|
35
44
|
"""Mark the step as finished and calculate duration.
|
|
36
45
|
|
|
37
46
|
Args:
|
|
38
47
|
duration_raw: Raw duration in seconds, or None to calculate from started_at
|
|
48
|
+
source: Optional duration source tag
|
|
39
49
|
"""
|
|
40
|
-
|
|
50
|
+
self.duration_unknown = False
|
|
51
|
+
if isinstance(duration_raw, (int, float)) and duration_raw > 0:
|
|
41
52
|
# Use provided duration if it's a positive number (even if very small)
|
|
42
53
|
self.duration_ms = round(float(duration_raw) * 1000)
|
|
54
|
+
self.duration_source = source or self.duration_source or "provided"
|
|
43
55
|
else:
|
|
44
56
|
# Calculate from started_at if duration_raw is None, negative, or zero
|
|
45
57
|
self.duration_ms = int((monotonic() - self.started_at) * 1000)
|
|
58
|
+
self.duration_source = source or self.duration_source or "monotonic"
|
|
46
59
|
self.status = "finished"
|
|
47
60
|
|
|
48
61
|
|
|
@@ -68,8 +81,4 @@ class RunStats:
|
|
|
68
81
|
Returns:
|
|
69
82
|
Duration in seconds if run is finished, None otherwise
|
|
70
83
|
"""
|
|
71
|
-
return (
|
|
72
|
-
None
|
|
73
|
-
if self.finished_at is None
|
|
74
|
-
else round(self.finished_at - self.started_at, 2)
|
|
75
|
-
)
|
|
84
|
+
return None if self.finished_at is None else round(self.finished_at - self.started_at, 2)
|
|
@@ -35,12 +35,9 @@ def make_silent_renderer() -> RichStreamRenderer:
|
|
|
35
35
|
cfg = RendererConfig(
|
|
36
36
|
live=False,
|
|
37
37
|
persist_live=False,
|
|
38
|
-
show_delegate_tool_panels=False,
|
|
39
38
|
render_thinking=False,
|
|
40
39
|
)
|
|
41
|
-
return RichStreamRenderer(
|
|
42
|
-
console=Console(file=io.StringIO(), force_terminal=False), cfg=cfg
|
|
43
|
-
)
|
|
40
|
+
return RichStreamRenderer(console=Console(file=io.StringIO(), force_terminal=False), cfg=cfg)
|
|
44
41
|
|
|
45
42
|
|
|
46
43
|
def make_minimal_renderer() -> RichStreamRenderer:
|
|
@@ -51,7 +48,6 @@ def make_minimal_renderer() -> RichStreamRenderer:
|
|
|
51
48
|
cfg = RendererConfig(
|
|
52
49
|
live=False,
|
|
53
50
|
persist_live=False,
|
|
54
|
-
show_delegate_tool_panels=False,
|
|
55
51
|
render_thinking=False,
|
|
56
52
|
)
|
|
57
53
|
return RichStreamRenderer(console=Console(), cfg=cfg)
|