glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.15b3__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 (154) hide show
  1. glaip_sdk/agents/__init__.py +27 -0
  2. glaip_sdk/agents/base.py +1196 -0
  3. glaip_sdk/cli/__init__.py +9 -0
  4. glaip_sdk/cli/account_store.py +540 -0
  5. glaip_sdk/cli/agent_config.py +78 -0
  6. glaip_sdk/cli/auth.py +699 -0
  7. glaip_sdk/cli/commands/__init__.py +5 -0
  8. glaip_sdk/cli/commands/accounts.py +746 -0
  9. glaip_sdk/cli/commands/agents.py +1509 -0
  10. glaip_sdk/cli/commands/common_config.py +104 -0
  11. glaip_sdk/cli/commands/configure.py +896 -0
  12. glaip_sdk/cli/commands/mcps.py +1356 -0
  13. glaip_sdk/cli/commands/models.py +69 -0
  14. glaip_sdk/cli/commands/tools.py +576 -0
  15. glaip_sdk/cli/commands/transcripts.py +755 -0
  16. glaip_sdk/cli/commands/update.py +61 -0
  17. glaip_sdk/cli/config.py +95 -0
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +150 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +851 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +355 -0
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +112 -0
  28. glaip_sdk/cli/main.py +615 -0
  29. glaip_sdk/cli/masking.py +136 -0
  30. glaip_sdk/cli/mcp_validators.py +287 -0
  31. glaip_sdk/cli/pager.py +266 -0
  32. glaip_sdk/cli/parsers/__init__.py +7 -0
  33. glaip_sdk/cli/parsers/json_input.py +177 -0
  34. glaip_sdk/cli/resolution.py +67 -0
  35. glaip_sdk/cli/rich_helpers.py +27 -0
  36. glaip_sdk/cli/slash/__init__.py +15 -0
  37. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  38. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  39. glaip_sdk/cli/slash/agent_session.py +285 -0
  40. glaip_sdk/cli/slash/prompt.py +256 -0
  41. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  42. glaip_sdk/cli/slash/session.py +1708 -0
  43. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  44. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  45. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  46. glaip_sdk/cli/slash/tui/loading.py +58 -0
  47. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  48. glaip_sdk/cli/transcript/__init__.py +31 -0
  49. glaip_sdk/cli/transcript/cache.py +536 -0
  50. glaip_sdk/cli/transcript/capture.py +329 -0
  51. glaip_sdk/cli/transcript/export.py +38 -0
  52. glaip_sdk/cli/transcript/history.py +815 -0
  53. glaip_sdk/cli/transcript/launcher.py +77 -0
  54. glaip_sdk/cli/transcript/viewer.py +374 -0
  55. glaip_sdk/cli/update_notifier.py +290 -0
  56. glaip_sdk/cli/utils.py +263 -0
  57. glaip_sdk/cli/validators.py +238 -0
  58. glaip_sdk/client/__init__.py +11 -0
  59. glaip_sdk/client/_agent_payloads.py +520 -0
  60. glaip_sdk/client/agent_runs.py +147 -0
  61. glaip_sdk/client/agents.py +1335 -0
  62. glaip_sdk/client/base.py +502 -0
  63. glaip_sdk/client/main.py +249 -0
  64. glaip_sdk/client/mcps.py +370 -0
  65. glaip_sdk/client/run_rendering.py +700 -0
  66. glaip_sdk/client/shared.py +21 -0
  67. glaip_sdk/client/tools.py +661 -0
  68. glaip_sdk/client/validators.py +198 -0
  69. glaip_sdk/config/constants.py +52 -0
  70. glaip_sdk/mcps/__init__.py +21 -0
  71. glaip_sdk/mcps/base.py +345 -0
  72. glaip_sdk/models/__init__.py +90 -0
  73. glaip_sdk/models/agent.py +47 -0
  74. glaip_sdk/models/agent_runs.py +116 -0
  75. glaip_sdk/models/common.py +42 -0
  76. glaip_sdk/models/mcp.py +33 -0
  77. glaip_sdk/models/tool.py +33 -0
  78. glaip_sdk/payload_schemas/__init__.py +7 -0
  79. glaip_sdk/payload_schemas/agent.py +85 -0
  80. glaip_sdk/registry/__init__.py +55 -0
  81. glaip_sdk/registry/agent.py +164 -0
  82. glaip_sdk/registry/base.py +139 -0
  83. glaip_sdk/registry/mcp.py +253 -0
  84. glaip_sdk/registry/tool.py +232 -0
  85. glaip_sdk/runner/__init__.py +59 -0
  86. glaip_sdk/runner/base.py +84 -0
  87. glaip_sdk/runner/deps.py +112 -0
  88. glaip_sdk/runner/langgraph.py +782 -0
  89. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  90. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  91. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  92. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  93. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  94. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  95. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  96. glaip_sdk/tools/__init__.py +22 -0
  97. glaip_sdk/tools/base.py +435 -0
  98. glaip_sdk/utils/__init__.py +86 -0
  99. glaip_sdk/utils/a2a/__init__.py +34 -0
  100. glaip_sdk/utils/a2a/event_processor.py +188 -0
  101. glaip_sdk/utils/agent_config.py +194 -0
  102. glaip_sdk/utils/bundler.py +267 -0
  103. glaip_sdk/utils/client.py +111 -0
  104. glaip_sdk/utils/client_utils.py +486 -0
  105. glaip_sdk/utils/datetime_helpers.py +58 -0
  106. glaip_sdk/utils/discovery.py +78 -0
  107. glaip_sdk/utils/display.py +135 -0
  108. glaip_sdk/utils/export.py +143 -0
  109. glaip_sdk/utils/general.py +61 -0
  110. glaip_sdk/utils/import_export.py +168 -0
  111. glaip_sdk/utils/import_resolver.py +492 -0
  112. glaip_sdk/utils/instructions.py +101 -0
  113. glaip_sdk/utils/rendering/__init__.py +115 -0
  114. glaip_sdk/utils/rendering/formatting.py +264 -0
  115. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  116. glaip_sdk/utils/rendering/layout/panels.py +156 -0
  117. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  118. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  119. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  120. glaip_sdk/utils/rendering/models.py +85 -0
  121. glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
  122. glaip_sdk/utils/rendering/renderer/base.py +1024 -0
  123. glaip_sdk/utils/rendering/renderer/config.py +27 -0
  124. glaip_sdk/utils/rendering/renderer/console.py +55 -0
  125. glaip_sdk/utils/rendering/renderer/debug.py +178 -0
  126. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  127. glaip_sdk/utils/rendering/renderer/stream.py +202 -0
  128. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  129. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  130. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  131. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  132. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  133. glaip_sdk/utils/rendering/state.py +204 -0
  134. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  135. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  136. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  137. glaip_sdk/utils/rendering/steps/format.py +176 -0
  138. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  139. glaip_sdk/utils/rendering/timing.py +36 -0
  140. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  141. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  142. glaip_sdk/utils/resource_refs.py +195 -0
  143. glaip_sdk/utils/run_renderer.py +41 -0
  144. glaip_sdk/utils/runtime_config.py +425 -0
  145. glaip_sdk/utils/serialization.py +424 -0
  146. glaip_sdk/utils/sync.py +142 -0
  147. glaip_sdk/utils/tool_detection.py +33 -0
  148. glaip_sdk/utils/validation.py +264 -0
  149. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/METADATA +1 -1
  150. glaip_sdk-0.6.15b3.dist-info/RECORD +160 -0
  151. glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
  152. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/WHEEL +0 -0
  153. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/entry_points.txt +0 -0
  154. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,135 @@
