glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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.
Files changed (161) hide show
  1. glaip_sdk/__init__.py +6 -3
  2. glaip_sdk/_version.py +12 -5
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +79 -15
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +699 -0
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +503 -183
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +774 -137
  14. glaip_sdk/cli/commands/mcps.py +1124 -181
  15. glaip_sdk/cli/commands/models.py +25 -10
  16. glaip_sdk/cli/commands/tools.py +144 -92
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +61 -0
  19. glaip_sdk/cli/config.py +95 -0
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +150 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +143 -53
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +24 -18
  30. glaip_sdk/cli/main.py +420 -145
  31. glaip_sdk/cli/masking.py +136 -0
  32. glaip_sdk/cli/mcp_validators.py +287 -0
  33. glaip_sdk/cli/pager.py +266 -0
  34. glaip_sdk/cli/parsers/__init__.py +7 -0
  35. glaip_sdk/cli/parsers/json_input.py +177 -0
  36. glaip_sdk/cli/resolution.py +28 -21
  37. glaip_sdk/cli/rich_helpers.py +27 -0
  38. glaip_sdk/cli/slash/__init__.py +15 -0
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +282 -0
  42. glaip_sdk/cli/slash/prompt.py +245 -0
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +1679 -0
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +31 -0
  52. glaip_sdk/cli/transcript/cache.py +536 -0
  53. glaip_sdk/cli/transcript/capture.py +329 -0
  54. glaip_sdk/cli/transcript/export.py +38 -0
  55. glaip_sdk/cli/transcript/history.py +815 -0
  56. glaip_sdk/cli/transcript/launcher.py +77 -0
  57. glaip_sdk/cli/transcript/viewer.py +372 -0
  58. glaip_sdk/cli/update_notifier.py +290 -0
  59. glaip_sdk/cli/utils.py +247 -1238
  60. glaip_sdk/cli/validators.py +16 -18
  61. glaip_sdk/client/__init__.py +2 -1
  62. glaip_sdk/client/_agent_payloads.py +520 -0
  63. glaip_sdk/client/agent_runs.py +147 -0
  64. glaip_sdk/client/agents.py +940 -574
  65. glaip_sdk/client/base.py +163 -48
  66. glaip_sdk/client/main.py +35 -12
  67. glaip_sdk/client/mcps.py +126 -18
  68. glaip_sdk/client/run_rendering.py +415 -0
  69. glaip_sdk/client/shared.py +21 -0
  70. glaip_sdk/client/tools.py +195 -37
  71. glaip_sdk/client/validators.py +20 -48
  72. glaip_sdk/config/constants.py +15 -5
  73. glaip_sdk/exceptions.py +16 -9
  74. glaip_sdk/icons.py +25 -0
  75. glaip_sdk/mcps/__init__.py +21 -0
  76. glaip_sdk/mcps/base.py +345 -0
  77. glaip_sdk/models/__init__.py +90 -0
  78. glaip_sdk/models/agent.py +47 -0
  79. glaip_sdk/models/agent_runs.py +116 -0
  80. glaip_sdk/models/common.py +42 -0
  81. glaip_sdk/models/mcp.py +33 -0
  82. glaip_sdk/models/tool.py +33 -0
  83. glaip_sdk/payload_schemas/__init__.py +7 -0
  84. glaip_sdk/payload_schemas/agent.py +85 -0
  85. glaip_sdk/registry/__init__.py +55 -0
  86. glaip_sdk/registry/agent.py +164 -0
  87. glaip_sdk/registry/base.py +139 -0
  88. glaip_sdk/registry/mcp.py +253 -0
  89. glaip_sdk/registry/tool.py +231 -0
  90. glaip_sdk/rich_components.py +98 -2
  91. glaip_sdk/runner/__init__.py +59 -0
  92. glaip_sdk/runner/base.py +84 -0
  93. glaip_sdk/runner/deps.py +115 -0
  94. glaip_sdk/runner/langgraph.py +597 -0
  95. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  99. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  102. glaip_sdk/tools/__init__.py +22 -0
  103. glaip_sdk/tools/base.py +435 -0
  104. glaip_sdk/utils/__init__.py +59 -13
  105. glaip_sdk/utils/a2a/__init__.py +34 -0
  106. glaip_sdk/utils/a2a/event_processor.py +188 -0
  107. glaip_sdk/utils/agent_config.py +53 -40
  108. glaip_sdk/utils/bundler.py +267 -0
  109. glaip_sdk/utils/client.py +111 -0
  110. glaip_sdk/utils/client_utils.py +58 -26
  111. glaip_sdk/utils/datetime_helpers.py +58 -0
  112. glaip_sdk/utils/discovery.py +78 -0
  113. glaip_sdk/utils/display.py +65 -32
  114. glaip_sdk/utils/export.py +143 -0
  115. glaip_sdk/utils/general.py +1 -36
  116. glaip_sdk/utils/import_export.py +20 -25
  117. glaip_sdk/utils/import_resolver.py +492 -0
  118. glaip_sdk/utils/instructions.py +101 -0
  119. glaip_sdk/utils/rendering/__init__.py +115 -1
  120. glaip_sdk/utils/rendering/formatting.py +85 -43
  121. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  122. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
  123. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  124. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  125. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  126. glaip_sdk/utils/rendering/models.py +39 -7
  127. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  128. glaip_sdk/utils/rendering/renderer/base.py +672 -759
  129. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  130. glaip_sdk/utils/rendering/renderer/debug.py +75 -22
  131. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  132. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  133. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  134. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  135. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  136. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  137. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  138. glaip_sdk/utils/rendering/state.py +204 -0
  139. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  140. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  141. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  142. glaip_sdk/utils/rendering/steps/format.py +176 -0
  143. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  144. glaip_sdk/utils/rendering/timing.py +36 -0
  145. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  146. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  147. glaip_sdk/utils/resource_refs.py +29 -26
  148. glaip_sdk/utils/runtime_config.py +422 -0
  149. glaip_sdk/utils/serialization.py +184 -51
  150. glaip_sdk/utils/sync.py +142 -0
  151. glaip_sdk/utils/tool_detection.py +33 -0
  152. glaip_sdk/utils/validation.py +21 -30
  153. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
  154. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  155. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  156. glaip_sdk/models.py +0 -250
  157. glaip_sdk/utils/rendering/renderer/progress.py +0 -118
  158. glaip_sdk/utils/rendering/steps.py +0 -232
  159. glaip_sdk/utils/rich_utils.py +0 -29
  160. glaip_sdk-0.0.7.dist-info/RECORD +0 -55
  161. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
