glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 +44 -4
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1413 -0
- glaip_sdk/branding.py +126 -2
- glaip_sdk/cli/account_store.py +555 -0
- glaip_sdk/cli/auth.py +260 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +728 -113
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +12 -8
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/transcripts_original.py +756 -0
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +49 -4
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +41 -20
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +340 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +12 -13
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
- glaip_sdk/cli/slash/session.py +1105 -153
- glaip_sdk/cli/slash/tui/__init__.py +36 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +80 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +66 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -463
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +227 -10
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +576 -44
- glaip_sdk/client/base.py +26 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -14
- glaip_sdk/client/mcps.py +165 -24
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +546 -92
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +206 -32
- glaip_sdk/config/constants.py +33 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +136 -0
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +48 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +1055 -0
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +488 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +524 -0
- glaip_sdk/utils/instructions.py +101 -0
- 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 +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +299 -1434
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -33
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- 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 -440
- 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 +25 -13
- glaip_sdk/utils/runtime_config.py +426 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
- glaip_sdk-0.7.17.dist-info/RECORD +224 -0
- {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1369
- glaip_sdk/cli/commands/mcps.py +0 -1187
- glaip_sdk/cli/commands/tools.py +0 -584
- glaip_sdk/cli/utils.py +0 -1278
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.2.dist-info/RECORD +0 -82
- glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
|
@@ -8,13 +8,17 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
|
-
import
|
|
12
|
-
from collections.abc import Iterable
|
|
11
|
+
import secrets
|
|
12
|
+
from collections.abc import Iterable, Iterator
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from datetime import datetime, timezone
|
|
14
|
+
from datetime import datetime, timedelta, timezone
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from glaip_sdk.utils.datetime_helpers import (
|
|
19
|
+
coerce_datetime as _coerce_datetime,
|
|
20
|
+
)
|
|
21
|
+
|
|
18
22
|
DEFAULT_CACHE_ROOT = Path(
|
|
19
23
|
os.getenv(
|
|
20
24
|
"AIP_TRANSCRIPT_CACHE_DIR",
|
|
@@ -22,6 +26,11 @@ DEFAULT_CACHE_ROOT = Path(
|
|
|
22
26
|
)
|
|
23
27
|
)
|
|
24
28
|
MANIFEST_FILENAME = "manifest.jsonl"
|
|
29
|
+
JSONL_SUFFIX = ".jsonl"
|
|
30
|
+
UTC_OFFSET_SUFFIX = "+00:00"
|
|
31
|
+
|
|
32
|
+
_RUN_ID_PREFIX = "run_"
|
|
33
|
+
_RUN_ID_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz"
|
|
25
34
|
|
|
26
35
|
|
|
27
36
|
@dataclass(slots=True)
|
|
@@ -61,6 +70,124 @@ class TranscriptCacheStats:
|
|
|
61
70
|
total_bytes: int
|
|
62
71
|
|
|
63
72
|
|
|
73
|
+
def generate_run_id(length: int = 6) -> str:
|
|
74
|
+
"""Return a short, human-friendly run identifier."""
|
|
75
|
+
length = max(4, min(int(length or 0), 16)) or 6
|
|
76
|
+
return _RUN_ID_PREFIX + "".join(secrets.choice(_RUN_ID_ALPHABET) for _ in range(length))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _timestamp_to_iso(value: Any) -> str | None:
|
|
80
|
+
"""Convert supported timestamp-like values to an ISO8601 string with UTC designator."""
|
|
81
|
+
dt = _coerce_datetime(value)
|
|
82
|
+
if dt is None:
|
|
83
|
+
return None
|
|
84
|
+
if dt.year < 2000:
|
|
85
|
+
return None
|
|
86
|
+
return dt.isoformat().replace(UTC_OFFSET_SUFFIX, "Z")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _compute_duration_seconds(start: Any, end: Any) -> int | None:
|
|
90
|
+
"""Compute whole-second duration between two timestamp-like values."""
|
|
91
|
+
start_dt = _coerce_datetime(start)
|
|
92
|
+
end_dt = _coerce_datetime(end)
|
|
93
|
+
if start_dt is None or end_dt is None:
|
|
94
|
+
return None
|
|
95
|
+
delta = (end_dt - start_dt).total_seconds()
|
|
96
|
+
if delta < 0:
|
|
97
|
+
return None
|
|
98
|
+
return int(round(delta))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _iter_candidate_paths(entry: dict[str, Any], directory: Path) -> Iterator[Path]:
|
|
102
|
+
"""Yield plausible transcript paths for a manifest entry, deduplicated."""
|
|
103
|
+
seen: set[str] = set()
|
|
104
|
+
|
|
105
|
+
def _offer(path: Path) -> Iterator[Path]:
|
|
106
|
+
key = str(path)
|
|
107
|
+
if key not in seen:
|
|
108
|
+
seen.add(key)
|
|
109
|
+
yield path
|
|
110
|
+
|
|
111
|
+
for candidate in _filename_candidate_paths(entry, directory):
|
|
112
|
+
yield from _offer(candidate)
|
|
113
|
+
for candidate in _cache_path_candidate_paths(entry):
|
|
114
|
+
yield from _offer(candidate)
|
|
115
|
+
for candidate in _run_id_candidate_paths(entry, directory):
|
|
116
|
+
yield from _offer(candidate)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _filename_candidate_paths(entry: dict[str, Any], directory: Path) -> tuple[Path, ...]:
|
|
120
|
+
"""Return possible transcript paths derived from the manifest filename."""
|
|
121
|
+
filename = entry.get("filename")
|
|
122
|
+
if not filename:
|
|
123
|
+
return ()
|
|
124
|
+
candidate = Path(str(filename))
|
|
125
|
+
if not candidate.is_absolute():
|
|
126
|
+
candidate = directory / candidate
|
|
127
|
+
return (candidate,)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _cache_path_candidate_paths(entry: dict[str, Any]) -> tuple[Path, ...]:
|
|
131
|
+
"""Return legacy cache_path-derived transcript candidates."""
|
|
132
|
+
cache_path = entry.get("cache_path")
|
|
133
|
+
if not cache_path:
|
|
134
|
+
return ()
|
|
135
|
+
return (Path(str(cache_path)).expanduser(),)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run_id_candidate_paths(entry: dict[str, Any], directory: Path) -> tuple[Path, ...]:
|
|
139
|
+
"""Return candidate transcript paths derived from the run id."""
|
|
140
|
+
run_id = entry.get("run_id")
|
|
141
|
+
if not run_id:
|
|
142
|
+
return ()
|
|
143
|
+
paths: list[Path] = []
|
|
144
|
+
for variant in _run_id_variants(str(run_id)):
|
|
145
|
+
name = variant if variant.endswith(JSONL_SUFFIX) else f"{variant}{JSONL_SUFFIX}"
|
|
146
|
+
paths.append(directory / name)
|
|
147
|
+
return tuple(paths)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _run_id_variants(run_id: str) -> set[str]:
|
|
151
|
+
"""Return plausible filename stems derived from a run id."""
|
|
152
|
+
variants = {run_id}
|
|
153
|
+
if run_id.startswith(_RUN_ID_PREFIX):
|
|
154
|
+
suffix = run_id[len(_RUN_ID_PREFIX) :]
|
|
155
|
+
if suffix:
|
|
156
|
+
variants.update({suffix, f"run-{suffix}"})
|
|
157
|
+
variants.add(f"run-{run_id}")
|
|
158
|
+
else:
|
|
159
|
+
variants.update({f"run-{run_id}", _RUN_ID_PREFIX + run_id})
|
|
160
|
+
return variants
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def transcript_path_candidates(entry: dict[str, Any], cache_dir: Path | None = None) -> list[Path]:
|
|
164
|
+
"""Return possible transcript file locations for a manifest entry."""
|
|
165
|
+
directory = ensure_cache_dir(cache_dir)
|
|
166
|
+
return list(_iter_candidate_paths(entry, directory))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def resolve_transcript_path(entry: dict[str, Any], cache_dir: Path | None = None) -> Path:
|
|
170
|
+
"""Resolve the cached transcript path for a manifest entry or raise informative errors."""
|
|
171
|
+
candidates = transcript_path_candidates(entry, cache_dir)
|
|
172
|
+
if not candidates:
|
|
173
|
+
raise FileNotFoundError("Cached transcript path missing from manifest.")
|
|
174
|
+
|
|
175
|
+
for candidate in candidates:
|
|
176
|
+
if candidate.exists():
|
|
177
|
+
return candidate
|
|
178
|
+
|
|
179
|
+
raise FileNotFoundError(f"Cached transcript file not found: {candidates[0]}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _manifest_sort_key(entry: dict[str, Any]) -> datetime:
|
|
183
|
+
"""Return a datetime for ordering manifest rows, defaulting to the distant past."""
|
|
184
|
+
for key in ("started_at", "created_at"):
|
|
185
|
+
dt = _coerce_datetime(entry.get(key))
|
|
186
|
+
if dt is not None:
|
|
187
|
+
return dt
|
|
188
|
+
return datetime.min.replace(tzinfo=timezone.utc)
|
|
189
|
+
|
|
190
|
+
|
|
64
191
|
def ensure_cache_dir(cache_dir: Path | None = None) -> Path:
|
|
65
192
|
"""Ensure the cache directory exists and return it."""
|
|
66
193
|
directory = cache_dir or DEFAULT_CACHE_ROOT
|
|
@@ -88,15 +215,17 @@ def manifest_path(cache_dir: Path | None = None) -> Path:
|
|
|
88
215
|
|
|
89
216
|
|
|
90
217
|
def _parse_iso(ts: str | None) -> datetime | None:
|
|
218
|
+
"""Parse metadata timestamps that may use the legacy Z suffix."""
|
|
91
219
|
if not ts:
|
|
92
220
|
return None
|
|
93
221
|
try:
|
|
94
|
-
return datetime.fromisoformat(ts.replace("Z",
|
|
222
|
+
return datetime.fromisoformat(ts.replace("Z", UTC_OFFSET_SUFFIX))
|
|
95
223
|
except Exception:
|
|
96
224
|
return None
|
|
97
225
|
|
|
98
226
|
|
|
99
227
|
def _load_manifest_entries(cache_dir: Path | None = None) -> list[dict[str, Any]]:
|
|
228
|
+
"""Read manifest entries from disk, returning an empty list when missing."""
|
|
100
229
|
path = manifest_path(cache_dir)
|
|
101
230
|
entries: list[dict[str, Any]] = []
|
|
102
231
|
if not path.exists():
|
|
@@ -125,11 +254,24 @@ def _json_default(value: Any) -> Any:
|
|
|
125
254
|
|
|
126
255
|
|
|
127
256
|
def _write_manifest(entries: Iterable[dict[str, Any]], cache_dir: Path | None = None) -> None:
|
|
257
|
+
"""Atomically write manifest entries back to disk."""
|
|
128
258
|
path = manifest_path(cache_dir)
|
|
129
|
-
|
|
259
|
+
tmp_path = path.with_name(f"{path.name}.tmp")
|
|
260
|
+
with tmp_path.open("w", encoding="utf-8") as fh:
|
|
130
261
|
for entry in entries:
|
|
131
262
|
fh.write(json.dumps(entry, ensure_ascii=False, default=_json_default))
|
|
132
263
|
fh.write("\n")
|
|
264
|
+
tmp_path.replace(path)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def load_manifest_entries(cache_dir: Path | None = None) -> list[dict[str, Any]]:
|
|
268
|
+
"""Public wrapper around manifest loading for downstream tooling."""
|
|
269
|
+
return _load_manifest_entries(cache_dir)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def write_manifest(entries: Iterable[dict[str, Any]], cache_dir: Path | None = None) -> None:
|
|
273
|
+
"""Persist manifest entries atomically."""
|
|
274
|
+
_write_manifest(entries, cache_dir)
|
|
133
275
|
|
|
134
276
|
|
|
135
277
|
def store_transcript(
|
|
@@ -139,10 +281,38 @@ def store_transcript(
|
|
|
139
281
|
) -> TranscriptStoreResult:
|
|
140
282
|
"""Persist a transcript to disk and update the manifest."""
|
|
141
283
|
directory = ensure_cache_dir(cache_dir)
|
|
142
|
-
filename =
|
|
284
|
+
filename = _normalise_run_filename(payload.run_id)
|
|
143
285
|
transcript_path = directory / filename
|
|
144
286
|
|
|
145
|
-
meta_line =
|
|
287
|
+
meta_line = _build_meta_line(payload)
|
|
288
|
+
transcript_path = _write_transcript_file(transcript_path, filename, meta_line, payload.events)
|
|
289
|
+
size_bytes = _safe_file_size(transcript_path)
|
|
290
|
+
manifest_entry = _build_manifest_entry(payload, transcript_path.name, size_bytes)
|
|
291
|
+
if transcript_path.parent != directory:
|
|
292
|
+
manifest_entry["cache_path"] = str(transcript_path)
|
|
293
|
+
|
|
294
|
+
existing_entries = _load_manifest_entries(directory)
|
|
295
|
+
existing_entries.append(manifest_entry)
|
|
296
|
+
_write_manifest(existing_entries, directory)
|
|
297
|
+
|
|
298
|
+
return TranscriptStoreResult(
|
|
299
|
+
path=transcript_path,
|
|
300
|
+
manifest_entry=manifest_entry,
|
|
301
|
+
pruned_entries=[],
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _normalise_run_filename(run_id: str) -> str:
|
|
306
|
+
"""Ensure cached run filenames always end with .jsonl."""
|
|
307
|
+
run_basename = run_id.rstrip()
|
|
308
|
+
if run_basename.endswith(JSONL_SUFFIX):
|
|
309
|
+
run_basename = run_basename[: -len(JSONL_SUFFIX)]
|
|
310
|
+
return f"{run_basename}{JSONL_SUFFIX}"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _build_meta_line(payload: TranscriptPayload) -> dict[str, Any]:
|
|
314
|
+
"""Return the metadata header stored at the top of transcript files."""
|
|
315
|
+
return {
|
|
146
316
|
"type": "meta",
|
|
147
317
|
"run_id": payload.run_id,
|
|
148
318
|
"agent_id": payload.agent_id,
|
|
@@ -158,11 +328,20 @@ def store_transcript(
|
|
|
158
328
|
"source": payload.source,
|
|
159
329
|
}
|
|
160
330
|
|
|
161
|
-
|
|
162
|
-
|
|
331
|
+
|
|
332
|
+
def _write_transcript_file(
|
|
333
|
+
path: Path,
|
|
334
|
+
filename: str,
|
|
335
|
+
meta_line: dict[str, Any],
|
|
336
|
+
events: list[dict[str, Any]],
|
|
337
|
+
) -> Path:
|
|
338
|
+
"""Persist the transcript JSONL file, falling back to cwd when necessary."""
|
|
339
|
+
|
|
340
|
+
def _write(target: Path) -> None:
|
|
341
|
+
with target.open("w", encoding="utf-8") as fh:
|
|
163
342
|
fh.write(json.dumps(meta_line, ensure_ascii=False, default=_json_default))
|
|
164
343
|
fh.write("\n")
|
|
165
|
-
for event in
|
|
344
|
+
for event in events:
|
|
166
345
|
fh.write(
|
|
167
346
|
json.dumps(
|
|
168
347
|
{"type": "event", "event": event},
|
|
@@ -173,34 +352,62 @@ def store_transcript(
|
|
|
173
352
|
fh.write("\n")
|
|
174
353
|
|
|
175
354
|
try:
|
|
176
|
-
|
|
355
|
+
_write(path)
|
|
356
|
+
return path
|
|
177
357
|
except PermissionError:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
358
|
+
fallback_dir = _fallback_cache_dir()
|
|
359
|
+
fallback_path = fallback_dir / filename
|
|
360
|
+
_write(fallback_path)
|
|
361
|
+
return fallback_path
|
|
362
|
+
|
|
181
363
|
|
|
182
|
-
|
|
183
|
-
|
|
364
|
+
def _safe_file_size(path: Path) -> int:
|
|
365
|
+
"""Return the file size, tolerating missing paths."""
|
|
366
|
+
try:
|
|
367
|
+
return path.stat().st_size
|
|
368
|
+
except FileNotFoundError:
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _build_manifest_entry(payload: TranscriptPayload, filename: str, size_bytes: int) -> dict[str, Any]:
|
|
373
|
+
"""Generate the manifest row corresponding to a stored transcript."""
|
|
374
|
+
entry: dict[str, Any] = {
|
|
184
375
|
"run_id": payload.run_id,
|
|
185
376
|
"agent_id": payload.agent_id,
|
|
186
377
|
"agent_name": payload.agent_name,
|
|
187
|
-
"
|
|
188
|
-
"
|
|
378
|
+
"started_at": _timestamp_to_iso(payload.started_at) or payload.created_at.isoformat(),
|
|
379
|
+
"finished_at": _timestamp_to_iso(payload.finished_at),
|
|
380
|
+
"duration_seconds": _compute_duration_seconds(payload.started_at, payload.finished_at),
|
|
189
381
|
"size_bytes": size_bytes,
|
|
382
|
+
"filename": filename,
|
|
190
383
|
"retained": True,
|
|
191
|
-
"
|
|
192
|
-
"server_run_id": payload.server_run_id,
|
|
384
|
+
"model": payload.model,
|
|
193
385
|
}
|
|
194
386
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
387
|
+
api_url = payload.meta.get("api_url")
|
|
388
|
+
if api_url:
|
|
389
|
+
entry["api_url"] = api_url
|
|
198
390
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
391
|
+
if entry["duration_seconds"] is None:
|
|
392
|
+
entry["duration_seconds"] = _coerce_duration_hint(payload.meta.get("final_duration_seconds"))
|
|
393
|
+
|
|
394
|
+
if entry.get("finished_at") is None and entry.get("started_at") and entry.get("duration_seconds") is not None:
|
|
395
|
+
start_dt = _coerce_datetime(entry["started_at"])
|
|
396
|
+
if start_dt is not None:
|
|
397
|
+
finished_dt = start_dt + timedelta(seconds=int(entry["duration_seconds"]))
|
|
398
|
+
entry["finished_at"] = finished_dt.isoformat().replace(UTC_OFFSET_SUFFIX, "Z")
|
|
399
|
+
|
|
400
|
+
return entry
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _coerce_duration_hint(value: Any) -> int | None:
|
|
404
|
+
"""Convert loose duration hints to whole seconds."""
|
|
405
|
+
try:
|
|
406
|
+
if value is None:
|
|
407
|
+
return None
|
|
408
|
+
return int(round(float(value)))
|
|
409
|
+
except Exception:
|
|
410
|
+
return None
|
|
204
411
|
|
|
205
412
|
|
|
206
413
|
def latest_manifest_entry(cache_dir: Path | None = None) -> dict[str, Any] | None:
|
|
@@ -208,10 +415,7 @@ def latest_manifest_entry(cache_dir: Path | None = None) -> dict[str, Any] | Non
|
|
|
208
415
|
entries = _load_manifest_entries(cache_dir)
|
|
209
416
|
if not entries:
|
|
210
417
|
return None
|
|
211
|
-
return max(
|
|
212
|
-
entries,
|
|
213
|
-
key=lambda e: _parse_iso(e.get("created_at")) or datetime.min.replace(tzinfo=timezone.utc),
|
|
214
|
-
)
|
|
418
|
+
return max(entries, key=_manifest_sort_key)
|
|
215
419
|
|
|
216
420
|
|
|
217
421
|
def resolve_manifest_entry(
|
|
@@ -238,13 +442,10 @@ def export_transcript(
|
|
|
238
442
|
if entry is None:
|
|
239
443
|
raise FileNotFoundError("No cached transcripts available for export.")
|
|
240
444
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
cache_file = Path(cache_path)
|
|
246
|
-
if not cache_file.exists():
|
|
247
|
-
raise FileNotFoundError(f"Cached transcript file not found: {cache_file}")
|
|
445
|
+
try:
|
|
446
|
+
cache_file = resolve_transcript_path(entry, directory)
|
|
447
|
+
except FileNotFoundError as exc:
|
|
448
|
+
raise FileNotFoundError(str(exc)) from exc
|
|
248
449
|
|
|
249
450
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
250
451
|
|
|
@@ -266,10 +467,20 @@ def export_transcript(
|
|
|
266
467
|
|
|
267
468
|
def suggest_filename(entry: dict[str, Any] | None = None) -> str:
|
|
268
469
|
"""Return a friendly filename suggestion for exporting a transcript."""
|
|
269
|
-
run_id = entry.get("run_id") if entry else
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
470
|
+
run_id = entry.get("run_id") if entry else None
|
|
471
|
+
if not run_id:
|
|
472
|
+
run_id = generate_run_id()
|
|
473
|
+
|
|
474
|
+
timestamp_source = None
|
|
475
|
+
if entry:
|
|
476
|
+
timestamp_source = entry.get("started_at") or entry.get("created_at")
|
|
477
|
+
|
|
478
|
+
if not timestamp_source:
|
|
479
|
+
timestamp_source = datetime.now(timezone.utc).isoformat()
|
|
480
|
+
|
|
481
|
+
timestamp = str(timestamp_source).replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
|
|
482
|
+
safe_run_id = str(run_id).replace("/", "-").replace(" ", "-")
|
|
483
|
+
return f"aip-run-{timestamp}-{safe_run_id}{JSONL_SUFFIX}"
|
|
273
484
|
|
|
274
485
|
|
|
275
486
|
def build_payload(
|
|
@@ -300,7 +511,7 @@ def build_payload(
|
|
|
300
511
|
created_at=datetime.now(timezone.utc),
|
|
301
512
|
source=source,
|
|
302
513
|
meta=meta,
|
|
303
|
-
run_id=
|
|
514
|
+
run_id=generate_run_id(),
|
|
304
515
|
)
|
|
305
516
|
|
|
306
517
|
|
|
@@ -8,8 +8,13 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
+
from io import StringIO
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.cli.auth import resolve_api_url_from_context
|
|
17
|
+
from glaip_sdk.cli.context import get_ctx_value
|
|
13
18
|
from glaip_sdk.cli.transcript.cache import (
|
|
14
19
|
TranscriptPayload,
|
|
15
20
|
TranscriptStoreResult,
|
|
@@ -18,7 +23,7 @@ from glaip_sdk.cli.transcript.cache import (
|
|
|
18
23
|
from glaip_sdk.cli.transcript.cache import (
|
|
19
24
|
build_payload as build_transcript_payload,
|
|
20
25
|
)
|
|
21
|
-
from glaip_sdk.utils.rendering.
|
|
26
|
+
from glaip_sdk.utils.rendering.layout.progress import format_tool_title
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
@dataclass(slots=True)
|
|
@@ -111,6 +116,15 @@ def register_last_transcript(ctx: Any, payload: TranscriptPayload, store_result:
|
|
|
111
116
|
ctx_obj["_last_transcript_path"] = str(store_result.path)
|
|
112
117
|
|
|
113
118
|
|
|
119
|
+
def _resolve_api_url(ctx: Any) -> str | None:
|
|
120
|
+
"""Resolve API URL from context or account store (CLI/palette ignores env creds)."""
|
|
121
|
+
return resolve_api_url_from_context(
|
|
122
|
+
ctx,
|
|
123
|
+
get_api_url=lambda c: get_ctx_value(c, "api_url"),
|
|
124
|
+
get_account_name=lambda c: get_ctx_value(c, "account_name"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
114
128
|
def _extract_step_summaries(renderer: Any) -> list[dict[str, Any]]:
|
|
115
129
|
"""Return lightweight step summaries for the transcript viewer."""
|
|
116
130
|
steps = getattr(renderer, "steps", None)
|
|
@@ -164,6 +178,38 @@ def _format_step_display_name(name: str) -> str:
|
|
|
164
178
|
return name
|
|
165
179
|
|
|
166
180
|
|
|
181
|
+
def _extract_step_summary_lines(renderer: Any) -> list[str]:
|
|
182
|
+
"""Render the live steps summary to plain text lines."""
|
|
183
|
+
if not hasattr(renderer, "_render_steps_text"):
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
renderable = renderer._render_steps_text()
|
|
188
|
+
except Exception:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
buffer = StringIO()
|
|
192
|
+
console = Console(file=buffer, record=True, force_terminal=False, width=120)
|
|
193
|
+
try:
|
|
194
|
+
console.print(renderable)
|
|
195
|
+
except Exception:
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
text = console.export_text() or buffer.getvalue()
|
|
199
|
+
lines = [line.rstrip() for line in text.splitlines()]
|
|
200
|
+
half = len(lines) // 2
|
|
201
|
+
if half and lines[:half] == lines[half : half * 2]:
|
|
202
|
+
return lines[:half]
|
|
203
|
+
start = 0
|
|
204
|
+
prefixes = ("🤖", "🔧", "💭", "├", "└", "│", "•")
|
|
205
|
+
for idx, line in enumerate(lines):
|
|
206
|
+
if line.lstrip().startswith(prefixes):
|
|
207
|
+
start = idx
|
|
208
|
+
break
|
|
209
|
+
trimmed = lines[start:]
|
|
210
|
+
return [line for line in trimmed if line]
|
|
211
|
+
|
|
212
|
+
|
|
167
213
|
def _collect_renderer_outputs(
|
|
168
214
|
renderer: Any, final_result: Any
|
|
169
215
|
) -> tuple[
|
|
@@ -203,11 +249,23 @@ def _derive_transcript_meta(
|
|
|
203
249
|
if step_summaries:
|
|
204
250
|
meta["transcript_steps"] = step_summaries
|
|
205
251
|
|
|
252
|
+
step_lines = _extract_step_summary_lines(renderer)
|
|
253
|
+
if step_lines:
|
|
254
|
+
meta["transcript_step_lines"] = step_lines
|
|
255
|
+
|
|
206
256
|
stream_processor = getattr(renderer, "stream_processor", None)
|
|
207
257
|
stream_started_at = (
|
|
208
258
|
getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
|
|
209
259
|
)
|
|
210
260
|
finished_at = compute_finished_at(renderer)
|
|
261
|
+
state = getattr(renderer, "state", None)
|
|
262
|
+
if state is not None:
|
|
263
|
+
duration_hint = getattr(state, "final_duration_seconds", None)
|
|
264
|
+
if duration_hint is not None:
|
|
265
|
+
try:
|
|
266
|
+
meta["final_duration_seconds"] = float(duration_hint)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
211
269
|
model_name = meta.get("model") or model
|
|
212
270
|
return meta, stream_started_at, finished_at, model_name
|
|
213
271
|
|
|
@@ -233,6 +291,13 @@ def store_transcript_for_session(
|
|
|
233
291
|
|
|
234
292
|
meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(renderer, model)
|
|
235
293
|
|
|
294
|
+
try:
|
|
295
|
+
api_url = _resolve_api_url(ctx)
|
|
296
|
+
except Exception:
|
|
297
|
+
api_url = None
|
|
298
|
+
if api_url:
|
|
299
|
+
meta["api_url"] = api_url
|
|
300
|
+
|
|
236
301
|
payload: TranscriptPayload = build_transcript_payload(
|
|
237
302
|
events=events,
|
|
238
303
|
renderer_output=aggregated_output,
|