glaip-sdk 0.1.3__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.
- glaip_sdk/__init__.py +5 -2
- glaip_sdk/_version.py +9 -0
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/auth.py +254 -15
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +213 -73
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +729 -113
- glaip_sdk/cli/commands/mcps.py +241 -72
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +49 -57
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/config.py +48 -4
- 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 +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +35 -19
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +228 -119
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +62 -21
- glaip_sdk/cli/slash/prompt.py +21 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +771 -140
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +255 -44
- glaip_sdk/cli/transcript/capture.py +27 -1
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/viewer.py +72 -499
- glaip_sdk/cli/update_notifier.py +14 -5
- glaip_sdk/cli/utils.py +243 -1252
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +45 -9
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +287 -29
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +122 -12
- glaip_sdk/client/run_rendering.py +133 -88
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +155 -10
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- 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 +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -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 +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +39 -7
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +0 -33
- glaip_sdk/utils/import_export.py +12 -7
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +26 -20
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -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/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- 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 +25 -13
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +18 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +16 -24
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -240
- glaip_sdk-0.1.3.dist-info/RECORD +0 -83
- {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""A2A event stream processing utilities.
|
|
2
|
+
|
|
3
|
+
This module provides helpers for processing the A2AEvent stream emitted by
|
|
4
|
+
agent execution backends (e.g., `arun_a2a_stream()`).
|
|
5
|
+
|
|
6
|
+
The MVP implementation focuses on extracting final response text;
|
|
7
|
+
full A2AConnector-equivalent normalization is deferred to follow-up PRs.
|
|
8
|
+
|
|
9
|
+
Authors:
|
|
10
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from gllm_core.utils import LoggerManager
|
|
19
|
+
|
|
20
|
+
logger = LoggerManager().get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
# A2A event type constants (matching aip_agents.schema.a2a.A2AStreamEventType)
|
|
23
|
+
EVENT_TYPE_FINAL_RESPONSE = "final_response"
|
|
24
|
+
EVENT_TYPE_STATUS_UPDATE = "status_update"
|
|
25
|
+
EVENT_TYPE_TOOL_CALL = "tool_call"
|
|
26
|
+
EVENT_TYPE_TOOL_RESULT = "tool_result"
|
|
27
|
+
EVENT_TYPE_ERROR = "error"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class A2AEventStreamProcessor:
|
|
32
|
+
"""Processor for `arun_a2a_stream()` event dictionaries.
|
|
33
|
+
|
|
34
|
+
The SDK uses lightweight dictionaries to represent A2A stream events.
|
|
35
|
+
This helper centralizes event-type normalization and MVP final-text extraction.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> processor = A2AEventStreamProcessor()
|
|
39
|
+
>>> events = [{"event_type": "final_response", "content": "Hello!", "is_final": True}]
|
|
40
|
+
>>> result = processor.extract_final_response(events)
|
|
41
|
+
>>> print(result)
|
|
42
|
+
Hello!
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def extract_final_response(self, events: list[dict[str, Any]]) -> str:
|
|
46
|
+
"""Extract the final response text from a list of A2AEvents.
|
|
47
|
+
|
|
48
|
+
Scans the event list for the final_response event and returns its content.
|
|
49
|
+
If no final_response is found, raises a RuntimeError.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
events: List of A2AEvent dictionaries from arun_a2a_stream().
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The content string from the final_response event.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError: If no final_response event is found in the stream.
|
|
59
|
+
"""
|
|
60
|
+
for event in reversed(events):
|
|
61
|
+
if self._is_final_response_event(event):
|
|
62
|
+
content = event.get("content", "")
|
|
63
|
+
logger.debug("Extracted final response: %d characters", len(str(content)))
|
|
64
|
+
return str(content)
|
|
65
|
+
|
|
66
|
+
# Fallback: check for events with is_final=True
|
|
67
|
+
for event in reversed(events):
|
|
68
|
+
if event.get("is_final", False):
|
|
69
|
+
content = event.get("content", "")
|
|
70
|
+
if content:
|
|
71
|
+
logger.debug("Extracted final from is_final flag: %d chars", len(str(content)))
|
|
72
|
+
return str(content)
|
|
73
|
+
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"No final response received from the agent. The agent execution completed without producing a final answer."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_event_type(self, event: dict[str, Any]) -> str:
|
|
79
|
+
"""Get the normalized event type string from an A2AEvent.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
event: An A2AEvent dictionary.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The event type as a lowercase string.
|
|
86
|
+
"""
|
|
87
|
+
event_type = event.get("event_type", "unknown")
|
|
88
|
+
if isinstance(event_type, str):
|
|
89
|
+
return event_type.lower()
|
|
90
|
+
# Handle enum types (A2AStreamEventType)
|
|
91
|
+
return getattr(event_type, "value", str(event_type)).lower()
|
|
92
|
+
|
|
93
|
+
def is_tool_event(self, event: dict[str, Any]) -> bool:
|
|
94
|
+
"""Check if an event is a tool-related event.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
event: An A2AEvent dictionary.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if this is a tool_call or tool_result event.
|
|
101
|
+
"""
|
|
102
|
+
event_type = self.get_event_type(event)
|
|
103
|
+
return event_type in (EVENT_TYPE_TOOL_CALL, EVENT_TYPE_TOOL_RESULT)
|
|
104
|
+
|
|
105
|
+
def is_error_event(self, event: dict[str, Any]) -> bool:
|
|
106
|
+
"""Check if an event is an error event.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
event: An A2AEvent dictionary.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if this is an error event.
|
|
113
|
+
"""
|
|
114
|
+
return self.get_event_type(event) == EVENT_TYPE_ERROR
|
|
115
|
+
|
|
116
|
+
def _is_final_response_event(self, event: dict[str, Any]) -> bool:
|
|
117
|
+
"""Check if an event is a final_response event.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
event: An A2AEvent dictionary.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if this is a final_response event, False otherwise.
|
|
124
|
+
"""
|
|
125
|
+
return self.get_event_type(event) == EVENT_TYPE_FINAL_RESPONSE
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Default processor instance for convenience functions
|
|
129
|
+
_DEFAULT_PROCESSOR = A2AEventStreamProcessor()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def extract_final_response(events: list[dict[str, Any]]) -> str:
|
|
133
|
+
"""Extract the final response text from a list of A2AEvents.
|
|
134
|
+
|
|
135
|
+
Convenience function that uses the default A2AEventStreamProcessor.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
events: List of A2AEvent dictionaries from arun_a2a_stream().
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The content string from the final_response event.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
RuntimeError: If no final_response event is found in the stream.
|
|
145
|
+
"""
|
|
146
|
+
return _DEFAULT_PROCESSOR.extract_final_response(events)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_event_type(event: dict[str, Any]) -> str:
|
|
150
|
+
"""Get the normalized event type string from an A2AEvent.
|
|
151
|
+
|
|
152
|
+
Convenience function that uses the default A2AEventStreamProcessor.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event: An A2AEvent dictionary.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The event type as a lowercase string.
|
|
159
|
+
"""
|
|
160
|
+
return _DEFAULT_PROCESSOR.get_event_type(event)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_tool_event(event: dict[str, Any]) -> bool:
|
|
164
|
+
"""Check if an event is a tool-related event.
|
|
165
|
+
|
|
166
|
+
Convenience function that uses the default A2AEventStreamProcessor.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
event: An A2AEvent dictionary.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if this is a tool_call or tool_result event.
|
|
173
|
+
"""
|
|
174
|
+
return _DEFAULT_PROCESSOR.is_tool_event(event)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_error_event(event: dict[str, Any]) -> bool:
|
|
178
|
+
"""Check if an event is an error event.
|
|
179
|
+
|
|
180
|
+
Convenience function that uses the default A2AEventStreamProcessor.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
event: An A2AEvent dictionary.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if this is an error event.
|
|
187
|
+
"""
|
|
188
|
+
return _DEFAULT_PROCESSOR.is_error_event(event)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tool source code bundling with import inlining.
|
|
2
|
+
|
|
3
|
+
This module provides the ToolBundler class for bundling Python tool source
|
|
4
|
+
code with all local dependencies inlined.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import inspect
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.utils.import_resolver import ImportResolver
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolBundler:
|
|
20
|
+
"""Bundles tool source code with inlined local imports.
|
|
21
|
+
|
|
22
|
+
This class handles the complex process of taking a tool class and
|
|
23
|
+
producing a single, self-contained source file with all local
|
|
24
|
+
dependencies inlined.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
tool_class: The tool class to bundle.
|
|
28
|
+
tool_file: Path to the file containing the tool class.
|
|
29
|
+
tool_dir: Directory containing the tool file.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> bundler = ToolBundler(MyToolClass)
|
|
33
|
+
>>> bundled_source = bundler.bundle()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, tool_class: type) -> None:
|
|
37
|
+
"""Initialize the ToolBundler.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tool_class: The tool class or decorated function to bundle.
|
|
41
|
+
"""
|
|
42
|
+
# If it's a gllm_core Tool, get the underlying function
|
|
43
|
+
if hasattr(tool_class, "__wrapped__"):
|
|
44
|
+
actual_func = tool_class.__wrapped__
|
|
45
|
+
else:
|
|
46
|
+
actual_func = tool_class
|
|
47
|
+
|
|
48
|
+
self.tool_class = tool_class
|
|
49
|
+
self.tool_file = Path(inspect.getfile(actual_func))
|
|
50
|
+
self.tool_dir = self.tool_file.parent
|
|
51
|
+
self._import_resolver = ImportResolver(self.tool_dir)
|
|
52
|
+
|
|
53
|
+
def bundle(self) -> str:
|
|
54
|
+
"""Bundle tool source code with inlined local imports.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Bundled source code with all local dependencies inlined.
|
|
58
|
+
"""
|
|
59
|
+
with open(self.tool_file, encoding="utf-8") as f:
|
|
60
|
+
full_source = f.read()
|
|
61
|
+
|
|
62
|
+
tree = ast.parse(full_source)
|
|
63
|
+
local_imports, external_imports = self._import_resolver.categorize_imports(tree)
|
|
64
|
+
|
|
65
|
+
# Extract main code nodes (excluding imports, docstrings, glaip_sdk.Tool subclasses)
|
|
66
|
+
main_code_nodes = self._extract_main_code_nodes(tree)
|
|
67
|
+
|
|
68
|
+
# Inline local imports and collect their external imports
|
|
69
|
+
inlined_code, inlined_external_imports = self._import_resolver.inline_local_imports(local_imports)
|
|
70
|
+
|
|
71
|
+
# Merge all external imports
|
|
72
|
+
all_external_imports = external_imports + inlined_external_imports
|
|
73
|
+
|
|
74
|
+
# Build bundled code
|
|
75
|
+
bundled_code = ["# Bundled tool with inlined local imports\n"]
|
|
76
|
+
bundled_code.extend(self._import_resolver.format_external_imports(all_external_imports))
|
|
77
|
+
|
|
78
|
+
# Add inlined dependencies FIRST (before main tool code)
|
|
79
|
+
bundled_code.extend(inlined_code)
|
|
80
|
+
|
|
81
|
+
# Then add main tool code
|
|
82
|
+
bundled_code.append("# Main tool code\n")
|
|
83
|
+
for node_code in main_code_nodes:
|
|
84
|
+
bundled_code.append(node_code + "\n")
|
|
85
|
+
bundled_code.append("\n")
|
|
86
|
+
|
|
87
|
+
return "".join(bundled_code)
|
|
88
|
+
|
|
89
|
+
def _extract_main_code_nodes(self, tree: ast.AST) -> list[str]:
|
|
90
|
+
"""Extract main code nodes from AST, excluding imports and Tool subclasses.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
tree: AST tree of the source file.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of unparsed code node strings.
|
|
97
|
+
"""
|
|
98
|
+
main_code_nodes = []
|
|
99
|
+
for node in tree.body:
|
|
100
|
+
# Skip imports
|
|
101
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
102
|
+
continue
|
|
103
|
+
# Skip module docstrings
|
|
104
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
|
|
105
|
+
continue
|
|
106
|
+
# Skip glaip_sdk.Tool subclasses
|
|
107
|
+
if isinstance(node, ast.ClassDef) and self._is_sdk_tool_subclass(node):
|
|
108
|
+
continue
|
|
109
|
+
main_code_nodes.append(ast.unparse(node))
|
|
110
|
+
return main_code_nodes
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _is_sdk_tool_subclass(node: ast.ClassDef) -> bool:
|
|
114
|
+
"""Check if AST class definition inherits from Tool.
|
|
115
|
+
|
|
116
|
+
These classes are only needed locally for upload configuration
|
|
117
|
+
and should be excluded from bundled code.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
node: AST ClassDef node to check.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if class inherits from Tool.
|
|
124
|
+
"""
|
|
125
|
+
for base in node.bases:
|
|
126
|
+
if isinstance(base, ast.Name) and base.id == "Tool":
|
|
127
|
+
return True
|
|
128
|
+
if (
|
|
129
|
+
isinstance(base, ast.Attribute)
|
|
130
|
+
and base.attr == "Tool"
|
|
131
|
+
and isinstance(base.value, ast.Name)
|
|
132
|
+
and base.value.id in ("glaip_sdk",)
|
|
133
|
+
):
|
|
134
|
+
return True
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def bundle_from_source(cls, file_path: Path) -> tuple[str, str, str]:
|
|
139
|
+
"""Extract tool info directly from source file without importing.
|
|
140
|
+
|
|
141
|
+
This is used as a fallback when the tool class cannot be imported
|
|
142
|
+
due to missing dependencies.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
file_path: Path to the tool source file.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (name, description, bundled_source_code).
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
FileNotFoundError: If the source file doesn't exist.
|
|
152
|
+
"""
|
|
153
|
+
if not file_path.exists():
|
|
154
|
+
raise FileNotFoundError(f"Tool source file not found: {file_path}")
|
|
155
|
+
|
|
156
|
+
with open(file_path, encoding="utf-8") as f:
|
|
157
|
+
source_code = f.read()
|
|
158
|
+
|
|
159
|
+
tree = ast.parse(source_code)
|
|
160
|
+
tool_dir = file_path.parent
|
|
161
|
+
import_resolver = ImportResolver(tool_dir)
|
|
162
|
+
|
|
163
|
+
# Find tool name and description from class definitions
|
|
164
|
+
tool_name, tool_description = cls._extract_tool_metadata(tree, file_path.stem)
|
|
165
|
+
|
|
166
|
+
# Categorize imports
|
|
167
|
+
local_imports, external_imports = import_resolver.categorize_imports(tree)
|
|
168
|
+
|
|
169
|
+
# Extract main code nodes
|
|
170
|
+
main_code_nodes = []
|
|
171
|
+
for node in tree.body:
|
|
172
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
173
|
+
continue
|
|
174
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant):
|
|
175
|
+
continue
|
|
176
|
+
main_code_nodes.append(ast.unparse(node))
|
|
177
|
+
|
|
178
|
+
# Inline local imports
|
|
179
|
+
inlined_code, inlined_external_imports = import_resolver.inline_local_imports(local_imports)
|
|
180
|
+
|
|
181
|
+
# Build bundled code
|
|
182
|
+
all_external_imports = external_imports + inlined_external_imports
|
|
183
|
+
bundled_code = ["# Bundled tool with inlined local imports\n"]
|
|
184
|
+
bundled_code.extend(import_resolver.format_external_imports(all_external_imports))
|
|
185
|
+
|
|
186
|
+
# Add main tool code
|
|
187
|
+
bundled_code.append("# Main tool code\n")
|
|
188
|
+
for node_code in main_code_nodes:
|
|
189
|
+
bundled_code.append(node_code + "\n")
|
|
190
|
+
bundled_code.append("\n")
|
|
191
|
+
|
|
192
|
+
# Then add inlined dependencies
|
|
193
|
+
bundled_code.extend(inlined_code)
|
|
194
|
+
|
|
195
|
+
bundled_source = "".join(bundled_code)
|
|
196
|
+
|
|
197
|
+
return tool_name, tool_description, bundled_source
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _extract_tool_metadata(tree: ast.AST, fallback_name: str) -> tuple[str, str]:
|
|
201
|
+
"""Extract tool name and description from AST.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
tree: AST tree of the source file.
|
|
205
|
+
fallback_name: Name to use if not found in source.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Tuple of (tool_name, tool_description).
|
|
209
|
+
"""
|
|
210
|
+
tool_name, tool_description = ToolBundler._find_class_attributes(tree)
|
|
211
|
+
|
|
212
|
+
if not tool_name:
|
|
213
|
+
# Convert class name to snake_case as fallback
|
|
214
|
+
tool_name = "".join(["_" + c.lower() if c.isupper() else c for c in fallback_name]).lstrip("_")
|
|
215
|
+
|
|
216
|
+
if not tool_description:
|
|
217
|
+
tool_description = f"Tool: {fallback_name}"
|
|
218
|
+
|
|
219
|
+
return tool_name, tool_description
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _find_class_attributes(tree: ast.AST) -> tuple[str | None, str | None]:
|
|
223
|
+
"""Find name and description attributes in class definitions.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
tree: AST tree to search.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (name, description) if found.
|
|
230
|
+
"""
|
|
231
|
+
for node in ast.walk(tree):
|
|
232
|
+
if not isinstance(node, ast.ClassDef):
|
|
233
|
+
continue
|
|
234
|
+
name, description = ToolBundler._extract_class_name_description(node)
|
|
235
|
+
if name or description:
|
|
236
|
+
return name, description
|
|
237
|
+
return None, None
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _extract_class_name_description(
|
|
241
|
+
class_node: ast.ClassDef,
|
|
242
|
+
) -> tuple[str | None, str | None]:
|
|
243
|
+
"""Extract name and description from a single class definition.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
class_node: AST ClassDef node.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (name, description) if found.
|
|
250
|
+
"""
|
|
251
|
+
name = None
|
|
252
|
+
description = None
|
|
253
|
+
|
|
254
|
+
for item in class_node.body:
|
|
255
|
+
if not isinstance(item, ast.AnnAssign):
|
|
256
|
+
continue
|
|
257
|
+
if not isinstance(item.target, ast.Name):
|
|
258
|
+
continue
|
|
259
|
+
if not isinstance(item.value, ast.Constant):
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
if item.target.id == "name":
|
|
263
|
+
name = item.value.value
|
|
264
|
+
elif item.target.id == "description":
|
|
265
|
+
description = item.value.value
|
|
266
|
+
|
|
267
|
+
return name, description
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Client singleton management for GLAIP SDK.
|
|
2
|
+
|
|
3
|
+
This module provides a singleton pattern for the GLAIP SDK client instance
|
|
4
|
+
used by the agents runtime. Uses a class-based singleton pattern consistent
|
|
5
|
+
with the registry implementations.
|
|
6
|
+
|
|
7
|
+
Thread Safety:
|
|
8
|
+
The singleton is created lazily on first access. In Python, the GIL ensures
|
|
9
|
+
that class attribute assignment is atomic, making this pattern safe for
|
|
10
|
+
multi-threaded access. For multiprocessing, each process gets its own
|
|
11
|
+
client instance (no shared state across processes).
|
|
12
|
+
|
|
13
|
+
Authors:
|
|
14
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
from glaip_sdk.client import Client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _ClientSingleton:
|
|
24
|
+
"""Singleton holder for GLAIP SDK Client.
|
|
25
|
+
|
|
26
|
+
This class follows the same pattern as registry singletons
|
|
27
|
+
(_ToolRegistrySingleton, _MCPRegistrySingleton, _AgentRegistrySingleton).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_instance: Client | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_instance(cls) -> Client:
|
|
34
|
+
"""Get or create the singleton client instance.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The singleton client instance.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> from glaip_sdk.utils.client import get_client
|
|
41
|
+
>>> client = get_client()
|
|
42
|
+
>>> agents = client.list_agents()
|
|
43
|
+
"""
|
|
44
|
+
if cls._instance is None:
|
|
45
|
+
load_dotenv()
|
|
46
|
+
cls._instance = Client()
|
|
47
|
+
return cls._instance
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def set_instance(cls, client: Client) -> None:
|
|
51
|
+
"""Set the singleton client instance.
|
|
52
|
+
|
|
53
|
+
Useful for testing or when you need to configure the client manually.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
client: The client instance to use.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> from glaip_sdk import Client
|
|
60
|
+
>>> from glaip_sdk.utils.client import set_client
|
|
61
|
+
>>> client = Client(api_key="my-key")
|
|
62
|
+
>>> set_client(client)
|
|
63
|
+
"""
|
|
64
|
+
cls._instance = client
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def reset(cls) -> None:
|
|
68
|
+
"""Reset the singleton client instance.
|
|
69
|
+
|
|
70
|
+
Useful for testing to ensure a fresh client is created.
|
|
71
|
+
"""
|
|
72
|
+
cls._instance = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_client() -> Client:
|
|
76
|
+
"""Get or create singleton client instance.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The singleton client instance.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> from glaip_sdk.utils.client import get_client
|
|
83
|
+
>>> client = get_client()
|
|
84
|
+
>>> agents = client.list_agents()
|
|
85
|
+
"""
|
|
86
|
+
return _ClientSingleton.get_instance()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def set_client(client: Client) -> None:
|
|
90
|
+
"""Set the singleton client instance.
|
|
91
|
+
|
|
92
|
+
Useful for testing or when you need to configure the client manually.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
client: The client instance to use.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> from glaip_sdk import Client
|
|
99
|
+
>>> from glaip_sdk.utils.client import set_client
|
|
100
|
+
>>> client = Client(api_key="my-key")
|
|
101
|
+
>>> set_client(client)
|
|
102
|
+
"""
|
|
103
|
+
_ClientSingleton.set_instance(client)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def reset_client() -> None:
|
|
107
|
+
"""Reset the singleton client instance.
|
|
108
|
+
|
|
109
|
+
Useful for testing to ensure a fresh client is created.
|
|
110
|
+
"""
|
|
111
|
+
_ClientSingleton.reset()
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -15,14 +15,10 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, BinaryIO, NoReturn
|
|
16
16
|
|
|
17
17
|
import httpx
|
|
18
|
-
|
|
19
18
|
from glaip_sdk.exceptions import AgentTimeoutError
|
|
20
|
-
from glaip_sdk.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
from glaip_sdk.utils.resource_refs import (
|
|
24
|
-
find_by_name as find_by_name_new,
|
|
25
|
-
)
|
|
19
|
+
from glaip_sdk.models import AgentResponse, MCPResponse, ToolResponse
|
|
20
|
+
from glaip_sdk.utils.resource_refs import extract_ids as extract_ids_new
|
|
21
|
+
from glaip_sdk.utils.resource_refs import find_by_name as find_by_name_new
|
|
26
22
|
|
|
27
23
|
# Set up module-level logger
|
|
28
24
|
logger = logging.getLogger("glaip_sdk.client_utils")
|
|
@@ -96,6 +92,10 @@ def create_model_instances(data: list[dict] | None, model_class: type, client: A
|
|
|
96
92
|
This is a common pattern used across different clients (agents, tools, mcps)
|
|
97
93
|
to create model instances and associate them with the client.
|
|
98
94
|
|
|
95
|
+
For runtime classes (Agent, Tool, MCP) that have a from_response method,
|
|
96
|
+
this function will use the corresponding Response model to parse the API data
|
|
97
|
+
and then create the runtime instance using from_response.
|
|
98
|
+
|
|
99
99
|
Args:
|
|
100
100
|
data: List of dictionaries from API response
|
|
101
101
|
model_class: The model class to instantiate
|
|
@@ -107,6 +107,25 @@ def create_model_instances(data: list[dict] | None, model_class: type, client: A
|
|
|
107
107
|
if not data:
|
|
108
108
|
return []
|
|
109
109
|
|
|
110
|
+
# Check if the model_class has a from_response method (runtime class pattern)
|
|
111
|
+
if hasattr(model_class, "from_response"):
|
|
112
|
+
# Map runtime classes to their response models
|
|
113
|
+
response_model_map = {
|
|
114
|
+
"Agent": AgentResponse,
|
|
115
|
+
"Tool": ToolResponse,
|
|
116
|
+
"MCP": MCPResponse,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
response_model = response_model_map.get(model_class.__name__)
|
|
120
|
+
if response_model:
|
|
121
|
+
instances = []
|
|
122
|
+
for item_data in data:
|
|
123
|
+
response = response_model(**item_data)
|
|
124
|
+
instance = model_class.from_response(response, client=client)
|
|
125
|
+
instances.append(instance)
|
|
126
|
+
return instances
|
|
127
|
+
|
|
128
|
+
# Fallback to direct instantiation for other classes
|
|
110
129
|
return [model_class(**item_data)._set_client(client) for item_data in data]
|
|
111
130
|
|
|
112
131
|
|
|
@@ -426,6 +445,19 @@ def _prepare_stream_entry(
|
|
|
426
445
|
)
|
|
427
446
|
|
|
428
447
|
|
|
448
|
+
def add_kwargs_to_payload(payload: dict[str, Any], kwargs: dict[str, Any], excluded_keys: set[str]) -> None:
|
|
449
|
+
"""Add kwargs to payload excluding specified keys.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
payload: Payload dictionary to update.
|
|
453
|
+
kwargs: Keyword arguments to add.
|
|
454
|
+
excluded_keys: Keys to exclude from kwargs.
|
|
455
|
+
"""
|
|
456
|
+
for key, value in kwargs.items():
|
|
457
|
+
if key not in excluded_keys:
|
|
458
|
+
payload[key] = value
|
|
459
|
+
|
|
460
|
+
|
|
429
461
|
def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
|
|
430
462
|
"""Prepare multipart form data for file uploads.
|
|
431
463
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared datetime parsing helpers used across CLI and rendering modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = ["coerce_datetime", "from_numeric_timestamp"]
|
|
9
|
+
|
|
10
|
+
_Z_SUFFIX = "+00:00"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def from_numeric_timestamp(raw_value: Any) -> datetime | None:
|
|
14
|
+
"""Convert unix timestamp-like values to datetime with sanity checks."""
|
|
15
|
+
try:
|
|
16
|
+
candidate = float(raw_value)
|
|
17
|
+
except Exception:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
if candidate < 1_000_000_000:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return datetime.fromtimestamp(candidate, tz=timezone.utc)
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_iso(value: str | None) -> datetime | None:
|
|
30
|
+
"""Parse ISO8601 strings while tolerating legacy 'Z' suffixes."""
|
|
31
|
+
if not value:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return datetime.fromisoformat(value.replace("Z", _Z_SUFFIX))
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def coerce_datetime(value: Any) -> datetime | None:
|
|
40
|
+
"""Best-effort conversion of assorted timestamp inputs to aware UTC datetimes."""
|
|
41
|
+
if value is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if isinstance(value, datetime):
|
|
45
|
+
dt = value
|
|
46
|
+
elif isinstance(value, (int, float)):
|
|
47
|
+
dt = from_numeric_timestamp(value)
|
|
48
|
+
elif isinstance(value, str):
|
|
49
|
+
dt = _parse_iso(value) or from_numeric_timestamp(value)
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
if dt is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
if dt.tzinfo is None:
|
|
57
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
58
|
+
return dt.astimezone(timezone.utc)
|