glaip-sdk 0.1.4__py3-none-any.whl → 0.2.0__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/__init__.py +1 -1
- glaip_sdk/_version.py +1 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +22 -37
- glaip_sdk/cli/commands/configure.py +67 -6
- glaip_sdk/cli/commands/mcps.py +19 -23
- glaip_sdk/cli/commands/tools.py +17 -41
- glaip_sdk/cli/commands/transcripts.py +747 -0
- glaip_sdk/cli/config.py +6 -1
- glaip_sdk/cli/display.py +1 -0
- glaip_sdk/cli/main.py +12 -31
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/prompt.py +2 -0
- glaip_sdk/cli/slash/session.py +251 -88
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +32 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +6 -2
- glaip_sdk/cli/utils.py +170 -0
- glaip_sdk/client/_agent_payloads.py +5 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/utils/__init__.py +12 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +9 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -20
- glaip_sdk/utils/rendering/renderer/debug.py +3 -20
- glaip_sdk/utils/rendering/steps.py +5 -6
- glaip_sdk/utils/resource_refs.py +2 -1
- glaip_sdk/utils/serialization.py +2 -0
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.0.dist-info}/METADATA +1 -1
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.0.dist-info}/RECORD +36 -33
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.1.4.dist-info → glaip_sdk-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -63,12 +63,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
63
63
|
console: Console,
|
|
64
64
|
ctx: ViewerContext,
|
|
65
65
|
export_callback: Callable[[Path], Path],
|
|
66
|
+
*,
|
|
67
|
+
initial_view: str = "default",
|
|
66
68
|
) -> None:
|
|
67
69
|
"""Initialize viewer state for a captured transcript."""
|
|
68
70
|
self.console = console
|
|
69
71
|
self.ctx = ctx
|
|
70
72
|
self._export_callback = export_callback
|
|
71
|
-
self._view_mode = "default"
|
|
73
|
+
self._view_mode = initial_view if initial_view in {"default", "transcript"} else "default"
|
|
72
74
|
|
|
73
75
|
def run(self) -> None:
|
|
74
76
|
"""Enter the interactive loop."""
|
|
@@ -795,7 +797,9 @@ def run_viewer_session(
|
|
|
795
797
|
console: Console,
|
|
796
798
|
ctx: ViewerContext,
|
|
797
799
|
export_callback: Callable[[Path], Path],
|
|
800
|
+
*,
|
|
801
|
+
initial_view: str = "default",
|
|
798
802
|
) -> None:
|
|
799
803
|
"""Entry point for creating and running the post-run viewer."""
|
|
800
|
-
viewer = PostRunViewer(console, ctx, export_callback)
|
|
804
|
+
viewer = PostRunViewer(console, ctx, export_callback, initial_view=initial_view)
|
|
801
805
|
viewer.run()
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import sys
|
|
15
15
|
from collections.abc import Callable, Iterable
|
|
16
16
|
from contextlib import AbstractContextManager, nullcontext
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
from typing import TYPE_CHECKING, Any, cast
|
|
18
19
|
|
|
19
20
|
import click
|
|
@@ -87,6 +88,166 @@ def detect_export_format(file_path: str | os.PathLike[str]) -> str:
|
|
|
87
88
|
return _detect_export_format(file_path)
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
def format_size(num: int | None) -> str:
|
|
92
|
+
"""Format byte counts using short human-friendly units.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
num: Number of bytes to format (can be None or 0)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Human-readable size string (e.g., "1.5KB", "2MB")
|
|
99
|
+
"""
|
|
100
|
+
if not num or num <= 0:
|
|
101
|
+
return "0B"
|
|
102
|
+
|
|
103
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
104
|
+
value = float(num)
|
|
105
|
+
for unit in units:
|
|
106
|
+
if value < 1024 or unit == units[-1]:
|
|
107
|
+
if unit == "B" or value >= 100:
|
|
108
|
+
return f"{value:.0f}{unit}"
|
|
109
|
+
if value >= 10:
|
|
110
|
+
return f"{value:.1f}{unit}"
|
|
111
|
+
return f"{value:.2f}{unit}"
|
|
112
|
+
value /= 1024
|
|
113
|
+
return f"{value:.1f}TB" # pragma: no cover - defensive fallback
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_json_line(line: str) -> dict[str, Any] | None:
|
|
117
|
+
"""Parse a JSON line into a dictionary payload.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
line: JSON line string to parse
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Parsed dictionary or None if parsing fails or result is not a dict
|
|
124
|
+
"""
|
|
125
|
+
line = line.strip()
|
|
126
|
+
if not line:
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
payload = json.loads(line)
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
return None
|
|
132
|
+
return payload if isinstance(payload, dict) else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def format_datetime_fields(
|
|
136
|
+
data: dict[str, Any], fields: tuple[str, ...] = ("created_at", "updated_at")
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Format datetime fields in a data dictionary for display.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
data: Dictionary containing the data to format
|
|
142
|
+
fields: Tuple of field names to format (default: created_at, updated_at)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
New dictionary with formatted datetime fields
|
|
146
|
+
"""
|
|
147
|
+
from glaip_sdk.utils import format_datetime
|
|
148
|
+
|
|
149
|
+
formatted = data.copy()
|
|
150
|
+
for field in fields:
|
|
151
|
+
if field in formatted:
|
|
152
|
+
formatted[field] = format_datetime(formatted[field])
|
|
153
|
+
return formatted
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def fetch_resource_for_export(
|
|
157
|
+
ctx: Any,
|
|
158
|
+
resource: Any,
|
|
159
|
+
resource_type: str,
|
|
160
|
+
get_by_id_func: Callable[[str], Any],
|
|
161
|
+
console_override: Console | None = None,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""Fetch full resource details for export, handling errors gracefully.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
ctx: Click context for spinner management
|
|
167
|
+
resource: Resource object to fetch details for
|
|
168
|
+
resource_type: Type of resource (e.g., "MCP", "Agent", "Tool")
|
|
169
|
+
get_by_id_func: Function to fetch resource by ID
|
|
170
|
+
console_override: Optional console override
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Resource object with full details, or original resource if fetch fails
|
|
174
|
+
"""
|
|
175
|
+
active_console = console_override or console
|
|
176
|
+
resource_id = str(getattr(resource, "id", "")).strip()
|
|
177
|
+
|
|
178
|
+
if not resource_id:
|
|
179
|
+
return resource
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
with spinner_context(
|
|
183
|
+
ctx,
|
|
184
|
+
f"[bold blue]Fetching {resource_type} details…[/bold blue]",
|
|
185
|
+
console_override=active_console,
|
|
186
|
+
):
|
|
187
|
+
return get_by_id_func(resource_id)
|
|
188
|
+
except Exception:
|
|
189
|
+
# Return original resource if fetch fails
|
|
190
|
+
return resource
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def handle_resource_export(
|
|
194
|
+
ctx: Any,
|
|
195
|
+
resource: Any,
|
|
196
|
+
export_path: Path,
|
|
197
|
+
resource_type: str,
|
|
198
|
+
get_by_id_func: Callable[[str], Any],
|
|
199
|
+
console_override: Console | None = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Handle resource export to file with format detection and error handling.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
ctx: Click context for spinner management
|
|
205
|
+
resource: Resource object to export
|
|
206
|
+
export_path: Target file path (format detected from extension)
|
|
207
|
+
resource_type: Type of resource (e.g., "agent", "tool")
|
|
208
|
+
get_by_id_func: Function to fetch resource by ID
|
|
209
|
+
console_override: Optional console override
|
|
210
|
+
"""
|
|
211
|
+
from glaip_sdk.cli.display import handle_rich_output
|
|
212
|
+
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
213
|
+
from glaip_sdk.cli.rich_helpers import print_markup
|
|
214
|
+
|
|
215
|
+
active_console = console_override or console
|
|
216
|
+
|
|
217
|
+
# Auto-detect format from file extension
|
|
218
|
+
detected_format = detect_export_format(export_path)
|
|
219
|
+
|
|
220
|
+
# Try to fetch full details for export
|
|
221
|
+
full_resource = fetch_resource_for_export(
|
|
222
|
+
ctx,
|
|
223
|
+
resource,
|
|
224
|
+
resource_type.capitalize(),
|
|
225
|
+
get_by_id_func,
|
|
226
|
+
console_override=active_console,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Export the resource
|
|
230
|
+
try:
|
|
231
|
+
with spinner_context(
|
|
232
|
+
ctx,
|
|
233
|
+
f"[bold blue]Exporting {resource_type}…[/bold blue]",
|
|
234
|
+
console_override=active_console,
|
|
235
|
+
):
|
|
236
|
+
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
237
|
+
except Exception:
|
|
238
|
+
handle_rich_output(
|
|
239
|
+
ctx,
|
|
240
|
+
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
241
|
+
)
|
|
242
|
+
# Fallback: export with available data
|
|
243
|
+
export_resource_to_file_with_validation(resource, export_path, detected_format)
|
|
244
|
+
|
|
245
|
+
print_markup(
|
|
246
|
+
f"[{SUCCESS_STYLE}]✅ {resource_type.capitalize()} exported to: {export_path} (format: {detected_format})[/]",
|
|
247
|
+
console=active_console,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
90
251
|
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
91
252
|
"""Return True when running inside the slash command palette."""
|
|
92
253
|
if ctx is None:
|
|
@@ -612,6 +773,7 @@ def _fuzzy_score(search: str, target: str) -> int:
|
|
|
612
773
|
|
|
613
774
|
|
|
614
775
|
def _coerce_result_payload(result: Any) -> Any:
|
|
776
|
+
"""Convert renderer outputs into plain dict/list structures when possible."""
|
|
615
777
|
try:
|
|
616
778
|
to_dict = getattr(result, "to_dict", None)
|
|
617
779
|
if callable(to_dict):
|
|
@@ -622,6 +784,7 @@ def _coerce_result_payload(result: Any) -> Any:
|
|
|
622
784
|
|
|
623
785
|
|
|
624
786
|
def _ensure_displayable(payload: Any) -> Any:
|
|
787
|
+
"""Best-effort coercion into JSON/str-safe payloads for console rendering."""
|
|
625
788
|
if isinstance(payload, (dict, list, str, int, float, bool)) or payload is None:
|
|
626
789
|
return payload
|
|
627
790
|
|
|
@@ -641,6 +804,7 @@ def _ensure_displayable(payload: Any) -> Any:
|
|
|
641
804
|
|
|
642
805
|
|
|
643
806
|
def _render_markdown_output(data: Any) -> None:
|
|
807
|
+
"""Render markdown output using Rich when available."""
|
|
644
808
|
try:
|
|
645
809
|
console.print(Markdown(str(data)))
|
|
646
810
|
except ImportError:
|
|
@@ -694,6 +858,7 @@ def output_result(
|
|
|
694
858
|
|
|
695
859
|
|
|
696
860
|
def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
861
|
+
"""Convert heterogeneous item lists into table rows."""
|
|
697
862
|
try:
|
|
698
863
|
rows: list[dict[str, Any]] = []
|
|
699
864
|
for item in items:
|
|
@@ -713,6 +878,7 @@ def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str,
|
|
|
713
878
|
|
|
714
879
|
|
|
715
880
|
def _render_plain_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
881
|
+
"""Display tabular data as a simple pipe-delimited list."""
|
|
716
882
|
if not rows:
|
|
717
883
|
click.echo(f"No {title.lower()} found.")
|
|
718
884
|
return
|
|
@@ -722,6 +888,7 @@ def _render_plain_list(rows: list[dict[str, Any]], title: str, columns: list[tup
|
|
|
722
888
|
|
|
723
889
|
|
|
724
890
|
def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[tuple]) -> None:
|
|
891
|
+
"""Display tabular data using markdown table syntax."""
|
|
725
892
|
if not rows:
|
|
726
893
|
click.echo(f"No {title.lower()} found.")
|
|
727
894
|
return
|
|
@@ -734,6 +901,7 @@ def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[
|
|
|
734
901
|
|
|
735
902
|
|
|
736
903
|
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
904
|
+
"""Return True when rows should be name-sorted prior to rendering."""
|
|
737
905
|
return (
|
|
738
906
|
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
739
907
|
and rows
|
|
@@ -743,6 +911,7 @@ def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
|
743
911
|
|
|
744
912
|
|
|
745
913
|
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|
|
914
|
+
"""Build a configured Rich table for the provided columns."""
|
|
746
915
|
table = AIPTable(title=title, expand=True)
|
|
747
916
|
for _key, header, style, width in columns:
|
|
748
917
|
table.add_column(header, style=style, width=width)
|
|
@@ -750,6 +919,7 @@ def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -
|
|
|
750
919
|
|
|
751
920
|
|
|
752
921
|
def _build_table_group(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> Group:
|
|
922
|
+
"""Return a Rich group containing the table and a small footer summary."""
|
|
753
923
|
table = _create_table(columns, title)
|
|
754
924
|
for row in rows:
|
|
755
925
|
table.add_row(*[str(row.get(key, "N/A")) for key, _, _, _ in columns])
|
|
@@ -406,6 +406,7 @@ __all__ = [
|
|
|
406
406
|
|
|
407
407
|
|
|
408
408
|
def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
409
|
+
"""Populate immutable agent update fields using request data or existing agent defaults."""
|
|
409
410
|
return {
|
|
410
411
|
"name": request.name.strip() if request.name is not None else getattr(current_agent, "name", None),
|
|
411
412
|
"instruction": request.instruction.strip()
|
|
@@ -418,6 +419,7 @@ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any)
|
|
|
418
419
|
|
|
419
420
|
|
|
420
421
|
def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
422
|
+
"""Resolve the language-model portion of an update request with sensible fallbacks."""
|
|
421
423
|
fields = resolve_language_model_fields(
|
|
422
424
|
model=request.model,
|
|
423
425
|
language_model_id=request.language_model_id,
|
|
@@ -430,6 +432,7 @@ def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_a
|
|
|
430
432
|
|
|
431
433
|
|
|
432
434
|
def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
435
|
+
"""Collect optional agent fields, preserving current values when updates are absent."""
|
|
433
436
|
result: dict[str, Any] = {}
|
|
434
437
|
|
|
435
438
|
for field_name, value in (
|
|
@@ -468,6 +471,7 @@ def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent:
|
|
|
468
471
|
|
|
469
472
|
|
|
470
473
|
def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
474
|
+
"""Return relationship identifiers (tools/agents/mcps) for an update request."""
|
|
471
475
|
return {
|
|
472
476
|
"tools": _resolve_relation_ids(request.tools, current_agent, "tools"),
|
|
473
477
|
"agents": _resolve_relation_ids(request.agents, current_agent, "agents"),
|
|
@@ -476,6 +480,7 @@ def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any
|
|
|
476
480
|
|
|
477
481
|
|
|
478
482
|
def _resolve_agent_config_update(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any] | None:
|
|
483
|
+
"""Determine the agent_config payload to send, if any."""
|
|
479
484
|
effective_agent_config = _sanitize_agent_config(request.agent_config)
|
|
480
485
|
if effective_agent_config is not None:
|
|
481
486
|
return effective_agent_config
|
|
@@ -4,16 +4,4 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
AgentImportOperation,
|
|
9
|
-
ImportFieldPlan,
|
|
10
|
-
get_import_field_plan,
|
|
11
|
-
list_server_only_fields,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
__all__ = [
|
|
15
|
-
"AgentImportOperation",
|
|
16
|
-
"ImportFieldPlan",
|
|
17
|
-
"get_import_field_plan",
|
|
18
|
-
"list_server_only_fields",
|
|
19
|
-
]
|
|
7
|
+
__all__: list[str] = []
|
glaip_sdk/utils/__init__.py
CHANGED
|
@@ -4,6 +4,10 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from glaip_sdk.utils.datetime_helpers import (
|
|
8
|
+
coerce_datetime,
|
|
9
|
+
from_numeric_timestamp,
|
|
10
|
+
)
|
|
7
11
|
from glaip_sdk.utils.display import (
|
|
8
12
|
RICH_AVAILABLE,
|
|
9
13
|
print_agent_created,
|
|
@@ -14,20 +18,15 @@ from glaip_sdk.utils.display import (
|
|
|
14
18
|
from glaip_sdk.utils.general import (
|
|
15
19
|
format_datetime,
|
|
16
20
|
format_file_size,
|
|
17
|
-
is_uuid,
|
|
18
21
|
progress_bar,
|
|
19
|
-
sanitize_name,
|
|
20
22
|
)
|
|
21
23
|
from glaip_sdk.utils.rendering.models import RunStats, Step
|
|
24
|
+
from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
|
|
22
25
|
from glaip_sdk.utils.rendering.steps import StepManager
|
|
23
|
-
from glaip_sdk.utils.
|
|
26
|
+
from glaip_sdk.utils.resource_refs import is_uuid, sanitize_name
|
|
24
27
|
|
|
25
28
|
__all__ = [
|
|
26
29
|
"RICH_AVAILABLE",
|
|
27
|
-
"RichStreamRenderer",
|
|
28
|
-
"RunStats",
|
|
29
|
-
"Step",
|
|
30
|
-
"StepManager",
|
|
31
30
|
"format_datetime",
|
|
32
31
|
"format_file_size",
|
|
33
32
|
"is_uuid",
|
|
@@ -37,4 +36,10 @@ __all__ = [
|
|
|
37
36
|
"print_agent_updated",
|
|
38
37
|
"progress_bar",
|
|
39
38
|
"sanitize_name",
|
|
39
|
+
"RichStreamRenderer",
|
|
40
|
+
"RunStats",
|
|
41
|
+
"Step",
|
|
42
|
+
"StepManager",
|
|
43
|
+
"coerce_datetime",
|
|
44
|
+
"from_numeric_timestamp",
|
|
40
45
|
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared datetime parsing helpers used across CLI and rendering modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = ["coerce_datetime", "from_numeric_timestamp"]
|
|
9
|
+
|
|
10
|
+
_Z_SUFFIX = "+00:00"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def from_numeric_timestamp(raw_value: Any) -> datetime | None:
|
|
14
|
+
"""Convert unix timestamp-like values to datetime with sanity checks."""
|
|
15
|
+
try:
|
|
16
|
+
candidate = float(raw_value)
|
|
17
|
+
except Exception:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
if candidate < 1_000_000_000:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return datetime.fromtimestamp(candidate, tz=timezone.utc)
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_iso(value: str | None) -> datetime | None:
|
|
30
|
+
"""Parse ISO8601 strings while tolerating legacy 'Z' suffixes."""
|
|
31
|
+
if not value:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return datetime.fromisoformat(value.replace("Z", _Z_SUFFIX))
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def coerce_datetime(value: Any) -> datetime | None:
|
|
40
|
+
"""Best-effort conversion of assorted timestamp inputs to aware UTC datetimes."""
|
|
41
|
+
if value is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if isinstance(value, datetime):
|
|
45
|
+
dt = value
|
|
46
|
+
elif isinstance(value, (int, float)):
|
|
47
|
+
dt = from_numeric_timestamp(value)
|
|
48
|
+
elif isinstance(value, str):
|
|
49
|
+
dt = _parse_iso(value) or from_numeric_timestamp(value)
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
if dt is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
if dt.tzinfo is None:
|
|
57
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
58
|
+
return dt.astimezone(timezone.utc)
|
glaip_sdk/utils/general.py
CHANGED
|
@@ -4,46 +4,13 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import re
|
|
8
7
|
from collections.abc import Iterable, Iterator
|
|
9
8
|
from datetime import datetime
|
|
10
9
|
from typing import Any
|
|
11
|
-
from uuid import UUID
|
|
12
10
|
|
|
13
11
|
import click
|
|
14
12
|
|
|
15
13
|
|
|
16
|
-
def is_uuid(value: str) -> bool:
|
|
17
|
-
"""Check if a string is a valid UUID.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
value: String to check
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
True if value is a valid UUID, False otherwise
|
|
24
|
-
"""
|
|
25
|
-
try:
|
|
26
|
-
UUID(value)
|
|
27
|
-
return True
|
|
28
|
-
except (ValueError, TypeError):
|
|
29
|
-
return False
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def sanitize_name(name: str) -> str:
|
|
33
|
-
"""Sanitize a name for resource creation.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
name: Raw name input
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
Sanitized name suitable for resource creation
|
|
40
|
-
"""
|
|
41
|
-
# Remove special characters and normalize
|
|
42
|
-
sanitized = re.sub(r"[^a-zA-Z0-9\-_]", "-", name.strip())
|
|
43
|
-
sanitized = re.sub(r"-+", "-", sanitized) # Collapse multiple dashes
|
|
44
|
-
return sanitized.lower().strip("-")
|
|
45
|
-
|
|
46
|
-
|
|
47
14
|
def format_file_size(size_bytes: int) -> str:
|
|
48
15
|
"""Format file size in human readable format.
|
|
49
16
|
|
glaip_sdk/utils/import_export.py
CHANGED
|
@@ -10,9 +10,16 @@ Authors:
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
13
|
+
def extract_ids_from_export(items: list[Any]) -> list[str]: # pylint: disable=duplicate-code
|
|
14
14
|
"""Extract IDs from export format (list of dicts with id/name fields).
|
|
15
15
|
|
|
16
|
+
This function is similar to `extract_ids` in `resource_refs.py` but differs in behavior:
|
|
17
|
+
- This function SKIPS items without IDs (doesn't convert to string)
|
|
18
|
+
- `extract_ids` converts items without IDs to strings as fallback
|
|
19
|
+
|
|
20
|
+
This difference is intentional: export format should only include actual IDs,
|
|
21
|
+
while general resource reference extraction may need fallback string conversion.
|
|
22
|
+
|
|
16
23
|
Args:
|
|
17
24
|
items: List of items (dicts with id/name or strings)
|
|
18
25
|
|
|
@@ -36,6 +43,7 @@ def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
|
36
43
|
elif isinstance(item, dict) and "id" in item:
|
|
37
44
|
ids.append(str(item["id"]))
|
|
38
45
|
# Skip items without ID (don't convert to string)
|
|
46
|
+
# Note: This differs from extract_ids() in resource_refs.py which converts all items to strings
|
|
39
47
|
|
|
40
48
|
return ids
|
|
41
49
|
|
|
@@ -13,18 +13,6 @@ from glaip_sdk.rich_components import AIPPanel
|
|
|
13
13
|
from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
|
|
14
14
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
15
15
|
from glaip_sdk.utils.rendering.renderer.console import CapturingConsole
|
|
16
|
-
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
17
|
-
from glaip_sdk.utils.rendering.renderer.panels import (
|
|
18
|
-
create_context_panel,
|
|
19
|
-
create_final_panel,
|
|
20
|
-
create_main_panel,
|
|
21
|
-
create_tool_panel,
|
|
22
|
-
)
|
|
23
|
-
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
24
|
-
format_tool_title,
|
|
25
|
-
is_delegation_tool,
|
|
26
|
-
)
|
|
27
|
-
from glaip_sdk.utils.rendering.renderer.stream import StreamProcessor
|
|
28
16
|
|
|
29
17
|
|
|
30
18
|
def make_silent_renderer() -> RichStreamRenderer:
|
|
@@ -79,15 +67,7 @@ __all__ = [
|
|
|
79
67
|
"RichStreamRenderer",
|
|
80
68
|
"RendererConfig",
|
|
81
69
|
"CapturingConsole",
|
|
82
|
-
"StreamProcessor",
|
|
83
70
|
"make_silent_renderer",
|
|
84
71
|
"make_minimal_renderer",
|
|
85
72
|
"print_panel",
|
|
86
|
-
"render_debug_event",
|
|
87
|
-
"create_main_panel",
|
|
88
|
-
"create_tool_panel",
|
|
89
|
-
"create_context_panel",
|
|
90
|
-
"create_final_panel",
|
|
91
|
-
"format_tool_title",
|
|
92
|
-
"is_delegation_tool",
|
|
93
73
|
]
|
|
@@ -4,6 +4,7 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
# pylint: disable=duplicate-code
|
|
7
8
|
import json
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from typing import Any
|
|
@@ -13,25 +14,7 @@ from rich.markdown import Markdown
|
|
|
13
14
|
|
|
14
15
|
from glaip_sdk.branding import PRIMARY, SUCCESS, WARNING
|
|
15
16
|
from glaip_sdk.rich_components import AIPPanel
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _coerce_datetime(value: Any) -> datetime | None:
|
|
19
|
-
"""Attempt to coerce an arbitrary value to an aware datetime."""
|
|
20
|
-
if value is None:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
if isinstance(value, datetime):
|
|
24
|
-
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
25
|
-
|
|
26
|
-
if isinstance(value, str):
|
|
27
|
-
try:
|
|
28
|
-
normalised = value.replace("Z", "+00:00")
|
|
29
|
-
dt = datetime.fromisoformat(normalised)
|
|
30
|
-
except ValueError:
|
|
31
|
-
return None
|
|
32
|
-
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
33
|
-
|
|
34
|
-
return None
|
|
17
|
+
from glaip_sdk.utils.datetime_helpers import coerce_datetime
|
|
35
18
|
|
|
36
19
|
|
|
37
20
|
def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None = None) -> datetime | None:
|
|
@@ -40,7 +23,7 @@ def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None =
|
|
|
40
23
|
return received_ts if received_ts.tzinfo else received_ts.replace(tzinfo=timezone.utc)
|
|
41
24
|
|
|
42
25
|
ts_value = event.get("timestamp") or (event.get("metadata") or {}).get("timestamp")
|
|
43
|
-
return
|
|
26
|
+
return coerce_datetime(ts_value)
|
|
44
27
|
|
|
45
28
|
|
|
46
29
|
def _format_timestamp_for_display(dt: datetime) -> str:
|
|
@@ -996,12 +996,11 @@ class StepManager:
|
|
|
996
996
|
|
|
997
997
|
@staticmethod
|
|
998
998
|
def _coerce_server_time(value: Any) -> float | None:
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
return None
|
|
999
|
+
"""Convert a raw SSE time payload into a float if possible."""
|
|
1000
|
+
# Reuse the implementation from base renderer
|
|
1001
|
+
from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
|
|
1002
|
+
|
|
1003
|
+
return RichStreamRenderer._coerce_server_time(value)
|
|
1005
1004
|
|
|
1006
1005
|
def _update_server_timestamps(self, step: Step, server_time: float | None, status: str) -> None:
|
|
1007
1006
|
if server_time is None:
|
glaip_sdk/utils/resource_refs.py
CHANGED
|
@@ -8,6 +8,7 @@ Authors:
|
|
|
8
8
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
# pylint: disable=duplicate-code
|
|
11
12
|
import re
|
|
12
13
|
from typing import Any
|
|
13
14
|
from uuid import UUID
|
|
@@ -29,7 +30,7 @@ def is_uuid(value: str) -> bool:
|
|
|
29
30
|
return False
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def extract_ids(items: list[str | Any] | None) -> list[str]:
|
|
33
|
+
def extract_ids(items: list[str | Any] | None) -> list[str]: # pylint: disable=duplicate-code
|
|
33
34
|
"""Extract IDs from a list of objects or strings.
|
|
34
35
|
|
|
35
36
|
This function unifies the behavior between CLI and SDK layers, always
|
glaip_sdk/utils/serialization.py
CHANGED
|
@@ -287,6 +287,7 @@ def _collect_from_dir(resource: Any, collect_func: Callable[[Iterable[str]], Non
|
|
|
287
287
|
|
|
288
288
|
|
|
289
289
|
def _safe_getattr(resource: Any, name: str) -> Any:
|
|
290
|
+
"""Return getattr(resource, name) but swallow any exception and return None."""
|
|
290
291
|
try:
|
|
291
292
|
return getattr(resource, name)
|
|
292
293
|
except Exception:
|
|
@@ -294,6 +295,7 @@ def _safe_getattr(resource: Any, name: str) -> Any:
|
|
|
294
295
|
|
|
295
296
|
|
|
296
297
|
def _should_include_attribute(key: str, value: Any) -> bool:
|
|
298
|
+
"""Return True when an attribute should be serialized."""
|
|
297
299
|
if key in _EXCLUDED_ATTRS or key in _EXCLUDED_NAMES:
|
|
298
300
|
return False
|
|
299
301
|
if key.startswith("_"):
|