glaip-sdk 0.3.0__py3-none-any.whl → 0.4.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/auth.py +2 -1
- glaip_sdk/cli/commands/agents.py +1 -1
- glaip_sdk/cli/commands/configure.py +2 -1
- glaip_sdk/cli/commands/mcps.py +191 -44
- glaip_sdk/cli/commands/transcripts.py +1 -1
- glaip_sdk/cli/display.py +1 -1
- glaip_sdk/cli/hints.py +58 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +2 -1
- glaip_sdk/cli/slash/agent_session.py +2 -1
- glaip_sdk/cli/slash/session.py +1 -1
- glaip_sdk/cli/transcript/capture.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +13 -646
- glaip_sdk/cli/update_notifier.py +2 -1
- glaip_sdk/cli/utils.py +63 -110
- glaip_sdk/client/agents.py +2 -4
- glaip_sdk/client/main.py +2 -18
- glaip_sdk/client/mcps.py +11 -1
- glaip_sdk/client/run_rendering.py +90 -111
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/models.py +8 -7
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/rendering/__init__.py +6 -13
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
- glaip_sdk/utils/rendering/renderer/base.py +214 -1469
- glaip_sdk/utils/rendering/renderer/debug.py +24 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/validation.py +13 -21
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +1 -1
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/RECORD +50 -34
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -26,7 +26,8 @@ from glaip_sdk.branding import (
|
|
|
26
26
|
)
|
|
27
27
|
from glaip_sdk.cli.commands.update import update_command
|
|
28
28
|
from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
|
|
29
|
-
from glaip_sdk.cli.
|
|
29
|
+
from glaip_sdk.cli.hints import format_command_hint
|
|
30
|
+
from glaip_sdk.cli.utils import command_hint
|
|
30
31
|
from glaip_sdk.rich_components import AIPPanel
|
|
31
32
|
|
|
32
33
|
FetchLatestVersion = Callable[[], str | None]
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -7,12 +7,12 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import asyncio
|
|
10
11
|
import importlib
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
13
14
|
import os
|
|
14
15
|
import sys
|
|
15
|
-
import asyncio
|
|
16
16
|
from collections.abc import Callable, Iterable
|
|
17
17
|
from contextlib import AbstractContextManager, contextmanager, nullcontext
|
|
18
18
|
from pathlib import Path
|
|
@@ -27,19 +27,21 @@ from rich.syntax import Syntax
|
|
|
27
27
|
from glaip_sdk import _version as _version_module
|
|
28
28
|
from glaip_sdk.branding import (
|
|
29
29
|
ACCENT_STYLE,
|
|
30
|
-
HINT_COMMAND_STYLE,
|
|
31
|
-
HINT_DESCRIPTION_COLOR,
|
|
32
30
|
SUCCESS_STYLE,
|
|
33
31
|
WARNING_STYLE,
|
|
34
32
|
)
|
|
35
33
|
from glaip_sdk.cli import masking, pager
|
|
36
|
-
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
37
34
|
from glaip_sdk.cli.config import load_config
|
|
35
|
+
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
38
36
|
from glaip_sdk.cli.context import (
|
|
39
37
|
_get_view,
|
|
40
|
-
detect_export_format as _detect_export_format,
|
|
41
38
|
get_ctx_value,
|
|
42
39
|
)
|
|
40
|
+
from glaip_sdk.cli.context import (
|
|
41
|
+
detect_export_format as _detect_export_format,
|
|
42
|
+
)
|
|
43
|
+
from glaip_sdk.cli import display as cli_display
|
|
44
|
+
from glaip_sdk.cli.hints import command_hint
|
|
43
45
|
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
44
46
|
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
45
47
|
from glaip_sdk.icons import ICON_AGENT
|
|
@@ -47,10 +49,35 @@ from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
|
47
49
|
from glaip_sdk.utils import format_datetime, is_uuid
|
|
48
50
|
from glaip_sdk.utils.rendering.renderer import (
|
|
49
51
|
CapturingConsole,
|
|
50
|
-
|
|
52
|
+
RendererFactoryOptions,
|
|
51
53
|
RichStreamRenderer,
|
|
54
|
+
make_default_renderer,
|
|
55
|
+
make_verbose_renderer,
|
|
52
56
|
)
|
|
53
57
|
|
|
58
|
+
questionary = None # type: ignore[assignment]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_questionary_module() -> tuple[Any | None, Any | None]:
|
|
62
|
+
"""Return the questionary module and Choice class if available."""
|
|
63
|
+
module = questionary
|
|
64
|
+
if module is not None:
|
|
65
|
+
return module, getattr(module, "Choice", None)
|
|
66
|
+
|
|
67
|
+
try: # pragma: no cover - optional dependency
|
|
68
|
+
module = __import__("questionary")
|
|
69
|
+
except ImportError:
|
|
70
|
+
return None, None
|
|
71
|
+
|
|
72
|
+
return module, getattr(module, "Choice", None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _make_questionary_choice(choice_cls: Any | None, **kwargs: Any) -> Any:
|
|
76
|
+
"""Create a questionary Choice instance or lightweight fallback."""
|
|
77
|
+
if choice_cls is None:
|
|
78
|
+
return kwargs
|
|
79
|
+
return choice_cls(**kwargs)
|
|
80
|
+
|
|
54
81
|
|
|
55
82
|
@contextmanager
|
|
56
83
|
def bind_slash_session_context(ctx: Any, session: Any) -> Any:
|
|
@@ -122,31 +149,28 @@ def prompt_export_choice_questionary(
|
|
|
122
149
|
Tuple of (choice, path) or None if cancelled/unavailable.
|
|
123
150
|
Choice can be "default", "custom", or "cancel".
|
|
124
151
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
import questionary
|
|
128
|
-
from questionary import Choice
|
|
129
|
-
except Exception: # pragma: no cover - optional dependency
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
|
-
if questionary is None or Choice is None:
|
|
152
|
+
questionary_module, choice_cls = _load_questionary_module()
|
|
153
|
+
if questionary_module is None or choice_cls is None:
|
|
133
154
|
return None
|
|
134
155
|
|
|
135
156
|
try:
|
|
136
|
-
question =
|
|
157
|
+
question = questionary_module.select(
|
|
137
158
|
"Export transcript",
|
|
138
159
|
choices=[
|
|
139
|
-
|
|
160
|
+
_make_questionary_choice(
|
|
161
|
+
choice_cls,
|
|
140
162
|
title=f"Save to default ({default_display})",
|
|
141
163
|
value=("default", default_path),
|
|
142
164
|
shortcut_key="1",
|
|
143
165
|
),
|
|
144
|
-
|
|
166
|
+
_make_questionary_choice(
|
|
167
|
+
choice_cls,
|
|
145
168
|
title="Choose a different path",
|
|
146
169
|
value=("custom", None),
|
|
147
170
|
shortcut_key="2",
|
|
148
171
|
),
|
|
149
|
-
|
|
172
|
+
_make_questionary_choice(
|
|
173
|
+
choice_cls,
|
|
150
174
|
title="Cancel",
|
|
151
175
|
value=("cancel", None),
|
|
152
176
|
shortcut_key="3",
|
|
@@ -194,9 +218,7 @@ def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) ->
|
|
|
194
218
|
run_callable = getattr(application, "run", None) if application is not None else None
|
|
195
219
|
if callable(run_callable):
|
|
196
220
|
try:
|
|
197
|
-
if patch_stdout:
|
|
198
|
-
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
199
|
-
|
|
221
|
+
if patch_stdout and pt_patch_stdout is not None:
|
|
200
222
|
with pt_patch_stdout():
|
|
201
223
|
return run_callable(in_thread=True)
|
|
202
224
|
return run_callable(in_thread=True)
|
|
@@ -226,6 +248,7 @@ _LiteralYamlDumper.add_representer(str, _literal_str_representer)
|
|
|
226
248
|
try:
|
|
227
249
|
from prompt_toolkit.buffer import Buffer
|
|
228
250
|
from prompt_toolkit.completion import Completion
|
|
251
|
+
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
229
252
|
from prompt_toolkit.selection import SelectionType
|
|
230
253
|
from prompt_toolkit.shortcuts import PromptSession, prompt
|
|
231
254
|
|
|
@@ -235,13 +258,9 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
235
258
|
SelectionType = None # type: ignore[assignment]
|
|
236
259
|
PromptSession = None # type: ignore[assignment]
|
|
237
260
|
prompt = None # type: ignore[assignment]
|
|
261
|
+
pt_patch_stdout = None # type: ignore[assignment]
|
|
238
262
|
_HAS_PTK = False
|
|
239
263
|
|
|
240
|
-
try:
|
|
241
|
-
import questionary
|
|
242
|
-
except Exception: # pragma: no cover - optional dependency
|
|
243
|
-
questionary = None
|
|
244
|
-
|
|
245
264
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
246
265
|
from glaip_sdk import Client
|
|
247
266
|
|
|
@@ -401,9 +420,7 @@ def handle_resource_export(
|
|
|
401
420
|
):
|
|
402
421
|
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
403
422
|
except Exception:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
handle_rich_output(
|
|
423
|
+
cli_display.handle_rich_output(
|
|
407
424
|
ctx,
|
|
408
425
|
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
409
426
|
)
|
|
@@ -416,73 +433,6 @@ def handle_resource_export(
|
|
|
416
433
|
)
|
|
417
434
|
|
|
418
435
|
|
|
419
|
-
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
420
|
-
"""Return True when running inside the slash command palette."""
|
|
421
|
-
if ctx is None:
|
|
422
|
-
try:
|
|
423
|
-
ctx = click.get_current_context(silent=True)
|
|
424
|
-
except RuntimeError:
|
|
425
|
-
ctx = None
|
|
426
|
-
|
|
427
|
-
if ctx is None:
|
|
428
|
-
return False
|
|
429
|
-
|
|
430
|
-
obj = getattr(ctx, "obj", None)
|
|
431
|
-
if isinstance(obj, dict):
|
|
432
|
-
return bool(obj.get("_slash_session"))
|
|
433
|
-
|
|
434
|
-
return bool(getattr(obj, "_slash_session", False))
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def command_hint(
|
|
438
|
-
cli_command: str | None,
|
|
439
|
-
slash_command: str | None = None,
|
|
440
|
-
*,
|
|
441
|
-
ctx: click.Context | None = None,
|
|
442
|
-
) -> str | None:
|
|
443
|
-
"""Return the appropriate command string for the current mode.
|
|
444
|
-
|
|
445
|
-
Args:
|
|
446
|
-
cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
|
|
447
|
-
slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
|
|
448
|
-
ctx: Optional Click context override.
|
|
449
|
-
|
|
450
|
-
Returns:
|
|
451
|
-
The formatted command string for the active mode, or ``None`` when no
|
|
452
|
-
equivalent command exists in that mode.
|
|
453
|
-
"""
|
|
454
|
-
if in_slash_mode(ctx):
|
|
455
|
-
if not slash_command:
|
|
456
|
-
return None
|
|
457
|
-
return slash_command if slash_command.startswith("/") else f"/{slash_command}"
|
|
458
|
-
|
|
459
|
-
if not cli_command:
|
|
460
|
-
return None
|
|
461
|
-
return f"aip {cli_command}"
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def format_command_hint(
|
|
465
|
-
command: str | None,
|
|
466
|
-
description: str | None = None,
|
|
467
|
-
) -> str | None:
|
|
468
|
-
"""Return a Rich markup string that highlights a command hint.
|
|
469
|
-
|
|
470
|
-
Args:
|
|
471
|
-
command: Command text to highlight (already formatted for the active mode).
|
|
472
|
-
description: Optional short description to display alongside the command.
|
|
473
|
-
|
|
474
|
-
Returns:
|
|
475
|
-
Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
|
|
476
|
-
"""
|
|
477
|
-
if not command:
|
|
478
|
-
return None
|
|
479
|
-
|
|
480
|
-
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
481
|
-
if description:
|
|
482
|
-
highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
483
|
-
return highlighted
|
|
484
|
-
|
|
485
|
-
|
|
486
436
|
def sdk_version() -> str:
|
|
487
437
|
"""Return the current SDK version, warning if metadata is unavailable."""
|
|
488
438
|
version = getattr(_version_module, "__version__", None)
|
|
@@ -1358,19 +1308,20 @@ def build_renderer(
|
|
|
1358
1308
|
|
|
1359
1309
|
# Configure renderer based on verbose mode and explicit overrides
|
|
1360
1310
|
live_enabled = bool(live) if live is not None else not verbose
|
|
1361
|
-
|
|
1362
|
-
live
|
|
1363
|
-
append_finished_snapshots
|
|
1364
|
-
|
|
1365
|
-
|
|
1311
|
+
cfg_overrides = {
|
|
1312
|
+
"live": live_enabled,
|
|
1313
|
+
"append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
|
|
1314
|
+
}
|
|
1315
|
+
renderer_console = (
|
|
1316
|
+
working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
|
|
1366
1317
|
)
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
verbose=verbose,
|
|
1318
|
+
factory = make_verbose_renderer if verbose else make_default_renderer
|
|
1319
|
+
factory_options = RendererFactoryOptions(
|
|
1320
|
+
console=renderer_console,
|
|
1321
|
+
cfg_overrides=cfg_overrides,
|
|
1322
|
+
verbose=verbose if factory is make_default_renderer else None,
|
|
1373
1323
|
)
|
|
1324
|
+
renderer = factory_options.build(factory)
|
|
1374
1325
|
|
|
1375
1326
|
# Link the renderer back to the slash session when running from the palette.
|
|
1376
1327
|
_register_renderer_with_session(_ctx, renderer)
|
|
@@ -1540,17 +1491,19 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
|
1540
1491
|
|
|
1541
1492
|
def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1542
1493
|
"""Handle ambiguity using questionary interactive interface."""
|
|
1543
|
-
|
|
1494
|
+
questionary_module, choice_cls = _load_questionary_module()
|
|
1495
|
+
if not (questionary_module and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1544
1496
|
raise click.ClickException("Interactive selection not available")
|
|
1545
1497
|
|
|
1546
1498
|
# Escape special characters for questionary
|
|
1547
1499
|
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1548
1500
|
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1549
1501
|
|
|
1550
|
-
picked_idx =
|
|
1502
|
+
picked_idx = questionary_module.select(
|
|
1551
1503
|
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1552
1504
|
choices=[
|
|
1553
|
-
|
|
1505
|
+
_make_questionary_choice(
|
|
1506
|
+
choice_cls,
|
|
1554
1507
|
title=(
|
|
1555
1508
|
f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
|
|
1556
1509
|
f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -29,6 +29,7 @@ from glaip_sdk.client.run_rendering import (
|
|
|
29
29
|
AgentRunRenderingManager,
|
|
30
30
|
compute_timeout_seconds,
|
|
31
31
|
)
|
|
32
|
+
from glaip_sdk.client.shared import build_shared_config
|
|
32
33
|
from glaip_sdk.client.tools import ToolClient
|
|
33
34
|
from glaip_sdk.config.constants import (
|
|
34
35
|
AGENT_CONFIG_FIELDS,
|
|
@@ -1235,9 +1236,6 @@ class AgentClient(BaseClient):
|
|
|
1235
1236
|
def runs(self) -> "AgentRunsClient":
|
|
1236
1237
|
"""Get the agent runs client."""
|
|
1237
1238
|
if self._runs_client is None:
|
|
1238
|
-
|
|
1239
|
-
from glaip_sdk.client.main import _build_shared_config
|
|
1240
|
-
|
|
1241
|
-
shared_config = _build_shared_config(self)
|
|
1239
|
+
shared_config = build_shared_config(self)
|
|
1242
1240
|
self._runs_client = AgentRunsClient(**shared_config)
|
|
1243
1241
|
return self._runs_client
|
glaip_sdk/client/main.py
CHANGED
|
@@ -10,27 +10,11 @@ from typing import Any
|
|
|
10
10
|
from glaip_sdk.client.agents import AgentClient
|
|
11
11
|
from glaip_sdk.client.base import BaseClient
|
|
12
12
|
from glaip_sdk.client.mcps import MCPClient
|
|
13
|
+
from glaip_sdk.client.shared import build_shared_config
|
|
13
14
|
from glaip_sdk.client.tools import ToolClient
|
|
14
15
|
from glaip_sdk.models import MCP, Agent, Tool
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def _build_shared_config(client: BaseClient) -> dict[str, Any]:
|
|
18
|
-
"""Build shared configuration dictionary for sub-clients.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
client: Base client instance.
|
|
22
|
-
|
|
23
|
-
Returns:
|
|
24
|
-
Dictionary with shared configuration.
|
|
25
|
-
"""
|
|
26
|
-
return {
|
|
27
|
-
"parent_client": client,
|
|
28
|
-
"api_url": client.api_url,
|
|
29
|
-
"api_key": client.api_key,
|
|
30
|
-
"timeout": client._timeout,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
18
|
class Client(BaseClient):
|
|
35
19
|
"""Main client that composes all specialized clients and shares one HTTP session."""
|
|
36
20
|
|
|
@@ -42,7 +26,7 @@ class Client(BaseClient):
|
|
|
42
26
|
"""
|
|
43
27
|
super().__init__(**kwargs)
|
|
44
28
|
# Share the single httpx.Client + config with sub-clients
|
|
45
|
-
shared_config =
|
|
29
|
+
shared_config = build_shared_config(self)
|
|
46
30
|
self.agents = AgentClient(**shared_config)
|
|
47
31
|
self.tools = ToolClient(**shared_config)
|
|
48
32
|
self.mcps = MCPClient(**shared_config)
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -204,7 +204,17 @@ class MCPClient(BaseClient):
|
|
|
204
204
|
def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
|
|
205
205
|
"""Get tools available from an MCP."""
|
|
206
206
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
|
|
207
|
-
|
|
207
|
+
if data is None:
|
|
208
|
+
return []
|
|
209
|
+
if isinstance(data, list):
|
|
210
|
+
return data
|
|
211
|
+
if isinstance(data, dict):
|
|
212
|
+
if "tools" in data:
|
|
213
|
+
return data.get("tools", []) or []
|
|
214
|
+
logger.warning("Unexpected MCP tools response keys %s; returning empty list", list(data.keys()))
|
|
215
|
+
return []
|
|
216
|
+
logger.warning("Unexpected MCP tools response type %s; returning empty list", type(data).__name__)
|
|
217
|
+
return []
|
|
208
218
|
|
|
209
219
|
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
210
220
|
"""Test MCP connection using configuration.
|
|
@@ -7,9 +7,9 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import io
|
|
11
10
|
import json
|
|
12
11
|
import logging
|
|
12
|
+
from collections.abc import Callable
|
|
13
13
|
from time import monotonic
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
@@ -19,8 +19,17 @@ from rich.console import Console as _Console
|
|
|
19
19
|
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
|
|
20
20
|
from glaip_sdk.utils.client_utils import iter_sse_events
|
|
21
21
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
22
|
-
from glaip_sdk.utils.rendering.renderer import
|
|
23
|
-
|
|
22
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
23
|
+
RendererFactoryOptions,
|
|
24
|
+
RichStreamRenderer,
|
|
25
|
+
make_default_renderer,
|
|
26
|
+
make_minimal_renderer,
|
|
27
|
+
make_silent_renderer,
|
|
28
|
+
make_verbose_renderer,
|
|
29
|
+
)
|
|
30
|
+
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
31
|
+
|
|
32
|
+
NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
def _coerce_to_string(value: Any) -> str:
|
|
@@ -36,41 +45,6 @@ def _has_visible_text(value: Any) -> bool:
|
|
|
36
45
|
return isinstance(value, str) and bool(value.strip())
|
|
37
46
|
|
|
38
47
|
|
|
39
|
-
def _update_state_transcript(state: Any, text_value: str) -> bool:
|
|
40
|
-
"""Inject transcript text into renderer state if possible."""
|
|
41
|
-
if state is None:
|
|
42
|
-
return False
|
|
43
|
-
|
|
44
|
-
updated = False
|
|
45
|
-
|
|
46
|
-
if hasattr(state, "final_text") and not _has_visible_text(getattr(state, "final_text", "")):
|
|
47
|
-
try:
|
|
48
|
-
state.final_text = text_value
|
|
49
|
-
updated = True
|
|
50
|
-
except Exception:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
buffer = getattr(state, "buffer", None)
|
|
54
|
-
if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
|
|
55
|
-
buffer.append(text_value)
|
|
56
|
-
updated = True
|
|
57
|
-
|
|
58
|
-
return updated
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
|
|
62
|
-
"""Populate the renderer (or its state) with the supplied text."""
|
|
63
|
-
state = getattr(renderer, "state", None)
|
|
64
|
-
if _update_state_transcript(state, text_value):
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
|
|
68
|
-
try:
|
|
69
|
-
renderer.final_text = text_value
|
|
70
|
-
except Exception:
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
|
|
74
48
|
class AgentRunRenderingManager:
|
|
75
49
|
"""Coordinate renderer creation and streaming event handling."""
|
|
76
50
|
|
|
@@ -81,6 +55,7 @@ class AgentRunRenderingManager:
|
|
|
81
55
|
logger: Optional logger instance, creates default if None
|
|
82
56
|
"""
|
|
83
57
|
self._logger = logger or logging.getLogger(__name__)
|
|
58
|
+
self._buffer_factory = TranscriptBuffer
|
|
84
59
|
|
|
85
60
|
# --------------------------------------------------------------------- #
|
|
86
61
|
# Renderer setup helpers
|
|
@@ -92,17 +67,38 @@ class AgentRunRenderingManager:
|
|
|
92
67
|
verbose: bool = False,
|
|
93
68
|
) -> RichStreamRenderer:
|
|
94
69
|
"""Create an appropriate renderer based on the supplied spec."""
|
|
70
|
+
transcript_buffer = self._buffer_factory()
|
|
71
|
+
base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
|
|
95
72
|
if isinstance(renderer_spec, RichStreamRenderer):
|
|
96
73
|
return renderer_spec
|
|
97
74
|
|
|
98
75
|
if isinstance(renderer_spec, str):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
76
|
+
lowered = renderer_spec.lower()
|
|
77
|
+
if lowered == "silent":
|
|
78
|
+
return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
|
|
79
|
+
if lowered == "minimal":
|
|
80
|
+
return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
|
|
81
|
+
if lowered == "verbose":
|
|
82
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
83
|
+
|
|
84
|
+
if verbose:
|
|
85
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
104
86
|
|
|
105
|
-
|
|
87
|
+
default_options = RendererFactoryOptions(
|
|
88
|
+
console=_Console(),
|
|
89
|
+
transcript_buffer=transcript_buffer,
|
|
90
|
+
verbose=verbose,
|
|
91
|
+
)
|
|
92
|
+
return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
|
|
96
|
+
"""Attach a captured transcript buffer to a renderer for later inspection."""
|
|
97
|
+
try:
|
|
98
|
+
renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return renderer
|
|
106
102
|
|
|
107
103
|
def build_initial_metadata(
|
|
108
104
|
self,
|
|
@@ -123,70 +119,6 @@ class AgentRunRenderingManager:
|
|
|
123
119
|
"""Notify renderer that streaming is starting."""
|
|
124
120
|
renderer.on_start(meta)
|
|
125
121
|
|
|
126
|
-
def _create_silent_renderer(self) -> RichStreamRenderer:
|
|
127
|
-
"""Create a silent renderer that outputs to a string buffer.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
RichStreamRenderer configured for silent output.
|
|
131
|
-
"""
|
|
132
|
-
silent_config = RendererConfig(
|
|
133
|
-
live=False,
|
|
134
|
-
persist_live=False,
|
|
135
|
-
render_thinking=False,
|
|
136
|
-
)
|
|
137
|
-
return RichStreamRenderer(
|
|
138
|
-
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
139
|
-
cfg=silent_config,
|
|
140
|
-
verbose=False,
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
def _create_minimal_renderer(self) -> RichStreamRenderer:
|
|
144
|
-
"""Create a minimal renderer with reduced output.
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
RichStreamRenderer configured for minimal output.
|
|
148
|
-
"""
|
|
149
|
-
minimal_config = RendererConfig(
|
|
150
|
-
live=False,
|
|
151
|
-
persist_live=False,
|
|
152
|
-
render_thinking=False,
|
|
153
|
-
)
|
|
154
|
-
return RichStreamRenderer(
|
|
155
|
-
console=_Console(),
|
|
156
|
-
cfg=minimal_config,
|
|
157
|
-
verbose=False,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
def _create_verbose_renderer(self) -> RichStreamRenderer:
|
|
161
|
-
"""Create a verbose renderer with detailed output.
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
RichStreamRenderer configured for verbose output.
|
|
165
|
-
"""
|
|
166
|
-
verbose_config = RendererConfig(
|
|
167
|
-
live=False,
|
|
168
|
-
append_finished_snapshots=False,
|
|
169
|
-
)
|
|
170
|
-
return RichStreamRenderer(
|
|
171
|
-
console=_Console(),
|
|
172
|
-
cfg=verbose_config,
|
|
173
|
-
verbose=True,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
177
|
-
"""Create the default renderer based on verbosity.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
verbose: Whether to create a verbose renderer.
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
RichStreamRenderer instance.
|
|
184
|
-
"""
|
|
185
|
-
if verbose:
|
|
186
|
-
return self._create_verbose_renderer()
|
|
187
|
-
default_config = RendererConfig()
|
|
188
|
-
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
189
|
-
|
|
190
122
|
# --------------------------------------------------------------------- #
|
|
191
123
|
# Streaming event handling
|
|
192
124
|
# --------------------------------------------------------------------- #
|
|
@@ -382,7 +314,52 @@ class AgentRunRenderingManager:
|
|
|
382
314
|
return
|
|
383
315
|
|
|
384
316
|
text_value = _coerce_to_string(text)
|
|
385
|
-
|
|
317
|
+
state = getattr(renderer, "state", None)
|
|
318
|
+
if state is None:
|
|
319
|
+
self._ensure_renderer_text(renderer, text_value)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
self._ensure_state_final_text(state, text_value)
|
|
323
|
+
self._ensure_state_buffer(state, text_value)
|
|
324
|
+
|
|
325
|
+
def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
|
|
326
|
+
"""Best-effort assignment for renderer.final_text."""
|
|
327
|
+
if not hasattr(renderer, "final_text"):
|
|
328
|
+
return
|
|
329
|
+
current_text = getattr(renderer, "final_text", "")
|
|
330
|
+
if _has_visible_text(current_text):
|
|
331
|
+
return
|
|
332
|
+
self._safe_set_attr(renderer, "final_text", text_value)
|
|
333
|
+
|
|
334
|
+
def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
|
|
335
|
+
"""Best-effort assignment for renderer.state.final_text."""
|
|
336
|
+
current_text = getattr(state, "final_text", "")
|
|
337
|
+
if _has_visible_text(current_text):
|
|
338
|
+
return
|
|
339
|
+
self._safe_set_attr(state, "final_text", text_value)
|
|
340
|
+
|
|
341
|
+
def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
|
|
342
|
+
"""Append fallback text to the state buffer when available."""
|
|
343
|
+
buffer = getattr(state, "buffer", None)
|
|
344
|
+
if not hasattr(buffer, "append"):
|
|
345
|
+
return
|
|
346
|
+
self._safe_append(buffer.append, text_value)
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _safe_set_attr(target: Any, attr: str, value: str) -> None:
|
|
350
|
+
"""Assign attribute while masking renderer-specific failures."""
|
|
351
|
+
try:
|
|
352
|
+
setattr(target, attr, value)
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _safe_append(appender: Callable[[str], Any], value: str) -> None:
|
|
358
|
+
"""Invoke append-like functions without leaking renderer errors."""
|
|
359
|
+
try:
|
|
360
|
+
appender(value)
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
386
363
|
|
|
387
364
|
# --------------------------------------------------------------------- #
|
|
388
365
|
# Finalisation helpers
|
|
@@ -409,7 +386,9 @@ class AgentRunRenderingManager:
|
|
|
409
386
|
elif hasattr(renderer, "buffer"):
|
|
410
387
|
buffer_values = renderer.buffer
|
|
411
388
|
|
|
412
|
-
if buffer_values
|
|
389
|
+
if isinstance(buffer_values, TranscriptBuffer):
|
|
390
|
+
rendered_text = buffer_values.render()
|
|
391
|
+
elif buffer_values is not None:
|
|
413
392
|
try:
|
|
414
393
|
rendered_text = "".join(buffer_values)
|
|
415
394
|
except TypeError:
|
|
@@ -420,7 +399,7 @@ class AgentRunRenderingManager:
|
|
|
420
399
|
self._ensure_renderer_final_content(renderer, fallback_text)
|
|
421
400
|
|
|
422
401
|
renderer.on_complete(st)
|
|
423
|
-
return final_text or rendered_text or
|
|
402
|
+
return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
|
|
424
403
|
|
|
425
404
|
|
|
426
405
|
def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared helpers for client configuration wiring.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from glaip_sdk.client.base import BaseClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_shared_config(client: BaseClient) -> dict[str, Any]:
|
|
15
|
+
"""Return the keyword arguments used to initialize sub-clients."""
|
|
16
|
+
return {
|
|
17
|
+
"parent_client": client,
|
|
18
|
+
"api_url": client.api_url,
|
|
19
|
+
"api_key": client.api_key,
|
|
20
|
+
"timeout": client._timeout,
|
|
21
|
+
}
|