glaip-sdk 0.6.25__py3-none-any.whl → 0.7.0__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/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/slash/tui/__init__.py +10 -1
- glaip_sdk/cli/slash/tui/context.py +51 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/client/agents.py +1 -1
- glaip_sdk/client/main.py +1 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/tools.py +52 -23
- glaip_sdk/registry/tool.py +193 -81
- glaip_sdk/tools/base.py +41 -10
- glaip_sdk/utils/import_resolver.py +40 -2
- {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.7.0.dist-info}/METADATA +2 -2
- {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.7.0.dist-info}/RECORD +51 -18
- glaip_sdk/cli/commands/agents.py +0 -1502
- glaip_sdk/cli/commands/mcps.py +0 -1355
- glaip_sdk/cli/commands/tools.py +0 -575
- /glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +0 -0
- {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.7.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.7.0.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Terminal capability detection for TUI applications.
|
|
2
|
+
|
|
3
|
+
This module provides terminal capability detection including TTY status, ANSI support,
|
|
4
|
+
OSC 52 clipboard support, mouse support, truecolor support, and OSC 11 background
|
|
5
|
+
color detection for automatic theme selection.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import select
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
# Windows compatibility: termios and tty may not be available
|
|
23
|
+
try:
|
|
24
|
+
import termios
|
|
25
|
+
import tty
|
|
26
|
+
|
|
27
|
+
_TERMIOS_AVAILABLE = True
|
|
28
|
+
except ImportError: # pragma: no cover
|
|
29
|
+
# Platform-specific: Windows doesn't have termios/tty modules
|
|
30
|
+
# This exception is only raised on Windows or systems without termios support
|
|
31
|
+
# Testing would require complex module reloading and platform-specific test setup
|
|
32
|
+
_TERMIOS_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TerminalCapabilities:
|
|
37
|
+
"""Terminal feature detection results.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
tty: Whether stdout is a TTY.
|
|
41
|
+
ansi: Whether ANSI escape sequences are supported.
|
|
42
|
+
osc52: Whether OSC 52 (clipboard) is supported.
|
|
43
|
+
osc11_bg: Raw RGB color string from OSC 11 query, or None if not detected.
|
|
44
|
+
mouse: Whether mouse support is available.
|
|
45
|
+
truecolor: Whether truecolor (24-bit) color is supported.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
tty: bool
|
|
49
|
+
ansi: bool
|
|
50
|
+
osc52: bool
|
|
51
|
+
osc11_bg: str | None
|
|
52
|
+
mouse: bool
|
|
53
|
+
truecolor: bool
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def background_mode(self) -> Literal["light", "dark"]:
|
|
57
|
+
"""Derive light/dark mode from OSC 11 background color.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
"light" if luminance > 0.5, "dark" otherwise. Defaults to "dark"
|
|
61
|
+
if osc11_bg is None.
|
|
62
|
+
"""
|
|
63
|
+
if self.osc11_bg is None:
|
|
64
|
+
return "dark"
|
|
65
|
+
|
|
66
|
+
rgb = _parse_color_response(self.osc11_bg)
|
|
67
|
+
if rgb is None:
|
|
68
|
+
return "dark"
|
|
69
|
+
|
|
70
|
+
luminance = _calculate_luminance(rgb[0], rgb[1], rgb[2])
|
|
71
|
+
return "light" if luminance > 0.5 else "dark"
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
async def detect(cls) -> TerminalCapabilities:
|
|
75
|
+
"""Detect terminal capabilities asynchronously with fast timeout.
|
|
76
|
+
|
|
77
|
+
This method performs capability detection including OSC 11 background
|
|
78
|
+
color detection with a 100ms timeout. The method completes quickly
|
|
79
|
+
(< 100ms) as required by the roadmap. OSC 11 detection may return None
|
|
80
|
+
if the terminal doesn't respond within the timeout; use
|
|
81
|
+
detect_terminal_background() for full 1-second timeout when needed.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
TerminalCapabilities instance with detected capabilities.
|
|
85
|
+
"""
|
|
86
|
+
tty_available = sys.stdout.isatty()
|
|
87
|
+
term = os.environ.get("TERM", "")
|
|
88
|
+
colorterm = os.environ.get("COLORTERM", "")
|
|
89
|
+
|
|
90
|
+
# Basic capability detection
|
|
91
|
+
ansi = tty_available and term not in ("dumb", "")
|
|
92
|
+
osc52 = _detect_osc52_support()
|
|
93
|
+
mouse = tty_available and term not in ("dumb", "")
|
|
94
|
+
truecolor = colorterm in ("truecolor", "24bit")
|
|
95
|
+
|
|
96
|
+
# OSC 11 detection: use fast path (<100ms timeout)
|
|
97
|
+
osc11_bg: str | None = await _detect_osc11_fast()
|
|
98
|
+
|
|
99
|
+
return cls(
|
|
100
|
+
tty=tty_available,
|
|
101
|
+
ansi=ansi,
|
|
102
|
+
osc52=osc52,
|
|
103
|
+
osc11_bg=osc11_bg,
|
|
104
|
+
mouse=mouse,
|
|
105
|
+
truecolor=truecolor,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def detect_terminal_background() -> str | None:
|
|
110
|
+
"""Detect terminal background color using OSC 11 with full timeout.
|
|
111
|
+
|
|
112
|
+
This function can be called separately to await OSC 11 detection with the
|
|
113
|
+
full 1-second timeout. Useful for theme initialization where a slight delay
|
|
114
|
+
is acceptable.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Raw RGB color string from terminal, or None if detection fails or times out.
|
|
118
|
+
"""
|
|
119
|
+
if not sys.stdout.isatty() or not sys.stdin.isatty():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if not _TERMIOS_AVAILABLE:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
return await _detect_osc11_full()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _detect_osc11_fast() -> str | None:
|
|
129
|
+
"""Fast-path OSC 11 detection (used by detect())."""
|
|
130
|
+
return await _detect_osc11_impl(timeout=0.1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _detect_osc11_full() -> str | None:
|
|
134
|
+
"""Full-timeout OSC 11 detection (used by detect_terminal_background())."""
|
|
135
|
+
return await _detect_osc11_impl(timeout=1.0)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _read_osc11_char_with_timeout(start_time: float, timeout_seconds: float) -> str | None:
|
|
139
|
+
"""Read a single character from stdin with timeout.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
start_time: Start time for timeout calculation.
|
|
143
|
+
timeout_seconds: Maximum time to wait.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Character read or None on timeout/error.
|
|
147
|
+
"""
|
|
148
|
+
elapsed = time.time() - start_time
|
|
149
|
+
if elapsed >= timeout_seconds:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
remaining = timeout_seconds - elapsed
|
|
154
|
+
ready, _, _ = select.select([sys.stdin], [], [], min(0.1, remaining))
|
|
155
|
+
if not ready:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
char = sys.stdin.read(1)
|
|
159
|
+
return char if char else None
|
|
160
|
+
except (OSError, ValueError):
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _check_osc11_complete(response_text: str, response_length: int) -> str | None:
|
|
165
|
+
"""Check if OSC 11 response is complete.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
response_text: Current response text.
|
|
169
|
+
response_length: Length of response characters.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Matched color string if complete, None otherwise.
|
|
173
|
+
"""
|
|
174
|
+
match = _match_osc11_response(response_text)
|
|
175
|
+
if match:
|
|
176
|
+
return match
|
|
177
|
+
|
|
178
|
+
# If we see BEL (\x07) terminator, check one more time then give up
|
|
179
|
+
if "\x07" in response_text and response_length >= 10:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _read_osc11_response_sync(timeout_seconds: float) -> str | None:
|
|
186
|
+
"""Synchronously read OSC 11 response from stdin.
|
|
187
|
+
|
|
188
|
+
This runs in a thread to avoid blocking the event loop.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
timeout_seconds: Maximum time to wait.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Color string or None.
|
|
195
|
+
"""
|
|
196
|
+
response_chars: list[str] = []
|
|
197
|
+
start_time = time.time()
|
|
198
|
+
max_chars = 200 # Reasonable limit to prevent infinite loops
|
|
199
|
+
|
|
200
|
+
while len(response_chars) < max_chars:
|
|
201
|
+
elapsed = time.time() - start_time
|
|
202
|
+
if elapsed >= timeout_seconds:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
char = _read_osc11_char_with_timeout(start_time, timeout_seconds)
|
|
206
|
+
if char is None:
|
|
207
|
+
# Check timeout again after failed read
|
|
208
|
+
if time.time() - start_time >= timeout_seconds:
|
|
209
|
+
return None
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
response_chars.append(char)
|
|
213
|
+
response_text = "".join(response_chars)
|
|
214
|
+
|
|
215
|
+
result = _check_osc11_complete(response_text, len(response_chars))
|
|
216
|
+
if result is not None:
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def _detect_osc11_impl(timeout: float) -> str | None:
|
|
223
|
+
"""Internal OSC 11 detection implementation.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
timeout: Maximum time to wait for terminal response in seconds.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Raw RGB color string, or None on timeout/error.
|
|
230
|
+
"""
|
|
231
|
+
if not _TERMIOS_AVAILABLE:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
old_settings = None
|
|
235
|
+
try:
|
|
236
|
+
# Save terminal settings
|
|
237
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
238
|
+
tty.setraw(sys.stdin.fileno())
|
|
239
|
+
|
|
240
|
+
# Send OSC 11 query
|
|
241
|
+
sys.stdout.write("\x1b]11;?\x07")
|
|
242
|
+
sys.stdout.flush()
|
|
243
|
+
|
|
244
|
+
# Read response in a thread to avoid blocking
|
|
245
|
+
try:
|
|
246
|
+
result = await asyncio.wait_for(asyncio.to_thread(_read_osc11_response_sync, timeout), timeout=timeout)
|
|
247
|
+
return result
|
|
248
|
+
except TimeoutError:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
except Exception:
|
|
252
|
+
return None
|
|
253
|
+
finally:
|
|
254
|
+
# Restore terminal settings
|
|
255
|
+
if old_settings is not None:
|
|
256
|
+
try:
|
|
257
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _match_osc11_response(text: str) -> str | None:
|
|
263
|
+
"""Extract OSC 11 color response from text.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
text: Raw text from stdin.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Color string (e.g., "rgb:RRRR/GGGG/BBBB") or None if not found.
|
|
270
|
+
"""
|
|
271
|
+
# Match OSC 11 response: \x1b]11;...\x07
|
|
272
|
+
match = re.search(r"\x1b\]11;([^\x07\x1b]+)", text)
|
|
273
|
+
if match:
|
|
274
|
+
return match.group(1)
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse_color_response(color_str: str) -> tuple[int, int, int] | None:
|
|
279
|
+
"""Parse RGB color from various terminal color formats.
|
|
280
|
+
|
|
281
|
+
Supports:
|
|
282
|
+
- rgb:RRRR/GGGG/BBBB (16-bit per channel)
|
|
283
|
+
- rgb:RR/GG/BB (8-bit per channel)
|
|
284
|
+
- #RRGGBB (hex)
|
|
285
|
+
- rgb(R,G,B) (decimal)
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
color_str: Color string from terminal.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Tuple of (R, G, B) values in 0-255 range, or None if parsing fails.
|
|
292
|
+
"""
|
|
293
|
+
if not color_str:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
if color_str.startswith("rgb:"):
|
|
298
|
+
# Format: rgb:RRRR/GGGG/BBBB (16-bit) or rgb:RR/GG/BB (8-bit)
|
|
299
|
+
parts = color_str[4:].split("/")
|
|
300
|
+
if len(parts) == 3:
|
|
301
|
+
r_val = int(parts[0], 16)
|
|
302
|
+
g_val = int(parts[1], 16)
|
|
303
|
+
b_val = int(parts[2], 16)
|
|
304
|
+
|
|
305
|
+
# Convert 16-bit to 8-bit: if hex string has 4 digits, it's 16-bit
|
|
306
|
+
# and we take the high byte (>> 8). If 2 digits, it's already 8-bit.
|
|
307
|
+
if len(parts[0]) == 4: # 16-bit format
|
|
308
|
+
r_val = r_val >> 8
|
|
309
|
+
if len(parts[1]) == 4: # 16-bit format
|
|
310
|
+
g_val = g_val >> 8
|
|
311
|
+
if len(parts[2]) == 4: # 16-bit format
|
|
312
|
+
b_val = b_val >> 8
|
|
313
|
+
|
|
314
|
+
return (r_val, g_val, b_val)
|
|
315
|
+
|
|
316
|
+
elif color_str.startswith("#"):
|
|
317
|
+
# Format: #RRGGBB
|
|
318
|
+
if len(color_str) == 7:
|
|
319
|
+
r = int(color_str[1:3], 16)
|
|
320
|
+
g = int(color_str[3:5], 16)
|
|
321
|
+
b = int(color_str[5:7], 16)
|
|
322
|
+
return (r, g, b)
|
|
323
|
+
|
|
324
|
+
elif color_str.startswith("rgb("):
|
|
325
|
+
# Format: rgb(R,G,B)
|
|
326
|
+
parts = color_str[4:-1].split(",")
|
|
327
|
+
if len(parts) == 3:
|
|
328
|
+
r = int(parts[0].strip())
|
|
329
|
+
g = int(parts[1].strip())
|
|
330
|
+
b = int(parts[2].strip())
|
|
331
|
+
return (r, g, b)
|
|
332
|
+
|
|
333
|
+
except (ValueError, IndexError):
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _calculate_luminance(r: int, g: int, b: int) -> float:
|
|
340
|
+
"""Calculate relative luminance from RGB values.
|
|
341
|
+
|
|
342
|
+
Uses the relative luminance formula from WCAG:
|
|
343
|
+
L = 0.299*R + 0.587*G + 0.114*B
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
r: Red component (0-255).
|
|
347
|
+
g: Green component (0-255).
|
|
348
|
+
b: Blue component (0-255).
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Luminance value normalized to 0.0-1.0 range.
|
|
352
|
+
"""
|
|
353
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _check_terminal_in_env(env_value: str, terminals: list[str]) -> bool:
|
|
357
|
+
"""Check if any terminal name appears in environment value.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
env_value: Environment variable value to check.
|
|
361
|
+
terminals: List of terminal names to search for.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
True if any terminal name is found in env_value.
|
|
365
|
+
"""
|
|
366
|
+
return any(terminal in env_value for terminal in terminals)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _detect_osc52_support() -> bool:
|
|
370
|
+
"""Check if terminal likely supports OSC 52 (clipboard).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if terminal name suggests OSC 52 support.
|
|
374
|
+
"""
|
|
375
|
+
term = os.environ.get("TERM", "").lower()
|
|
376
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
377
|
+
term_program_version = os.environ.get("TERM_PROGRAM_VERSION", "").lower()
|
|
378
|
+
|
|
379
|
+
# Known terminals that support OSC 52
|
|
380
|
+
osc52_terminals = [
|
|
381
|
+
"iterm",
|
|
382
|
+
"kitty",
|
|
383
|
+
"alacritty",
|
|
384
|
+
"wezterm",
|
|
385
|
+
"vscode",
|
|
386
|
+
"windows terminal",
|
|
387
|
+
"mintty", # Windows terminal emulator
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
# Check TERM_PROGRAM first (most reliable)
|
|
391
|
+
if term_program and _check_terminal_in_env(term_program, osc52_terminals):
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
# Check TERM_PROGRAM_VERSION (VS Code uses this)
|
|
395
|
+
if term_program_version and _check_terminal_in_env(term_program_version, osc52_terminals):
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
# Check TERM (less reliable but sometimes works)
|
|
399
|
+
if term and _check_terminal_in_env(term, osc52_terminals):
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
return False
|
glaip_sdk/client/agents.py
CHANGED
glaip_sdk/client/main.py
CHANGED
|
@@ -19,7 +19,7 @@ from glaip_sdk.client.tools import ToolClient
|
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING: # pragma: no cover
|
|
21
21
|
from glaip_sdk.agents import Agent
|
|
22
|
-
from glaip_sdk.client.
|
|
22
|
+
from glaip_sdk.client.payloads.agent import AgentListResult
|
|
23
23
|
from glaip_sdk.mcps import MCP
|
|
24
24
|
from glaip_sdk.tools import Tool
|
|
25
25
|
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -85,26 +85,56 @@ class MCPClient(BaseClient):
|
|
|
85
85
|
response = MCPResponse(**full_mcp_data)
|
|
86
86
|
return MCP.from_response(response, client=self)
|
|
87
87
|
|
|
88
|
-
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
88
|
+
def update_mcp(self, mcp_id: str | MCP, **kwargs) -> MCP:
|
|
89
89
|
"""Update an existing MCP.
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
Notes:
|
|
92
|
+
- Payload construction is centralized via ``_build_update_payload`` so required
|
|
93
|
+
defaults (e.g., ``type``) and value normalization stay consistent across SDK and CLI.
|
|
94
|
+
- For backward compatibility, still chooses PATCH vs PUT based on which fields the
|
|
95
|
+
caller provided, but uses the SDK payload builder for the final payload.
|
|
95
96
|
"""
|
|
96
|
-
#
|
|
97
|
+
# Backward-compatible: allow passing an MCP instance to avoid an extra fetch.
|
|
98
|
+
if isinstance(mcp_id, MCP):
|
|
99
|
+
current_mcp = mcp_id
|
|
100
|
+
if not current_mcp.id:
|
|
101
|
+
raise ValueError("MCP instance has no id; cannot update.")
|
|
102
|
+
mcp_id_value = str(current_mcp.id)
|
|
103
|
+
else:
|
|
104
|
+
current_mcp = None
|
|
105
|
+
mcp_id_value = mcp_id
|
|
106
|
+
|
|
97
107
|
required_fields = {"name", "config", "transport"}
|
|
98
108
|
provided_fields = set(kwargs.keys())
|
|
109
|
+
method = "PUT" if required_fields.issubset(provided_fields) else "PATCH"
|
|
110
|
+
|
|
111
|
+
if not kwargs:
|
|
112
|
+
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json={})
|
|
113
|
+
response = MCPResponse(**data)
|
|
114
|
+
return MCP.from_response(response, client=self)
|
|
115
|
+
|
|
116
|
+
if current_mcp is None:
|
|
117
|
+
current_mcp = self.get_mcp_by_id(mcp_id_value)
|
|
118
|
+
|
|
119
|
+
payload_kwargs = kwargs.copy()
|
|
120
|
+
name = payload_kwargs.pop("name", None)
|
|
121
|
+
description = payload_kwargs.pop("description", None)
|
|
122
|
+
full_payload = self._build_update_payload(
|
|
123
|
+
current_mcp=current_mcp,
|
|
124
|
+
name=name,
|
|
125
|
+
description=description,
|
|
126
|
+
**payload_kwargs,
|
|
127
|
+
)
|
|
99
128
|
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
method = "PUT"
|
|
129
|
+
if method == "PUT":
|
|
130
|
+
json_payload = full_payload
|
|
103
131
|
else:
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
json_payload = {key: full_payload[key] for key in provided_fields if key in full_payload}
|
|
133
|
+
json_payload["type"] = full_payload["type"]
|
|
134
|
+
if "config" in provided_fields and "transport" not in provided_fields and "transport" in full_payload:
|
|
135
|
+
json_payload["transport"] = full_payload["transport"]
|
|
106
136
|
|
|
107
|
-
data = self._request(method, f"{MCPS_ENDPOINT}{
|
|
137
|
+
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json=json_payload)
|
|
108
138
|
response = MCPResponse(**data)
|
|
109
139
|
return MCP.from_response(response, client=self)
|
|
110
140
|
|
|
@@ -188,7 +218,8 @@ class MCPClient(BaseClient):
|
|
|
188
218
|
**kwargs,
|
|
189
219
|
) -> MCP:
|
|
190
220
|
"""Find by name and update, or create if not found."""
|
|
191
|
-
|
|
221
|
+
all_mcps = self.list_mcps()
|
|
222
|
+
existing = [mcp for mcp in all_mcps if mcp.name.lower() == name.lower()]
|
|
192
223
|
|
|
193
224
|
if len(existing) == 1:
|
|
194
225
|
logger.info("Updating existing MCP: %s", name)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Agent payload types for requests and responses.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.client.payloads.agent.requests import (
|
|
8
|
+
AgentCreateRequest,
|
|
9
|
+
AgentListParams,
|
|
10
|
+
AgentUpdateRequest,
|
|
11
|
+
merge_payload_fields,
|
|
12
|
+
resolve_language_model_fields,
|
|
13
|
+
)
|
|
14
|
+
from glaip_sdk.client.payloads.agent.responses import AgentListResult
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgentCreateRequest",
|
|
18
|
+
"AgentListParams",
|
|
19
|
+
"AgentListResult",
|
|
20
|
+
"AgentUpdateRequest",
|
|
21
|
+
"merge_payload_fields",
|
|
22
|
+
"resolve_language_model_fields",
|
|
23
|
+
]
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
"""Shared helpers for Agent client payload construction and query handling."""
|
|
1
|
+
"""Agent request payload types and helpers.
|
|
3
2
|
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=duplicate-code
|
|
4
8
|
from __future__ import annotations
|
|
5
9
|
|
|
6
10
|
from collections.abc import Callable, Mapping, MutableMapping, Sequence
|
|
7
11
|
from copy import deepcopy
|
|
8
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass
|
|
9
13
|
from typing import Any
|
|
10
14
|
|
|
11
15
|
from glaip_sdk.config.constants import (
|
|
@@ -273,38 +277,6 @@ class AgentListParams:
|
|
|
273
277
|
params[f"metadata.{key}"] = value
|
|
274
278
|
|
|
275
279
|
|
|
276
|
-
@dataclass(slots=True)
|
|
277
|
-
class AgentListResult:
|
|
278
|
-
"""Structured response for list_agents that retains pagination metadata."""
|
|
279
|
-
|
|
280
|
-
items: list[Any] = field(default_factory=list)
|
|
281
|
-
total: int | None = None
|
|
282
|
-
page: int | None = None
|
|
283
|
-
limit: int | None = None
|
|
284
|
-
has_next: bool | None = None
|
|
285
|
-
has_prev: bool | None = None
|
|
286
|
-
message: str | None = None
|
|
287
|
-
|
|
288
|
-
def __len__(self) -> int: # pragma: no cover - simple delegation
|
|
289
|
-
"""Return the number of items in the result list."""
|
|
290
|
-
return len(self.items)
|
|
291
|
-
|
|
292
|
-
def __iter__(self): # pragma: no cover - simple delegation
|
|
293
|
-
"""Return an iterator over the items in the result list."""
|
|
294
|
-
return iter(self.items)
|
|
295
|
-
|
|
296
|
-
def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
|
|
297
|
-
"""Get an item from the result list by index.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
index: Index of the item to retrieve.
|
|
301
|
-
|
|
302
|
-
Returns:
|
|
303
|
-
The item at the specified index.
|
|
304
|
-
"""
|
|
305
|
-
return self.items[index]
|
|
306
|
-
|
|
307
|
-
|
|
308
280
|
@dataclass(slots=True)
|
|
309
281
|
class AgentCreateRequest:
|
|
310
282
|
"""Declarative representation of an agent creation payload."""
|
|
@@ -422,16 +394,6 @@ class AgentUpdateRequest:
|
|
|
422
394
|
return payload
|
|
423
395
|
|
|
424
396
|
|
|
425
|
-
__all__ = [
|
|
426
|
-
"AgentCreateRequest",
|
|
427
|
-
"AgentListParams",
|
|
428
|
-
"AgentListResult",
|
|
429
|
-
"AgentUpdateRequest",
|
|
430
|
-
"merge_payload_fields",
|
|
431
|
-
"resolve_language_model_fields",
|
|
432
|
-
]
|
|
433
|
-
|
|
434
|
-
|
|
435
397
|
def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
436
398
|
"""Populate immutable agent update fields using request data or existing agent defaults."""
|
|
437
399
|
# Support both "agent_type" (runtime class) and "type" (API response) attributes
|
|
@@ -451,14 +413,27 @@ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any)
|
|
|
451
413
|
|
|
452
414
|
def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
453
415
|
"""Resolve the language-model portion of an update request with sensible fallbacks."""
|
|
416
|
+
# Check if any LM inputs were provided
|
|
417
|
+
has_lm_inputs = any(
|
|
418
|
+
[
|
|
419
|
+
request.model is not None,
|
|
420
|
+
request.language_model_id is not None,
|
|
421
|
+
request.provider is not None,
|
|
422
|
+
request.model_name is not None,
|
|
423
|
+
]
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if not has_lm_inputs:
|
|
427
|
+
# No LM inputs provided - preserve existing fields
|
|
428
|
+
return _existing_language_model_fields(current_agent)
|
|
429
|
+
|
|
430
|
+
# LM inputs provided - resolve them (may return defaults if only partial info)
|
|
454
431
|
fields = resolve_language_model_fields(
|
|
455
432
|
model=request.model,
|
|
456
433
|
language_model_id=request.language_model_id,
|
|
457
434
|
provider=request.provider,
|
|
458
435
|
model_name=request.model_name,
|
|
459
436
|
)
|
|
460
|
-
if not fields:
|
|
461
|
-
fields = _existing_language_model_fields(current_agent)
|
|
462
437
|
return fields
|
|
463
438
|
|
|
464
439
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Agent response payload types.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=duplicate-code
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class AgentListResult:
|
|
16
|
+
"""Structured response for list_agents that retains pagination metadata."""
|
|
17
|
+
|
|
18
|
+
items: list[Any] = field(default_factory=list)
|
|
19
|
+
total: int | None = None
|
|
20
|
+
page: int | None = None
|
|
21
|
+
limit: int | None = None
|
|
22
|
+
has_next: bool | None = None
|
|
23
|
+
has_prev: bool | None = None
|
|
24
|
+
message: str | None = None
|
|
25
|
+
|
|
26
|
+
def __len__(self) -> int: # pragma: no cover - simple delegation
|
|
27
|
+
"""Return the number of items in the result list."""
|
|
28
|
+
return len(self.items)
|
|
29
|
+
|
|
30
|
+
def __iter__(self): # pragma: no cover - simple delegation
|
|
31
|
+
"""Return an iterator over the items in the result list."""
|
|
32
|
+
return iter(self.items)
|
|
33
|
+
|
|
34
|
+
def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
|
|
35
|
+
"""Get an item from the result list by index.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
index: Index of the item to retrieve.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The item at the specified index.
|
|
42
|
+
"""
|
|
43
|
+
return self.items[index]
|