glaip-sdk 0.7.3__py3-none-any.whl → 0.7.4__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.
@@ -0,0 +1,20 @@
1
+ """Entry point wrapper for early logging configuration.
2
+
3
+ This must be imported BEFORE glaip_sdk.cli.main to catch import-time warnings.
4
+
5
+ Authors:
6
+ Raymond Christopher (raymond.christopher@gdplabs.id)
7
+ """
8
+
9
+ import sys
10
+
11
+ # Configure logging BEFORE importing anything else
12
+ from glaip_sdk.runner.logging_config import setup_cli_logging
13
+
14
+ setup_cli_logging()
15
+
16
+ # Now import and run CLI
17
+ from glaip_sdk.cli import main # noqa: E402
18
+
19
+ if __name__ == "__main__":
20
+ sys.exit(main()) # pylint: disable=no-value-for-parameter
glaip_sdk/cli/main.py CHANGED
@@ -73,6 +73,12 @@ def _resolve_client_class() -> type[Any]:
73
73
 
74
74
  def _suppress_chatty_loggers() -> None:
75
75
  """Silence noisy SDK/httpx logs for CLI output."""
76
+ # Ensure CLI logging is configured (idempotent)
77
+ from glaip_sdk.runner.logging_config import setup_cli_logging # noqa: PLC0415
78
+
79
+ setup_cli_logging()
80
+
81
+ # Also suppress SDK-specific loggers
76
82
  noisy_loggers = [
77
83
  "glaip_sdk.client",
78
84
  "httpx",
@@ -13,10 +13,16 @@ Authors:
13
13
  GLAIP SDK Team
14
14
  """
15
15
 
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ # These don't require aip_agents, so import them directly
16
19
  from glaip_sdk.hitl.base import HITLCallback, HITLDecision, HITLRequest, HITLResponse
17
- from glaip_sdk.hitl.local import LocalPromptHandler, PauseResumeCallback
20
+ from glaip_sdk.hitl.callback import PauseResumeCallback
18
21
  from glaip_sdk.hitl.remote import RemoteHITLHandler
19
22
 
23
+ if TYPE_CHECKING:
24
+ from glaip_sdk.hitl.local import LocalPromptHandler
25
+
20
26
  __all__ = [
21
27
  "LocalPromptHandler",
22
28
  "PauseResumeCallback",
@@ -26,3 +32,17 @@ __all__ = [
26
32
  "HITLResponse",
27
33
  "RemoteHITLHandler",
28
34
  ]
35
+
36
+
37
+ def __getattr__(name: str) -> Any: # noqa: ANN401
38
+ """Lazy import for LocalPromptHandler.
39
+
40
+ This defers the import of aip_agents until LocalPromptHandler is actually accessed,
41
+ preventing ImportError when aip-agents is not installed but HITL is not being used.
42
+ """
43
+ if name == "LocalPromptHandler":
44
+ from glaip_sdk.hitl.local import LocalPromptHandler # noqa: PLC0415
45
+
46
+ globals()["LocalPromptHandler"] = LocalPromptHandler
47
+ return LocalPromptHandler
48
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,43 @@
1
+ """Pause/resume callback for HITL renderer control.
2
+
3
+ This module provides PauseResumeCallback which allows HITL prompt handlers
4
+ to control the live renderer without directly coupling to the renderer implementation.
5
+
6
+ Author:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ from typing import Any
11
+
12
+
13
+ class PauseResumeCallback:
14
+ """Simple callback object for pausing/resuming the live renderer.
15
+
16
+ This allows the LocalPromptHandler to control the renderer without
17
+ directly coupling to the renderer implementation.
18
+ """
19
+
20
+ def __init__(self) -> None:
21
+ """Initialize the callback."""
22
+ self._renderer: Any | None = None
23
+
24
+ def set_renderer(self, renderer: Any) -> None:
25
+ """Set the renderer instance.
26
+
27
+ Args:
28
+ renderer: RichStreamRenderer instance with pause_live() and resume_live() methods.
29
+ """
30
+ self._renderer = renderer
31
+
32
+ def pause(self) -> None:
33
+ """Pause the live renderer before prompting."""
34
+ if self._renderer and hasattr(self._renderer, "_shutdown_live"):
35
+ self._renderer._shutdown_live()
36
+
37
+ def resume(self) -> None:
38
+ """Resume the live renderer after prompting."""
39
+ if self._renderer and hasattr(self._renderer, "_ensure_live"):
40
+ self._renderer._ensure_live()
41
+
42
+
43
+ __all__ = ["PauseResumeCallback"]
glaip_sdk/hitl/local.py CHANGED
@@ -118,34 +118,4 @@ class LocalPromptHandler(BasePromptHandler):
118
118
  self._console.print(f"[dim]Context: {request.context}[/dim]")
119
119
 
120
120
 
121
- class PauseResumeCallback:
122
- """Simple callback object for pausing/resuming the live renderer.
123
-
124
- This allows the LocalPromptHandler to control the renderer without
125
- directly coupling to the renderer implementation.
126
- """
127
-
128
- def __init__(self) -> None:
129
- """Initialize the callback."""
130
- self._renderer: Any | None = None
131
-
132
- def set_renderer(self, renderer: Any) -> None:
133
- """Set the renderer instance.
134
-
135
- Args:
136
- renderer: RichStreamRenderer instance with pause_live() and resume_live() methods.
137
- """
138
- self._renderer = renderer
139
-
140
- def pause(self) -> None:
141
- """Pause the live renderer before prompting."""
142
- if self._renderer and hasattr(self._renderer, "_shutdown_live"):
143
- self._renderer._shutdown_live()
144
-
145
- def resume(self) -> None:
146
- """Resume the live renderer after prompting."""
147
- if self._renderer and hasattr(self._renderer, "_ensure_live"):
148
- self._renderer._ensure_live()
149
-
150
-
151
- __all__ = ["LocalPromptHandler", "PauseResumeCallback"]
121
+ __all__ = ["LocalPromptHandler"]
@@ -19,18 +19,19 @@ Example:
19
19
  >>> result = runner.run(agent, "Hello!")
20
20
  """
21
21
 
22
+ from typing import TYPE_CHECKING, Any
23
+
22
24
  from glaip_sdk.runner.deps import (
23
25
  LOCAL_RUNTIME_AVAILABLE,
24
26
  check_local_runtime_available,
25
27
  get_local_runtime_missing_message,
26
28
  )
27
- from glaip_sdk.runner.langgraph import LangGraphRunner
28
29
 
29
30
  # Default runner instance
30
- _default_runner: LangGraphRunner | None = None
31
+ _default_runner: Any | None = None
31
32
 
32
33
 
33
- def get_default_runner() -> LangGraphRunner:
34
+ def get_default_runner() -> Any:
34
35
  """Get the default runner instance for local agent execution.
35
36
 
36
37
  Returns:
@@ -45,11 +46,17 @@ def get_default_runner() -> LangGraphRunner:
45
46
  raise RuntimeError(get_local_runtime_missing_message())
46
47
 
47
48
  if _default_runner is None:
49
+ # Lazy import to avoid requiring aip-agents when runner is not used
50
+ from glaip_sdk.runner.langgraph import LangGraphRunner # noqa: PLC0415
51
+
48
52
  _default_runner = LangGraphRunner()
49
53
 
50
54
  return _default_runner
51
55
 
52
56
 
57
+ if TYPE_CHECKING:
58
+ from glaip_sdk.runner.langgraph import LangGraphRunner
59
+
53
60
  __all__ = [
54
61
  "LOCAL_RUNTIME_AVAILABLE",
55
62
  "LangGraphRunner",
@@ -57,3 +64,13 @@ __all__ = [
57
64
  "get_default_runner",
58
65
  "get_local_runtime_missing_message",
59
66
  ]
67
+
68
+
69
+ def __getattr__(name: str) -> Any:
70
+ """Lazy import for LangGraphRunner to avoid requiring aip-agents when not used."""
71
+ if name == "LangGraphRunner":
72
+ from glaip_sdk.runner.langgraph import LangGraphRunner # noqa: PLC0415
73
+
74
+ globals()["LangGraphRunner"] = LangGraphRunner
75
+ return LangGraphRunner
76
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -23,11 +23,10 @@ import logging
23
23
  from dataclasses import dataclass
24
24
  from typing import TYPE_CHECKING, Any
25
25
 
26
- from aip_agents.agent.hitl.manager import ApprovalManager # noqa: PLC0415
27
26
  from gllm_core.utils import LoggerManager
28
27
 
29
28
  from glaip_sdk.client.run_rendering import AgentRunRenderingManager
30
- from glaip_sdk.hitl import LocalPromptHandler, PauseResumeCallback
29
+ from glaip_sdk.hitl import PauseResumeCallback
31
30
  from glaip_sdk.runner.base import BaseRunner
32
31
  from glaip_sdk.runner.deps import (
33
32
  check_local_runtime_available,
@@ -434,6 +433,9 @@ class LangGraphRunner(BaseRunner):
434
433
  hitl_enabled = merged_agent_config.get("hitl_enabled", False)
435
434
  if hitl_enabled:
436
435
  try:
436
+ from aip_agents.agent.hitl.manager import ApprovalManager # noqa: PLC0415
437
+ from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
438
+
437
439
  local_agent.hitl_manager = ApprovalManager(
438
440
  prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
439
441
  )
@@ -0,0 +1,77 @@
1
+ """Logging configuration for CLI to suppress noisy dependency warnings.
2
+
3
+ This module provides centralized logging suppression for optional dependencies
4
+ that emit noisy warnings during CLI usage. Warnings are suppressed by default
5
+ but can be shown using GLAIP_LOG_LEVEL=DEBUG.
6
+
7
+ Authors:
8
+ Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import warnings
14
+
15
+ NOISY_LOGGERS = ["transformers", "gllm_privacy", "google.cloud.aiplatform"]
16
+
17
+
18
+ class NameFilter(logging.Filter):
19
+ """Filter logs by logger name prefix."""
20
+
21
+ def __init__(self, prefixes: list[str]) -> None:
22
+ """Initialize filter with logger name prefixes to suppress.
23
+
24
+ Args:
25
+ prefixes: List of logger name prefixes to filter out.
26
+ """
27
+ super().__init__()
28
+ self.prefixes = prefixes
29
+
30
+ def filter(self, record: logging.LogRecord) -> bool:
31
+ """Filter log records by name prefix.
32
+
33
+ Args:
34
+ record: Log record to filter.
35
+
36
+ Returns:
37
+ False if record should be suppressed, True otherwise.
38
+ """
39
+ return not any(record.name.startswith(p) for p in self.prefixes)
40
+
41
+
42
+ def setup_cli_logging() -> None:
43
+ """Suppress INFO from noisy third-party libraries.
44
+
45
+ Use GLAIP_LOG_LEVEL=DEBUG to see all warnings.
46
+ This function is idempotent - calling it multiple times is safe.
47
+ """
48
+ # Check env level FIRST before any suppression
49
+ env_level = os.getenv("GLAIP_LOG_LEVEL", "").upper()
50
+ is_debug = env_level == "DEBUG"
51
+
52
+ if is_debug:
53
+ # Debug mode: show everything, no suppression
54
+ if env_level and hasattr(logging, env_level):
55
+ logging.basicConfig(level=getattr(logging, env_level))
56
+ return
57
+
58
+ # Default mode: suppress noisy warnings
59
+ if env_level and hasattr(logging, env_level):
60
+ logging.basicConfig(level=getattr(logging, env_level))
61
+
62
+ # Add handler filter to suppress by name prefix (handles child loggers)
63
+ # Check if filter already exists to ensure idempotency
64
+ root_logger = logging.getLogger()
65
+ has_name_filter = any(isinstance(f, NameFilter) for h in root_logger.handlers for f in h.filters)
66
+
67
+ if not has_name_filter:
68
+ handler = logging.StreamHandler()
69
+ handler.addFilter(NameFilter(NOISY_LOGGERS))
70
+ root_logger.addHandler(handler)
71
+
72
+ # Suppress FutureWarning for GCS (idempotent - multiple calls are safe)
73
+ warnings.filterwarnings(
74
+ "ignore",
75
+ category=FutureWarning,
76
+ message=r".*google-cloud-storage.*",
77
+ )
@@ -14,6 +14,7 @@ import inspect
14
14
  from pathlib import Path
15
15
 
16
16
  from glaip_sdk.utils.import_resolver import ImportResolver
17
+ from glaip_sdk.utils.tool_detection import is_tool_plugin_decorator
17
18
 
18
19
 
19
20
  class ToolBundler:
@@ -50,9 +51,14 @@ class ToolBundler:
50
51
  self.tool_dir = self.tool_file.parent
51
52
  self._import_resolver = ImportResolver(self.tool_dir)
52
53
 
53
- def bundle(self) -> str:
54
+ def bundle(self, add_tool_plugin_decorator: bool = True) -> str:
54
55
  """Bundle tool source code with inlined local imports.
55
56
 
57
+ Args:
58
+ add_tool_plugin_decorator: If True, add @tool_plugin decorator to BaseTool classes.
59
+ Set to False for newer servers (0.1.85+) where decorator is optional.
60
+ Defaults to True for backward compatibility with older servers.
61
+
56
62
  Returns:
57
63
  Bundled source code with all local dependencies inlined.
58
64
  """
@@ -62,6 +68,16 @@ class ToolBundler:
62
68
  tree = ast.parse(full_source)
63
69
  local_imports, external_imports = self._import_resolver.categorize_imports(tree)
64
70
 
71
+ # NOTE: The @tool_plugin decorator is REQUIRED by older servers (< 0.1.85) for remote execution.
72
+ # Newer servers (0.1.85+) make the decorator optional.
73
+ # The server validates uploaded tool code and will reject tools without the decorator
74
+ # with error: "No classes found with @tool_plugin decorator".
75
+ # See: docs/resources/reference/schemas/tools.md - "Plugin Requirements"
76
+ # TESTED: Commenting out this decorator addition causes HTTP 400 ValidationError from older servers.
77
+ # We try without decorator first (for new servers), then retry with decorator if validation fails.
78
+ if add_tool_plugin_decorator:
79
+ self._add_tool_plugin_decorator(tree)
80
+
65
81
  # Extract main code nodes (excluding imports, docstrings, glaip_sdk.Tool subclasses)
66
82
  main_code_nodes = self._extract_main_code_nodes(tree)
67
83
 
@@ -71,6 +87,13 @@ class ToolBundler:
71
87
  # Merge all external imports
72
88
  all_external_imports = external_imports + inlined_external_imports
73
89
 
90
+ # NOTE: The gllm_plugin.tools import is REQUIRED when decorator is added.
91
+ # Without this import, the decorator will cause a NameError when the server executes the code.
92
+ # TESTED: Commenting out this import causes NameError when server tries to use the decorator.
93
+ # This import is added automatically during bundling so source files can remain clean.
94
+ if add_tool_plugin_decorator:
95
+ self._ensure_tool_plugin_import(all_external_imports)
96
+
74
97
  # Build bundled code
75
98
  bundled_code = ["# Bundled tool with inlined local imports\n"]
76
99
  bundled_code.extend(self._import_resolver.format_external_imports(all_external_imports))
@@ -109,6 +132,103 @@ class ToolBundler:
109
132
  main_code_nodes.append(ast.unparse(node))
110
133
  return main_code_nodes
111
134
 
135
+ @staticmethod
136
+ def _add_tool_plugin_decorator(tree: ast.AST) -> None:
137
+ """Add @tool_plugin decorator to BaseTool classes that don't have it.
138
+
139
+ This allows tools to be clean (without decorator) for local use,
140
+ while the decorator is automatically added during bundling for remote execution.
141
+
142
+ Args:
143
+ tree: AST tree to modify in-place.
144
+ """
145
+ for node in ast.walk(tree):
146
+ if not isinstance(node, ast.ClassDef):
147
+ continue
148
+
149
+ if not ToolBundler._inherits_from_base_tool(node):
150
+ continue
151
+
152
+ if ToolBundler._has_tool_plugin_decorator(node):
153
+ continue
154
+
155
+ decorator_call = ToolBundler._create_tool_plugin_decorator()
156
+ node.decorator_list.insert(0, decorator_call)
157
+
158
+ @staticmethod
159
+ def _inherits_from_base_tool(class_node: ast.ClassDef) -> bool:
160
+ """Check if a class inherits from BaseTool.
161
+
162
+ Args:
163
+ class_node: AST ClassDef node to check.
164
+
165
+ Returns:
166
+ True if class inherits from BaseTool.
167
+ """
168
+ for base in class_node.bases:
169
+ if isinstance(base, ast.Name) and base.id == "BaseTool":
170
+ return True
171
+ if isinstance(base, ast.Attribute) and base.attr == "BaseTool":
172
+ # Handle nested attributes like langchain_core.tools.BaseTool
173
+ # Check if the value chain leads to langchain_core
174
+ value = base.value
175
+ while isinstance(value, ast.Attribute):
176
+ value = value.value
177
+ if isinstance(value, ast.Name) and value.id == "langchain_core":
178
+ return True
179
+ return False
180
+
181
+ @staticmethod
182
+ def _has_tool_plugin_decorator(class_node: ast.ClassDef) -> bool:
183
+ """Check if a class already has the @tool_plugin decorator.
184
+
185
+ Args:
186
+ class_node: AST ClassDef node to check.
187
+
188
+ Returns:
189
+ True if decorator already exists.
190
+ """
191
+ for decorator in class_node.decorator_list:
192
+ if is_tool_plugin_decorator(decorator):
193
+ return True
194
+ return False
195
+
196
+ @staticmethod
197
+ def _create_tool_plugin_decorator() -> ast.Call:
198
+ """Create a @tool_plugin decorator AST node.
199
+
200
+ Returns:
201
+ AST Call node representing @tool_plugin(version="1.0.0").
202
+ """
203
+ return ast.Call(
204
+ func=ast.Name(id="tool_plugin", ctx=ast.Load()),
205
+ args=[],
206
+ keywords=[ast.keyword(arg="version", value=ast.Constant(value="1.0.0"))],
207
+ )
208
+
209
+ @staticmethod
210
+ def _ensure_tool_plugin_import(external_imports: list) -> None:
211
+ """Ensure gllm_plugin.tools import is present in external imports.
212
+
213
+ Args:
214
+ external_imports: List of external import nodes (modified in-place).
215
+ """
216
+ # Check if import already exists
217
+ for import_node in external_imports:
218
+ if isinstance(import_node, ast.ImportFrom) and import_node.module == "gllm_plugin.tools":
219
+ # Check if tool_plugin is in the names
220
+ for alias in import_node.names:
221
+ if alias.name == "tool_plugin":
222
+ return # Import already present
223
+
224
+ # Add the import
225
+ import_node = ast.ImportFrom(
226
+ module="gllm_plugin.tools",
227
+ names=[ast.alias(name="tool_plugin")],
228
+ level=0,
229
+ )
230
+ external_imports.append(import_node)
231
+
112
232
  @staticmethod
113
233
  def _is_sdk_tool_subclass(node: ast.ClassDef) -> bool:
114
234
  """Check if AST class definition inherits from Tool.
@@ -135,7 +255,7 @@ class ToolBundler:
135
255
  return False
136
256
 
137
257
  @classmethod
138
- def bundle_from_source(cls, file_path: Path) -> tuple[str, str, str]:
258
+ def bundle_from_source(cls, file_path: Path, add_tool_plugin_decorator: bool = True) -> tuple[str, str, str]:
139
259
  """Extract tool info directly from source file without importing.
140
260
 
141
261
  This is used as a fallback when the tool class cannot be imported
@@ -143,6 +263,9 @@ class ToolBundler:
143
263
 
144
264
  Args:
145
265
  file_path: Path to the tool source file.
266
+ add_tool_plugin_decorator: If True, add @tool_plugin decorator to BaseTool classes.
267
+ Set to False for newer servers (0.1.85+) where decorator is optional.
268
+ Defaults to True for backward compatibility with older servers.
146
269
 
147
270
  Returns:
148
271
  Tuple of (name, description, bundled_source_code).
@@ -160,6 +283,12 @@ class ToolBundler:
160
283
  tool_dir = file_path.parent
161
284
  import_resolver = ImportResolver(tool_dir)
162
285
 
286
+ # NOTE: The @tool_plugin decorator is REQUIRED by older servers (< 0.1.85) for remote execution.
287
+ # Newer servers (0.1.85+) make the decorator optional.
288
+ # See bundle() method for detailed explanation.
289
+ if add_tool_plugin_decorator:
290
+ cls._add_tool_plugin_decorator(tree)
291
+
163
292
  # Find tool name and description from class definitions
164
293
  tool_name, tool_description = cls._extract_tool_metadata(tree, file_path.stem)
165
294
 
@@ -180,6 +309,13 @@ class ToolBundler:
180
309
 
181
310
  # Build bundled code
182
311
  all_external_imports = external_imports + inlined_external_imports
312
+
313
+ # NOTE: The gllm_plugin.tools import is REQUIRED when decorator is added.
314
+ # See bundle() method for detailed explanation.
315
+ # TESTED: Commenting out this import causes NameError when server tries to use the decorator.
316
+ if add_tool_plugin_decorator:
317
+ cls._ensure_tool_plugin_import(all_external_imports)
318
+
183
319
  bundled_code = ["# Bundled tool with inlined local imports\n"]
184
320
  bundled_code.extend(import_resolver.format_external_imports(all_external_imports))
185
321
 
@@ -13,6 +13,8 @@ import ast
13
13
  import importlib
14
14
  from pathlib import Path
15
15
 
16
+ from glaip_sdk.utils.tool_detection import is_tool_plugin_decorator
17
+
16
18
 
17
19
  class ImportResolver:
18
20
  """Resolves and categorizes Python imports for tool bundling.
@@ -482,15 +484,7 @@ class ImportResolver:
482
484
  Returns:
483
485
  True if decorator is @tool_plugin.
484
486
  """
485
- if isinstance(decorator, ast.Name) and decorator.id == "tool_plugin":
486
- return True
487
- if (
488
- isinstance(decorator, ast.Call)
489
- and isinstance(decorator.func, ast.Name)
490
- and decorator.func.id == "tool_plugin"
491
- ):
492
- return True
493
- return False
487
+ return is_tool_plugin_decorator(decorator)
494
488
 
495
489
  @staticmethod
496
490
  def _filter_bases(bases: list) -> list:
glaip_sdk/utils/sync.py CHANGED
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
+ from glaip_sdk.exceptions import ValidationError
18
19
  from glaip_sdk.utils.bundler import ToolBundler
19
20
  from glaip_sdk.utils.import_resolver import load_class
20
21
  from gllm_core.utils import LoggerManager
@@ -94,19 +95,38 @@ def update_or_create_tool(tool_ref: Any) -> Tool:
94
95
  tool_name = _extract_tool_name(tool_class)
95
96
  tool_description = _extract_tool_description(tool_class)
96
97
 
97
- # Bundle source code
98
+ # Bundle source code - try without decorator first (for newer servers 0.1.85+)
99
+ # If validation fails, retry with decorator for older servers (< 0.1.85)
98
100
  bundler = ToolBundler(tool_class)
99
- bundled_source = bundler.bundle()
100
101
 
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
- )
102
+ try:
103
+ # Try without decorator first (for newer servers where it's optional)
104
+ bundled_source = bundler.bundle(add_tool_plugin_decorator=False)
105
+ logger.info("Tool info: name='%s', description='%s...'", tool_name, tool_description[:50])
106
+ logger.info("Bundled source code (without decorator): %d characters", len(bundled_source))
107
+
108
+ # Attempt upload without decorator
109
+ return client.tools.upsert_tool(
110
+ tool_name,
111
+ code=bundled_source,
112
+ description=tool_description,
113
+ )
114
+ except ValidationError as e:
115
+ # Check if error is about missing @tool_plugin decorator
116
+ error_message = str(e).lower()
117
+ if "@tool_plugin decorator" in error_message or "no classes found" in error_message:
118
+ # Retry with decorator for older servers (< 0.1.85)
119
+ logger.info("Server requires @tool_plugin decorator, retrying with decorator added")
120
+ bundled_source = bundler.bundle(add_tool_plugin_decorator=True)
121
+ logger.info("Bundled source code (with decorator): %d characters", len(bundled_source))
122
+
123
+ return client.tools.upsert_tool(
124
+ tool_name,
125
+ code=bundled_source,
126
+ description=tool_description,
127
+ )
128
+ # Re-raise if it's a different validation error
129
+ raise
110
130
 
111
131
 
112
132
  def update_or_create_agent(agent_config: dict[str, Any]) -> Agent:
@@ -4,6 +4,7 @@ Authors:
4
4
  Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
5
  """
6
6
 
7
+ import ast
7
8
  from typing import Any
8
9
 
9
10
 
@@ -31,3 +32,23 @@ def is_langchain_tool(ref: Any) -> bool:
31
32
  pass
32
33
 
33
34
  return False
35
+
36
+
37
+ def is_tool_plugin_decorator(decorator: ast.expr) -> bool:
38
+ """Check if an AST decorator node is @tool_plugin.
39
+
40
+ Shared by:
41
+ - ToolBundler._has_tool_plugin_decorator() (for bundling)
42
+ - ImportResolver._is_tool_plugin_decorator() (for import resolution)
43
+
44
+ Args:
45
+ decorator: AST decorator expression node to check.
46
+
47
+ Returns:
48
+ True if decorator is @tool_plugin.
49
+ """
50
+ if isinstance(decorator, ast.Name) and decorator.id == "tool_plugin":
51
+ return True
52
+ if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name) and decorator.func.id == "tool_plugin":
53
+ return True
54
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glaip-sdk
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Python SDK and CLI for GL AIP (GDP Labs AI Agent Package) - Build, run, and manage AI agents
5
5
  Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
6
6
  License: MIT
@@ -14,9 +14,10 @@ glaip_sdk/cli/config.py,sha256=s0_xBB1e5YE4I_Wc4q-ayY3dwsBU1JrHAF-8ySlim7Y,3040
14
14
  glaip_sdk/cli/constants.py,sha256=zqcVtzfj6huW97gbCmhkFqntge1H-c1vnkGqTazADgU,895
15
15
  glaip_sdk/cli/context.py,sha256=--Y5vc6lgoAV7cRoUAr9UxSQaLmkMg29FolA7EwoRqM,3803
16
16
  glaip_sdk/cli/display.py,sha256=ojgWdGeD5KUnGOmWNqqK4JP-1EaWHWX--DWze3BmIz0,12137
17
+ glaip_sdk/cli/entrypoint.py,sha256=ODrNZT1c7mFtNuXn4CrJgs06-xIhrqUMi1rKzkYJ21c,516
17
18
  glaip_sdk/cli/hints.py,sha256=ca4krG103IS43s5BSLr0-N7uRMpte1_LY4nAXVvgDxo,1596
18
19
  glaip_sdk/cli/io.py,sha256=ChP6CRKbtuENsNomNEaMDfPDU0iqO-WuVvl4_y7F2io,3871
19
- glaip_sdk/cli/main.py,sha256=mSaiIRl9UDcJ5LvahHo3gHioZxd1owaKVrFFSFiOfqs,24478
20
+ glaip_sdk/cli/main.py,sha256=bi_SBrRWWMcdbl28zbNMrzp8i5SKCKTb2CWN-hLIx94,24680
20
21
  glaip_sdk/cli/masking.py,sha256=2lrXQ-pfL7N-vNEQRT1s4Xq3JPDPDT8RC61OdaTtkkc,4060
21
22
  glaip_sdk/cli/mcp_validators.py,sha256=cwbz7p_p7_9xVuuF96OBQOdmEgo5UObU6iWWQ2X03PI,10047
22
23
  glaip_sdk/cli/pager.py,sha256=TmiMDNpUMuZju7QJ6A_ITqIoEf8Dhv8U6mTXx2Fga1k,7935
@@ -116,9 +117,10 @@ glaip_sdk/client/payloads/agent/__init__.py,sha256=gItEH2zt2secVq6n60oGA-ztdE5mc
116
117
  glaip_sdk/client/payloads/agent/requests.py,sha256=5FuGEuypaEXlWBhB07JrDca_ecLg4bvo8mjyFBxAV9U,17139
117
118
  glaip_sdk/client/payloads/agent/responses.py,sha256=1eRMI4JAIGqTB5zY_7D9ILQDRHPXR06U7JqHSmRp3Qs,1243
118
119
  glaip_sdk/config/constants.py,sha256=Y03c6op0e7K0jTQ8bmWXhWAqsnjWxkAhWniq8Z0iEKY,1081
119
- glaip_sdk/hitl/__init__.py,sha256=PHayqIUL9BOh9QZQilisThisPL3G7ylG-XaPdllC_zc,851
120
+ glaip_sdk/hitl/__init__.py,sha256=hi_SwW1oBimNnSFPo9Yc-mZWVPzpytlnDWNq2h1_fPo,1572
120
121
  glaip_sdk/hitl/base.py,sha256=EUN2igzydlYZ6_qmHU46Gyk3Bk9uyalZkCJ06XMRKJ8,1484
121
- glaip_sdk/hitl/local.py,sha256=rzmaRK15BxgRX7cmklUcGQUotMYg8x2Gd9BWf39k6hw,5661
122
+ glaip_sdk/hitl/callback.py,sha256=icKxxa_f8lxFQuXrZVoTt6baWivFL4a4YioWG_U_8k8,1336
123
+ glaip_sdk/hitl/local.py,sha256=7Qf-O62YcVXpOHdckm1-g4wwvHQCvwg4D1ikK-xwgqA,4642
122
124
  glaip_sdk/hitl/remote.py,sha256=cdO-wWwRGdyb0HYNMwIvHfvKwOqhqp-l7efnaC9b85M,18914
123
125
  glaip_sdk/mcps/__init__.py,sha256=4jYrt8K__oxrxexHRcmnRBXt-W_tbJN61H9Kf2lVh4Q,551
124
126
  glaip_sdk/mcps/base.py,sha256=jWwHjDF67_mtDGRp9p5SolANjVeB8jt1PSwPBtX876M,11654
@@ -136,10 +138,11 @@ glaip_sdk/registry/agent.py,sha256=F0axW4BIUODqnttIOzxnoS5AqQkLZ1i48FTeZNnYkhA,5
136
138
  glaip_sdk/registry/base.py,sha256=0x2ZBhiERGUcf9mQeWlksSYs5TxDG6FxBYQToYZa5D4,4143
137
139
  glaip_sdk/registry/mcp.py,sha256=kNJmiijIbZL9Btx5o2tFtbaT-WG6O4Xf_nl3wz356Ow,7978
138
140
  glaip_sdk/registry/tool.py,sha256=QnbAlk09lYvEb9PEdCsvpg4CGxlLbvvFWBS8WkM1ZoM,12955
139
- glaip_sdk/runner/__init__.py,sha256=8RrngoGfpF8x9X27RPdX4gJjch75ZvhtVt_6UV0ULLQ,1615
141
+ glaip_sdk/runner/__init__.py,sha256=orJ3nLR9P-n1qMaAMWZ_xRS4368YnDpdltg-bX5BlUk,2210
140
142
  glaip_sdk/runner/base.py,sha256=KIjcSAyDCP9_mn2H4rXR5gu1FZlwD9pe0gkTBmr6Yi4,2663
141
143
  glaip_sdk/runner/deps.py,sha256=Du3hr2R5RHOYCRAv7RVmx661x-ayVXIeZ8JD7ODirTA,3884
142
- glaip_sdk/runner/langgraph.py,sha256=-3BMJRww3S3dboS3uyR3QrxV-3p-1i2X5ObxdTTGRdg,32955
144
+ glaip_sdk/runner/langgraph.py,sha256=HWzEkmkQqvAOKat3lENVf0zDaypj7HMeny5thDyprWY,33031
145
+ glaip_sdk/runner/logging_config.py,sha256=OrQgW23t42qQRqEXKH8U4bFg4JG5EEkUJTlbvtU65iE,2528
143
146
  glaip_sdk/runner/mcp_adapter/__init__.py,sha256=Rdttfg3N6kg3-DaTCKqaGXKByZyBt0Mwf6FV8s_5kI8,462
144
147
  glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py,sha256=ic56fKgb3zgVZZQm3ClWUZi7pE1t4EVq8mOg6AM6hdA,1374
145
148
  glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py,sha256=b58GuadPz7q7aXoJyTYs0eeJ_oqp-wLR1tcr_5cbV1s,9723
@@ -153,7 +156,7 @@ glaip_sdk/tools/__init__.py,sha256=rhGzEqQFCzeMrxmikBuNrMz4PyYczwic28boDKVmoHs,5
153
156
  glaip_sdk/tools/base.py,sha256=hkz2NZFHW1PqsRiXh3kKTVLIjMPR274Uwml944vH5tg,16325
154
157
  glaip_sdk/utils/__init__.py,sha256=ntohV7cxlY2Yksi2nFuFm_Mg2XVJbBbSJVRej7Mi9YE,2770
155
158
  glaip_sdk/utils/agent_config.py,sha256=RhcHsSOVwOaSC2ggnPuHn36Aa0keGJhs8KGb2InvzRk,7262
156
- glaip_sdk/utils/bundler.py,sha256=fQWAv0e5qNjF1Lah-FGoA9W5Q59YaHbQfX_4ZB84YRw,9120
159
+ glaip_sdk/utils/bundler.py,sha256=fLumFj1MqqqGA1Mwn05v_cEKPALv3rIPEMvaURpxZ80,15171
157
160
  glaip_sdk/utils/client.py,sha256=otPUOIDvLCCsvFBNR8YMZFtRrORggmvvlFjl3YeeTqQ,3121
158
161
  glaip_sdk/utils/client_utils.py,sha256=hzHxxNuM37mK4HhgIdS0qg4AqjAA5ai2irPO6Nr1Uzo,15350
159
162
  glaip_sdk/utils/datetime_helpers.py,sha256=QLknNLEAY56628-MTRKnCXAffATkF33erOqBubKmU98,1544
@@ -162,14 +165,14 @@ glaip_sdk/utils/display.py,sha256=zu3SYqxj9hPyEN8G1vIXv_yXBkV8jLLCXEg2rs8NlzM,44
162
165
  glaip_sdk/utils/export.py,sha256=1NxxE3wGsA1auzecG5oJw5ELB4VmPljoeIkGhrGOh1I,5006
163
166
  glaip_sdk/utils/general.py,sha256=3HSVIopUsIymPaim-kP2lqLX75TkkdIVLe6g3UKabZ0,1507
164
167
  glaip_sdk/utils/import_export.py,sha256=RCvoydm_6_L7_J1igcE6IYDunqgS5mQUbWT4VGrytMw,5510
165
- glaip_sdk/utils/import_resolver.py,sha256=x--2oo_QXqmESd8gdYzYD3gGIWOkC-IgfTziA300r5w,17358
168
+ glaip_sdk/utils/import_resolver.py,sha256=X2qUV4_XmwStccGjnQ0YcxXAFyxZzwaKpfxjAW4Ev2o,17159
166
169
  glaip_sdk/utils/instructions.py,sha256=MTk93lsq3I8aRnvnRMSXXNMzcpnaIM_Pm3Aiiiq3GBc,2997
167
170
  glaip_sdk/utils/resource_refs.py,sha256=vF34kyAtFBLnaKnQVrsr2st1JiSxVbIZ4yq0DelJvCI,5966
168
171
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
169
172
  glaip_sdk/utils/runtime_config.py,sha256=Gl9-CQ4lYZ39vRSgtdfcSU3CXshVDDuTOdSzjvsCgG0,14070
170
173
  glaip_sdk/utils/serialization.py,sha256=z-qpvWLSBrGK3wbUclcA1UIKLXJedTnMSwPdq-FF4lo,13308
171
- glaip_sdk/utils/sync.py,sha256=3VKqs1UfNGWSobgRXohBKP7mMMzdUW3SU0bJQ1uxOgw,4872
172
- glaip_sdk/utils/tool_detection.py,sha256=g410GNug_PhLye8rd9UU-LVFIKq3jHPbmSItEkLxPTc,807
174
+ glaip_sdk/utils/sync.py,sha256=71egWp5qm_8tYpWZyGazvnP4NnyW16rcmzjGVicmQEE,6043
175
+ glaip_sdk/utils/tool_detection.py,sha256=6dHp0naLnvY6jwy_38k4kyTgQnizgcsq9hpeLSjAmcc,1471
173
176
  glaip_sdk/utils/tool_storage_provider.py,sha256=lampwUeWu4Uy8nBG7C4ZT-M6AHoWZS0m67HdLx21VDg,5396
174
177
  glaip_sdk/utils/validation.py,sha256=hB_k3lvHdIFUiSwHStrC0Eqnhx0OG2UvwqASeem0HuQ,6859
175
178
  glaip_sdk/utils/a2a/__init__.py,sha256=_X8AvDOsHeppo5n7rP5TeisVxlAdkZDTFReBk_9lmxo,876
@@ -203,8 +206,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
203
206
  glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
204
207
  glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
205
208
  glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
206
- glaip_sdk-0.7.3.dist-info/METADATA,sha256=9tLJ5ibX4VkSGKji1SulcZiJ58d6Jefg8ts_UaLdU_E,8365
207
- glaip_sdk-0.7.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
208
- glaip_sdk-0.7.3.dist-info/entry_points.txt,sha256=65vNPUggyYnVGhuw7RhNJ8Fp2jygTcX0yxJBcBY3iLU,48
209
- glaip_sdk-0.7.3.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
210
- glaip_sdk-0.7.3.dist-info/RECORD,,
209
+ glaip_sdk-0.7.4.dist-info/METADATA,sha256=r36rVJdxsiV-fUHo3SRNf_pakOeHayQy1KyL3ZCqN-M,8365
210
+ glaip_sdk-0.7.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
211
+ glaip_sdk-0.7.4.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
212
+ glaip_sdk-0.7.4.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
213
+ glaip_sdk-0.7.4.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aip = glaip_sdk.cli.entrypoint:main
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- aip = glaip_sdk.cli.main:main