glaip-sdk 0.1.2__py3-none-any.whl → 0.6.5b3__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 (129) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1090 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents.py +214 -74
  11. glaip_sdk/cli/commands/common_config.py +101 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -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 +846 -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 +41 -20
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +228 -119
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +58 -20
  36. glaip_sdk/cli/slash/prompt.py +10 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +736 -134
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +66 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +70 -463
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1258
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +287 -29
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +133 -90
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +153 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/mcps/__init__.py +21 -0
  65. glaip_sdk/mcps/base.py +345 -0
  66. glaip_sdk/models/__init__.py +90 -0
  67. glaip_sdk/models/agent.py +47 -0
  68. glaip_sdk/models/agent_runs.py +116 -0
  69. glaip_sdk/models/common.py +42 -0
  70. glaip_sdk/models/mcp.py +33 -0
  71. glaip_sdk/models/tool.py +33 -0
  72. glaip_sdk/payload_schemas/__init__.py +1 -13
  73. glaip_sdk/registry/__init__.py +55 -0
  74. glaip_sdk/registry/agent.py +164 -0
  75. glaip_sdk/registry/base.py +139 -0
  76. glaip_sdk/registry/mcp.py +253 -0
  77. glaip_sdk/registry/tool.py +238 -0
  78. glaip_sdk/rich_components.py +58 -2
  79. glaip_sdk/tools/__init__.py +22 -0
  80. glaip_sdk/tools/base.py +435 -0
  81. glaip_sdk/utils/__init__.py +58 -12
  82. glaip_sdk/utils/bundler.py +267 -0
  83. glaip_sdk/utils/client.py +111 -0
  84. glaip_sdk/utils/client_utils.py +39 -7
  85. glaip_sdk/utils/datetime_helpers.py +58 -0
  86. glaip_sdk/utils/discovery.py +78 -0
  87. glaip_sdk/utils/display.py +23 -15
  88. glaip_sdk/utils/export.py +143 -0
  89. glaip_sdk/utils/general.py +0 -33
  90. glaip_sdk/utils/import_export.py +12 -7
  91. glaip_sdk/utils/import_resolver.py +492 -0
  92. glaip_sdk/utils/instructions.py +101 -0
  93. glaip_sdk/utils/rendering/__init__.py +115 -1
  94. glaip_sdk/utils/rendering/formatting.py +5 -30
  95. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  96. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  97. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  98. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  99. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  100. glaip_sdk/utils/rendering/models.py +1 -0
  101. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  102. glaip_sdk/utils/rendering/renderer/base.py +241 -1434
  103. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  104. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  105. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  106. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  107. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  108. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  109. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  110. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  111. glaip_sdk/utils/rendering/state.py +204 -0
  112. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  113. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  114. glaip_sdk/utils/rendering/steps/format.py +176 -0
  115. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  116. glaip_sdk/utils/rendering/timing.py +36 -0
  117. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  118. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  119. glaip_sdk/utils/resource_refs.py +25 -13
  120. glaip_sdk/utils/runtime_config.py +306 -0
  121. glaip_sdk/utils/serialization.py +18 -0
  122. glaip_sdk/utils/sync.py +142 -0
  123. glaip_sdk/utils/validation.py +16 -24
  124. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/METADATA +39 -4
  125. glaip_sdk-0.6.5b3.dist-info/RECORD +145 -0
  126. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/WHEEL +1 -1
  127. glaip_sdk/models.py +0 -240
  128. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  129. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,267 @@