1
+ """Rich display utilities for enhanced output.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
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)
60
+
61
+
62
+ def print_agent_output(output: str, title: str = "Agent Output") -> None:
63
+ """Print agent output with rich formatting.
64
+
65
+ Args:
66
+ output: The agent's response text
67
+ title: Title for the output panel
68
+ """
69
+ if RICH_AVAILABLE:
70
+ console = _create_console()
71
+ panel = _create_panel(
72
+ _create_text(output, style=SUCCESS),
73
+ title=title,
74
+ border_style=SUCCESS,
75
+ )
76
+ console.print(panel)
77
+ else:
78
+ print(f"\n=== {title} ===")
79
+ print(output)
80
+ print("=" * (len(title) + 8))
81
+
82
+
83
+ def print_agent_created(agent: Any, title: str = f"{ICON_AGENT} Agent Created") -> None:
84
+ """Print agent creation success with rich formatting.
85
+
86
+ Args:
87
+ agent: The created agent object
88
+ title: Title for the output panel
89
+ """
90
+ if RICH_AVAILABLE:
91
+ console = _create_console()
92
+ panel = _create_panel(
93
+ f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' created successfully![/]\n\n"
94
+ f"ID: {agent.id}\n"
95
+ f"Model: {getattr(agent, 'model', 'N/A')}\n"
96
+ f"Type: {getattr(agent, 'type', 'config')}\n"
97
+ f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
98
+ f"Version: {getattr(agent, 'version', '1.0')}",
99
+ title=title,
100
+ border_style=SUCCESS,
101
+ )
102
+ console.print(panel)
103
+ else:
104
+ print(f"✅ Agent '{agent.name}' created successfully!")
105
+ print(f"ID: {agent.id}")
106
+ print(f"Model: {getattr(agent, 'model', 'N/A')}")
107
+ print(f"Type: {getattr(agent, 'type', 'config')}")
108
+ print(f"Framework: {getattr(agent, 'framework', 'langchain')}")
109
+ print(f"Version: {getattr(agent, 'version', '1.0')}")
110
+
111
+
112
+ def print_agent_updated(agent: Any) -> None:
113
+ """Print agent update success with rich formatting.
114
+
115
+ Args:
116
+ agent: The updated agent object
117
+ """
118
+ if RICH_AVAILABLE:
119
+ console = _create_console()
120
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]")
121
+ else:
122
+ print(f"✅ Agent '{agent.name}' updated successfully")
123
+
124
+
125
+ def print_agent_deleted(agent_id: str) -> None:
126
+ """Print agent deletion success with rich formatting.
127
+
128
+ Args:
129
+ agent_id: The deleted agent's ID
130
+ """
131
+ if RICH_AVAILABLE:
132
+ console = _create_console()
133
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]")
134
+ else:
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")
@@ -0,0 +1,61 @@
1
+ """General utility functions for AIP SDK.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from collections.abc import Iterable, Iterator
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+
14
+ def format_file_size(size_bytes: int) -> str:
15
+ """Format file size in human readable format.
16
+
17
+ Args:
18
+ size_bytes: Size in bytes
19
+
20
+ Returns:
21
+ Human readable size string (e.g., "1.5 MB")
22
+ """
23
+ for unit in ["B", "KB", "MB", "GB"]:
24
+ if size_bytes < 1024.0:
25
+ return f"{size_bytes:.1f} {unit}"
26
+ size_bytes /= 1024.0
27
+ return f"{size_bytes:.1f} TB"
28
+
29
+
30
+ def format_datetime(dt: datetime | str | None) -> str:
31
+ """Format datetime object to readable string.
32
+
33
+ Args:
34
+ dt: Datetime object or string to format
35
+
36
+ Returns:
37
+ Formatted datetime string or "N/A" if None
38
+ """
39
+ if isinstance(dt, datetime):
40
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
41
+ elif dt is None:
42
+ return "N/A"
43
+ return str(dt)
44
+
45
+
46
+ def progress_bar(iterable: Iterable[Any], description: str = "Processing") -> Iterator[Any]:
47
+ """Simple progress bar using click.
48
+
49
+ Args:
50
+ iterable: Iterable to process
51
+ description: Progress description
52
+
53
+ Yields:
54
+ Items from iterable with progress display
55
+ """
56
+ try:
57
+ with click.progressbar(iterable, label=description) as bar:
58
+ yield from bar
59
+ except ImportError:
60
+ # Fallback if click not available
61
+ yield from iterable
@@ -0,0 +1,168 @@
1
+ """Import/export utilities for schema transforms and data merging.
2
+
3
+ This module provides functions for converting between export and import formats,
4
+ merging imported data with CLI arguments, and handling relationship flattening.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from glaip_sdk.utils.resource_refs import _extract_id_from_item
13
+
14
+
15
+ def extract_ids_from_export(items: list[Any]) -> list[str]:
16
+ """Extract IDs from export format (list of dicts with id/name fields).
17
+
18
+ This function is similar to `extract_ids` in `resource_refs.py` but differs in behavior:
19
+ - This function SKIPS items without IDs (doesn't convert to string)
20
+ - `extract_ids` converts items without IDs to strings as fallback
21
+
22
+ This difference is intentional: export format should only include actual IDs,
23
+ while general resource reference extraction may need fallback string conversion.
24
+
25
+ Args:
26
+ items: List of items (dicts with id/name or strings)
27
+
28
+ Returns:
29
+ List of extracted IDs (only items with actual IDs)
30
+
31
+ Examples:
32
+ extract_ids_from_export([{"id": "123", "name": "tool"}]) -> ["123"]
33
+ extract_ids_from_export(["123", "456"]) -> ["123", "456"]
34
+ extract_ids_from_export([{"name": "tool"}, "123"]) -> ["123"] # Skip items without ID
35
+ """
36
+ if not items:
37
+ return []
38
+
39
+ ids = []
40
+ for item in items:
41
+ extracted = _extract_id_from_item(item, skip_missing=True)
42
+ if extracted is not None:
43
+ ids.append(extracted)
44
+
45
+ return ids
46
+
47
+
48
+ def convert_export_to_import_format(
49
+ data: dict[str, Any],
50
+ ) -> dict[str, Any]:
51
+ """Convert export format to import-compatible format (extract IDs from objects).
52
+
53
+ Args:
54
+ data: Export format data with full objects
55
+
56
+ Returns:
57
+ Import format data with extracted IDs
58
+
59
+ Notes:
60
+ - Converts tools/agents from dict objects to ID lists
61
+ - Preserves all other data unchanged
62
+ """
63
+ import_data = data.copy()
64
+
65
+ for key in ["tools", "agents", "mcps"]:
66
+ if key in import_data and isinstance(import_data[key], list):
67
+ import_data[key] = extract_ids_from_export(import_data[key])
68
+
69
+ return import_data
70
+
71
+
72
+ def _get_default_array_fields() -> list[str]:
73
+ """Get default array fields that should be merged."""
74
+ return ["tools", "agents", "mcps"]
75
+
76
+
77
+ def _should_use_cli_value(cli_value: Any) -> bool:
78
+ """Check if CLI value should be used."""
79
+ return cli_value is not None and (not isinstance(cli_value, (list, tuple)) or len(cli_value) > 0)
80
+
81
+
82
+ def _handle_array_field_merge(key: str, cli_value: Any, import_data: dict[str, Any]) -> Any:
83
+ """Handle merging of array fields."""
84
+ import_value = import_data[key]
85
+ if isinstance(import_value, list):
86
+ return list(cli_value) + import_value
87
+ else:
88
+ return cli_value
89
+
90
+
91
+ def _merge_cli_values_with_import(
92
+ merged: dict[str, Any],
93
+ cli_args: dict[str, Any],
94
+ import_data: dict[str, Any],
95
+ array_fields: list[str],
96
+ ) -> None:
97
+ """Merge CLI values into merged dict."""
98
+ for key, cli_value in cli_args.items():
99
+ if _should_use_cli_value(cli_value):
100
+ # CLI value takes precedence (for non-empty values)
101
+ if key in array_fields and key in import_data:
102
+ # For array fields, combine CLI and imported values
103
+ merged[key] = _handle_array_field_merge(key, cli_value, import_data)
104
+ else:
105
+ merged[key] = cli_value
106
+ elif key in import_data:
107
+ # Use imported value if no CLI value
108
+ merged[key] = import_data[key]
109
+
110
+
111
+ def _add_import_only_fields(merged: dict[str, Any], import_data: dict[str, Any]) -> None:
112
+ """Add fields that exist only in import data."""
113
+ for key, import_value in import_data.items():
114
+ if key not in merged:
115
+ merged[key] = import_value
116
+
117
+
118
+ def merge_import_with_cli_args(
119
+ import_data: dict[str, Any],
120
+ cli_args: dict[str, Any],
121
+ array_fields: list[str] = None,
122
+ ) -> dict[str, Any]:
123
+ """Merge imported data with CLI arguments, preferring CLI args.
124
+
125
+ Args:
126
+ import_data: Data loaded from import file
127
+ cli_args: Arguments passed via CLI
128
+ array_fields: Fields that should be combined (merged) rather than replaced
129
+
130
+ Returns:
131
+ Merged data dictionary
132
+
133
+ Notes:
134
+ - CLI arguments take precedence over imported data
135
+ - Array fields (tools, agents, mcps) are combined rather than replaced
136
+ - Empty arrays/lists are treated as None (no override)
137
+ """
138
+ if array_fields is None:
139
+ array_fields = _get_default_array_fields()
140
+
141
+ merged = {}
142
+ _merge_cli_values_with_import(merged, cli_args, import_data, array_fields)
143
+ _add_import_only_fields(merged, import_data)
144
+
145
+ return merged
146
+
147
+
148
+ def flatten_relationships_for_import(
149
+ data: dict[str, Any], fields: tuple[str, ...] = ("tools", "agents")
150
+ ) -> dict[str, Any]:
151
+ """Flatten relationship fields for import format.
152
+
153
+ This is an alias for convert_export_to_import_format with configurable fields.
154
+
155
+ Args:
156
+ data: Export format data with full objects
157
+ fields: Tuple of field names to flatten to IDs
158
+
159
+ Returns:
160
+ Import format data with specified fields flattened to IDs
161
+ """
162
+ import_data = data.copy()
163
+
164
+ for field in fields:
165
+ if field in import_data and isinstance(import_data[field], list):
166
+ import_data[field] = extract_ids_from_export(import_data[field])
167
+
168
+ return import_data