glaip-sdk 0.2.2__py3-none-any.whl → 0.4.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/cli/auth.py +2 -1
- glaip_sdk/cli/commands/agents.py +51 -36
- glaip_sdk/cli/commands/configure.py +2 -1
- glaip_sdk/cli/commands/mcps.py +219 -62
- glaip_sdk/cli/commands/models.py +3 -5
- glaip_sdk/cli/commands/tools.py +27 -16
- glaip_sdk/cli/commands/transcripts.py +1 -1
- glaip_sdk/cli/constants.py +3 -0
- glaip_sdk/cli/display.py +1 -1
- glaip_sdk/cli/hints.py +58 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +4 -13
- glaip_sdk/cli/slash/prompt.py +3 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +139 -48
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/capture.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +19 -678
- glaip_sdk/cli/update_notifier.py +2 -1
- glaip_sdk/cli/utils.py +228 -101
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +40 -22
- glaip_sdk/client/main.py +2 -6
- glaip_sdk/client/mcps.py +13 -5
- glaip_sdk/client/run_rendering.py +90 -111
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +2 -3
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/models/__init__.py +56 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models.py +8 -7
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +24 -1
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -439
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/validation.py +13 -21
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.4.0.dist-info/RECORD +110 -0
- glaip_sdk-0.2.2.dist-info/RECORD +0 -87
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agent run models for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Type alias for SSE event dictionaries
|
|
16
|
+
RunOutputChunk = dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RunSummary(BaseModel):
|
|
20
|
+
"""Represents a single agent run in list/table views with metadata only."""
|
|
21
|
+
|
|
22
|
+
id: UUID
|
|
23
|
+
agent_id: UUID
|
|
24
|
+
run_type: Literal["manual", "schedule"]
|
|
25
|
+
schedule_id: UUID | None = None
|
|
26
|
+
status: Literal["started", "success", "failed", "cancelled", "aborted", "unavailable"]
|
|
27
|
+
started_at: datetime
|
|
28
|
+
completed_at: datetime | None = None
|
|
29
|
+
input: str | None = None
|
|
30
|
+
config: dict[str, Any] | None = None
|
|
31
|
+
created_at: datetime
|
|
32
|
+
updated_at: datetime
|
|
33
|
+
|
|
34
|
+
@field_validator("completed_at")
|
|
35
|
+
@classmethod
|
|
36
|
+
def validate_completed_after_started(cls, v: datetime | None, info) -> datetime | None:
|
|
37
|
+
"""Validate that completed_at is after started_at if present."""
|
|
38
|
+
if v is not None and "started_at" in info.data:
|
|
39
|
+
started_at = info.data["started_at"]
|
|
40
|
+
if v < started_at:
|
|
41
|
+
raise ValueError("completed_at must be after started_at")
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
def duration(self) -> timedelta | None:
|
|
45
|
+
"""Calculate duration from started_at to completed_at.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Duration as timedelta if completed_at exists, None otherwise
|
|
49
|
+
"""
|
|
50
|
+
if self.completed_at is not None:
|
|
51
|
+
return self.completed_at - self.started_at
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def duration_formatted(self) -> str:
|
|
55
|
+
"""Format duration as HH:MM:SS string.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Formatted duration string or "—" if not completed
|
|
59
|
+
"""
|
|
60
|
+
duration = self.duration()
|
|
61
|
+
if duration is None:
|
|
62
|
+
return "—"
|
|
63
|
+
total_seconds = int(duration.total_seconds())
|
|
64
|
+
hours = total_seconds // 3600
|
|
65
|
+
minutes = (total_seconds % 3600) // 60
|
|
66
|
+
seconds = total_seconds % 60
|
|
67
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
68
|
+
|
|
69
|
+
def input_preview(self, max_length: int = 120) -> str:
|
|
70
|
+
"""Generate truncated input preview for table display.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
max_length: Maximum length of preview string
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Truncated input string or "—" if input is None or empty
|
|
77
|
+
"""
|
|
78
|
+
if not self.input:
|
|
79
|
+
return "—"
|
|
80
|
+
# Strip newlines and collapse whitespace
|
|
81
|
+
preview = " ".join(self.input.split())
|
|
82
|
+
if len(preview) > max_length:
|
|
83
|
+
return preview[:max_length] + "…"
|
|
84
|
+
return preview
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RunsPage(BaseModel):
|
|
88
|
+
"""Represents a paginated collection of run summaries from the list endpoint."""
|
|
89
|
+
|
|
90
|
+
data: list[RunSummary]
|
|
91
|
+
total: int = Field(ge=0)
|
|
92
|
+
page: int = Field(ge=1)
|
|
93
|
+
limit: int = Field(ge=1, le=100)
|
|
94
|
+
has_next: bool
|
|
95
|
+
has_prev: bool
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def validate_pagination_consistency(self) -> "RunsPage":
|
|
99
|
+
"""Validate pagination consistency."""
|
|
100
|
+
# If has_next is True, then page * limit < total
|
|
101
|
+
if self.has_next and self.page * self.limit >= self.total:
|
|
102
|
+
raise ValueError("has_next inconsistency: page * limit must be < total when has_next is True")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RunWithOutput(RunSummary):
|
|
107
|
+
"""Extends RunSummary with the complete SSE event stream for detailed viewing."""
|
|
108
|
+
|
|
109
|
+
output: list[RunOutputChunk] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
@field_validator("output", mode="before")
|
|
112
|
+
@classmethod
|
|
113
|
+
def normalize_output(cls, v: Any) -> list[RunOutputChunk]:
|
|
114
|
+
"""Normalize output field to empty list when null."""
|
|
115
|
+
if v is None:
|
|
116
|
+
return []
|
|
117
|
+
return v
|
glaip_sdk/models.py
CHANGED
|
@@ -23,21 +23,22 @@ class Agent(BaseModel):
|
|
|
23
23
|
id: str
|
|
24
24
|
name: str
|
|
25
25
|
instruction: str | None = None
|
|
26
|
-
description: str | None = None
|
|
26
|
+
description: str | None = None
|
|
27
27
|
type: str | None = None
|
|
28
28
|
framework: str | None = None
|
|
29
29
|
version: str | None = None
|
|
30
|
-
tools: list[dict[str, Any]] | None = None
|
|
31
|
-
agents: list[dict[str, Any]] | None = None
|
|
32
|
-
mcps: list[dict[str, Any]] | None = None
|
|
33
|
-
tool_configs: dict[str, Any] | None = None
|
|
30
|
+
tools: list[dict[str, Any]] | None = None
|
|
31
|
+
agents: list[dict[str, Any]] | None = None
|
|
32
|
+
mcps: list[dict[str, Any]] | None = None
|
|
33
|
+
tool_configs: dict[str, Any] | None = None
|
|
34
|
+
mcp_configs: dict[str, Any] | None = None
|
|
34
35
|
agent_config: dict[str, Any] | None = None
|
|
35
36
|
timeout: int = DEFAULT_AGENT_RUN_TIMEOUT
|
|
36
37
|
metadata: dict[str, Any] | None = None
|
|
37
38
|
language_model_id: str | None = None
|
|
38
39
|
a2a_profile: dict[str, Any] | None = None
|
|
39
|
-
created_at: datetime | None = None
|
|
40
|
-
updated_at: datetime | None = None
|
|
40
|
+
created_at: datetime | None = None
|
|
41
|
+
updated_at: datetime | None = None
|
|
41
42
|
_client: Any = None
|
|
42
43
|
|
|
43
44
|
def _set_client(self, client: Any) -> "Agent":
|
glaip_sdk/rich_components.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
"""Custom Rich components with copy-friendly defaults.
|
|
1
|
+
"""Custom Rich components with copy-friendly defaults.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
from rich import box
|
|
6
10
|
from rich.panel import Panel
|
|
7
11
|
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class AIPPanel(Panel):
|
|
@@ -66,4 +71,55 @@ class AIPGrid(Table):
|
|
|
66
71
|
)
|
|
67
72
|
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
class RemoteRunsTable(AIPTable):
|
|
75
|
+
"""Rich Table for displaying remote agent runs with pagination support."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, *args, **kwargs):
|
|
78
|
+
"""Initialize RemoteRunsTable with columns for run display.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
*args: Positional arguments passed to AIPTable
|
|
82
|
+
**kwargs: Keyword arguments passed to AIPTable
|
|
83
|
+
"""
|
|
84
|
+
kwargs.setdefault("row_styles", ("dim", "none"))
|
|
85
|
+
kwargs.setdefault("show_header", True)
|
|
86
|
+
super().__init__(*args, **kwargs)
|
|
87
|
+
# Add columns for run display
|
|
88
|
+
self.add_column("", width=2, no_wrap=True) # Selection gutter
|
|
89
|
+
self.add_column("Run UUID", style="cyan", width=36, no_wrap=True)
|
|
90
|
+
self.add_column("Type", style="yellow", width=8, no_wrap=True)
|
|
91
|
+
self.add_column("Status", style="magenta", width=12, no_wrap=True)
|
|
92
|
+
self.add_column("Started (UTC)", style="dim", width=20, no_wrap=True)
|
|
93
|
+
self.add_column("Completed (UTC)", style="dim", width=20, no_wrap=True)
|
|
94
|
+
self.add_column("Duration", style="green", width=10, no_wrap=True)
|
|
95
|
+
self.add_column("Input Preview", style="white", width=40, overflow="ellipsis")
|
|
96
|
+
|
|
97
|
+
def add_run_row(
|
|
98
|
+
self,
|
|
99
|
+
run_uuid: str,
|
|
100
|
+
run_type: str,
|
|
101
|
+
status: str,
|
|
102
|
+
started: str,
|
|
103
|
+
completed: str,
|
|
104
|
+
duration: str,
|
|
105
|
+
preview: str,
|
|
106
|
+
*,
|
|
107
|
+
selected: bool = False,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Append a run row with optional selection styling."""
|
|
110
|
+
gutter = Text("› ", style="bold bright_cyan") if selected else Text(" ")
|
|
111
|
+
row_style = "reverse" if selected else None
|
|
112
|
+
self.add_row(
|
|
113
|
+
gutter,
|
|
114
|
+
run_uuid,
|
|
115
|
+
run_type,
|
|
116
|
+
status,
|
|
117
|
+
started,
|
|
118
|
+
completed,
|
|
119
|
+
duration,
|
|
120
|
+
preview,
|
|
121
|
+
style=row_style,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["AIPPanel", "AIPTable", "AIPGrid", "RemoteRunsTable"]
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -426,6 +426,19 @@ def _prepare_stream_entry(
|
|
|
426
426
|
)
|
|
427
427
|
|
|
428
428
|
|
|
429
|
+
def add_kwargs_to_payload(payload: dict[str, Any], kwargs: dict[str, Any], excluded_keys: set[str]) -> None:
|
|
430
|
+
"""Add kwargs to payload excluding specified keys.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
payload: Payload dictionary to update.
|
|
434
|
+
kwargs: Keyword arguments to add.
|
|
435
|
+
excluded_keys: Keys to exclude from kwargs.
|
|
436
|
+
"""
|
|
437
|
+
for key, value in kwargs.items():
|
|
438
|
+
if key not in excluded_keys:
|
|
439
|
+
payload[key] = value
|
|
440
|
+
|
|
441
|
+
|
|
429
442
|
def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
|
|
430
443
|
"""Prepare multipart form data for file uploads.
|
|
431
444
|
|
glaip_sdk/utils/display.py
CHANGED
|
@@ -4,6 +4,9 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from importlib import import_module
|
|
7
10
|
from typing import TYPE_CHECKING, Any
|
|
8
11
|
|
|
9
12
|
from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE
|
|
@@ -13,42 +16,47 @@ if TYPE_CHECKING: # pragma: no cover - import-time typing helpers
|
|
|
13
16
|
from rich.console import Console
|
|
14
17
|
from rich.text import Text
|
|
15
18
|
|
|
16
|
-
from glaip_sdk.rich_components import
|
|
19
|
+
from glaip_sdk.rich_components import AIPanel
|
|
20
|
+
else: # pragma: no cover - runtime fallback for type checking
|
|
21
|
+
AIPanel = Any # type: ignore[assignment]
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def _check_rich_available() -> bool:
|
|
20
|
-
"""
|
|
25
|
+
"""Return True when core Rich display dependencies are importable."""
|
|
21
26
|
try:
|
|
22
27
|
__import__("rich.console")
|
|
23
28
|
__import__("rich.text")
|
|
24
29
|
__import__("glaip_sdk.rich_components")
|
|
25
|
-
return True
|
|
26
30
|
except Exception:
|
|
27
31
|
return False
|
|
32
|
+
return True
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
RICH_AVAILABLE = _check_rich_available()
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
def _create_console() ->
|
|
38
|
+
def _create_console() -> Console:
|
|
34
39
|
"""Return a Console instance with lazy import to ease mocking."""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
if not RICH_AVAILABLE: # pragma: no cover - defensive guard
|
|
41
|
+
raise RuntimeError("Rich Console is not available")
|
|
42
|
+
console_module = import_module("rich.console")
|
|
43
|
+
return console_module.Console()
|
|
38
44
|
|
|
39
45
|
|
|
40
|
-
def _create_text(*args: Any, **kwargs: Any) ->
|
|
46
|
+
def _create_text(*args: Any, **kwargs: Any) -> Text:
|
|
41
47
|
"""Return a Text instance with lazy import to ease mocking."""
|
|
42
|
-
|
|
48
|
+
if not RICH_AVAILABLE: # pragma: no cover - defensive guard
|
|
49
|
+
raise RuntimeError("Rich Text is not available")
|
|
50
|
+
text_module = import_module("rich.text")
|
|
51
|
+
return text_module.Text(*args, **kwargs)
|
|
43
52
|
|
|
44
|
-
return Text(*args, **kwargs)
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
def _create_panel(*args: Any, **kwargs: Any) -> "AIPPanel":
|
|
54
|
+
def _create_panel(*args: Any, **kwargs: Any) -> AIPanel:
|
|
48
55
|
"""Return an AIPPanel instance with lazy import to ease mocking."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
if not RICH_AVAILABLE: # pragma: no cover - defensive guard
|
|
57
|
+
raise RuntimeError("AIPPanel is not available")
|
|
58
|
+
components = import_module("glaip_sdk.rich_components")
|
|
59
|
+
return components.AIPPanel(*args, **kwargs)
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export utilities for remote agent run transcripts.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.models.agent_runs import RunWithOutput, RunOutputChunk
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def export_remote_transcript_jsonl(
|
|
17
|
+
run: RunWithOutput,
|
|
18
|
+
destination: Path,
|
|
19
|
+
*,
|
|
20
|
+
overwrite: bool = False,
|
|
21
|
+
agent_name: str | None = None,
|
|
22
|
+
model: str | None = None,
|
|
23
|
+
) -> Path:
|
|
24
|
+
"""Export a remote run transcript to JSONL format compatible with local transcript viewers.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
run: RunWithOutput instance to export
|
|
28
|
+
destination: Target file path for JSONL export
|
|
29
|
+
overwrite: Whether to overwrite existing file
|
|
30
|
+
agent_name: Optional agent name for metadata
|
|
31
|
+
model: Optional model name for metadata (extracted from run.config if not provided)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Path to the exported file
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
FileExistsError: If destination exists and overwrite is False
|
|
38
|
+
OSError: If file cannot be written
|
|
39
|
+
"""
|
|
40
|
+
if destination.exists() and not overwrite:
|
|
41
|
+
raise FileExistsError(f"File already exists: {destination}")
|
|
42
|
+
|
|
43
|
+
# Ensure parent directory exists
|
|
44
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
model_name = model or _extract_model(run)
|
|
47
|
+
final_output_text = _extract_final_output(run.output) or ""
|
|
48
|
+
|
|
49
|
+
meta_payload = _build_meta_payload(run, agent_name, model_name)
|
|
50
|
+
meta_record = _build_meta_record(run, agent_name, model_name, final_output_text, meta_payload)
|
|
51
|
+
|
|
52
|
+
_write_jsonl_file(destination, meta_record, run.output)
|
|
53
|
+
|
|
54
|
+
return destination
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_meta_payload(run: RunWithOutput, agent_name: str | None, model_name: str | None) -> dict[str, Any]:
|
|
58
|
+
"""Build the meta payload dictionary."""
|
|
59
|
+
return {
|
|
60
|
+
"agent_name": agent_name,
|
|
61
|
+
"model": model_name,
|
|
62
|
+
"input_message": run.input,
|
|
63
|
+
"status": run.status,
|
|
64
|
+
"run_type": run.run_type,
|
|
65
|
+
"schedule_id": str(run.schedule_id) if run.schedule_id else None,
|
|
66
|
+
"config": run.config or {},
|
|
67
|
+
"created_at": run.created_at.isoformat() if run.created_at else None,
|
|
68
|
+
"updated_at": run.updated_at.isoformat() if run.updated_at else None,
|
|
69
|
+
"event_count": len(run.output),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_meta_record(
|
|
74
|
+
run: RunWithOutput,
|
|
75
|
+
agent_name: str | None,
|
|
76
|
+
model_name: str | None,
|
|
77
|
+
final_output_text: str,
|
|
78
|
+
meta_payload: dict[str, Any],
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Build the meta record dictionary."""
|
|
81
|
+
return {
|
|
82
|
+
"type": "meta",
|
|
83
|
+
"run_id": str(run.id),
|
|
84
|
+
"agent_id": str(run.agent_id),
|
|
85
|
+
"agent_name": agent_name,
|
|
86
|
+
"model": model_name,
|
|
87
|
+
"created_at": run.created_at.isoformat() if run.created_at else None,
|
|
88
|
+
"default_output": final_output_text,
|
|
89
|
+
"final_output": final_output_text,
|
|
90
|
+
"server_run_id": str(run.id),
|
|
91
|
+
"started_at": run.started_at.isoformat() if run.started_at else None,
|
|
92
|
+
"finished_at": run.completed_at.isoformat() if run.completed_at else None,
|
|
93
|
+
"meta": meta_payload,
|
|
94
|
+
"source": "remote_history",
|
|
95
|
+
# Back-compat fields used by older tooling
|
|
96
|
+
"run_type": run.run_type,
|
|
97
|
+
"schedule_id": str(run.schedule_id) if run.schedule_id else None,
|
|
98
|
+
"status": run.status,
|
|
99
|
+
"input": run.input,
|
|
100
|
+
"config": run.config or {},
|
|
101
|
+
"updated_at": run.updated_at.isoformat() if run.updated_at else None,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_jsonl_file(destination: Path, meta_record: dict[str, Any], events: list[RunOutputChunk]) -> None:
|
|
106
|
+
"""Write the JSONL file with meta and event records."""
|
|
107
|
+
records: list[dict[str, Any]] = [meta_record]
|
|
108
|
+
records.extend({"type": "event", "event": event} for event in events)
|
|
109
|
+
|
|
110
|
+
with destination.open("w", encoding="utf-8") as fh:
|
|
111
|
+
for idx, record in enumerate(records):
|
|
112
|
+
json.dump(record, fh, ensure_ascii=False, indent=2, default=_json_default)
|
|
113
|
+
fh.write("\n")
|
|
114
|
+
if idx != len(records) - 1:
|
|
115
|
+
fh.write("\n")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _extract_model(run: RunWithOutput) -> str | None:
|
|
119
|
+
"""Best-effort extraction of the model name from run metadata."""
|
|
120
|
+
config = run.config or {}
|
|
121
|
+
if isinstance(config, dict):
|
|
122
|
+
model = config.get("model") or config.get("llm", {}).get("model")
|
|
123
|
+
if isinstance(model, str):
|
|
124
|
+
return model
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _extract_final_output(events: list[RunOutputChunk]) -> str | None:
|
|
129
|
+
"""Return the final response content from the event stream."""
|
|
130
|
+
for chunk in reversed(events):
|
|
131
|
+
content = chunk.get("content")
|
|
132
|
+
if not content:
|
|
133
|
+
continue
|
|
134
|
+
if chunk.get("event_type") == "final_response" or chunk.get("final"):
|
|
135
|
+
return str(content)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _json_default(obj: Any) -> Any:
|
|
140
|
+
"""JSON serializer for datetime objects."""
|
|
141
|
+
if isinstance(obj, datetime):
|
|
142
|
+
return obj.isoformat()
|
|
143
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
glaip_sdk/utils/import_export.py
CHANGED
|
@@ -9,8 +9,10 @@ Authors:
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from glaip_sdk.utils.resource_refs import _extract_id_from_item
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
14
16
|
"""Extract IDs from export format (list of dicts with id/name fields).
|
|
15
17
|
|
|
16
18
|
This function is similar to `extract_ids` in `resource_refs.py` but differs in behavior:
|
|
@@ -36,14 +38,9 @@ def extract_ids_from_export(items: list[Any]) -> list[str]: # pylint: disable=d
|
|
|
36
38
|
|
|
37
39
|
ids = []
|
|
38
40
|
for item in items:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ids.append(str(item.id))
|
|
43
|
-
elif isinstance(item, dict) and "id" in item:
|
|
44
|
-
ids.append(str(item["id"]))
|
|
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
|
|
41
|
+
extracted = _extract_id_from_item(item, skip_missing=True)
|
|
42
|
+
if extracted is not None:
|
|
43
|
+
ids.append(extracted)
|
|
47
44
|
|
|
48
45
|
return ids
|
|
49
46
|
|
|
@@ -1 +1,115 @@
|
|
|
1
|
-
"""Rendering utilities package (formatting, models, steps, debug).
|
|
1
|
+
"""Rendering utilities package (formatting, models, steps, debug).
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.models.agent_runs import RunWithOutput
|
|
13
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event, render_debug_event_stream
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_event_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
17
|
+
"""Parse received_at timestamp from SSE event.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
event: SSE event dictionary
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Parsed datetime or None if not available
|
|
24
|
+
"""
|
|
25
|
+
received_at = event.get("received_at")
|
|
26
|
+
if not received_at:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
if isinstance(received_at, datetime):
|
|
30
|
+
return received_at
|
|
31
|
+
|
|
32
|
+
if isinstance(received_at, str):
|
|
33
|
+
try:
|
|
34
|
+
# Try ISO format first
|
|
35
|
+
return datetime.fromisoformat(received_at.replace("Z", "+00:00"))
|
|
36
|
+
except ValueError:
|
|
37
|
+
try:
|
|
38
|
+
# Try common formats
|
|
39
|
+
return datetime.strptime(received_at, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
40
|
+
except ValueError:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_remote_sse_transcript(
|
|
47
|
+
run: RunWithOutput,
|
|
48
|
+
console: Console,
|
|
49
|
+
*,
|
|
50
|
+
show_metadata: bool = True,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Render remote SSE transcript events for a RunWithOutput.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
run: RunWithOutput instance containing events
|
|
56
|
+
console: Rich console to render to
|
|
57
|
+
show_metadata: Whether to show run metadata summary
|
|
58
|
+
"""
|
|
59
|
+
if show_metadata:
|
|
60
|
+
# Render metadata summary
|
|
61
|
+
console.print(f"[bold]Run: {run.id}[/bold]")
|
|
62
|
+
console.print(f"[dim]Agent: {run.agent_id}[/dim]")
|
|
63
|
+
console.print(f"[dim]Status: {run.status}[/dim]")
|
|
64
|
+
console.print(f"[dim]Type: {run.run_type}[/dim]")
|
|
65
|
+
if run.schedule_id:
|
|
66
|
+
console.print(f"[dim]Schedule ID: {run.schedule_id}[/dim]")
|
|
67
|
+
else:
|
|
68
|
+
console.print("[dim]Schedule: —[/dim]")
|
|
69
|
+
console.print(f"[dim]Started: {run.started_at.isoformat()}[/dim]")
|
|
70
|
+
if run.completed_at:
|
|
71
|
+
console.print(f"[dim]Completed: {run.completed_at.isoformat()}[/dim]")
|
|
72
|
+
console.print(f"[dim]Duration: {run.duration_formatted()}[/dim]")
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
# Render events
|
|
76
|
+
if not run.output:
|
|
77
|
+
console.print("[dim]No SSE events available for this run.[/dim]")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
console.print("[bold]SSE Events[/bold]")
|
|
81
|
+
console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
82
|
+
|
|
83
|
+
render_debug_event_stream(
|
|
84
|
+
run.output,
|
|
85
|
+
console,
|
|
86
|
+
resolve_timestamp=_parse_event_received_timestamp,
|
|
87
|
+
)
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RemoteSSETranscriptRenderer:
|
|
92
|
+
"""Renderer for remote SSE transcripts from RunWithOutput."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, console: Console | None = None):
|
|
95
|
+
"""Initialize the renderer.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
console: Rich console instance (creates default if None)
|
|
99
|
+
"""
|
|
100
|
+
self.console = console or Console()
|
|
101
|
+
|
|
102
|
+
def render(self, run: RunWithOutput, *, show_metadata: bool = True) -> None:
|
|
103
|
+
"""Render a remote run transcript.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
run: RunWithOutput instance to render
|
|
107
|
+
show_metadata: Whether to show run metadata summary
|
|
108
|
+
"""
|
|
109
|
+
render_remote_sse_transcript(run, self.console, show_metadata=show_metadata)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
"render_remote_sse_transcript",
|
|
114
|
+
"RemoteSSETranscriptRenderer",
|
|
115
|
+
]
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import re
|
|
11
|
-
import time
|
|
12
11
|
from collections.abc import Callable
|
|
13
12
|
from typing import Any
|
|
14
13
|
|
|
@@ -47,11 +46,6 @@ SENSITIVE_PATTERNS = re.compile(
|
|
|
47
46
|
r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
|
|
48
47
|
re.IGNORECASE,
|
|
49
48
|
)
|
|
50
|
-
CONNECTOR_VERTICAL = "│ "
|
|
51
|
-
CONNECTOR_EMPTY = " "
|
|
52
|
-
CONNECTOR_BRANCH = "├─ "
|
|
53
|
-
CONNECTOR_LAST = "└─ "
|
|
54
|
-
ROOT_MARKER = ""
|
|
55
49
|
SECRET_MASK = "••••••"
|
|
56
50
|
STATUS_GLYPHS = {
|
|
57
51
|
"success": ICON_STATUS_SUCCESS,
|
|
@@ -140,20 +134,11 @@ def glyph_for_status(icon_key: str | None) -> str | None:
|
|
|
140
134
|
|
|
141
135
|
def normalise_display_label(label: str | None) -> str:
|
|
142
136
|
"""Return a user facing label or the Unknown fallback."""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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)
|
|
137
|
+
if not isinstance(label, str):
|
|
138
|
+
text = ""
|
|
139
|
+
else:
|
|
140
|
+
text = label.strip()
|
|
141
|
+
return text or "Unknown step detail"
|
|
157
142
|
|
|
158
143
|
|
|
159
144
|
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
@@ -202,16 +187,6 @@ def pretty_out(output: any, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
|
202
187
|
return _truncate_string(output_str, max_len)
|
|
203
188
|
|
|
204
189
|
|
|
205
|
-
def get_spinner_char() -> str:
|
|
206
|
-
"""Get the next character for a spinner animation.
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
A single character from the spinner frames based on current time
|
|
210
|
-
"""
|
|
211
|
-
frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
212
|
-
return frames[int(time.time() * 10) % len(frames)]
|
|
213
|
-
|
|
214
|
-
|
|
215
190
|
def get_step_icon(step_kind: str) -> str:
|
|
216
191
|
"""Get the appropriate icon for a step kind."""
|
|
217
192
|
if step_kind == "tool":
|