1
+ """Tool source code bundling with import inlining.
2
+
3
+ This module provides the ToolBundler class for bundling Python tool source
4
+ code with all local dependencies inlined.
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
+ import ast
13
+ import inspect
14
+ from pathlib import Path
15
+
16
+ from glaip_sdk.utils.import_resolver import ImportResolver
17
+
18
+
19
+ class ToolBundler:
20
+ """Bundles tool source code with inlined local imports.
21
+
22
+ This class handles the complex process of taking a tool class and
23
+ producing a single, self-contained source file with all local
24
+ dependencies inlined.
25
+
26
+ Attributes:
27
+ tool_class: The tool class to bundle.
28
+ tool_file: Path to the file containing the tool class.
29
+ tool_dir: Directory containing the tool file.
30
+
31
+ Example:
32
+ >>> bundler = ToolBundler(MyToolClass)
33
+ >>> bundled_source = bundler.bundle()
34
+ """
35
+
36
+ def __init__(self, tool_class: type) -> None:
37
+ """Initialize the ToolBundler.
38
+
39
+ Args:
40
+ tool_class: The tool class or decorated function to bundle.
41
+ """
42
+ # If it's a gllm_core Tool, get the underlying function
43
+ if hasattr(tool_class, "__wrapped__"):
44
+ actual_func = tool_class.__wrapped__
45
+ else:
46
+ actual_func = tool_class
47
+
48
+ self.tool_class = tool_class
49
+ self.tool_file = Path(inspect.getfile(actual_func))
50
+ self.tool_dir = self.tool_file.parent
51
+ self._import_resolver = ImportResolver(self.tool_dir)
52
+
53
+ def bundle(self) -> str:
54
+ """Bundle tool source code with inlined local imports.
55
+
56
+ Returns:
57
+ Bundled source code with all local dependencies inlined.
58
+ """
59
+ with open(self.tool_file, encoding="utf-8") as f:
60
+ full_source = f.read()
61
+
62
+ tree = ast.parse(full_source)
63
+ local_imports, external_imports = self._import_resolver.categorize_imports(tree)
64
+
65
+ # Extract main code nodes (excluding imports, docstrings, glaip_sdk.Tool subclasses)
66
+ main_code_nodes = self._extract_main_code_nodes(tree)
67
+
68
+ # Inline local imports and collect their external imports
69
+ inlined_code, inlined_external_imports = self._import_resolver.inline_local_imports(local_imports)
70
+
71
+ # Merge all external imports
72
+ all_external_imports = external_imports + inlined_external_imports
73
+
74
+ # Build bundled code
75
+ bundled_code = ["# Bundled tool with inlined local imports\n"]
76
+ bundled_code.extend(self._import_resolver.format_external_imports(all_external_imports))
77
+
78
+ # Add inlined dependencies FIRST (before main tool code)
79
+ bundled_code.extend(inlined_code)
80
+
81
+ # Then add main tool code
82
+ bundled_code.append("# Main tool code\n")
83
+ for node_code in main_code_nodes:
84
+ bundled_code.append(node_code + "\n")
85
+ bundled_code.append("\n")
86
+
87
+ return "".join(bundled_code)
88
+
89
+ def _extract_main_code_nodes(self, tree: ast.AST) -> list[str]:
90
+ """Extract main code nodes from AST, excluding imports and Tool subclasses.
91
+
92
+ Args:
93
+ tree: AST tree of the source file.
94
+
95
+ Returns:
96
+ List of unparsed code node strings.
97
+ """
98
+ main_code_nodes = []
99
+ for node in tree.body:
100
+ # Skip imports
101
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
102
+ continue
103
+ # Skip module docstrings
104
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
105
+ continue
106
+ # Skip glaip_sdk.Tool subclasses
107
+ if isinstance(node, ast.ClassDef) and self._is_sdk_tool_subclass(node):
108
+ continue
109
+ main_code_nodes.append(ast.unparse(node))
110
+ return main_code_nodes
111
+
112
+ @staticmethod
113
+ def _is_sdk_tool_subclass(node: ast.ClassDef) -> bool:
114
+ """Check if AST class definition inherits from Tool.
115
+
116
+ These classes are only needed locally for upload configuration
117
+ and should be excluded from bundled code.
118
+
119
+ Args:
120
+ node: AST ClassDef node to check.
121
+
122
+ Returns:
123
+ True if class inherits from Tool.
124
+ """
125
+ for base in node.bases:
126
+ if isinstance(base, ast.Name) and base.id == "Tool":
127
+ return True
128
+ if (
129
+ isinstance(base, ast.Attribute)
130
+ and base.attr == "Tool"
131
+ and isinstance(base.value, ast.Name)
132
+ and base.value.id in ("glaip_sdk",)
133
+ ):
134
+ return True
135
+ return False
136
+
137
+ @classmethod
138
+ def bundle_from_source(cls, file_path: Path) -> tuple[str, str, str]:
139
+ """Extract tool info directly from source file without importing.
140
+
141
+ This is used as a fallback when the tool class cannot be imported
142
+ due to missing dependencies.
143
+
144
+ Args:
145
+ file_path: Path to the tool source file.
146
+
147
+ Returns:
148
+ Tuple of (name, description, bundled_source_code).
149
+
150
+ Raises:
151
+ FileNotFoundError: If the source file doesn't exist.
152
+ """
153
+ if not file_path.exists():
154
+ raise FileNotFoundError(f"Tool source file not found: {file_path}")
155
+
156
+ with open(file_path, encoding="utf-8") as f:
157
+ source_code = f.read()
158
+
159
+ tree = ast.parse(source_code)
160
+ tool_dir = file_path.parent
161
+ import_resolver = ImportResolver(tool_dir)
162
+
163
+ # Find tool name and description from class definitions
164
+ tool_name, tool_description = cls._extract_tool_metadata(tree, file_path.stem)
165
+
166
+ # Categorize imports
167
+ local_imports, external_imports = import_resolver.categorize_imports(tree)
168
+
169
+ # Extract main code nodes
170
+ main_code_nodes = []
171
+ for node in tree.body:
172
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
173
+ continue
174
+ if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
175
+ continue
176
+ main_code_nodes.append(ast.unparse(node))
177
+
178
+ # Inline local imports
179
+ inlined_code, inlined_external_imports = import_resolver.inline_local_imports(local_imports)
180
+
181
+ # Build bundled code
182
+ all_external_imports = external_imports + inlined_external_imports
183
+ bundled_code = ["# Bundled tool with inlined local imports\n"]
184
+ bundled_code.extend(import_resolver.format_external_imports(all_external_imports))
185
+
186
+ # Add main tool code
187
+ bundled_code.append("# Main tool code\n")
188
+ for node_code in main_code_nodes:
189
+ bundled_code.append(node_code + "\n")
190
+ bundled_code.append("\n")
191
+
192
+ # Then add inlined dependencies
193
+ bundled_code.extend(inlined_code)
194
+
195
+ bundled_source = "".join(bundled_code)
196
+
197
+ return tool_name, tool_description, bundled_source
198
+
199
+ @staticmethod
200
+ def _extract_tool_metadata(tree: ast.AST, fallback_name: str) -> tuple[str, str]:
201
+ """Extract tool name and description from AST.
202
+
203
+ Args:
204
+ tree: AST tree of the source file.
205
+ fallback_name: Name to use if not found in source.
206
+
207
+ Returns:
208
+ Tuple of (tool_name, tool_description).
209
+ """
210
+ tool_name, tool_description = ToolBundler._find_class_attributes(tree)
211
+
212
+ if not tool_name:
213
+ # Convert class name to snake_case as fallback
214
+ tool_name = "".join(["_" + c.lower() if c.isupper() else c for c in fallback_name]).lstrip("_")
215
+
216
+ if not tool_description:
217
+ tool_description = f"Tool: {fallback_name}"
218
+
219
+ return tool_name, tool_description
220
+
221
+ @staticmethod
222
+ def _find_class_attributes(tree: ast.AST) -> tuple[str | None, str | None]:
223
+ """Find name and description attributes in class definitions.
224
+
225
+ Args:
226
+ tree: AST tree to search.
227
+
228
+ Returns:
229
+ Tuple of (name, description) if found.
230
+ """
231
+ for node in ast.walk(tree):
232
+ if not isinstance(node, ast.ClassDef):
233
+ continue
234
+ name, description = ToolBundler._extract_class_name_description(node)
235
+ if name or description:
236
+ return name, description
237
+ return None, None
238
+
239
+ @staticmethod
240
+ def _extract_class_name_description(
241
+ class_node: ast.ClassDef,
242
+ ) -> tuple[str | None, str | None]:
243
+ """Extract name and description from a single class definition.
244
+
245
+ Args:
246
+ class_node: AST ClassDef node.
247
+
248
+ Returns:
249
+ Tuple of (name, description) if found.
250
+ """
251
+ name = None
252
+ description = None
253
+
254
+ for item in class_node.body:
255
+ if not isinstance(item, ast.AnnAssign):
256
+ continue
257
+ if not isinstance(item.target, ast.Name):
258
+ continue
259
+ if not isinstance(item.value, ast.Constant):
260
+ continue
261
+
262
+ if item.target.id == "name":
263
+ name = item.value.value
264
+ elif item.target.id == "description":
265
+ description = item.value.value
266
+
267
+ return name, description
@@ -0,0 +1,111 @@
1
+ """Client singleton management for GLAIP SDK.
2
+
3
+ This module provides a singleton pattern for the GLAIP SDK client instance
4
+ used by the agents runtime. Uses a class-based singleton pattern consistent
5
+ with the registry implementations.
6
+
7
+ Thread Safety:
8
+ The singleton is created lazily on first access. In Python, the GIL ensures
9
+ that class attribute assignment is atomic, making this pattern safe for
10
+ multi-threaded access. For multiprocessing, each process gets its own
11
+ client instance (no shared state across processes).
12
+
13
+ Authors:
14
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dotenv import load_dotenv
20
+ from glaip_sdk.client import Client
21
+
22
+
23
+ class _ClientSingleton:
24
+ """Singleton holder for GLAIP SDK Client.
25
+
26
+ This class follows the same pattern as registry singletons
27
+ (_ToolRegistrySingleton, _MCPRegistrySingleton, _AgentRegistrySingleton).
28
+ """
29
+
30
+ _instance: Client | None = None
31
+
32
+ @classmethod
33
+ def get_instance(cls) -> Client:
34
+ """Get or create the singleton client instance.
35
+
36
+ Returns:
37
+ The singleton client instance.
38
+
39
+ Example:
40
+ >>> from glaip_sdk.utils.client import get_client
41
+ >>> client = get_client()
42
+ >>> agents = client.list_agents()
43
+ """
44
+ if cls._instance is None:
45
+ load_dotenv()
46
+ cls._instance = Client()
47
+ return cls._instance
48
+
49
+ @classmethod
50
+ def set_instance(cls, client: Client) -> None:
51
+ """Set the singleton client instance.
52
+
53
+ Useful for testing or when you need to configure the client manually.
54
+
55
+ Args:
56
+ client: The client instance to use.
57
+
58
+ Example:
59
+ >>> from glaip_sdk import Client
60
+ >>> from glaip_sdk.utils.client import set_client
61
+ >>> client = Client(api_key="my-key")
62
+ >>> set_client(client)
63
+ """
64
+ cls._instance = client
65
+
66
+ @classmethod
67
+ def reset(cls) -> None:
68
+ """Reset the singleton client instance.
69
+
70
+ Useful for testing to ensure a fresh client is created.
71
+ """
72
+ cls._instance = None
73
+
74
+
75
+ def get_client() -> Client:
76
+ """Get or create singleton client instance.
77
+
78
+ Returns:
79
+ The singleton client instance.
80
+
81
+ Example:
82
+ >>> from glaip_sdk.utils.client import get_client
83
+ >>> client = get_client()
84
+ >>> agents = client.list_agents()
85
+ """
86
+ return _ClientSingleton.get_instance()
87
+
88
+
89
+ def set_client(client: Client) -> None:
90
+ """Set the singleton client instance.
91
+
92
+ Useful for testing or when you need to configure the client manually.
93
+
94
+ Args:
95
+ client: The client instance to use.
96
+
97
+ Example:
98
+ >>> from glaip_sdk import Client
99
+ >>> from glaip_sdk.utils.client import set_client
100
+ >>> client = Client(api_key="my-key")
101
+ >>> set_client(client)
102
+ """
103
+ _ClientSingleton.set_instance(client)
104
+
105
+
106
+ def reset_client() -> None:
107
+ """Reset the singleton client instance.
108
+
109
+ Useful for testing to ensure a fresh client is created.
110
+ """
111
+ _ClientSingleton.reset()
@@ -15,14 +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
20
- from glaip_sdk.utils.resource_refs import (
21
- extract_ids as extract_ids_new,
22
- )
23
- from glaip_sdk.utils.resource_refs import (
24
- find_by_name as find_by_name_new,
25
- )
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
26
22
 
