glaip-sdk 0.7.17__py3-none-any.whl → 0.7.18__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/agents/base.py +14 -1
- glaip_sdk/agents/component.py +221 -0
- glaip_sdk/cli/slash/session.py +8 -1
- glaip_sdk/cli/slash/tui/__init__.py +2 -1
- glaip_sdk/cli/slash/tui/accounts_app.py +30 -16
- glaip_sdk/cli/slash/tui/clipboard.py +128 -7
- glaip_sdk/cli/slash/tui/remote_runs_app.py +27 -1
- glaip_sdk/cli/slash/tui/toast.py +5 -5
- {glaip_sdk-0.7.17.dist-info → glaip_sdk-0.7.18.dist-info}/METADATA +4 -1
- {glaip_sdk-0.7.17.dist-info → glaip_sdk-0.7.18.dist-info}/RECORD +13 -12
- {glaip_sdk-0.7.17.dist-info → glaip_sdk-0.7.18.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.7.17.dist-info → glaip_sdk-0.7.18.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.7.17.dist-info → glaip_sdk-0.7.18.dist-info}/top_level.txt +0 -0
glaip_sdk/agents/base.py
CHANGED
|
@@ -54,8 +54,8 @@ from glaip_sdk.utils.resource_refs import is_uuid
|
|
|
54
54
|
|
|
55
55
|
if TYPE_CHECKING:
|
|
56
56
|
from glaip_sdk.client.schedules import AgentScheduleManager
|
|
57
|
-
from glaip_sdk.models import AgentResponse, Model
|
|
58
57
|
from glaip_sdk.guardrails import GuardrailManager
|
|
58
|
+
from glaip_sdk.models import AgentResponse, Model
|
|
59
59
|
from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
|
|
60
60
|
|
|
61
61
|
# Import model validation utility
|
|
@@ -929,6 +929,19 @@ class Agent:
|
|
|
929
929
|
|
|
930
930
|
return content
|
|
931
931
|
|
|
932
|
+
def to_component(self) -> Any:
|
|
933
|
+
"""Convert this Agent into a pipeline-compatible Component.
|
|
934
|
+
|
|
935
|
+
The returned AgentComponent wraps this agent instance and allows it
|
|
936
|
+
to be used within a Pipeline (from gllm-pipeline).
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
An AgentComponent instance wrapping this agent.
|
|
940
|
+
"""
|
|
941
|
+
from glaip_sdk.agents.component import AgentComponent # noqa: PLC0415
|
|
942
|
+
|
|
943
|
+
return AgentComponent(self)
|
|
944
|
+
|
|
932
945
|
# =========================================================================
|
|
933
946
|
# API Methods - Available after deploy()
|
|
934
947
|
# =========================================================================
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Agent Component for Glaip SDK.
|
|
2
|
+
|
|
3
|
+
This module provides the AgentComponent class, which wraps an Agent
|
|
4
|
+
to be used as a reusable component in pipelines.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from gllm_core.schema import Chunk, Component
|
|
15
|
+
from gllm_core.utils import LoggerManager
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from glaip_sdk.agents import Agent
|
|
19
|
+
|
|
20
|
+
logger = LoggerManager().get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentComponent(Component):
|
|
24
|
+
"""A Component that wraps a GL Agent for pipeline integration.
|
|
25
|
+
|
|
26
|
+
This component acts as a bridge between structured pipeline state
|
|
27
|
+
and the natural language interface of an Agent. It compiles inputs
|
|
28
|
+
(query, context, history) into a prompt and executes the agent.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, agent: Agent) -> None:
|
|
32
|
+
"""Initialize the AgentComponent.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
agent: The Agent instance to wrap.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.agent = agent
|
|
39
|
+
|
|
40
|
+
def _format_context(self, context: list[Chunk | str | dict[str, Any] | Any] | None) -> str:
|
|
41
|
+
"""Format the context list into a string.
|
|
42
|
+
|
|
43
|
+
Supports Chunk objects (extracting content), strings, and dicts.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
context: List of context items.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Formatted context string.
|
|
50
|
+
"""
|
|
51
|
+
if not context:
|
|
52
|
+
return "No context provided."
|
|
53
|
+
|
|
54
|
+
formatted_items = []
|
|
55
|
+
for item in context:
|
|
56
|
+
if isinstance(item, Chunk):
|
|
57
|
+
content = item.content
|
|
58
|
+
elif isinstance(item, dict):
|
|
59
|
+
content = str(item)
|
|
60
|
+
else:
|
|
61
|
+
content = str(item)
|
|
62
|
+
formatted_items.append(f"- {content}")
|
|
63
|
+
|
|
64
|
+
return "\n".join(formatted_items)
|
|
65
|
+
|
|
66
|
+
def _format_history(self, history: list[Any] | None) -> str:
|
|
67
|
+
"""Format the chat history into a string.
|
|
68
|
+
|
|
69
|
+
Supports gllm_inference Message objects and dicts.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
history: List of history items.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Formatted history string.
|
|
76
|
+
"""
|
|
77
|
+
if not history:
|
|
78
|
+
return "No previous history."
|
|
79
|
+
|
|
80
|
+
# Try to use gllm_inference schema if available for robust handling
|
|
81
|
+
try:
|
|
82
|
+
from gllm_inference.schema import Message # noqa: PLC0415
|
|
83
|
+
except ImportError:
|
|
84
|
+
Message = None
|
|
85
|
+
|
|
86
|
+
formatted_items = []
|
|
87
|
+
for item in history:
|
|
88
|
+
if Message and isinstance(item, Message):
|
|
89
|
+
# Message object has role and contents (list)
|
|
90
|
+
role = item.role.capitalize()
|
|
91
|
+
# Use standard content property if available, or join contents
|
|
92
|
+
content = getattr(item, "content", None)
|
|
93
|
+
if content is None and hasattr(item, "contents"):
|
|
94
|
+
content = "\n".join([str(c) for c in item.contents])
|
|
95
|
+
formatted_items.append(f"{role}: {content}")
|
|
96
|
+
elif isinstance(item, dict):
|
|
97
|
+
role = str(item.get("role", "User")).capitalize()
|
|
98
|
+
content = str(item.get("content", ""))
|
|
99
|
+
formatted_items.append(f"{role}: {content}")
|
|
100
|
+
else:
|
|
101
|
+
formatted_items.append(str(item))
|
|
102
|
+
return "\n".join(formatted_items)
|
|
103
|
+
|
|
104
|
+
def _compile_prompt(
|
|
105
|
+
self,
|
|
106
|
+
query: str,
|
|
107
|
+
context: list[Any] | None,
|
|
108
|
+
chat_history: list[Any] | None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Compile the raw inputs into a single text prompt.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
query: The user query.
|
|
114
|
+
context: List of context items.
|
|
115
|
+
chat_history: List of conversation history items.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The compiled prompt string.
|
|
119
|
+
"""
|
|
120
|
+
parts = []
|
|
121
|
+
|
|
122
|
+
if chat_history:
|
|
123
|
+
history_str = self._format_history(chat_history)
|
|
124
|
+
parts.append(f"Conversation History:\n{history_str}\n")
|
|
125
|
+
|
|
126
|
+
if context:
|
|
127
|
+
context_str = self._format_context(context)
|
|
128
|
+
parts.append(f"Relevant Context:\n{context_str}\n")
|
|
129
|
+
|
|
130
|
+
parts.append(f"User Question: {query}\n")
|
|
131
|
+
|
|
132
|
+
return "\n".join(parts)
|
|
133
|
+
|
|
134
|
+
async def run_agent(
|
|
135
|
+
self,
|
|
136
|
+
query: str,
|
|
137
|
+
context: list[Chunk | Any] | None = None,
|
|
138
|
+
chat_history: list[Any] | None = None,
|
|
139
|
+
runtime_config: dict[str, Any] | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""Run the agent with the provided context and history.
|
|
142
|
+
|
|
143
|
+
This method is the main entry point for the component logic.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
query: The user's input string.
|
|
147
|
+
context: List of retrieved documents/chunks or data.
|
|
148
|
+
chat_history: List of previous conversation turns.
|
|
149
|
+
runtime_config: Optional configuration.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The raw response dictionary from the agent.
|
|
153
|
+
"""
|
|
154
|
+
if not query:
|
|
155
|
+
raise ValueError("Query is required")
|
|
156
|
+
|
|
157
|
+
self._logger.info("Compiling prompt for agent: %s", self.agent.name)
|
|
158
|
+
|
|
159
|
+
prompt = self._compile_prompt(
|
|
160
|
+
query=query,
|
|
161
|
+
context=context,
|
|
162
|
+
chat_history=chat_history,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Execute agent asynchronously
|
|
166
|
+
last_chunk = {}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
async for chunk in self.agent.arun(message=prompt, runtime_config=runtime_config):
|
|
170
|
+
if isinstance(chunk, dict):
|
|
171
|
+
last_chunk = chunk
|
|
172
|
+
# For non-streaming (or final output), we often get 'final_response' event
|
|
173
|
+
# but local runner might yield other events first.
|
|
174
|
+
if chunk.get("event_type") == "final_response":
|
|
175
|
+
return chunk
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise RuntimeError(f"AgentComponent '{self.agent.name}' failed during execution: {e}") from e
|
|
178
|
+
|
|
179
|
+
return last_chunk
|
|
180
|
+
|
|
181
|
+
def _extract_content_string(self, result: Any) -> str:
|
|
182
|
+
"""Extract the content string from the agent response.
|
|
183
|
+
|
|
184
|
+
Assumes the result is always a string or a dictionary containing a content field.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
result: The agent response (dict or string).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The content string extracted from the response.
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(result, dict):
|
|
193
|
+
content = result.get("content")
|
|
194
|
+
if content is not None:
|
|
195
|
+
return str(content)
|
|
196
|
+
# Fallback: if no content field, return string representation
|
|
197
|
+
return str(result)
|
|
198
|
+
|
|
199
|
+
# If result is already a string, return it as-is
|
|
200
|
+
return str(result) if result is not None else ""
|
|
201
|
+
|
|
202
|
+
async def _run(self, **kwargs: Any) -> str:
|
|
203
|
+
"""Execute the component logic.
|
|
204
|
+
|
|
205
|
+
This method is called by the Component.run() method (and by Pipeline steps).
|
|
206
|
+
It delegates to run_agent() and extracts the content string from the response.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
**kwargs: Keyword arguments passed to run_agent.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
str: The content string from the agent response.
|
|
213
|
+
"""
|
|
214
|
+
result = await self.run_agent(
|
|
215
|
+
query=kwargs.get("query"),
|
|
216
|
+
context=kwargs.get("context"),
|
|
217
|
+
chat_history=kwargs.get("chat_history"),
|
|
218
|
+
runtime_config=kwargs.get("runtime_config"),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return self._extract_content_string(result)
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -455,8 +455,15 @@ class SlashSession:
|
|
|
455
455
|
# Small delay to ensure animation starts.
|
|
456
456
|
time.sleep(self.ANIMATION_STARTUP_DELAY)
|
|
457
457
|
|
|
458
|
+
def update_status(status: str) -> None:
|
|
459
|
+
state.current_status[0] = status
|
|
460
|
+
|
|
458
461
|
# Run initialization tasks.
|
|
459
|
-
if not self._run_initialization_tasks(
|
|
462
|
+
if not self._run_initialization_tasks(
|
|
463
|
+
state.current_status,
|
|
464
|
+
state.animation_running,
|
|
465
|
+
status_callback=update_status,
|
|
466
|
+
):
|
|
460
467
|
return False
|
|
461
468
|
|
|
462
469
|
# Stop animation and show final banner.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Textual UI helpers for slash commands."""
|
|
2
2
|
|
|
3
|
-
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
|
|
3
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardReadResult, ClipboardResult
|
|
4
4
|
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
5
5
|
from glaip_sdk.cli.slash.tui.indicators import PulseIndicator
|
|
6
6
|
from glaip_sdk.cli.slash.tui.keybind_registry import (
|
|
@@ -31,6 +31,7 @@ __all__ = [
|
|
|
31
31
|
"parse_key_sequence",
|
|
32
32
|
"format_key_sequence",
|
|
33
33
|
"ClipboardAdapter",
|
|
34
|
+
"ClipboardReadResult",
|
|
34
35
|
"ClipboardResult",
|
|
35
36
|
"PulseIndicator",
|
|
36
37
|
]
|
|
@@ -499,7 +499,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
|
|
|
499
499
|
self._account_callbacks = callbacks
|
|
500
500
|
self._keybinds: KeybindRegistry | None = None
|
|
501
501
|
self._toast_bus: ToastBus | None = None
|
|
502
|
-
self.
|
|
502
|
+
self._clip_cache: ClipboardAdapter | None = None
|
|
503
503
|
self._filter_text: str = ""
|
|
504
504
|
self._is_switching = False
|
|
505
505
|
self._selected_account: dict[str, str | bool] | None = None
|
|
@@ -579,12 +579,12 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
|
|
|
579
579
|
ctx.clipboard = ClipboardAdapter(terminal=ctx.terminal)
|
|
580
580
|
self._keybinds = ctx.keybinds
|
|
581
581
|
self._toast_bus = ctx.toasts
|
|
582
|
-
self.
|
|
582
|
+
self._clip_cache = ctx.clipboard
|
|
583
583
|
else:
|
|
584
584
|
terminal = TerminalCapabilities(
|
|
585
585
|
tty=True, ansi=True, osc52=False, osc11_bg=None, mouse=False, truecolor=False
|
|
586
586
|
)
|
|
587
|
-
self.
|
|
587
|
+
self._clip_cache = ClipboardAdapter(terminal=terminal)
|
|
588
588
|
if ToastBus is not None:
|
|
589
589
|
self._toast_bus = ToastBus(on_change=_notify)
|
|
590
590
|
|
|
@@ -962,7 +962,7 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
|
|
|
962
962
|
return
|
|
963
963
|
|
|
964
964
|
text = f"Account: {name}\nURL: {account.get('api_url', '')}"
|
|
965
|
-
adapter = self.
|
|
965
|
+
adapter = self._clip_adapter()
|
|
966
966
|
writer = self._osc52_writer()
|
|
967
967
|
if writer:
|
|
968
968
|
result = adapter.copy(text, writer=writer)
|
|
@@ -981,18 +981,18 @@ class AccountsHarlequinScreen( # pragma: no cover - interactive
|
|
|
981
981
|
self._toast_bus.show(message=f"Copy failed: {result.message}", variant=ToastVariant.WARNING)
|
|
982
982
|
self._set_status(f"Copy failed: {result.message}", "red")
|
|
983
983
|
|
|
984
|
-
def
|
|
984
|
+
def _clip_adapter(self) -> ClipboardAdapter:
|
|
985
985
|
"""Get clipboard adapter."""
|
|
986
986
|
ctx = self.ctx if hasattr(self, "ctx") else getattr(self, "_ctx", None)
|
|
987
987
|
if ctx is not None and ctx.clipboard is not None:
|
|
988
988
|
return cast(ClipboardAdapter, ctx.clipboard)
|
|
989
|
-
if self.
|
|
990
|
-
return self.
|
|
989
|
+
if self._clip_cache is not None:
|
|
990
|
+
return self._clip_cache
|
|
991
991
|
adapter = ClipboardAdapter(terminal=ctx.terminal if ctx else None)
|
|
992
992
|
if ctx is not None:
|
|
993
993
|
ctx.clipboard = adapter
|
|
994
994
|
else:
|
|
995
|
-
self.
|
|
995
|
+
self._clip_cache = adapter
|
|
996
996
|
return adapter
|
|
997
997
|
|
|
998
998
|
def _osc52_writer(self) -> Callable[[str], Any] | None:
|
|
@@ -1190,11 +1190,25 @@ class AccountsTextualApp( # pragma: no cover - interactive
|
|
|
1190
1190
|
self._ctx = ctx
|
|
1191
1191
|
self._keybinds: KeybindRegistry | None = None
|
|
1192
1192
|
self._toast_bus: ToastBus | None = None
|
|
1193
|
-
self.
|
|
1193
|
+
self._clip_cache: ClipboardAdapter | None = None
|
|
1194
1194
|
self._filter_text: str = ""
|
|
1195
1195
|
self._is_switching = False
|
|
1196
1196
|
self._initialize_context_services()
|
|
1197
1197
|
|
|
1198
|
+
@property
|
|
1199
|
+
def clipboard(self) -> str:
|
|
1200
|
+
"""Return clipboard text for Input paste actions."""
|
|
1201
|
+
result = self._clip_adapter().read()
|
|
1202
|
+
if result.success:
|
|
1203
|
+
return result.text
|
|
1204
|
+
return super().clipboard
|
|
1205
|
+
|
|
1206
|
+
@clipboard.setter
|
|
1207
|
+
def clipboard(self, value: str) -> None:
|
|
1208
|
+
setter = App.clipboard.fset
|
|
1209
|
+
if setter is not None:
|
|
1210
|
+
setter(self, value)
|
|
1211
|
+
|
|
1198
1212
|
def compose(self) -> ComposeResult:
|
|
1199
1213
|
"""Build the Textual app (empty, screen is pushed on mount)."""
|
|
1200
1214
|
# The app itself is empty; AccountsHarlequinScreen is pushed on mount
|
|
@@ -1227,13 +1241,13 @@ class AccountsTextualApp( # pragma: no cover - interactive
|
|
|
1227
1241
|
self._ctx.clipboard = ClipboardAdapter(terminal=self._ctx.terminal)
|
|
1228
1242
|
self._keybinds = self._ctx.keybinds
|
|
1229
1243
|
self._toast_bus = self._ctx.toasts
|
|
1230
|
-
self.
|
|
1244
|
+
self._clip_cache = self._ctx.clipboard
|
|
1231
1245
|
else:
|
|
1232
1246
|
# Fallback: create services independently when ctx is None
|
|
1233
1247
|
terminal = TerminalCapabilities(
|
|
1234
1248
|
tty=True, ansi=True, osc52=False, osc11_bg=None, mouse=False, truecolor=False
|
|
1235
1249
|
)
|
|
1236
|
-
self.
|
|
1250
|
+
self._clip_cache = ClipboardAdapter(terminal=terminal)
|
|
1237
1251
|
if ToastBus is not None:
|
|
1238
1252
|
self._toast_bus = ToastBus(on_change=_notify)
|
|
1239
1253
|
|
|
@@ -1654,7 +1668,7 @@ class AccountsTextualApp( # pragma: no cover - interactive
|
|
|
1654
1668
|
return
|
|
1655
1669
|
|
|
1656
1670
|
text = f"Account: {name}\nURL: {account.get('api_url', '')}"
|
|
1657
|
-
adapter = self.
|
|
1671
|
+
adapter = self._clip_adapter()
|
|
1658
1672
|
writer = self._osc52_writer()
|
|
1659
1673
|
if writer:
|
|
1660
1674
|
result = adapter.copy(text, writer=writer)
|
|
@@ -1673,16 +1687,16 @@ class AccountsTextualApp( # pragma: no cover - interactive
|
|
|
1673
1687
|
self._toast_bus.show(message=f"Copy failed: {result.message}", variant=ToastVariant.WARNING)
|
|
1674
1688
|
self._set_status(f"Copy failed: {result.message}", "red")
|
|
1675
1689
|
|
|
1676
|
-
def
|
|
1690
|
+
def _clip_adapter(self) -> ClipboardAdapter:
|
|
1677
1691
|
if self._ctx is not None and self._ctx.clipboard is not None:
|
|
1678
1692
|
return cast(ClipboardAdapter, self._ctx.clipboard)
|
|
1679
|
-
if self.
|
|
1680
|
-
return self.
|
|
1693
|
+
if self._clip_cache is not None:
|
|
1694
|
+
return self._clip_cache
|
|
1681
1695
|
adapter = ClipboardAdapter(terminal=self._ctx.terminal if self._ctx else None)
|
|
1682
1696
|
if self._ctx is not None:
|
|
1683
1697
|
self._ctx.clipboard = adapter
|
|
1684
1698
|
else:
|
|
1685
|
-
self.
|
|
1699
|
+
self._clip_cache = adapter
|
|
1686
1700
|
return adapter
|
|
1687
1701
|
|
|
1688
1702
|
def _osc52_writer(self) -> Callable[[str], Any] | None:
|
|
@@ -37,6 +37,16 @@ class ClipboardResult:
|
|
|
37
37
|
message: str
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ClipboardReadResult:
|
|
42
|
+
"""Result of a clipboard read operation."""
|
|
43
|
+
|
|
44
|
+
success: bool
|
|
45
|
+
method: ClipboardMethod
|
|
46
|
+
message: str
|
|
47
|
+
text: str
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
_SUBPROCESS_COMMANDS: dict[ClipboardMethod, list[str]] = {
|
|
41
51
|
ClipboardMethod.PBCOPY: ["pbcopy"],
|
|
42
52
|
ClipboardMethod.XCLIP: ["xclip", "-selection", "clipboard"],
|
|
@@ -44,6 +54,12 @@ _SUBPROCESS_COMMANDS: dict[ClipboardMethod, list[str]] = {
|
|
|
44
54
|
ClipboardMethod.WL_COPY: ["wl-copy"],
|
|
45
55
|
ClipboardMethod.CLIP: ["clip"],
|
|
46
56
|
}
|
|
57
|
+
_SUBPROCESS_READ_COMMANDS: dict[ClipboardMethod, list[str]] = {
|
|
58
|
+
ClipboardMethod.PBCOPY: ["pbpaste"],
|
|
59
|
+
ClipboardMethod.XCLIP: ["xclip", "-selection", "clipboard", "-o"],
|
|
60
|
+
ClipboardMethod.XSEL: ["xsel", "--clipboard", "--output"],
|
|
61
|
+
ClipboardMethod.WL_COPY: ["wl-paste", "--no-newline"],
|
|
62
|
+
}
|
|
47
63
|
|
|
48
64
|
_ENV_CLIPBOARD_METHOD = "AIP_TUI_CLIPBOARD_METHOD"
|
|
49
65
|
_ENV_CLIPBOARD_FORCE = "AIP_TUI_CLIPBOARD_FORCE"
|
|
@@ -58,6 +74,8 @@ _ENV_METHOD_MAP = {
|
|
|
58
74
|
"none": ClipboardMethod.NONE,
|
|
59
75
|
}
|
|
60
76
|
|
|
77
|
+
_SUBPROCESS_TIMEOUT = 2.0
|
|
78
|
+
|
|
61
79
|
|
|
62
80
|
def _resolve_env_method() -> ClipboardMethod | None:
|
|
63
81
|
raw = os.getenv(_ENV_CLIPBOARD_METHOD)
|
|
@@ -76,6 +94,13 @@ def _is_env_force_enabled() -> bool:
|
|
|
76
94
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
77
95
|
|
|
78
96
|
|
|
97
|
+
def _resolve_windows_read_command() -> list[str] | None:
|
|
98
|
+
for shell in ("powershell", "pwsh"):
|
|
99
|
+
if shutil.which(shell):
|
|
100
|
+
return [shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"]
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
79
104
|
class ClipboardAdapter:
|
|
80
105
|
"""Cross-platform clipboard access with OSC 52 fallback."""
|
|
81
106
|
|
|
@@ -88,6 +113,7 @@ class ClipboardAdapter:
|
|
|
88
113
|
"""Initialize the adapter."""
|
|
89
114
|
self._terminal = terminal
|
|
90
115
|
self._force_method = False
|
|
116
|
+
self._fallback_methods_cache: list[ClipboardMethod] | None = None
|
|
91
117
|
if method is not None:
|
|
92
118
|
self._method = method
|
|
93
119
|
else:
|
|
@@ -122,12 +148,30 @@ class ClipboardAdapter:
|
|
|
122
148
|
|
|
123
149
|
result = self._copy_subprocess(command, text)
|
|
124
150
|
if not result.success:
|
|
125
|
-
if self._force_method:
|
|
151
|
+
if self._force_method or "timed out" in result.message:
|
|
126
152
|
return result
|
|
127
153
|
return self._copy_osc52(text, writer=writer)
|
|
128
154
|
|
|
129
155
|
return result
|
|
130
156
|
|
|
157
|
+
def read(self) -> ClipboardReadResult:
|
|
158
|
+
"""Read text from the clipboard using the best available method."""
|
|
159
|
+
result = self._read_with_method(self._method)
|
|
160
|
+
if result.success or self._force_method:
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
if self._fallback_methods_cache is None:
|
|
164
|
+
self._fallback_methods_cache = self._fallback_read_methods()
|
|
165
|
+
|
|
166
|
+
for method in self._fallback_methods_cache:
|
|
167
|
+
if method is self._method:
|
|
168
|
+
continue
|
|
169
|
+
fallback = self._read_with_method(method)
|
|
170
|
+
if fallback.success:
|
|
171
|
+
return fallback
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
|
|
131
175
|
def _detect_method(self) -> ClipboardMethod:
|
|
132
176
|
system = platform.system()
|
|
133
177
|
method = ClipboardMethod.NONE
|
|
@@ -153,12 +197,10 @@ class ClipboardAdapter:
|
|
|
153
197
|
if not os.getenv("DISPLAY") and not os.getenv("WAYLAND_DISPLAY"):
|
|
154
198
|
return ClipboardMethod.NONE
|
|
155
199
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
):
|
|
161
|
-
if shutil.which(cmd):
|
|
200
|
+
# Order of preference: Wayland then X11 tools
|
|
201
|
+
for method in (ClipboardMethod.WL_COPY, ClipboardMethod.XCLIP, ClipboardMethod.XSEL):
|
|
202
|
+
cmd = _SUBPROCESS_COMMANDS.get(method)
|
|
203
|
+
if cmd and shutil.which(cmd[0]):
|
|
162
204
|
return method
|
|
163
205
|
return ClipboardMethod.NONE
|
|
164
206
|
|
|
@@ -185,7 +227,10 @@ class ClipboardAdapter:
|
|
|
185
227
|
cmd,
|
|
186
228
|
input=text.encode("utf-8"),
|
|
187
229
|
check=False,
|
|
230
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
188
231
|
)
|
|
232
|
+
except subprocess.TimeoutExpired:
|
|
233
|
+
return ClipboardResult(False, self._method, f"Clipboard command timed out after {_SUBPROCESS_TIMEOUT}s")
|
|
189
234
|
except OSError as exc:
|
|
190
235
|
return ClipboardResult(False, self._method, str(exc))
|
|
191
236
|
|
|
@@ -193,3 +238,79 @@ class ClipboardAdapter:
|
|
|
193
238
|
return ClipboardResult(True, self._method, "Copied to clipboard")
|
|
194
239
|
|
|
195
240
|
return ClipboardResult(False, self._method, f"Command failed: {completed.returncode}")
|
|
241
|
+
|
|
242
|
+
def _read_with_method(self, method: ClipboardMethod) -> ClipboardReadResult:
|
|
243
|
+
if method is ClipboardMethod.OSC52:
|
|
244
|
+
# OSC 52 read requires an asynchronous terminal response (DSR) which is
|
|
245
|
+
# significantly more complex to implement than synchronous subprocess reads.
|
|
246
|
+
# Currently out of scope.
|
|
247
|
+
return ClipboardReadResult(False, method, "OSC 52 clipboard read is unsupported.", "")
|
|
248
|
+
if method is ClipboardMethod.NONE:
|
|
249
|
+
return ClipboardReadResult(False, method, "Clipboard backend unavailable.", "")
|
|
250
|
+
|
|
251
|
+
if method is ClipboardMethod.CLIP:
|
|
252
|
+
command = _resolve_windows_read_command()
|
|
253
|
+
if command is None:
|
|
254
|
+
return ClipboardReadResult(False, method, "PowerShell clipboard read unavailable.", "")
|
|
255
|
+
return self._read_subprocess(command, method)
|
|
256
|
+
|
|
257
|
+
command = _SUBPROCESS_READ_COMMANDS.get(method)
|
|
258
|
+
if command is None:
|
|
259
|
+
return ClipboardReadResult(False, method, "Clipboard read method unavailable.", "")
|
|
260
|
+
|
|
261
|
+
return self._read_subprocess(command, method)
|
|
262
|
+
|
|
263
|
+
def _read_subprocess(self, cmd: list[str], method: ClipboardMethod) -> ClipboardReadResult:
|
|
264
|
+
try:
|
|
265
|
+
completed = subprocess.run(
|
|
266
|
+
cmd,
|
|
267
|
+
capture_output=True,
|
|
268
|
+
text=True,
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
check=False,
|
|
271
|
+
timeout=_SUBPROCESS_TIMEOUT,
|
|
272
|
+
)
|
|
273
|
+
except subprocess.TimeoutExpired:
|
|
274
|
+
return ClipboardReadResult(False, method, f"Clipboard command timed out after {_SUBPROCESS_TIMEOUT}s", "")
|
|
275
|
+
except OSError as exc:
|
|
276
|
+
return ClipboardReadResult(False, method, str(exc), "")
|
|
277
|
+
|
|
278
|
+
if completed.returncode == 0:
|
|
279
|
+
return ClipboardReadResult(True, method, "Read from clipboard", completed.stdout)
|
|
280
|
+
|
|
281
|
+
return ClipboardReadResult(False, method, f"Command failed: {completed.returncode}", "")
|
|
282
|
+
|
|
283
|
+
def _fallback_read_methods(self) -> list[ClipboardMethod]:
|
|
284
|
+
system = platform.system()
|
|
285
|
+
if system == "Darwin":
|
|
286
|
+
return self._fallback_darwin()
|
|
287
|
+
if system == "Linux":
|
|
288
|
+
return self._fallback_linux()
|
|
289
|
+
if system == "Windows":
|
|
290
|
+
return self._fallback_windows()
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
def _fallback_darwin(self) -> list[ClipboardMethod]:
|
|
294
|
+
cmd = _SUBPROCESS_READ_COMMANDS.get(ClipboardMethod.PBCOPY)
|
|
295
|
+
if cmd and shutil.which(cmd[0]):
|
|
296
|
+
return [ClipboardMethod.PBCOPY]
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
def _fallback_linux(self) -> list[ClipboardMethod]:
|
|
300
|
+
methods: list[ClipboardMethod] = []
|
|
301
|
+
for method in (ClipboardMethod.WL_COPY, ClipboardMethod.XCLIP, ClipboardMethod.XSEL):
|
|
302
|
+
cmd = _SUBPROCESS_READ_COMMANDS.get(method)
|
|
303
|
+
if not cmd:
|
|
304
|
+
continue
|
|
305
|
+
if method == ClipboardMethod.WL_COPY and not os.getenv("WAYLAND_DISPLAY"):
|
|
306
|
+
continue
|
|
307
|
+
if method in (ClipboardMethod.XCLIP, ClipboardMethod.XSEL) and not os.getenv("DISPLAY"):
|
|
308
|
+
continue
|
|
309
|
+
if shutil.which(cmd[0]):
|
|
310
|
+
methods.append(method)
|
|
311
|
+
return methods
|
|
312
|
+
|
|
313
|
+
def _fallback_windows(self) -> list[ClipboardMethod]:
|
|
314
|
+
if _resolve_windows_read_command():
|
|
315
|
+
return [ClipboardMethod.CLIP]
|
|
316
|
+
return []
|
|
@@ -121,7 +121,7 @@ class RunDetailScreen(ToastHandlerMixin, ClipboardToastMixin, ModalScreen[None])
|
|
|
121
121
|
self.detail = detail
|
|
122
122
|
self._on_export = on_export
|
|
123
123
|
self._ctx = ctx
|
|
124
|
-
self.
|
|
124
|
+
self._clip_cache: ClipboardAdapter | None = None
|
|
125
125
|
self._local_toasts: ToastBus | None = None
|
|
126
126
|
|
|
127
127
|
def compose(self) -> ComposeResult:
|
|
@@ -366,11 +366,37 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
|
|
|
366
366
|
self.agent_name = (agent_name or "").strip()
|
|
367
367
|
self.agent_id = (agent_id or "").strip()
|
|
368
368
|
self._ctx = ctx
|
|
369
|
+
self._clip_cache: ClipboardAdapter | None = None
|
|
369
370
|
self._active_export_tasks: set[asyncio.Task[None]] = set()
|
|
370
371
|
self._page_loader_task: asyncio.Task[Any] | None = None
|
|
371
372
|
self._detail_loader_task: asyncio.Task[Any] | None = None
|
|
372
373
|
self._table_spinner_active = False
|
|
373
374
|
|
|
375
|
+
@property
|
|
376
|
+
def clipboard(self) -> str:
|
|
377
|
+
"""Return clipboard text for Input paste actions."""
|
|
378
|
+
if self._ctx is not None:
|
|
379
|
+
adapter = self._ctx.clipboard
|
|
380
|
+
if adapter is None:
|
|
381
|
+
adapter = ClipboardAdapter(terminal=self._ctx.terminal)
|
|
382
|
+
self._ctx.clipboard = adapter
|
|
383
|
+
result = adapter.read()
|
|
384
|
+
if result.success:
|
|
385
|
+
return result.text
|
|
386
|
+
if self._ctx is None and self._clip_cache is None:
|
|
387
|
+
self._clip_cache = ClipboardAdapter(terminal=None)
|
|
388
|
+
if self._clip_cache is not None:
|
|
389
|
+
result = self._clip_cache.read()
|
|
390
|
+
if result.success:
|
|
391
|
+
return result.text
|
|
392
|
+
return super().clipboard
|
|
393
|
+
|
|
394
|
+
@clipboard.setter
|
|
395
|
+
def clipboard(self, value: str) -> None:
|
|
396
|
+
setter = App.clipboard.fset
|
|
397
|
+
if setter is not None:
|
|
398
|
+
setter(self, value)
|
|
399
|
+
|
|
374
400
|
def compose(self) -> ComposeResult:
|
|
375
401
|
"""Build layout."""
|
|
376
402
|
yield Header()
|
glaip_sdk/cli/slash/tui/toast.py
CHANGED
|
@@ -179,11 +179,11 @@ class ClipboardToastMixin:
|
|
|
179
179
|
|
|
180
180
|
Expected attributes:
|
|
181
181
|
_ctx: TUIContext | None - Shared TUI context (optional)
|
|
182
|
-
|
|
182
|
+
_clip_cache: ClipboardAdapter | None - Cached clipboard adapter (optional)
|
|
183
183
|
_local_toasts: ToastBus | None - Local toast bus instance (optional)
|
|
184
184
|
"""
|
|
185
185
|
|
|
186
|
-
def
|
|
186
|
+
def _clip_adapter(self) -> Any: # ClipboardAdapter
|
|
187
187
|
"""Get or create a clipboard adapter instance.
|
|
188
188
|
|
|
189
189
|
Returns:
|
|
@@ -193,7 +193,7 @@ class ClipboardToastMixin:
|
|
|
193
193
|
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter # noqa: PLC0415
|
|
194
194
|
|
|
195
195
|
ctx = getattr(self, "_ctx", None)
|
|
196
|
-
clipboard = getattr(self, "
|
|
196
|
+
clipboard = getattr(self, "_clip_cache", None)
|
|
197
197
|
|
|
198
198
|
if ctx is not None and ctx.clipboard is not None:
|
|
199
199
|
return cast(ClipboardAdapter, ctx.clipboard)
|
|
@@ -204,7 +204,7 @@ class ClipboardToastMixin:
|
|
|
204
204
|
if ctx is not None:
|
|
205
205
|
ctx.clipboard = adapter
|
|
206
206
|
else:
|
|
207
|
-
self.
|
|
207
|
+
self._clip_cache = adapter
|
|
208
208
|
return adapter
|
|
209
209
|
|
|
210
210
|
def _osc52_writer(self) -> Callable[[str], Any] | None:
|
|
@@ -258,7 +258,7 @@ class ClipboardToastMixin:
|
|
|
258
258
|
text: The text to copy to clipboard.
|
|
259
259
|
label: Optional label for what was copied (e.g., "Run ID", "JSON").
|
|
260
260
|
"""
|
|
261
|
-
adapter = self.
|
|
261
|
+
adapter = self._clip_adapter()
|
|
262
262
|
writer = self._osc52_writer()
|
|
263
263
|
if writer:
|
|
264
264
|
result = adapter.copy(text, writer=writer)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glaip-sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.18
|
|
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
|
|
@@ -27,6 +27,9 @@ Provides-Extra: privacy
|
|
|
27
27
|
Requires-Dist: aip-agents-binary[privacy]>=0.5.23; (python_version >= "3.11" and python_version < "3.13") and extra == "privacy"
|
|
28
28
|
Provides-Extra: guardrails
|
|
29
29
|
Requires-Dist: aip-agents-binary[guardrails]>=0.5.23; (python_version >= "3.11" and python_version < "3.13") and extra == "guardrails"
|
|
30
|
+
Provides-Extra: pipeline
|
|
31
|
+
Requires-Dist: gllm-pipeline-binary==0.4.13; extra == "pipeline"
|
|
32
|
+
Requires-Dist: gllm-inference-binary<0.6.0,>=0.5.0; extra == "pipeline"
|
|
30
33
|
Provides-Extra: dev
|
|
31
34
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
35
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -5,7 +5,8 @@ glaip_sdk/exceptions.py,sha256=iAChFClkytXRBLP0vZq1_YjoZxA9i4m4bW1gDLiGR1g,2321
|
|
|
5
5
|
glaip_sdk/icons.py,sha256=J5THz0ReAmDwIiIooh1_G3Le-mwTJyEjhJDdJ13KRxM,524
|
|
6
6
|
glaip_sdk/rich_components.py,sha256=44Z0V1ZQleVh9gUDGwRR5mriiYFnVGOhm7fFxZYbP8c,4052
|
|
7
7
|
glaip_sdk/agents/__init__.py,sha256=VfYov56edbWuySXFEbWJ_jLXgwnFzPk1KB-9-mfsUCc,776
|
|
8
|
-
glaip_sdk/agents/base.py,sha256=
|
|
8
|
+
glaip_sdk/agents/base.py,sha256=67rm67YWqwYjPcKD2-0Qc9Il7O6-8eRtd7lla_ZvALY,51259
|
|
9
|
+
glaip_sdk/agents/component.py,sha256=rY7Te7K62ABinB4JdzrKS-QmPIus9DcFuyjmZzyepqk,7279
|
|
9
10
|
glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
|
|
10
11
|
glaip_sdk/cli/account_store.py,sha256=u_memecwEQssustZs2wYBrHbEmKUlDfmmL-zO1F3n3A,19034
|
|
11
12
|
glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
|
|
@@ -78,19 +79,19 @@ glaip_sdk/cli/slash/accounts_shared.py,sha256=Mq5HxlI0YsVEQ0KKISWvyBZhzOFFWCzwRb
|
|
|
78
79
|
glaip_sdk/cli/slash/agent_session.py,sha256=tuVOme-NbEyr6rwJvsBEKZYWQmsaRf4piJeRvIGu0ns,11384
|
|
79
80
|
glaip_sdk/cli/slash/prompt.py,sha256=q4f1c2zr7ZMUeO6AgOBF2Nz4qgMOXrVPt6WzPRQMbAM,8501
|
|
80
81
|
glaip_sdk/cli/slash/remote_runs_controller.py,sha256=iLl4a-mu9QU7dcedgEILewPtDIVtFUJkbKGtcx1F66U,21445
|
|
81
|
-
glaip_sdk/cli/slash/session.py,sha256=
|
|
82
|
-
glaip_sdk/cli/slash/tui/__init__.py,sha256=
|
|
82
|
+
glaip_sdk/cli/slash/session.py,sha256=XhtWvm9Nl0yF4nN4bWkfW6ugih4QBEanS_GSWaZ8Lns,76458
|
|
83
|
+
glaip_sdk/cli/slash/tui/__init__.py,sha256=N0nRo_IGIQ3l5LikZTDrwbK5HX9nqYNzwpFeM9crJQg,1109
|
|
83
84
|
glaip_sdk/cli/slash/tui/accounts.tcss,sha256=5iVZZfS10CTJhnoZ9AFJejtj8nyQXH9xV7u9k8jSkGE,2411
|
|
84
|
-
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=
|
|
85
|
+
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=CFjAHV0JbTSMoMoCQ0CIGa_8C8xypjHgV-VLDji-uzk,73590
|
|
85
86
|
glaip_sdk/cli/slash/tui/background_tasks.py,sha256=SAe1mV2vXB3mJcSGhelU950vf8Lifjhws9iomyIVFKw,2422
|
|
86
|
-
glaip_sdk/cli/slash/tui/clipboard.py,sha256=
|
|
87
|
+
glaip_sdk/cli/slash/tui/clipboard.py,sha256=Rb1n6nYsjTgMfSMTVo4HisW8ZM3na2REtd3OHEy-Lz0,11255
|
|
87
88
|
glaip_sdk/cli/slash/tui/context.py,sha256=mzI4TDXnfZd42osACp5uo10d10y1_A0z6IxRK1KVoVk,3320
|
|
88
89
|
glaip_sdk/cli/slash/tui/indicators.py,sha256=jV3fFvEVWQ0inWJJ-B1fMsdkF0Uq2zwX3xcl0YWPHSE,11768
|
|
89
90
|
glaip_sdk/cli/slash/tui/keybind_registry.py,sha256=_rK05BxTxNudYc4iJ9gDxpgeUkjDAq8rarIT-9A-jyM,6739
|
|
90
91
|
glaip_sdk/cli/slash/tui/loading.py,sha256=Ku7HyQ_h-r2dJQ5aIEaCOi5PUu5gSsYle8oiKHIxfKI,2336
|
|
91
|
-
glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=
|
|
92
|
+
glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=HtjrSKC_mqrzVBmK3ycehaRtaBEK3HsqDDkDp16kbvc,30356
|
|
92
93
|
glaip_sdk/cli/slash/tui/terminal.py,sha256=ZAC3sB17TGpl-GFeRVm_nI8DQTN3pyti3ynlZ41wT_A,12323
|
|
93
|
-
glaip_sdk/cli/slash/tui/toast.py,sha256=
|
|
94
|
+
glaip_sdk/cli/slash/tui/toast.py,sha256=3M7mtJAZfEWtMNhC8f1SpUCDZ_jlqhXRt_ll2Mohfg8,12435
|
|
94
95
|
glaip_sdk/cli/slash/tui/layouts/__init__.py,sha256=KT77pZHa7Wz84QlHYT2mfhQ_AXUA-T0eHv_HtAvc1ac,473
|
|
95
96
|
glaip_sdk/cli/slash/tui/layouts/harlequin.py,sha256=JOsaK18jTojzZ-Py-87foxfijuRDWwi8LIWmqM6qS0k,5644
|
|
96
97
|
glaip_sdk/cli/slash/tui/theme/__init__.py,sha256=rtM2ik83YNCRcI1qh_Sf3rnxco2OvCNNT3NbHY6cLvw,432
|
|
@@ -217,8 +218,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
|
|
|
217
218
|
glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
|
|
218
219
|
glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
|
|
219
220
|
glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
|
|
220
|
-
glaip_sdk-0.7.
|
|
221
|
-
glaip_sdk-0.7.
|
|
222
|
-
glaip_sdk-0.7.
|
|
223
|
-
glaip_sdk-0.7.
|
|
224
|
-
glaip_sdk-0.7.
|
|
221
|
+
glaip_sdk-0.7.18.dist-info/METADATA,sha256=jOAf3xHLC_UL7GxuMpLomL-wMFC1iReLx7iBywrK1YA,8690
|
|
222
|
+
glaip_sdk-0.7.18.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
223
|
+
glaip_sdk-0.7.18.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
|
|
224
|
+
glaip_sdk-0.7.18.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
|
|
225
|
+
glaip_sdk-0.7.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|