glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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.
- glaip_sdk/__init__.py +44 -4
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1250 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +271 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +734 -143
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +14 -12
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/transcripts_original.py +756 -0
- glaip_sdk/cli/commands/update.py +164 -23
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +344 -167
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +15 -22
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +5 -10
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +827 -232
- glaip_sdk/cli/slash/tui/__init__.py +34 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -329
- glaip_sdk/cli/update_notifier.py +385 -24
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +370 -100
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -10
- glaip_sdk/client/mcps.py +166 -27
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +583 -79
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +214 -56
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/icons.py +9 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +107 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +872 -0
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +468 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +524 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +38 -23
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +18 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +534 -882
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1412
- glaip_sdk/cli/commands/mcps.py +0 -1225
- glaip_sdk/cli/commands/tools.py +0 -597
- glaip_sdk/cli/utils.py +0 -1330
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
glaip_sdk/utils/import_export.py
CHANGED
|
@@ -9,10 +9,19 @@ Authors:
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from glaip_sdk.utils.resource_refs import _extract_id_from_item
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
14
16
|
"""Extract IDs from export format (list of dicts with id/name fields).
|
|
15
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
|
+
|
|
16
25
|
Args:
|
|
17
26
|
items: List of items (dicts with id/name or strings)
|
|
18
27
|
|
|
@@ -29,13 +38,9 @@ def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
|
29
38
|
|
|
30
39
|
ids = []
|
|
31
40
|
for item in items:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ids.append(str(item.id))
|
|
36
|
-
elif isinstance(item, dict) and "id" in item:
|
|
37
|
-
ids.append(str(item["id"]))
|
|
38
|
-
# Skip items without ID (don't convert to string)
|
|
41
|
+
extracted = _extract_id_from_item(item, skip_missing=True)
|
|
42
|
+
if extracted is not None:
|
|
43
|
+
ids.append(extracted)
|
|
39
44
|
|
|
40
45
|
return ids
|
|
41
46
|
|
|
@@ -71,14 +76,10 @@ def _get_default_array_fields() -> list[str]:
|
|
|
71
76
|
|
|
72
77
|
def _should_use_cli_value(cli_value: Any) -> bool:
|
|
73
78
|
"""Check if CLI value should be used."""
|
|
74
|
-
return cli_value is not None and (
|
|
75
|
-
not isinstance(cli_value, list | tuple) or len(cli_value) > 0
|
|
76
|
-
)
|
|
79
|
+
return cli_value is not None and (not isinstance(cli_value, (list, tuple)) or len(cli_value) > 0)
|
|
77
80
|
|
|
78
81
|
|
|
79
|
-
def _handle_array_field_merge(
|
|
80
|
-
key: str, cli_value: Any, import_data: dict[str, Any]
|
|
81
|
-
) -> Any:
|
|
82
|
+
def _handle_array_field_merge(key: str, cli_value: Any, import_data: dict[str, Any]) -> Any:
|
|
82
83
|
"""Handle merging of array fields."""
|
|
83
84
|
import_value = import_data[key]
|
|
84
85
|
if isinstance(import_value, list):
|
|
@@ -107,9 +108,7 @@ def _merge_cli_values_with_import(
|
|
|
107
108
|
merged[key] = import_data[key]
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
def _add_import_only_fields(
|
|
111
|
-
merged: dict[str, Any], import_data: dict[str, Any]
|
|
112
|
-
) -> None:
|
|
111
|
+
def _add_import_only_fields(merged: dict[str, Any], import_data: dict[str, Any]) -> None:
|
|
113
112
|
"""Add fields that exist only in import data."""
|
|
114
113
|
for key, import_value in import_data.items():
|
|
115
114
|
if key not in merged:
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""Import resolution and categorization for tool bundling.
|
|
2
|
+
|
|
3
|
+
This module provides the ImportResolver class for handling Python import
|
|
4
|
+
analysis and resolution during tool bundling operations.
|
|
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 importlib
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.utils.tool_detection import is_tool_plugin_decorator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImportResolver:
|
|
20
|
+
"""Resolves and categorizes Python imports for tool bundling.
|
|
21
|
+
|
|
22
|
+
This class handles the complex logic of determining which imports
|
|
23
|
+
are local (and need to be inlined) versus external (and need to
|
|
24
|
+
be preserved as import statements).
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
tool_dir: The directory containing the tool file being bundled.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> resolver = ImportResolver(Path("/path/to/tool/dir"))
|
|
31
|
+
>>> local, external = resolver.categorize_imports(ast_tree)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Modules to exclude from bundled code (only needed locally)
|
|
35
|
+
EXCLUDED_MODULES: set[str] = {
|
|
36
|
+
"glaip_sdk.agents",
|
|
37
|
+
"glaip_sdk.tools",
|
|
38
|
+
"glaip_sdk.mcps",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __init__(self, tool_dir: Path) -> None:
|
|
42
|
+
"""Initialize the ImportResolver.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tool_dir: Directory containing the tool file being processed.
|
|
46
|
+
"""
|
|
47
|
+
self.tool_dir = tool_dir
|
|
48
|
+
self._processed_modules: set[str] = set()
|
|
49
|
+
|
|
50
|
+
def categorize_imports(self, tree: ast.AST) -> tuple[list, list]:
|
|
51
|
+
"""Categorize imports into local and external.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
tree: AST tree of the source file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (local_imports, external_imports) where local_imports
|
|
58
|
+
contains tuples of (module_name, file_path, import_node).
|
|
59
|
+
"""
|
|
60
|
+
local_imports = []
|
|
61
|
+
external_imports = []
|
|
62
|
+
|
|
63
|
+
for node in ast.walk(tree):
|
|
64
|
+
if isinstance(node, ast.ImportFrom):
|
|
65
|
+
if self.is_local_import(node):
|
|
66
|
+
module_file = self.resolve_module_path(node.module)
|
|
67
|
+
local_imports.append((node.module, module_file, node))
|
|
68
|
+
else:
|
|
69
|
+
external_imports.append(node)
|
|
70
|
+
elif isinstance(node, ast.Import):
|
|
71
|
+
external_imports.append(node)
|
|
72
|
+
|
|
73
|
+
return local_imports, external_imports
|
|
74
|
+
|
|
75
|
+
def is_local_import(self, node: ast.ImportFrom) -> bool:
|
|
76
|
+
"""Check if import is local to the tool directory.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
node: Import node to check.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if import is local.
|
|
83
|
+
"""
|
|
84
|
+
if not node.module:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Handle package imports
|
|
88
|
+
if "." in node.module:
|
|
89
|
+
return self._is_local_package_import(node.module)
|
|
90
|
+
|
|
91
|
+
potential_file = self.tool_dir / f"{node.module}.py"
|
|
92
|
+
return potential_file.exists()
|
|
93
|
+
|
|
94
|
+
def _is_local_package_import(self, module: str) -> bool:
|
|
95
|
+
"""Check if a dotted module path is local.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
module: Module path like 'tools.config' or 'sub.module'.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if the module is local to tool_dir.
|
|
102
|
+
"""
|
|
103
|
+
parts = module.split(".")
|
|
104
|
+
|
|
105
|
+
# Case 1: First part matches current directory name
|
|
106
|
+
if parts[0] == self.tool_dir.name:
|
|
107
|
+
remaining_parts = parts[1:]
|
|
108
|
+
if len(remaining_parts) == 1:
|
|
109
|
+
module_path = self.tool_dir / f"{remaining_parts[0]}.py"
|
|
110
|
+
if module_path.exists():
|
|
111
|
+
return True
|
|
112
|
+
elif len(remaining_parts) > 1:
|
|
113
|
+
module_path = self.tool_dir / "/".join(remaining_parts[:-1]) / f"{remaining_parts[-1]}.py"
|
|
114
|
+
if module_path.exists():
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
# Case 2: First part is a subdirectory of tool_dir
|
|
118
|
+
package_dir = self.tool_dir / parts[0]
|
|
119
|
+
if package_dir.is_dir():
|
|
120
|
+
module_path = self.tool_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py"
|
|
121
|
+
if module_path.exists():
|
|
122
|
+
return True
|
|
123
|
+
module_path = self.tool_dir / "/".join(parts) / "__init__.py"
|
|
124
|
+
return module_path.exists()
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def resolve_module_path(self, module_name: str) -> Path:
|
|
129
|
+
"""Resolve module name to file path.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
module_name: Module name (e.g., 'config' or 'tools.config').
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Path to the module file.
|
|
136
|
+
"""
|
|
137
|
+
if "." in module_name:
|
|
138
|
+
return self._resolve_dotted_module_path(module_name)
|
|
139
|
+
return self.tool_dir / f"{module_name}.py"
|
|
140
|
+
|
|
141
|
+
def _resolve_dotted_module_path(self, module_name: str) -> Path:
|
|
142
|
+
"""Resolve a dotted module path to a file path.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
module_name: Dotted module path like 'tools.config'.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Path to the module file.
|
|
149
|
+
"""
|
|
150
|
+
parts = module_name.split(".")
|
|
151
|
+
|
|
152
|
+
# Case 1: First part matches current directory name
|
|
153
|
+
if parts[0] == self.tool_dir.name:
|
|
154
|
+
remaining_parts = parts[1:]
|
|
155
|
+
if len(remaining_parts) == 1:
|
|
156
|
+
module_path = self.tool_dir / f"{remaining_parts[0]}.py"
|
|
157
|
+
if module_path.exists():
|
|
158
|
+
return module_path
|
|
159
|
+
elif len(remaining_parts) > 1:
|
|
160
|
+
module_path = self.tool_dir / "/".join(remaining_parts[:-1]) / f"{remaining_parts[-1]}.py"
|
|
161
|
+
if module_path.exists():
|
|
162
|
+
return module_path
|
|
163
|
+
|
|
164
|
+
# Case 2: Standard package/module.py
|
|
165
|
+
module_path = self.tool_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py"
|
|
166
|
+
if module_path.exists():
|
|
167
|
+
return module_path
|
|
168
|
+
|
|
169
|
+
# Try package/__init__.py
|
|
170
|
+
return self.tool_dir / "/".join(parts) / "__init__.py"
|
|
171
|
+
|
|
172
|
+
def format_external_imports(self, external_imports: list) -> list[str]:
|
|
173
|
+
"""Format external imports as code strings.
|
|
174
|
+
|
|
175
|
+
__future__ imports are placed first, then other imports.
|
|
176
|
+
Excluded modules are filtered out.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
external_imports: List of external import nodes.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Formatted import statements.
|
|
183
|
+
"""
|
|
184
|
+
future_imports, regular_imports = self._categorize_by_future(external_imports)
|
|
185
|
+
return self._build_import_strings(future_imports, regular_imports)
|
|
186
|
+
|
|
187
|
+
def _categorize_by_future(self, external_imports: list) -> tuple[list, list]:
|
|
188
|
+
"""Separate imports into __future__ and regular imports.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
external_imports: List of import nodes.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Tuple of (future_imports, regular_imports).
|
|
195
|
+
"""
|
|
196
|
+
future_imports = []
|
|
197
|
+
regular_imports = []
|
|
198
|
+
|
|
199
|
+
for node in external_imports:
|
|
200
|
+
if self._should_skip_import(node):
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
|
204
|
+
future_imports.append(node)
|
|
205
|
+
else:
|
|
206
|
+
regular_imports.append(node)
|
|
207
|
+
|
|
208
|
+
return future_imports, regular_imports
|
|
209
|
+
|
|
210
|
+
def _should_skip_import(self, node: ast.Import | ast.ImportFrom) -> bool:
|
|
211
|
+
"""Check if import should be skipped (excluded modules).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
node: Import node to check.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
True if import should be skipped.
|
|
218
|
+
"""
|
|
219
|
+
if isinstance(node, ast.ImportFrom):
|
|
220
|
+
return self._should_skip_import_from(node)
|
|
221
|
+
if isinstance(node, ast.Import):
|
|
222
|
+
return self._should_skip_regular_import(node)
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def _should_skip_import_from(self, node: ast.ImportFrom) -> bool:
|
|
226
|
+
"""Check if ImportFrom node should be skipped.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
node: ImportFrom node to check.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if import should be skipped.
|
|
233
|
+
"""
|
|
234
|
+
if not node.module:
|
|
235
|
+
return False
|
|
236
|
+
return self._is_module_excluded(node.module)
|
|
237
|
+
|
|
238
|
+
def _should_skip_regular_import(self, node: ast.Import) -> bool:
|
|
239
|
+
"""Check if Import node should be skipped.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
node: Import node to check.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if any alias should be skipped.
|
|
246
|
+
"""
|
|
247
|
+
return any(self._is_module_excluded(alias.name) for alias in node.names)
|
|
248
|
+
|
|
249
|
+
def _is_module_excluded(self, module_name: str) -> bool:
|
|
250
|
+
"""Check if a module name should be excluded.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
module_name: Module name to check.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if module is excluded.
|
|
257
|
+
"""
|
|
258
|
+
# Exact match for glaip_sdk or match excluded submodules with boundary
|
|
259
|
+
if module_name == "glaip_sdk":
|
|
260
|
+
return True
|
|
261
|
+
return any(module_name == m or module_name.startswith(m + ".") for m in self.EXCLUDED_MODULES)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _build_import_strings(future_imports: list, regular_imports: list) -> list[str]:
|
|
265
|
+
"""Build formatted import strings from import nodes.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
future_imports: List of __future__ import nodes.
|
|
269
|
+
regular_imports: List of regular import nodes.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Formatted import statements.
|
|
273
|
+
"""
|
|
274
|
+
result = []
|
|
275
|
+
|
|
276
|
+
if future_imports:
|
|
277
|
+
result.append("# Future imports\n")
|
|
278
|
+
for node in future_imports:
|
|
279
|
+
result.append(ast.unparse(node) + "\n")
|
|
280
|
+
result.append("\n")
|
|
281
|
+
|
|
282
|
+
if regular_imports:
|
|
283
|
+
result.append("# External imports\n")
|
|
284
|
+
for node in regular_imports:
|
|
285
|
+
result.append(ast.unparse(node) + "\n")
|
|
286
|
+
result.append("\n")
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
def inline_local_imports(
|
|
291
|
+
self,
|
|
292
|
+
local_imports: list,
|
|
293
|
+
processed_modules: set[str] | None = None,
|
|
294
|
+
) -> tuple[list[str], list]:
|
|
295
|
+
"""Inline local imports into bundled code and collect their external imports.
|
|
296
|
+
|
|
297
|
+
Recursively inlines nested local imports.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
local_imports: List of (module_name, file_path, import_node) tuples.
|
|
301
|
+
processed_modules: Set of already processed module paths to avoid duplicates.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tuple of (inlined_code_strings, collected_external_imports).
|
|
305
|
+
"""
|
|
306
|
+
if not local_imports:
|
|
307
|
+
return [], []
|
|
308
|
+
|
|
309
|
+
if processed_modules is None:
|
|
310
|
+
processed_modules = set()
|
|
311
|
+
|
|
312
|
+
result = ["# Inlined local imports\n"]
|
|
313
|
+
all_external_imports = []
|
|
314
|
+
|
|
315
|
+
for module_name, file_path, _ in local_imports:
|
|
316
|
+
if str(file_path) in processed_modules:
|
|
317
|
+
continue
|
|
318
|
+
processed_modules.add(str(file_path))
|
|
319
|
+
|
|
320
|
+
# Recursively inline nested local imports
|
|
321
|
+
nested_resolver = ImportResolver(file_path.parent)
|
|
322
|
+
with open(file_path, encoding="utf-8") as f:
|
|
323
|
+
source = f.read()
|
|
324
|
+
tree = ast.parse(source)
|
|
325
|
+
nested_local_imports, nested_external_imports = nested_resolver.categorize_imports(tree)
|
|
326
|
+
|
|
327
|
+
if nested_local_imports:
|
|
328
|
+
nested_code, nested_ext_imports = nested_resolver.inline_local_imports(
|
|
329
|
+
nested_local_imports, processed_modules
|
|
330
|
+
)
|
|
331
|
+
result.extend(nested_code)
|
|
332
|
+
all_external_imports.extend(nested_ext_imports)
|
|
333
|
+
|
|
334
|
+
# Inline this module's code
|
|
335
|
+
result.append(f"# --- Inlined from {module_name}.py ---\n")
|
|
336
|
+
code_lines, external_imports = self._extract_module_code(file_path, collect_imports=True)
|
|
337
|
+
result.extend(code_lines)
|
|
338
|
+
all_external_imports.extend(external_imports)
|
|
339
|
+
all_external_imports.extend(nested_external_imports)
|
|
340
|
+
result.append(f"# --- End of {module_name}.py ---\n\n")
|
|
341
|
+
|
|
342
|
+
return result, all_external_imports
|
|
343
|
+
|
|
344
|
+
def _extract_module_code(
|
|
345
|
+
self,
|
|
346
|
+
file_path: Path,
|
|
347
|
+
*,
|
|
348
|
+
collect_imports: bool = False,
|
|
349
|
+
) -> tuple[list[str], list] | list[str]:
|
|
350
|
+
"""Extract code from module, excluding imports and docstrings.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
file_path: Path to the module file.
|
|
354
|
+
collect_imports: If True, also return external imports.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
If collect_imports is True: tuple of (code_lines, external_import_nodes)
|
|
358
|
+
Otherwise: list of code lines.
|
|
359
|
+
"""
|
|
360
|
+
with open(file_path, encoding="utf-8") as f:
|
|
361
|
+
local_source = f.read()
|
|
362
|
+
|
|
363
|
+
local_tree = ast.parse(local_source)
|
|
364
|
+
tool_dir = file_path.parent
|
|
365
|
+
|
|
366
|
+
result, external_imports = self._process_module_nodes(local_tree, tool_dir, collect_imports)
|
|
367
|
+
|
|
368
|
+
if collect_imports:
|
|
369
|
+
return result, external_imports
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
def _process_module_nodes(
|
|
373
|
+
self,
|
|
374
|
+
tree: ast.AST,
|
|
375
|
+
tool_dir: Path,
|
|
376
|
+
collect_imports: bool,
|
|
377
|
+
) -> tuple[list[str], list]:
|
|
378
|
+
"""Process AST nodes from a module.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
tree: AST tree of the module.
|
|
382
|
+
tool_dir: Directory containing the module.
|
|
383
|
+
collect_imports: Whether to collect external imports.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Tuple of (code_lines, external_imports).
|
|
387
|
+
"""
|
|
388
|
+
result = []
|
|
389
|
+
external_imports = []
|
|
390
|
+
|
|
391
|
+
for local_node in tree.body:
|
|
392
|
+
if isinstance(local_node, (ast.Import, ast.ImportFrom)):
|
|
393
|
+
if collect_imports:
|
|
394
|
+
ext_import = self._get_external_import(local_node, tool_dir)
|
|
395
|
+
if ext_import:
|
|
396
|
+
external_imports.append(ext_import)
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
if self._is_docstring(local_node):
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if isinstance(local_node, ast.ClassDef):
|
|
403
|
+
local_node = self._remove_tool_plugin_decorator(local_node)
|
|
404
|
+
|
|
405
|
+
result.append(ast.unparse(local_node) + "\n")
|
|
406
|
+
|
|
407
|
+
return result, external_imports
|
|
408
|
+
|
|
409
|
+
def _get_external_import(
|
|
410
|
+
self,
|
|
411
|
+
node: ast.Import | ast.ImportFrom,
|
|
412
|
+
tool_dir: Path,
|
|
413
|
+
) -> ast.Import | ast.ImportFrom | None:
|
|
414
|
+
"""Get external import node if not local.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
node: Import node to check.
|
|
418
|
+
tool_dir: Directory containing the tool.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
The node if external, None if local.
|
|
422
|
+
"""
|
|
423
|
+
if isinstance(node, ast.ImportFrom):
|
|
424
|
+
if node.module and node.module.startswith("."):
|
|
425
|
+
return None
|
|
426
|
+
temp_resolver = ImportResolver(tool_dir)
|
|
427
|
+
if temp_resolver.is_local_import(node):
|
|
428
|
+
return None
|
|
429
|
+
return node
|
|
430
|
+
if isinstance(node, ast.Import):
|
|
431
|
+
return node
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
@staticmethod
|
|
435
|
+
def _is_docstring(node: ast.stmt) -> bool:
|
|
436
|
+
"""Check if AST node is a docstring.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
node: AST statement node.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
True if node is a docstring.
|
|
443
|
+
"""
|
|
444
|
+
return isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant)
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _remove_tool_plugin_decorator(node: ast.ClassDef) -> ast.ClassDef:
|
|
448
|
+
"""Remove @tool_plugin decorator and BaseTool inheritance from a class node.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
node: AST ClassDef node.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Modified ClassDef node with decorator and base class removed.
|
|
455
|
+
"""
|
|
456
|
+
node.decorator_list = ImportResolver._filter_decorators(node.decorator_list)
|
|
457
|
+
node.bases = ImportResolver._filter_bases(node.bases)
|
|
458
|
+
return node
|
|
459
|
+
|
|
460
|
+
@staticmethod
|
|
461
|
+
def _filter_decorators(decorator_list: list) -> list:
|
|
462
|
+
"""Filter out @tool_plugin decorators.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
decorator_list: List of decorator nodes.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Filtered decorator list.
|
|
469
|
+
"""
|
|
470
|
+
filtered = []
|
|
471
|
+
for decorator in decorator_list:
|
|
472
|
+
if ImportResolver._is_tool_plugin_decorator(decorator):
|
|
473
|
+
continue
|
|
474
|
+
filtered.append(decorator)
|
|
475
|
+
return filtered
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _is_tool_plugin_decorator(decorator: ast.expr) -> bool:
|
|
479
|
+
"""Check if decorator is @tool_plugin.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
decorator: Decorator AST node.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
True if decorator is @tool_plugin.
|
|
486
|
+
"""
|
|
487
|
+
return is_tool_plugin_decorator(decorator)
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def _filter_bases(bases: list) -> list:
|
|
491
|
+
"""Filter out BaseTool from base classes.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
bases: List of base class nodes.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Filtered base class list.
|
|
498
|
+
"""
|
|
499
|
+
filtered = []
|
|
500
|
+
for base in bases:
|
|
501
|
+
is_base_tool = isinstance(base, ast.Name) and base.id == "BaseTool"
|
|
502
|
+
if not is_base_tool:
|
|
503
|
+
filtered.append(base)
|
|
504
|
+
return filtered if filtered else []
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def load_class(import_path: str) -> type:
|
|
508
|
+
"""Dynamically load a class from its import path.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
import_path: Python import path (e.g., 'package.module.ClassName').
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
The loaded class.
|
|
515
|
+
|
|
516
|
+
Raises:
|
|
517
|
+
ImportError: If the module or class cannot be imported.
|
|
518
|
+
"""
|
|
519
|
+
try:
|
|
520
|
+
module_path, class_name = import_path.rsplit(".", 1)
|
|
521
|
+
module = importlib.import_module(module_path)
|
|
522
|
+
return getattr(module, class_name)
|
|
523
|
+
except (ValueError, ImportError, AttributeError) as e:
|
|
524
|
+
raise ImportError(f"Failed to load class from '{import_path}': {e}") from e
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Instruction template loading and processing.
|
|
2
|
+
|
|
3
|
+
This module provides functions for loading and processing instruction
|
|
4
|
+
templates from markdown files.
|
|
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 pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_instruction_from_file(
|
|
16
|
+
instructions_path: str,
|
|
17
|
+
variables: dict[str, str] | None = None,
|
|
18
|
+
base_path: Path | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Load and process instruction template from markdown file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
instructions_path: Path to instructions markdown file.
|
|
24
|
+
variables: Template variables for {{variable}} substitution.
|
|
25
|
+
base_path: Base path to resolve relative paths from.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Processed instructions with variables substituted.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
FileNotFoundError: If instructions file doesn't exist.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> instructions = load_instruction_from_file(
|
|
35
|
+
... "instructions.md",
|
|
36
|
+
... variables={"agent_name": "Weather Bot"}
|
|
37
|
+
... )
|
|
38
|
+
"""
|
|
39
|
+
full_path = _resolve_instructions_path(instructions_path, base_path)
|
|
40
|
+
|
|
41
|
+
if not full_path.exists():
|
|
42
|
+
raise FileNotFoundError(f"Instructions file not found: {full_path}")
|
|
43
|
+
|
|
44
|
+
with open(full_path, encoding="utf-8") as f:
|
|
45
|
+
template = f.read()
|
|
46
|
+
|
|
47
|
+
if variables:
|
|
48
|
+
template = _substitute_variables(template, variables)
|
|
49
|
+
|
|
50
|
+
return template
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Alias for backwards compatibility
|
|
54
|
+
load_instructions = load_instruction_from_file
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resolve_instructions_path(instructions_path: str, base_path: Path | None) -> Path:
|
|
58
|
+
"""Resolve instructions path to an absolute path.
|
|
59
|
+
|
|
60
|
+
Handles absolute paths, tilde expansion, and relative paths.
|
|
61
|
+
Guards against path traversal escaping base_path when provided.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
instructions_path: Path to instructions file (may be relative).
|
|
65
|
+
base_path: Base path for relative resolution.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Resolved absolute path.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If resolved path escapes base_path.
|
|
72
|
+
"""
|
|
73
|
+
raw_path = Path(instructions_path).expanduser()
|
|
74
|
+
|
|
75
|
+
if raw_path.is_absolute():
|
|
76
|
+
return raw_path.resolve()
|
|
77
|
+
|
|
78
|
+
base = (base_path or Path.cwd()).resolve()
|
|
79
|
+
relative_part = raw_path.relative_to(".") if instructions_path.startswith("./") else raw_path
|
|
80
|
+
resolved = (base / relative_part).resolve()
|
|
81
|
+
|
|
82
|
+
if base_path and not resolved.is_relative_to(base):
|
|
83
|
+
raise ValueError(f"Resolved instructions path escapes base path: {resolved}")
|
|
84
|
+
|
|
85
|
+
return resolved
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _substitute_variables(template: str, variables: dict[str, str]) -> str:
|
|
89
|
+
"""Substitute template variables in the format {{variable_name}}.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
template: Template string with {{variable}} placeholders.
|
|
93
|
+
variables: Dictionary of variable names to values.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Template with variables substituted.
|
|
97
|
+
"""
|
|
98
|
+
result = template
|
|
99
|
+
for key, value in variables.items():
|
|
100
|
+
result = result.replace(f"{{{{{key}}}}}", value)
|
|
101
|
+
return result
|