27
23
  # Set up module-level logger
28
24
  logger = logging.getLogger("glaip_sdk.client_utils")
@@ -96,6 +92,10 @@ def create_model_instances(data: list[dict] | None, model_class: type, client: A
96
92
  This is a common pattern used across different clients (agents, tools, mcps)
97
93
  to create model instances and associate them with the client.
98
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
+
99
99
  Args:
100
100
  data: List of dictionaries from API response
101
101
  model_class: The model class to instantiate
@@ -107,6 +107,25 @@ def create_model_instances(data: list[dict] | None, model_class: type, client: A
107
107
  if not data:
108
108
  return []
109
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
110
129
  return [model_class(**item_data)._set_client(client) for item_data in data]
111
130
 
112
131
 
@@ -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,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 AIPPanel
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
- """Check if Rich and our custom components can be imported."""
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() -> "Console":
38
+ def _create_console() -> Console:
34
39
  """Return a Console instance with lazy import to ease mocking."""
35
- from rich.console import Console # Local import for test friendliness
36
-
37
- return Console()
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) -> "Text":
46
+ def _create_text(*args: Any, **kwargs: Any) -> Text:
41
47
  """Return a Text instance with lazy import to ease mocking."""
42
- from rich.text import Text # Local import for test friendliness
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
- from glaip_sdk.rich_components import AIPPanel # Local import for test friendliness
50
-
51
- return AIPPanel(*args, **kwargs)
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: