glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 (156) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +265 -45
  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 +251 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +266 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -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 +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  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 +876 -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 +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -67,20 +67,12 @@ def read_yaml(file_path: Path) -> dict[str, Any]:
67
67
  data = yaml.safe_load(f)
68
68
 
69
69
  # Handle instruction_lines array format for user-friendly YAML
70
- if (
71
- isinstance(data, dict)
72
- and "instruction_lines" in data
73
- and isinstance(data["instruction_lines"], list)
74
- ):
70
+ if isinstance(data, dict) and "instruction_lines" in data and isinstance(data["instruction_lines"], list):
75
71
  data["instruction"] = "\n\n".join(data["instruction_lines"])
76
72
  del data["instruction_lines"]
77
73
 
78
74
  # Handle instruction as list from YAML export (convert back to string)
79
- if (
80
- isinstance(data, dict)
81
- and "instruction" in data
82
- and isinstance(data["instruction"], list)
83
- ):
75
+ if isinstance(data, dict) and "instruction" in data and isinstance(data["instruction"], list):
84
76
  data["instruction"] = "\n\n".join(data["instruction"])
85
77
 
86
78
  return data
@@ -96,11 +88,20 @@ def write_yaml(file_path: Path, data: dict[str, Any]) -> None:
96
88
 
97
89
  # Custom YAML dumper for user-friendly instruction formatting
98
90
  class LiteralString(str):
91
+ """String subclass for YAML literal block scalar formatting."""
92
+
99
93
  pass
100
94
 
101
- def literal_string_representer(
102
- dumper: yaml.Dumper, data: "LiteralString"
103
- ) -> yaml.nodes.Node:
95
+ def literal_string_representer(dumper: yaml.Dumper, data: "LiteralString") -> yaml.nodes.Node:
96
+ """YAML representer for LiteralString to use literal block scalar style.
97
+
98
+ Args:
99
+ dumper: YAML dumper instance.
100
+ data: LiteralString instance to represent.
101
+
102
+ Returns:
103
+ YAML node with literal block scalar style for multiline strings.
104
+ """
104
105
  # Use literal block scalar (|) for multiline strings to preserve formatting
105
106
  if "\n" in data:
106
107
  return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
@@ -115,9 +116,7 @@ def write_yaml(file_path: Path, data: dict[str, Any]) -> None:
115
116
  data["instruction"] = LiteralString(data["instruction"])
116
117
 
117
118
  with open(file_path, "w", encoding="utf-8") as f:
118
- yaml.dump(
119
- data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
120
- )
119
+ yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
121
120
 
122
121
 
123
122
  def load_resource_from_file(file_path: Path) -> dict[str, Any]:
@@ -137,15 +136,10 @@ def load_resource_from_file(file_path: Path) -> dict[str, Any]:
137
136
  elif file_path.suffix.lower() == ".json":
138
137
  return read_json(file_path)
139
138
  else:
140
- raise ValueError(
141
- f"Unsupported file format: {file_path.suffix}. "
142
- f"Only JSON and YAML files are supported."
143
- )
139
+ raise ValueError(f"Unsupported file format: {file_path.suffix}. Only JSON and YAML files are supported.")
144
140
 
145
141
 
146
- def write_resource_export(
147
- file_path: Path, data: dict[str, Any], format: str = "json"
148
- ) -> None:
142
+ def write_resource_export(file_path: Path, data: dict[str, Any], format: str = "json") -> None:
149
143
  """Write resource export data to file.
150
144
 
151
145
  Args:
@@ -190,13 +184,8 @@ def collect_attributes_for_export(resource: Any) -> dict[str, Any]:
190
184
  data.
191
185
  """
192
186
  mapping = _coerce_resource_to_mapping(resource)
193
- if (
194
- mapping is None
195
- ): # pragma: no cover - defensive fallback when attribute introspection fails
196
- items = (
197
- (name, _safe_getattr(resource, name))
198
- for name in _iter_public_attribute_names(resource)
199
- )
187
+ if mapping is None: # pragma: no cover - defensive fallback when attribute introspection fails
188
+ items = ((name, _safe_getattr(resource, name)) for name in _iter_public_attribute_names(resource))
200
189
  else:
201
190
  items = mapping.items()
202
191
 
@@ -249,9 +238,7 @@ def _coerce_resource_to_mapping(resource: Any) -> dict[str, Any] | None:
249
238
  try:
250
239
  if hasattr(resource, "__dict__"):
251
240
  return dict(resource.__dict__)
252
- except (
253
- Exception
254
- ): # pragma: no cover - pathological objects can still defeat coercion
241
+ except Exception: # pragma: no cover - pathological objects can still defeat coercion
255
242
  return None
256
243
 
257
244
  return None
@@ -263,6 +250,11 @@ def _iter_public_attribute_names(resource: Any) -> Iterable[str]:
263
250
  names: list[str] = []
264
251
 
265
252
  def _collect(candidates: Iterable[str] | None) -> None:
253
+ """Collect unique candidate attribute names.
254
+
255
+ Args:
256
+ candidates: Iterable of candidate attribute names.
257
+ """
266
258
  for candidate in candidates or ():
267
259
  if candidate not in seen:
268
260
  seen.add(candidate)
@@ -284,9 +276,7 @@ def _iter_public_attribute_names(resource: Any) -> Iterable[str]:
284
276
  return iter(names)
285
277
 
286
278
 
287
- def _collect_from_dict(
288
- resource: Any, collect_func: Callable[[Iterable[str]], None]
289
- ) -> None:
279
+ def _collect_from_dict(resource: Any, collect_func: Callable[[Iterable[str]], None]) -> None:
290
280
  """Safely collect attribute names from __dict__."""
291
281
  try:
292
282
  if hasattr(resource, "__dict__"):
@@ -297,18 +287,14 @@ def _collect_from_dict(
297
287
  pass
298
288
 
299
289
 
300
- def _collect_from_annotations(
301
- resource: Any, collect_func: Callable[[Iterable[str]], None]
302
- ) -> None:
290
+ def _collect_from_annotations(resource: Any, collect_func: Callable[[Iterable[str]], None]) -> None:
303
291
  """Safely collect attribute names from __annotations__."""
304
292
  annotations = getattr(resource, "__annotations__", {})
305
293
  if annotations:
306
294
  collect_func(annotations.keys())
307
295
 
308
296
 
309
- def _collect_from_dir(
310
- resource: Any, collect_func: Callable[[Iterable[str]], None]
311
- ) -> None:
297
+ def _collect_from_dir(resource: Any, collect_func: Callable[[Iterable[str]], None]) -> None:
312
298
  """Safely collect attribute names from dir()."""
313
299
  try:
314
300
  collect_func(name for name in dir(resource) if not name.startswith("__"))
@@ -317,6 +303,7 @@ def _collect_from_dir(
317
303
 
318
304
 
319
305
  def _safe_getattr(resource: Any, name: str) -> Any:
306
+ """Return getattr(resource, name) but swallow any exception and return None."""
320
307
  try:
321
308
  return getattr(resource, name)
322
309
  except Exception:
@@ -324,6 +311,7 @@ def _safe_getattr(resource: Any, name: str) -> Any:
324
311
 
325
312
 
326
313
  def _should_include_attribute(key: str, value: Any) -> bool:
314
+ """Return True when an attribute should be serialized."""
327
315
  if key in _EXCLUDED_ATTRS or key in _EXCLUDED_NAMES:
328
316
  return False
329
317
  if key.startswith("_"):
@@ -387,9 +375,7 @@ def build_mcp_export_payload(
387
375
  ImportError: If required modules (auth helpers) are not available
388
376
  """
389
377
  auth_module = importlib.import_module("glaip_sdk.cli.auth")
390
- prepare_authentication_export = getattr(
391
- auth_module, "prepare_authentication_export"
392
- )
378
+ prepare_authentication_export = auth_module.prepare_authentication_export
393
379
 
394
380
  # Start with model dump (excludes None values automatically)
395
381
  payload = mcp.model_dump(exclude_none=True)
@@ -435,4 +421,4 @@ def validate_json_string(json_str: str) -> dict[str, Any]:
435
421
  try:
436
422
  return json.loads(json_str)
437
423
  except json.JSONDecodeError as e:
438
- raise ValueError(f"Invalid JSON: {e}")
424
+ raise ValueError(f"Invalid JSON: {e}") from e
@@ -0,0 +1,142 @@
1
+ """Agent and tool synchronization (create/update) operations.
2
+
3
+ This module provides convenience functions for tool classes that need bundling.
4
+
5
+ For direct upsert operations, use the client methods:
6
+ - client.agents.upsert_agent(identifier, **kwargs)
7
+ - client.tools.upsert_tool(identifier, code, **kwargs)
8
+ - client.mcps.upsert_mcp(identifier, **kwargs)
9
+
10
+ Authors:
11
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from glaip_sdk.utils.bundler import ToolBundler
19
+ from glaip_sdk.utils.import_resolver import load_class
20
+ from gllm_core.utils import LoggerManager
21
+
22
+ if TYPE_CHECKING:
23
+ from glaip_sdk.models import Agent, Tool
24
+
25
+ logger = LoggerManager().get_logger(__name__)
26
+
27
+
28
+ def _extract_tool_name(tool_class: Any) -> str:
29
+ """Extract tool name from a class, handling Pydantic v2 models."""
30
+ # Direct attribute access (works for non-Pydantic classes)
31
+ if hasattr(tool_class, "name"):
32
+ name = getattr(tool_class, "name", None)
33
+ if isinstance(name, str):
34
+ return name
35
+
36
+ # Pydantic v2 model - check model_fields
37
+ if hasattr(tool_class, "model_fields"):
38
+ model_fields = getattr(tool_class, "model_fields", {})
39
+ if "name" in model_fields:
40
+ field_info = model_fields["name"]
41
+ if hasattr(field_info, "default") and isinstance(field_info.default, str):
42
+ return field_info.default
43
+
44
+ raise ValueError(f"Cannot extract name from tool class: {tool_class}")
45
+
46
+
47
+ def _extract_tool_description(tool_class: Any) -> str:
48
+ """Extract tool description from a class, handling Pydantic v2 models."""
49
+ # Direct attribute access
50
+ if hasattr(tool_class, "description"):
51
+ desc = getattr(tool_class, "description", None)
52
+ if isinstance(desc, str):
53
+ return desc
54
+
55
+ # Pydantic v2 model - check model_fields
56
+ if hasattr(tool_class, "model_fields"):
57
+ model_fields = getattr(tool_class, "model_fields", {})
58
+ if "description" in model_fields:
59
+ field_info = model_fields["description"]
60
+ if hasattr(field_info, "default") and isinstance(field_info.default, str):
61
+ return field_info.default
62
+
63
+ return ""
64
+
65
+
66
+ def update_or_create_tool(tool_ref: Any) -> Tool:
67
+ """Create or update a tool from a tool class with bundled source code.
68
+
69
+ This function takes a tool class (LangChain BaseTool), bundles its source
70
+ code with inlined imports, and creates/updates it in the backend.
71
+
72
+ Args:
73
+ tool_ref: A tool class (LangChain BaseTool subclass) or import path string.
74
+
75
+ Returns:
76
+ The created or updated tool.
77
+
78
+ Example:
79
+ >>> from glaip_sdk.utils.sync import update_or_create_tool
80
+ >>> from my_tools import WeatherAPITool
81
+ >>> tool = update_or_create_tool(WeatherAPITool)
82
+ """
83
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
84
+
85
+ client = get_client()
86
+
87
+ # Handle string import path
88
+ if isinstance(tool_ref, str):
89
+ tool_class = load_class(tool_ref)
90
+ else:
91
+ tool_class = tool_ref
92
+
93
+ # Get tool info - handle Pydantic v2 model classes
94
+ tool_name = _extract_tool_name(tool_class)
95
+ tool_description = _extract_tool_description(tool_class)
96
+
97
+ # Bundle source code
98
+ bundler = ToolBundler(tool_class)
99
+ bundled_source = bundler.bundle()
100
+
101
+ logger.info("Tool info: name='%s', description='%s...'", tool_name, tool_description[:50])
102
+ logger.info("Bundled source code: %d characters", len(bundled_source))
103
+
104
+ # Use client's upsert method
105
+ return client.tools.upsert_tool(
106
+ tool_name,
107
+ code=bundled_source,
108
+ description=tool_description,
109
+ )
110
+
111
+
112
+ def update_or_create_agent(agent_config: dict[str, Any]) -> Agent:
113
+ """Create or update an agent from configuration.
114
+
115
+ Args:
116
+ agent_config: Agent configuration dictionary containing:
117
+ - name (str): Agent name (required)
118
+ - description (str): Agent description
119
+ - instruction (str): Agent instruction
120
+ - tools (list, optional): List of tool IDs
121
+ - agents (list, optional): List of sub-agent IDs
122
+ - metadata (dict, optional): Additional metadata
123
+
124
+ Returns:
125
+ The created or updated agent.
126
+
127
+ Example:
128
+ >>> from glaip_sdk.utils.sync import update_or_create_agent
129
+ >>> config = {
130
+ ... "name": "weather_reporter",
131
+ ... "description": "Weather reporting agent",
132
+ ... "instruction": "You are a weather reporter.",
133
+ ... }
134
+ >>> agent = update_or_create_agent(config)
135
+ """
136
+ from glaip_sdk.utils.client import get_client # noqa: PLC0415
137
+
138
+ client = get_client()
139
+ agent_name = agent_config.pop("name")
140
+
141
+ # Use client's upsert method
142
+ return client.agents.upsert_agent(agent_name, **agent_config)
@@ -0,0 +1,33 @@
1
+ """Shared utilities for tool type detection.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ """
6
+
7
+ from typing import Any
8
+
9
+
10
+ def is_langchain_tool(ref: Any) -> bool:
11
+ """Check if ref is a LangChain BaseTool class or instance.
12
+
13
+ Shared by:
14
+ - ToolRegistry._is_custom_tool() (for upload detection)
15
+ - LangChainToolAdapter._is_langchain_tool() (for adaptation)
16
+
17
+ Args:
18
+ ref: Object to check.
19
+
20
+ Returns:
21
+ True if ref is a LangChain BaseTool class or instance.
22
+ """
23
+ try:
24
+ from langchain_core.tools import BaseTool # noqa: PLC0415
25
+
26
+ if isinstance(ref, type) and issubclass(ref, BaseTool):
27
+ return True
28
+ if isinstance(ref, BaseTool):
29
+ return True
30
+ except ImportError:
31
+ pass
32
+
33
+ return False
@@ -18,6 +18,16 @@ from glaip_sdk.utils.resource_refs import validate_name_format
18
18
  RESERVED_NAMES = ["admin", "root", "system", "api", "test", "demo"]
19
19
 
20
20
 
21
+ def _validate_named_resource(name: str, resource_type: str) -> str:
22
+ """Shared validator that prevents reserved-name duplication."""
23
+ cleaned_name = validate_name_format(name, resource_type)
24
+
25
+ if cleaned_name.lower() in RESERVED_NAMES:
26
+ raise ValueError(f"{resource_type.capitalize()} name '{cleaned_name}' is reserved and cannot be used")
27
+
28
+ return cleaned_name
29
+
30
+
21
31
  def validate_agent_name(name: str) -> str:
22
32
  """Validate agent name and return cleaned version.
23
33
 
@@ -30,13 +40,7 @@ def validate_agent_name(name: str) -> str:
30
40
  Raises:
31
41
  ValueError: If name is invalid
32
42
  """
33
- cleaned_name = validate_name_format(name, "agent")
34
-
35
- # Check for reserved names
36
- if cleaned_name.lower() in RESERVED_NAMES:
37
- raise ValueError(f"'{cleaned_name}' is a reserved name and cannot be used")
38
-
39
- return cleaned_name
43
+ return _validate_named_resource(name, "agent")
40
44
 
41
45
 
42
46
  def validate_agent_instruction(instruction: str) -> str:
@@ -74,13 +78,7 @@ def validate_tool_name(name: str) -> str:
74
78
  Raises:
75
79
  ValueError: If name is invalid
76
80
  """
77
- cleaned_name = validate_name_format(name, "tool")
78
-
79
- # Check for reserved names
80
- if cleaned_name.lower() in RESERVED_NAMES:
81
- raise ValueError(f"'{cleaned_name}' is a reserved name and cannot be used")
82
-
83
- return cleaned_name
81
+ return _validate_named_resource(name, "tool")
84
82
 
85
83
 
86
84
  def validate_mcp_name(name: str) -> str:
@@ -95,13 +93,7 @@ def validate_mcp_name(name: str) -> str:
95
93
  Raises:
96
94
  ValueError: If name is invalid
97
95
  """
98
- cleaned_name = validate_name_format(name, "mcp")
99
-
100
- # Check for reserved names
101
- if cleaned_name.lower() in RESERVED_NAMES:
102
- raise ValueError(f"'{cleaned_name}' is a reserved name and cannot be used")
103
-
104
- return cleaned_name
96
+ return _validate_named_resource(name, "mcp")
105
97
 
106
98
 
107
99
  def validate_timeout(timeout: int) -> int:
@@ -155,13 +147,13 @@ def coerce_timeout(value: Any) -> int:
155
147
  try:
156
148
  fval = float(value)
157
149
  return validate_timeout(int(fval))
158
- except ValueError:
159
- raise ValueError(f"Invalid timeout value: {value}")
150
+ except ValueError as err:
151
+ raise ValueError(f"Invalid timeout value: {value}") from err
160
152
  else:
161
153
  try:
162
154
  return validate_timeout(int(value))
163
- except (TypeError, ValueError):
164
- raise ValueError(f"Invalid timeout value: {value}")
155
+ except (TypeError, ValueError) as err:
156
+ raise ValueError(f"Invalid timeout value: {value}") from err
165
157
 
166
158
 
167
159
  def validate_file_path(file_path: str | Path, must_exist: bool = True) -> Path:
@@ -213,7 +205,7 @@ def validate_directory_path(dir_path: str | Path, must_exist: bool = True) -> Pa
213
205
 
214
206
 
215
207
  def validate_url(url: str) -> str:
216
- """Validate URL format.
208
+ """Validate URL format (HTTPS only).
217
209
 
218
210
  Args:
219
211
  url: URL to validate
@@ -225,7 +217,7 @@ def validate_url(url: str) -> str:
225
217
  ValueError: If URL is invalid
226
218
  """
227
219
  url_pattern = re.compile(
228
- r"^https?://" # http:// or https://
220
+ r"^https://" # https:// only
229
221
  r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|" # domain...
230
222
  r"localhost|" # localhost...
231
223
  r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
@@ -235,7 +227,7 @@ def validate_url(url: str) -> str:
235
227
  )
236
228
 
237
229
  if not url_pattern.match(url):
238
- raise ValueError(f"Invalid URL format: {url}")
230
+ raise ValueError("API URL must start with https:// and be a valid host.")
239
231
 
240
232
  return url
241
233
 
@@ -1,25 +1,42 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: glaip-sdk
3
- Version: 0.1.0
3
+ Version: 0.6.10
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
7
7
  Author-email: raymond.christopher@gdplabs.id
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11,<3.13
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Provides-Extra: dev
14
+ Provides-Extra: memory
15
+ Provides-Extra: privacy
16
+ Requires-Dist: aip-agents-binary (>=0.5.1)
17
+ Requires-Dist: aip-agents[memory] (>=0.5.1) ; (python_version >= "3.11" and python_version < "3.13") and (extra == "memory")
18
+ Requires-Dist: aip-agents[privacy] (>=0.5.1) ; (python_version >= "3.11" and python_version < "3.13") and (extra == "privacy")
14
19
  Requires-Dist: click (>=8.2.0,<8.3.0)
20
+ Requires-Dist: gllm-core-binary (>=0.1.0)
21
+ Requires-Dist: gllm-tools-binary (>=0.1.3)
15
22
  Requires-Dist: httpx (>=0.28.1)
23
+ Requires-Dist: langchain-core (>=0.3.0)
16
24
  Requires-Dist: packaging (>=23.2)
25
+ Requires-Dist: pre-commit (>=4.3.0) ; extra == "dev"
17
26
  Requires-Dist: pydantic (>=2.0.0)
27
+ Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
28
+ Requires-Dist: pytest-asyncio (>=0.23.6) ; extra == "dev"
29
+ Requires-Dist: pytest-cov (>=4.0.0) ; extra == "dev"
30
+ Requires-Dist: pytest-dotenv (>=0.5.2) ; extra == "dev"
31
+ Requires-Dist: pytest-timeout (>=2.3.1) ; extra == "dev"
32
+ Requires-Dist: pytest-xdist (>=3.8.0) ; extra == "dev"
18
33
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
19
34
  Requires-Dist: pyyaml (>=6.0.0)
20
35
  Requires-Dist: questionary (>=2.1.0,<3.0.0)
21
36
  Requires-Dist: readchar (>=4.2.1,<5.0.0)
22
37
  Requires-Dist: rich (>=13.0.0)
38
+ Requires-Dist: ruff (>=0.14.0) ; extra == "dev"
39
+ Requires-Dist: textual (>=0.52.0)
23
40
  Description-Content-Type: text/markdown
24
41
 
25
42
  # GL AIP — GDP Labs AI Agents Package
@@ -193,3 +210,24 @@ Quick links:
193
210
  - **[MCP Integration](https://gdplabs.gitbook.io/gl-aip/gl-aip-sdk/guides/mcps-guide)**: Connect external services
194
211
  - **[API Reference](https://gdplabs.gitbook.io/gl-aip/gl-aip-sdk/reference/python-sdk-reference)**: Complete SDK reference
195
212
 
213
+ ## 🧪 Simulate the Update Notifier
214
+
215
+ Need to verify the in-session upgrade flow without hitting PyPI or actually running `pip install`? Use the bundled helper:
216
+
217
+ ```bash
218
+ cd python/glaip-sdk
219
+ poetry run python scripts/mock_update_notifier.py
220
+ # or customize the mock payload:
221
+ # poetry run python scripts/mock_update_notifier.py --version 3.3.3 --marker "[nightly build]"
222
+ ```
223
+
224
+ The script:
225
+
226
+ - Launches a SlashSession with prompt-toolkit disabled (so it runs cleanly in tests/CI).
227
+ - Forces the notifier to believe a newer version exists (`--version 9.9.9` by default).
228
+ - Appends a visible marker (default `[mock update]`) to the banner so you can prove the branding reload happened; pass `--marker ""` to skip.
229
+ - Auto-selects “Update now”, mocks the install step, and runs the real branding refresh logic.
230
+ - Resets module metadata afterwards so your environment remains untouched.
231
+
232
+ You should see the Rich banner re-render with the mocked version (and optional marker) at the end of the run.
233
+