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 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)
@@ -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(state.current_status, state.animation_running, status_callback=None):
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._clipboard: ClipboardAdapter | None = None
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._clipboard = ctx.clipboard
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._clipboard = ClipboardAdapter(terminal=terminal)
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._clipboard_adapter()
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 _clipboard_adapter(self) -> ClipboardAdapter:
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._clipboard is not None:
990
- return self._clipboard
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._clipboard = adapter
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._clipboard: ClipboardAdapter | None = None
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._clipboard = self._ctx.clipboard
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._clipboard = ClipboardAdapter(terminal=terminal)
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._clipboard_adapter()
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 _clipboard_adapter(self) -> ClipboardAdapter:
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._clipboard is not None:
1680
- return self._clipboard
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._clipboard = adapter
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
- for cmd, method in (
157
- ("xclip", ClipboardMethod.XCLIP),
158
- ("xsel", ClipboardMethod.XSEL),
159
- ("wl-copy", ClipboardMethod.WL_COPY),
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._clipboard: ClipboardAdapter | None = None
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()
@@ -179,11 +179,11 @@ class ClipboardToastMixin:
179
179
 
180
180
  Expected attributes:
181
181
  _ctx: TUIContext | None - Shared TUI context (optional)
182
- _clipboard: ClipboardAdapter | None - Cached clipboard adapter (optional)
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 _clipboard_adapter(self) -> Any: # ClipboardAdapter
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, "_clipboard", None)
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._clipboard = adapter
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._clipboard_adapter()
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.17
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=OCJP2yBOo1rYqUAzpwdcEb2ZjIGHj9-ivDvldzfQX48,50813
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=jWTPrt374tDTt3tN-nBQ5wb2ssc60yMSAcSp4FNej2Y,76308
82
- glaip_sdk/cli/slash/tui/__init__.py,sha256=hAjH4ULBhRpQzA6fBLWRV6LiVm8UM3lgPurjoX9muYU,1061
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=5FtQy57xdUtBfOVRsqjXSbWLsLna7wIoMOz9t3h_ptQ,73187
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=7fEshhTwHYaj-n7n0W0AsWTs8W0RLZw_9luXxrFTrtw,6227
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=MVbzx-VJGi-wGSzr_CjQbdZbcWboVEbrCZUCuH2AeJM,29388
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=XGITLHhO40xIGmtg9hanPmDsPCQY2hQXzoM_9mJXQyg,12442
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.17.dist-info/METADATA,sha256=2UIAIq_AbfbpO5alFsJOnFnT6sH-Vkyweaf6cqyPCxM,8528
221
- glaip_sdk-0.7.17.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
222
- glaip_sdk-0.7.17.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
223
- glaip_sdk-0.7.17.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
224
- glaip_sdk-0.7.17.dist-info/RECORD,,
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,,