@@ -15,8 +15,10 @@ from pathlib import Path
15
15
  from typing import Any, BinaryIO, NoReturn
16
16
 
17
17
  import httpx
18
-
19
18
  from glaip_sdk.exceptions import AgentTimeoutError
19
+ from glaip_sdk.models import AgentResponse, MCPResponse, ToolResponse
20
+ from glaip_sdk.utils.resource_refs import extract_ids as extract_ids_new
21
+ from glaip_sdk.utils.resource_refs import find_by_name as find_by_name_new
20
22
 
21
23
  # Set up module-level logger
22
24
  logger = logging.getLogger("glaip_sdk.client_utils")
@@ -41,6 +43,11 @@ class MultipartData:
41
43
  self._exit_stack.close()
42
44
 
43
45
  def __enter__(self) -> "MultipartData":
46
+ """Enter context manager.
47
+
48
+ Returns:
49
+ Self instance for context manager protocol
50
+ """
44
51
  return self
45
52
 
46
53
  def __exit__(
@@ -49,6 +56,13 @@ class MultipartData:
49
56
  _exc_val: BaseException | None,
50
57
  _exc_tb: Any,
51
58
  ) -> None:
59
+ """Exit context manager and close all file handles.
60
+
61
+ Args:
62
+ _exc_type: Exception type (unused)
63
+ _exc_val: Exception value (unused)
64
+ _exc_tb: Exception traceback (unused)
65
+ """
52
66
  self.close()
53
67
 
54
68
 
@@ -65,8 +79,6 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
65
79
  This function maintains backward compatibility by returning None for empty input.
66
80
  New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
67
81
  """
68
- from .resource_refs import extract_ids as extract_ids_new
69
-
70
82
  if not items:
71
83
  return None
72
84
 
@@ -74,14 +86,16 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
74
86
  return result if result else None
75
87
 
76
88
 
77
- def create_model_instances(
78
- data: list[dict] | None, model_class: type, client: Any
79
- ) -> list[Any]:
89
+ def create_model_instances(data: list[dict] | None, model_class: type, client: Any) -> list[Any]:
80
90
  """Create model instances from API data with client association.
81
91
 
82
92
  This is a common pattern used across different clients (agents, tools, mcps)
83
93
  to create model instances and associate them with the client.
84
94
 
95
+ For runtime classes (Agent, Tool, MCP) that have a from_response method,
96
+ this function will use the corresponding Response model to parse the API data
97
+ and then create the runtime instance using from_response.
98
+
85
99
  Args:
86
100
  data: List of dictionaries from API response
87
101
  model_class: The model class to instantiate
@@ -93,12 +107,29 @@ def create_model_instances(
93
107
  if not data:
94
108
  return []
95
109
 
110
+ # Check if the model_class has a from_response method (runtime class pattern)
111
+ if hasattr(model_class, "from_response"):
112
+ # Map runtime classes to their response models
113
+ response_model_map = {
114
+ "Agent": AgentResponse,
115
+ "Tool": ToolResponse,
116
+ "MCP": MCPResponse,
117
+ }
118
+
119
+ response_model = response_model_map.get(model_class.__name__)
120
+ if response_model:
121
+ instances = []
122
+ for item_data in data:
123
+ response = response_model(**item_data)
124
+ instance = model_class.from_response(response, client=client)
125
+ instances.append(instance)
126
+ return instances
127
+
128
+ # Fallback to direct instantiation for other classes
96
129
  return [model_class(**item_data)._set_client(client) for item_data in data]
97
130
 
98
131
 
99
- def find_by_name(
100
- items: list[Any], name: str, case_sensitive: bool = False
101
- ) -> list[Any]:
132
+ def find_by_name(items: list[Any], name: str, case_sensitive: bool = False) -> list[Any]:
102
133
  """Filter items by name with optional case sensitivity.
103
134
 
104
135
  This is a common pattern used across different clients for client-side
@@ -115,8 +146,6 @@ def find_by_name(
115
146
  Note:
116
147
  This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
117
148
  """
118
- from .resource_refs import find_by_name as find_by_name_new
119
-
120
149
  return find_by_name_new(items, name, case_sensitive)
121
150
 
122
151
 
@@ -220,9 +249,7 @@ def _handle_streaming_error(
220
249
  if isinstance(e, httpx.ReadTimeout):
221
250
  logger.error(f"Read timeout during streaming: {e}")
222
251
  logger.error("This usually indicates the backend is taking too long to respond")
223
- logger.error(
224
- "Consider increasing the timeout value or checking backend performance"
225
- )
252
+ logger.error("Consider increasing the timeout value or checking backend performance")
226
253
  raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
227
254
 
228
255
  elif isinstance(e, httpx.TimeoutException):
@@ -261,9 +288,7 @@ def _yield_event_data(event_data: dict[str, Any] | None) -> Iterator[dict[str, A
261
288
  yield event_data
262
289
 
263
290
 
264
- def _flush_remaining_buffer(
265
- buf: list[str], event_type: str | None, event_id: str | None
266
- ) -> Iterator[dict[str, Any]]:
291
+ def _flush_remaining_buffer(buf: list[str], event_type: str | None, event_id: str | None) -> Iterator[dict[str, Any]]:
267
292
  """Flush any remaining data in buffer."""
268
293
  if buf:
269
294
  yield {
@@ -303,9 +328,7 @@ def iter_sse_events(
303
328
  if line is None:
304
329
  continue
305
330
 
306
- buf, event_type, event_id, event_data, completed = _process_sse_line(
307
- line, buf, event_type, event_id
308
- )
331
+ buf, event_type, event_id, event_data, completed = _process_sse_line(line, buf, event_type, event_id)
309
332
 
310
333
  yield from _yield_event_data(event_data)
311
334
  if completed:
@@ -371,9 +394,7 @@ def _create_form_data(message: str) -> dict[str, Any]:
371
394
  return {"input": message, "message": message, "stream": True}
372
395
 
373
396
 
374
- def _prepare_file_entry(
375
- item: str | BinaryIO, stack: ExitStack
376
- ) -> tuple[str, tuple[str, BinaryIO, str]]:
397
+ def _prepare_file_entry(item: str | BinaryIO, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
377
398
  """Prepare a single file entry for multipart data."""
378
399
  if isinstance(item, str):
379
400
  return _prepare_path_entry(item, stack)
@@ -381,9 +402,7 @@ def _prepare_file_entry(
381
402
  return _prepare_stream_entry(item)
382
403
 
383
404
 
384
- def _prepare_path_entry(
385
- path_str: str, stack: ExitStack
386
- ) -> tuple[str, tuple[str, BinaryIO, str]]:
405
+ def _prepare_path_entry(path_str: str, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
387
406
  """Prepare a file path entry."""
388
407
  file_path = Path(path_str)
389
408
  if not file_path.exists():
@@ -426,6 +445,19 @@ def _prepare_stream_entry(
426
445
  )
427
446
 
428
447
 
448
+ def add_kwargs_to_payload(payload: dict[str, Any], kwargs: dict[str, Any], excluded_keys: set[str]) -> None:
449
+ """Add kwargs to payload excluding specified keys.
450
+
451
+ Args:
452
+ payload: Payload dictionary to update.
453
+ kwargs: Keyword arguments to add.
454
+ excluded_keys: Keys to exclude from kwargs.
455
+ """
456
+ for key, value in kwargs.items():
457
+ if key not in excluded_keys:
458
+ payload[key] = value
459
+
460
+
429
461
  def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
430
462
  """Prepare multipart form data for file uploads.
431
463
 
@@ -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)
@@ -0,0 +1,78 @@
1
+ """Agent and tool discovery functions.
2
+
3
+ This module provides functions for finding agents and tools
4
+ from the GLAIP backend.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from gllm_core.utils import LoggerManager
15
+
16
+ if TYPE_CHECKING:
17
+ from glaip_sdk.agents import Agent
18
+ from glaip_sdk.tools import Tool
19
+
20
+ logger = LoggerManager().get_logger(__name__)
21
+
22
+
23
+ def find_agent(name: str) -> Agent | None:
24
+ """Find an agent by name using GLAIP SDK.
25
+
26
+ Args:
27
+ name: The name of the agent to find.
28
+
29
+ Returns:
30
+ The agent if found, None otherwise.
31
+
32
+ Example:
33
+ >>> from glaip_sdk.utils.discovery import find_agent
34
+ >>> agent = find_agent("weather_reporter")
35
+ >>> if agent:
36
+ ... print(f"Found agent: {agent.name}")
37
+ """
38
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
39
+
40
+ client = get_client()
41
+ try:
42
+ agents = client.list_agents()
43
+ for agent in agents:
44
+ if agent.name == name:
45
+ return agent
46
+ return None
47
+ except Exception as e:
48
+ logger.error("Error finding agent '%s': %s", name, e)
49
+ return None
50
+
51
+
52
+ def find_tool(name: str) -> Tool | None:
53
+ """Find a tool by name using GLAIP SDK.
54
+
55
+ Args:
56
+ name: The name of the tool to find.
57
+
58
+ Returns:
59
+ The tool if found, None otherwise.
60
+
61
+ Example:
62
+ >>> from glaip_sdk.utils.discovery import find_tool
63
+ >>> tool = find_tool("weather_api")
64
+ >>> if tool:
65
+ ... print(f"Found tool: {tool.name}")
66
+ """
67
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
68
+
69
+ client = get_client()
70
+ try:
71
+ tools = client.find_tools(name)
72
+ for tool in tools:
73
+ if tool.name == name:
74
+ return tool
75
+ return None
76
+ except Exception as e:
77
+ logger.error("Error finding tool '%s': %s", name, e)
78
+ return None
@@ -4,9 +4,59 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- from typing import Any
7
+ from __future__ import annotations
8
8
 
9
- from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
9
+ from importlib import import_module
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE
13
+ from glaip_sdk.icons import ICON_AGENT
14
+
15
+ if TYPE_CHECKING: # pragma: no cover - import-time typing helpers
16
+ from rich.console import Console
17
+ from rich.text import Text
18
+
19
+ from glaip_sdk.rich_components import AIPanel
20
+ else: # pragma: no cover - runtime fallback for type checking
21
+ AIPanel = Any # type: ignore[assignment]
22
+
23
+
24
+ def _check_rich_available() -> bool:
25
+ """Return True when core Rich display dependencies are importable."""
26
+ try:
27
+ __import__("rich.console")
28
+ __import__("rich.text")
29
+ __import__("glaip_sdk.rich_components")
30
+ except Exception:
31
+ return False
32
+ return True
33
+
34
+
35
+ RICH_AVAILABLE = _check_rich_available()
36
+
37
+
38
+ def _create_console() -> Console:
39
+ """Return a Console instance with lazy import to ease mocking."""
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()
44
+
45
+
46
+ def _create_text(*args: Any, **kwargs: Any) -> Text:
47
+ """Return a Text instance with lazy import to ease mocking."""
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)
52
+
53
+
54
+ def _create_panel(*args: Any, **kwargs: Any) -> AIPanel:
55
+ """Return an AIPPanel instance with lazy import to ease mocking."""
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)
10
60
 
11
61
 
12
62
  def print_agent_output(output: str, title: str = "Agent Output") -> None:
@@ -17,17 +67,11 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
17
67
  title: Title for the output panel
18
68
  """
19
69
  if RICH_AVAILABLE:
20
- # Lazy import Rich components
21
- from rich.console import Console
22
- from rich.text import Text
23
-
24
- from glaip_sdk.rich_components import AIPPanel
25
-
26
- console = Console()
27
- panel = AIPPanel(
28
- Text(output, style="green"),
70
+ console = _create_console()
71
+ panel = _create_panel(
72
+ _create_text(output, style=SUCCESS),
29
73
  title=title,
30
- border_style="green",
74
+ border_style=SUCCESS,
31
75
  )
32
76
  console.print(panel)
33
77
  else:
@@ -36,7 +80,7 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
36
80
  print("=" * (len(title) + 8))
37
81
 
38
82
 
39
- def print_agent_created(agent: Any, title: str = "🤖 Agent Created") -> None:
83
+ def print_agent_created(agent: Any, title: str = f"{ICON_AGENT} Agent Created") -> None:
40
84
  """Print agent creation success with rich formatting.
41
85
 
42
86
  Args:
@@ -44,21 +88,16 @@ def print_agent_created(agent: Any, title: str = "🤖 Agent Created") -> None:
44
88
  title: Title for the output panel
45
89
  """
46
90
  if RICH_AVAILABLE:
47
- # Lazy import Rich components
48
- from rich.console import Console
49
-
50
- from glaip_sdk.rich_components import AIPPanel
51
-
52
- console = Console()
53
- panel = AIPPanel(
54
- f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
91
+ console = _create_console()
92
+ panel = _create_panel(
93
+ f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' created successfully![/]\n\n"
55
94
  f"ID: {agent.id}\n"
56
95
  f"Model: {getattr(agent, 'model', 'N/A')}\n"
57
96
  f"Type: {getattr(agent, 'type', 'config')}\n"
58
97
  f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
59
98
  f"Version: {getattr(agent, 'version', '1.0')}",
60
99
  title=title,
61
- border_style="green",
100
+ border_style=SUCCESS,
62
101
  )
63
102
  console.print(panel)
64
103
  else:
@@ -77,11 +116,8 @@ def print_agent_updated(agent: Any) -> None:
77
116
  agent: The updated agent object
78
117
  """
79
118
  if RICH_AVAILABLE:
80
- # Lazy import Rich components
81
- from rich.console import Console
82
-
83
- console = Console()
84
- console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
119
+ console = _create_console()
120
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]")
85
121
  else:
86
122
  print(f"✅ Agent '{agent.name}' updated successfully")
87
123
 
@@ -93,10 +129,7 @@ def print_agent_deleted(agent_id: str) -> None:
93
129
  agent_id: The deleted agent's ID
94
130
  """
95
131
  if RICH_AVAILABLE:
96
- # Lazy import Rich components
97
- from rich.console import Console
98
-
99
- console = Console()
100
- console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
132
+ console = _create_console()
133
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]")
101
134
  else:
102
135
  print(f"✅ Agent deleted successfully (ID: {agent_id})")
@@ -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")
@@ -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
 
@@ -76,9 +43,7 @@ def format_datetime(dt: datetime | str | None) -> str:
76
43
  return str(dt)
77
44
 
78
45
 
79
- def progress_bar(
80
- iterable: Iterable[Any], description: str = "Processing"
81
- ) -> Iterator[Any]:
46
+ def progress_bar(iterable: Iterable[Any], description: str = "Processing") -> Iterator[Any]:
82
47
  """Simple progress bar using click.
83
48
 
84
49
  Args: