glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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 +283 -30
- glaip_sdk/agents/component.py +233 -0
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -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/common_config.py +1 -1
- glaip_sdk/cli/commands/configure.py +1 -2
- 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/models.py +2 -4
- 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/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +112 -35
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/agent_session.py +1 -1
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +343 -20
- glaip_sdk/cli/slash/tui/__init__.py +29 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
- glaip_sdk/cli/slash/tui/clipboard.py +316 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +43 -21
- glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +1 -1
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +293 -17
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -5
- 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} +28 -48
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +109 -30
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +52 -23
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +91 -0
- glaip_sdk/hitl/__init__.py +35 -2
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +1 -31
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +47 -1
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +2 -1
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/ptc.py +145 -0
- glaip_sdk/registry/tool.py +270 -57
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +4 -1
- glaip_sdk/runner/langgraph.py +251 -27
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
- glaip_sdk/runner/ptc_adapter.py +98 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +427 -49
- glaip_sdk/utils/runtime_config.py +3 -2
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
- glaip_sdk-0.7.27.dist-info/RECORD +227 -0
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
- glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.19.dist-info/RECORD +0 -163
- glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
glaip_sdk/client/agents.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
2
2
|
"""Agent client for AIP SDK.
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
@@ -9,23 +9,28 @@ Authors:
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
+
import warnings
|
|
12
13
|
from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
|
|
13
14
|
from contextlib import asynccontextmanager
|
|
14
15
|
from os import PathLike
|
|
15
16
|
from pathlib import Path
|
|
16
|
-
from typing import Any, BinaryIO
|
|
17
|
+
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from glaip_sdk.client.schedules import ScheduleClient
|
|
21
|
+
from glaip_sdk.hitl.remote import RemoteHITLHandler
|
|
17
22
|
|
|
18
23
|
import httpx
|
|
19
24
|
from glaip_sdk.agents import Agent
|
|
20
|
-
from glaip_sdk.client.
|
|
25
|
+
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
26
|
+
from glaip_sdk.client.base import BaseClient
|
|
27
|
+
from glaip_sdk.client.mcps import MCPClient
|
|
28
|
+
from glaip_sdk.client.payloads.agent import (
|
|
21
29
|
AgentCreateRequest,
|
|
22
30
|
AgentListParams,
|
|
23
31
|
AgentListResult,
|
|
24
32
|
AgentUpdateRequest,
|
|
25
33
|
)
|
|
26
|
-
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
27
|
-
from glaip_sdk.client.base import BaseClient
|
|
28
|
-
from glaip_sdk.client.mcps import MCPClient
|
|
29
34
|
from glaip_sdk.client.run_rendering import (
|
|
30
35
|
AgentRunRenderingManager,
|
|
31
36
|
compute_timeout_seconds,
|
|
@@ -38,10 +43,10 @@ from glaip_sdk.config.constants import (
|
|
|
38
43
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
39
44
|
DEFAULT_AGENT_TYPE,
|
|
40
45
|
DEFAULT_AGENT_VERSION,
|
|
41
|
-
DEFAULT_MODEL,
|
|
42
46
|
)
|
|
43
47
|
from glaip_sdk.exceptions import NotFoundError, ValidationError
|
|
44
48
|
from glaip_sdk.models import AgentResponse
|
|
49
|
+
from glaip_sdk.models.constants import DEFAULT_MODEL
|
|
45
50
|
from glaip_sdk.payload_schemas.agent import list_server_only_fields
|
|
46
51
|
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
47
52
|
from glaip_sdk.utils.client_utils import (
|
|
@@ -251,19 +256,134 @@ class AgentClient(BaseClient):
|
|
|
251
256
|
self,
|
|
252
257
|
*,
|
|
253
258
|
parent_client: BaseClient | None = None,
|
|
259
|
+
lm_cache_ttl: float = 3600.0,
|
|
254
260
|
**kwargs: Any,
|
|
255
261
|
) -> None:
|
|
256
262
|
"""Initialize the agent client.
|
|
257
263
|
|
|
258
264
|
Args:
|
|
259
|
-
parent_client: Parent client to adopt session/config from
|
|
260
|
-
|
|
265
|
+
parent_client: Parent client to adopt session/config from.
|
|
266
|
+
lm_cache_ttl: TTL for the language model list cache in seconds.
|
|
267
|
+
Defaults to 3600 (1 hour).
|
|
268
|
+
**kwargs: Additional arguments for standalone initialization.
|
|
261
269
|
"""
|
|
262
270
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
263
271
|
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
264
272
|
self._tool_client: ToolClient | None = None
|
|
265
273
|
self._mcp_client: MCPClient | None = None
|
|
266
274
|
self._runs_client: AgentRunsClient | None = None
|
|
275
|
+
self._schedule_client: ScheduleClient | None = None
|
|
276
|
+
|
|
277
|
+
self._lm_cache: list[dict[str, Any]] | None = None
|
|
278
|
+
self._lm_cache_time: float = 0.0
|
|
279
|
+
self._lm_cache_ttl: float = lm_cache_ttl
|
|
280
|
+
|
|
281
|
+
def clear_language_model_cache(self) -> None:
|
|
282
|
+
"""Invalidate the language model list cache.
|
|
283
|
+
|
|
284
|
+
Forces the next call to list_language_models() to fetch a fresh list
|
|
285
|
+
from the server.
|
|
286
|
+
"""
|
|
287
|
+
self._lm_cache = None
|
|
288
|
+
self._lm_cache_time = 0.0
|
|
289
|
+
logger.debug("Language model cache invalidated.")
|
|
290
|
+
|
|
291
|
+
def _resolve_language_model_id(self, model_str: str | None) -> str | None:
|
|
292
|
+
"""Resolve a friendly model name to a server language model ID.
|
|
293
|
+
|
|
294
|
+
Handles provider name mapping (e.g., 'deepinfra/model' → 'openai-compatible/model')
|
|
295
|
+
by checking both the original provider name and its driver equivalent.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
model_str: The model string to resolve (e.g., 'openai/gpt-4o', 'deepinfra/Qwen3-30B').
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The resolved server model ID (UUID), or None if not found.
|
|
302
|
+
|
|
303
|
+
Examples:
|
|
304
|
+
>>> _resolve_language_model_id("openai/gpt-4o")
|
|
305
|
+
"uuid-1234-..."
|
|
306
|
+
>>> _resolve_language_model_id("deepinfra/Qwen3-30B") # Maps to openai-compatible
|
|
307
|
+
"uuid-5678-..."
|
|
308
|
+
"""
|
|
309
|
+
if not model_str:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
# If resolution is explicitly disabled (e.g. in unit tests to avoid extra API calls), skip it
|
|
313
|
+
if getattr(self, "_skip_model_resolution", False):
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
models = self.list_language_models()
|
|
318
|
+
|
|
319
|
+
# Try exact match first
|
|
320
|
+
model_id = self._find_exact_model_match(model_str, models)
|
|
321
|
+
if model_id:
|
|
322
|
+
return model_id
|
|
323
|
+
|
|
324
|
+
# Try with provider-to-driver mapping
|
|
325
|
+
return self._try_resolve_with_driver_mapping(model_str, models)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def _find_exact_model_match(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
|
|
332
|
+
"""Find exact model match in models list.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
model_str: Model string to match.
|
|
336
|
+
models: List of language model dictionaries from server.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Model ID (UUID) if found, None otherwise.
|
|
340
|
+
"""
|
|
341
|
+
for model_info in models:
|
|
342
|
+
provider = model_info.get("provider")
|
|
343
|
+
name = model_info.get("name")
|
|
344
|
+
if provider and name:
|
|
345
|
+
full_name = f"{provider}/{name}"
|
|
346
|
+
if full_name == model_str:
|
|
347
|
+
return model_info.get("id")
|
|
348
|
+
if name == model_str:
|
|
349
|
+
return model_info.get("id")
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def _try_resolve_with_driver_mapping(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
|
|
353
|
+
"""Try to resolve model using provider-to-driver mapping.
|
|
354
|
+
|
|
355
|
+
Maps provider names to their driver implementations (e.g., deepinfra → openai-compatible)
|
|
356
|
+
and searches the models list with the driver name.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
model_str: Model string in provider/model format (e.g., "deepinfra/Qwen3-30B").
|
|
360
|
+
models: List of language model dictionaries from server.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Model ID (UUID) if found, None otherwise.
|
|
364
|
+
"""
|
|
365
|
+
if "/" not in model_str:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
from glaip_sdk.models._provider_mappings import get_driver # noqa: PLC0415
|
|
369
|
+
|
|
370
|
+
provider, model_name = model_str.split("/", 1)
|
|
371
|
+
driver = get_driver(provider)
|
|
372
|
+
|
|
373
|
+
# Only try with driver if it's different from provider
|
|
374
|
+
if driver == provider:
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
driver_model_str = f"{driver}/{model_name}"
|
|
378
|
+
for model_info in models:
|
|
379
|
+
provider_field = model_info.get("provider")
|
|
380
|
+
name_field = model_info.get("name")
|
|
381
|
+
if provider_field and name_field:
|
|
382
|
+
full_name = f"{provider_field}/{name_field}"
|
|
383
|
+
if full_name == driver_model_str:
|
|
384
|
+
return model_info.get("id")
|
|
385
|
+
|
|
386
|
+
return None
|
|
267
387
|
|
|
268
388
|
def list_agents(
|
|
269
389
|
self,
|
|
@@ -411,6 +531,7 @@ class AgentClient(BaseClient):
|
|
|
411
531
|
timeout_seconds: float,
|
|
412
532
|
agent_name: str | None,
|
|
413
533
|
meta: dict[str, Any],
|
|
534
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
414
535
|
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
415
536
|
"""Process stream events from an HTTP response.
|
|
416
537
|
|
|
@@ -420,6 +541,7 @@ class AgentClient(BaseClient):
|
|
|
420
541
|
timeout_seconds: Timeout in seconds.
|
|
421
542
|
agent_name: Optional agent name.
|
|
422
543
|
meta: Metadata dictionary.
|
|
544
|
+
hitl_handler: Optional HITL handler for approval callbacks.
|
|
423
545
|
|
|
424
546
|
Returns:
|
|
425
547
|
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
@@ -431,6 +553,7 @@ class AgentClient(BaseClient):
|
|
|
431
553
|
timeout_seconds,
|
|
432
554
|
agent_name,
|
|
433
555
|
meta,
|
|
556
|
+
hitl_handler=hitl_handler,
|
|
434
557
|
)
|
|
435
558
|
|
|
436
559
|
def _finalize_renderer(
|
|
@@ -453,11 +576,18 @@ class AgentClient(BaseClient):
|
|
|
453
576
|
Returns:
|
|
454
577
|
Final text string.
|
|
455
578
|
"""
|
|
456
|
-
from glaip_sdk.client.run_rendering import
|
|
579
|
+
from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
|
|
580
|
+
finalize_render_manager,
|
|
581
|
+
)
|
|
457
582
|
|
|
458
583
|
manager = self._get_renderer_manager()
|
|
459
584
|
return finalize_render_manager(
|
|
460
|
-
manager,
|
|
585
|
+
manager,
|
|
586
|
+
renderer,
|
|
587
|
+
final_text,
|
|
588
|
+
stats_usage,
|
|
589
|
+
started_monotonic,
|
|
590
|
+
finished_monotonic,
|
|
461
591
|
)
|
|
462
592
|
|
|
463
593
|
def _get_tool_client(self) -> ToolClient:
|
|
@@ -480,6 +610,20 @@ class AgentClient(BaseClient):
|
|
|
480
610
|
self._mcp_client = MCPClient(parent_client=self)
|
|
481
611
|
return self._mcp_client
|
|
482
612
|
|
|
613
|
+
@property
|
|
614
|
+
def schedules(self) -> "ScheduleClient":
|
|
615
|
+
"""Get or create the schedule client instance.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
ScheduleClient instance.
|
|
619
|
+
"""
|
|
620
|
+
if self._schedule_client is None:
|
|
621
|
+
# Import here to avoid circular import
|
|
622
|
+
from glaip_sdk.client.schedules import ScheduleClient # noqa: PLC0415
|
|
623
|
+
|
|
624
|
+
self._schedule_client = ScheduleClient(parent_client=self)
|
|
625
|
+
return self._schedule_client
|
|
626
|
+
|
|
483
627
|
def _normalise_reference_entry(
|
|
484
628
|
self,
|
|
485
629
|
entry: Any,
|
|
@@ -747,10 +891,18 @@ class AgentClient(BaseClient):
|
|
|
747
891
|
plural_label="MCPs",
|
|
748
892
|
)
|
|
749
893
|
|
|
750
|
-
def
|
|
751
|
-
"""
|
|
752
|
-
known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
|
|
894
|
+
def _validate_agent_basics(self, known: dict[str, Any]) -> tuple[str, str]:
|
|
895
|
+
"""Validate and extract basic agent fields.
|
|
753
896
|
|
|
897
|
+
Args:
|
|
898
|
+
known: Known fields dictionary.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Tuple of (name, validated_instruction).
|
|
902
|
+
|
|
903
|
+
Raises:
|
|
904
|
+
ValueError: If name or instruction is empty/whitespace.
|
|
905
|
+
"""
|
|
754
906
|
name = known.pop("name", None)
|
|
755
907
|
instruction = known.pop("instruction", None)
|
|
756
908
|
if not name or not str(name).strip():
|
|
@@ -759,9 +911,20 @@ class AgentClient(BaseClient):
|
|
|
759
911
|
raise ValueError("Agent instruction cannot be empty or whitespace")
|
|
760
912
|
|
|
761
913
|
validated_instruction = validate_agent_instruction(str(instruction))
|
|
762
|
-
|
|
914
|
+
return str(name).strip(), validated_instruction
|
|
763
915
|
|
|
764
|
-
|
|
916
|
+
def _resolve_all_resources(
|
|
917
|
+
self, known: dict[str, Any], extras: dict[str, Any]
|
|
918
|
+
) -> tuple[list[str] | None, list[str] | None, list[str] | None]:
|
|
919
|
+
"""Resolve all resource IDs (tools, agents, mcps).
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
known: Known fields dictionary.
|
|
923
|
+
extras: Extra fields dictionary.
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
Tuple of (resolved_tools, resolved_agents, resolved_mcps).
|
|
927
|
+
"""
|
|
765
928
|
tool_refs = extras.pop("_tool_refs", None)
|
|
766
929
|
agent_refs = extras.pop("_agent_refs", None)
|
|
767
930
|
mcp_refs = extras.pop("_mcp_refs", None)
|
|
@@ -774,10 +937,56 @@ class AgentClient(BaseClient):
|
|
|
774
937
|
resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
|
|
775
938
|
resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
|
|
776
939
|
|
|
940
|
+
return resolved_tools, resolved_agents, resolved_mcps
|
|
941
|
+
|
|
942
|
+
def _process_model_fields(
|
|
943
|
+
self, resolved_model: Any, known: dict[str, Any]
|
|
944
|
+
) -> tuple[str, str | None, str | None, str | None]:
|
|
945
|
+
"""Process model fields and extract language model ID.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
resolved_model: Resolved model (string or Model object).
|
|
949
|
+
known: Known fields dictionary.
|
|
950
|
+
|
|
951
|
+
Returns:
|
|
952
|
+
Tuple of (resolved_model_str, language_model_id, provider, model_name).
|
|
953
|
+
"""
|
|
954
|
+
from glaip_sdk.models import Model # noqa: PLC0415
|
|
955
|
+
|
|
956
|
+
if isinstance(resolved_model, Model):
|
|
957
|
+
if resolved_model.credentials or resolved_model.hyperparameters or resolved_model.base_url:
|
|
958
|
+
warnings.warn(
|
|
959
|
+
"Model object contains local configuration (credentials, hyperparameters, or base_url) "
|
|
960
|
+
"which is ignored for remote deployment. These fields are only used for local execution.",
|
|
961
|
+
UserWarning,
|
|
962
|
+
stacklevel=2,
|
|
963
|
+
)
|
|
964
|
+
resolved_model = resolved_model.id
|
|
965
|
+
|
|
966
|
+
# Validate and normalize string models (handles bare name deprecation)
|
|
967
|
+
if isinstance(resolved_model, str):
|
|
968
|
+
from glaip_sdk.models._validation import _validate_model # noqa: PLC0415
|
|
969
|
+
|
|
970
|
+
resolved_model = _validate_model(resolved_model)
|
|
971
|
+
|
|
777
972
|
language_model_id = known.pop("language_model_id", None)
|
|
973
|
+
if not language_model_id and isinstance(resolved_model, str):
|
|
974
|
+
language_model_id = self._resolve_language_model_id(resolved_model)
|
|
975
|
+
|
|
778
976
|
provider = known.pop("provider", None)
|
|
779
977
|
model_name = known.pop("model_name", None)
|
|
780
978
|
|
|
979
|
+
return resolved_model, language_model_id, provider, model_name
|
|
980
|
+
|
|
981
|
+
def _extract_agent_metadata(self, known: dict[str, Any]) -> tuple[str, str, str]:
|
|
982
|
+
"""Extract agent type, framework, and version.
|
|
983
|
+
|
|
984
|
+
Args:
|
|
985
|
+
known: Known fields dictionary.
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
Tuple of (agent_type, framework, version).
|
|
989
|
+
"""
|
|
781
990
|
agent_type_value = known.pop("agent_type", None)
|
|
782
991
|
fallback_type_value = known.pop("type", None)
|
|
783
992
|
if agent_type_value is None:
|
|
@@ -785,6 +994,26 @@ class AgentClient(BaseClient):
|
|
|
785
994
|
|
|
786
995
|
framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
|
|
787
996
|
version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
|
|
997
|
+
|
|
998
|
+
return agent_type_value, framework_value, version_value
|
|
999
|
+
|
|
1000
|
+
def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
|
|
1001
|
+
"""Create an agent using a fully prepared payload mapping."""
|
|
1002
|
+
known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
|
|
1003
|
+
|
|
1004
|
+
# Validate and extract basic fields
|
|
1005
|
+
name, validated_instruction = self._validate_agent_basics(known)
|
|
1006
|
+
_normalise_sequence_fields(known)
|
|
1007
|
+
|
|
1008
|
+
# Resolve model and resources
|
|
1009
|
+
resolved_model = known.pop("model", None) or DEFAULT_MODEL
|
|
1010
|
+
resolved_tools, resolved_agents, resolved_mcps = self._resolve_all_resources(known, extras)
|
|
1011
|
+
|
|
1012
|
+
# Process model and language model ID
|
|
1013
|
+
resolved_model, language_model_id, provider, model_name = self._process_model_fields(resolved_model, known)
|
|
1014
|
+
|
|
1015
|
+
# Extract agent type, framework, version
|
|
1016
|
+
agent_type_value, framework_value, version_value = self._extract_agent_metadata(known)
|
|
788
1017
|
account_id = known.pop("account_id", None)
|
|
789
1018
|
description = known.pop("description", None)
|
|
790
1019
|
metadata = _prepare_agent_metadata(known.pop("metadata", None))
|
|
@@ -797,7 +1026,7 @@ class AgentClient(BaseClient):
|
|
|
797
1026
|
final_extras.setdefault("model", resolved_model)
|
|
798
1027
|
|
|
799
1028
|
request = AgentCreateRequest(
|
|
800
|
-
name=
|
|
1029
|
+
name=name,
|
|
801
1030
|
instruction=validated_instruction,
|
|
802
1031
|
model=resolved_model,
|
|
803
1032
|
language_model_id=language_model_id,
|
|
@@ -875,6 +1104,29 @@ class AgentClient(BaseClient):
|
|
|
875
1104
|
"""Backward-compatible helper to create an agent from a configuration file."""
|
|
876
1105
|
return self.create_agent(file=file_path, **overrides)
|
|
877
1106
|
|
|
1107
|
+
def _resolve_update_model_fields(self, known: dict[str, Any]) -> None:
|
|
1108
|
+
"""Resolve model fields in-place for update payload if present.
|
|
1109
|
+
|
|
1110
|
+
If 'model' or 'language_model_id' keys exist in the known fields dict,
|
|
1111
|
+
this method resolves them into 'language_model_id', 'provider', and 'model_name'
|
|
1112
|
+
using the standard resolution logic, ensuring consistency with create_agent.
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
known: The dictionary of known fields to check and update in-place.
|
|
1116
|
+
"""
|
|
1117
|
+
if "model" in known or "language_model_id" in known:
|
|
1118
|
+
model_val = known.pop("model", None)
|
|
1119
|
+
r_model, r_id, r_prov, r_name = self._process_model_fields(model_val, known)
|
|
1120
|
+
|
|
1121
|
+
if r_model is not None:
|
|
1122
|
+
known["model"] = r_model
|
|
1123
|
+
if r_id is not None:
|
|
1124
|
+
known["language_model_id"] = r_id
|
|
1125
|
+
if r_prov is not None:
|
|
1126
|
+
known["provider"] = r_prov
|
|
1127
|
+
if r_name is not None:
|
|
1128
|
+
known["model_name"] = r_name
|
|
1129
|
+
|
|
878
1130
|
def _update_agent_from_payload(
|
|
879
1131
|
self,
|
|
880
1132
|
agent_id: str,
|
|
@@ -900,6 +1152,8 @@ class AgentClient(BaseClient):
|
|
|
900
1152
|
if mcps_value is not None:
|
|
901
1153
|
mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
|
|
902
1154
|
|
|
1155
|
+
self._resolve_update_model_fields(known)
|
|
1156
|
+
|
|
903
1157
|
request = AgentUpdateRequest(
|
|
904
1158
|
name=known.pop("name", None),
|
|
905
1159
|
instruction=known.pop("instruction", None),
|
|
@@ -1094,6 +1348,7 @@ class AgentClient(BaseClient):
|
|
|
1094
1348
|
*,
|
|
1095
1349
|
renderer: RichStreamRenderer | str | None = "auto",
|
|
1096
1350
|
runtime_config: dict[str, Any] | None = None,
|
|
1351
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1097
1352
|
**kwargs,
|
|
1098
1353
|
) -> str:
|
|
1099
1354
|
"""Run an agent with a message, streaming via a renderer.
|
|
@@ -1111,6 +1366,8 @@ class AgentClient(BaseClient):
|
|
|
1111
1366
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1112
1367
|
"agent_config": {"planning": True},
|
|
1113
1368
|
}
|
|
1369
|
+
hitl_handler: Optional RemoteHITLHandler for approval callbacks.
|
|
1370
|
+
Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
|
|
1114
1371
|
**kwargs: Additional arguments to pass to the run API.
|
|
1115
1372
|
|
|
1116
1373
|
Returns:
|
|
@@ -1167,6 +1424,7 @@ class AgentClient(BaseClient):
|
|
|
1167
1424
|
timeout_seconds,
|
|
1168
1425
|
agent_name,
|
|
1169
1426
|
meta,
|
|
1427
|
+
hitl_handler=hitl_handler,
|
|
1170
1428
|
)
|
|
1171
1429
|
|
|
1172
1430
|
except KeyboardInterrupt:
|
|
@@ -1183,6 +1441,13 @@ class AgentClient(BaseClient):
|
|
|
1183
1441
|
if multipart_data:
|
|
1184
1442
|
multipart_data.close()
|
|
1185
1443
|
|
|
1444
|
+
# Wait for pending HITL decisions before returning
|
|
1445
|
+
if hitl_handler and hasattr(hitl_handler, "wait_for_pending_decisions"):
|
|
1446
|
+
try:
|
|
1447
|
+
hitl_handler.wait_for_pending_decisions(timeout=30)
|
|
1448
|
+
except Exception as e:
|
|
1449
|
+
logger.warning(f"Error waiting for HITL decisions: {e}")
|
|
1450
|
+
|
|
1186
1451
|
return self._finalize_renderer(
|
|
1187
1452
|
r,
|
|
1188
1453
|
final_text,
|
|
@@ -1264,6 +1529,7 @@ class AgentClient(BaseClient):
|
|
|
1264
1529
|
*,
|
|
1265
1530
|
request_timeout: float | None = None,
|
|
1266
1531
|
runtime_config: dict[str, Any] | None = None,
|
|
1532
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1267
1533
|
**kwargs,
|
|
1268
1534
|
) -> AsyncGenerator[dict, None]:
|
|
1269
1535
|
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
@@ -1280,16 +1546,26 @@ class AgentClient(BaseClient):
|
|
|
1280
1546
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1281
1547
|
"agent_config": {"planning": True},
|
|
1282
1548
|
}
|
|
1549
|
+
hitl_handler: Optional HITL handler for remote approval requests.
|
|
1550
|
+
Note: Async HITL support is currently deferred. This parameter
|
|
1551
|
+
is accepted for API consistency but will raise NotImplementedError
|
|
1552
|
+
if provided.
|
|
1283
1553
|
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
1284
1554
|
|
|
1285
1555
|
Yields:
|
|
1286
1556
|
Dictionary containing parsed JSON chunks from the streaming response
|
|
1287
1557
|
|
|
1288
1558
|
Raises:
|
|
1559
|
+
NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
|
|
1289
1560
|
AgentTimeoutError: When agent execution times out
|
|
1290
1561
|
httpx.TimeoutException: When general timeout occurs
|
|
1291
1562
|
Exception: For other unexpected errors
|
|
1292
1563
|
"""
|
|
1564
|
+
if hitl_handler is not None:
|
|
1565
|
+
raise NotImplementedError(
|
|
1566
|
+
"Async HITL support is currently deferred. "
|
|
1567
|
+
"Please use the synchronous run_agent() method with hitl_handler."
|
|
1568
|
+
)
|
|
1293
1569
|
# Include runtime_config in kwargs only when caller hasn't already provided it
|
|
1294
1570
|
if runtime_config is not None and "runtime_config" not in kwargs:
|
|
1295
1571
|
kwargs["runtime_config"] = runtime_config
|
glaip_sdk/client/base.py
CHANGED
|
@@ -8,6 +8,7 @@ Authors:
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import time
|
|
11
12
|
from collections.abc import Iterable, Mapping
|
|
12
13
|
from typing import Any, NoReturn, Union
|
|
13
14
|
|
|
@@ -89,6 +90,11 @@ class BaseClient:
|
|
|
89
90
|
client_log.info(f"Initializing client with API URL: {self.api_url}")
|
|
90
91
|
self.http_client = self._build_client(timeout)
|
|
91
92
|
|
|
93
|
+
# Language model cache (shared by all clients)
|
|
94
|
+
self._lm_cache: list[dict[str, Any]] | None = None
|
|
95
|
+
self._lm_cache_time: float = 0.0
|
|
96
|
+
self._lm_cache_ttl: float = 300.0 # 5 minutes TTL
|
|
97
|
+
|
|
92
98
|
def _build_client(self, timeout: float) -> httpx.Client:
|
|
93
99
|
"""Build HTTP client with configuration."""
|
|
94
100
|
# For streaming operations, we need more generous read timeouts
|
|
@@ -481,6 +487,25 @@ class BaseClient:
|
|
|
481
487
|
request_id=request_id,
|
|
482
488
|
)
|
|
483
489
|
|
|
490
|
+
def list_language_models(self, force_refresh: bool = False) -> list[dict[str, Any]]:
|
|
491
|
+
"""List available language models with TTL caching.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
force_refresh: Whether to ignore cache and fetch fresh list.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
List of available language models.
|
|
498
|
+
"""
|
|
499
|
+
now = time.monotonic()
|
|
500
|
+
if not force_refresh and self._lm_cache is not None:
|
|
501
|
+
if now - self._lm_cache_time < self._lm_cache_ttl:
|
|
502
|
+
return self._lm_cache
|
|
503
|
+
|
|
504
|
+
models = self._request("GET", "/language-models") or []
|
|
505
|
+
self._lm_cache = models
|
|
506
|
+
self._lm_cache_time = now
|
|
507
|
+
return models
|
|
508
|
+
|
|
484
509
|
def close(self) -> None:
|
|
485
510
|
"""Close the HTTP client."""
|
|
486
511
|
if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
|
glaip_sdk/client/hitl.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HITL REST client for manual approval operations.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
GLAIP SDK Team
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.client.base import BaseClient
|
|
11
|
+
from glaip_sdk.hitl.base import HITLDecision
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HITLClient(BaseClient):
|
|
15
|
+
"""Client for HITL REST endpoints.
|
|
16
|
+
|
|
17
|
+
Use for manual approval workflows separate from agent runs.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> # List pending approvals
|
|
21
|
+
>>> pending = client.hitl.list_pending()
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Approve a request
|
|
24
|
+
>>> client.hitl.approve(
|
|
25
|
+
... request_id="bc4d0a77-7800-470e-a91c-7fd663a66b4d",
|
|
26
|
+
... operator_input="Verified and approved",
|
|
27
|
+
... )
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def approve(
|
|
31
|
+
self,
|
|
32
|
+
request_id: str,
|
|
33
|
+
operator_input: str | None = None,
|
|
34
|
+
run_id: str | None = None,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Approve a HITL request.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
request_id: HITL request ID from SSE stream
|
|
40
|
+
operator_input: Optional notes/reason for approval
|
|
41
|
+
run_id: Optional client-side run correlation ID
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Response dict: {"status": "ok", "message": "..."}
|
|
45
|
+
"""
|
|
46
|
+
return self._post_decision(
|
|
47
|
+
request_id,
|
|
48
|
+
HITLDecision.APPROVED,
|
|
49
|
+
operator_input,
|
|
50
|
+
run_id,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def reject(
|
|
54
|
+
self,
|
|
55
|
+
request_id: str,
|
|
56
|
+
operator_input: str | None = None,
|
|
57
|
+
run_id: str | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Reject a HITL request.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
request_id: HITL request ID
|
|
63
|
+
operator_input: Optional reason for rejection
|
|
64
|
+
run_id: Optional run correlation ID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Response dict
|
|
68
|
+
"""
|
|
69
|
+
return self._post_decision(
|
|
70
|
+
request_id,
|
|
71
|
+
HITLDecision.REJECTED,
|
|
72
|
+
operator_input,
|
|
73
|
+
run_id,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def skip(
|
|
77
|
+
self,
|
|
78
|
+
request_id: str,
|
|
79
|
+
operator_input: str | None = None,
|
|
80
|
+
run_id: str | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Skip a HITL request.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
request_id: HITL request ID
|
|
86
|
+
operator_input: Optional notes
|
|
87
|
+
run_id: Optional run correlation ID
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Response dict
|
|
91
|
+
"""
|
|
92
|
+
return self._post_decision(
|
|
93
|
+
request_id,
|
|
94
|
+
HITLDecision.SKIPPED,
|
|
95
|
+
operator_input,
|
|
96
|
+
run_id,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _post_decision(
|
|
100
|
+
self,
|
|
101
|
+
request_id: str,
|
|
102
|
+
decision: HITLDecision,
|
|
103
|
+
operator_input: str | None,
|
|
104
|
+
run_id: str | None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Post HITL decision to backend."""
|
|
107
|
+
payload = {
|
|
108
|
+
"request_id": request_id,
|
|
109
|
+
"decision": decision.value,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if operator_input:
|
|
113
|
+
payload["operator_input"] = operator_input
|
|
114
|
+
if run_id:
|
|
115
|
+
payload["run_id"] = run_id
|
|
116
|
+
|
|
117
|
+
return self._request("POST", "/agents/hitl/decision", json=payload)
|
|
118
|
+
|
|
119
|
+
def list_pending(self) -> list[dict[str, Any]]:
|
|
120
|
+
"""List all pending HITL requests.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of pending request dicts with metadata:
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
"request_id": "...",
|
|
127
|
+
"tool": "...",
|
|
128
|
+
"arguments": {...},
|
|
129
|
+
"created_at": "...",
|
|
130
|
+
"agent_id": "...",
|
|
131
|
+
"hitl_metadata": {...},
|
|
132
|
+
},
|
|
133
|
+
...
|
|
134
|
+
]
|
|
135
|
+
"""
|
|
136
|
+
return self._request("GET", "/agents/hitl/pending")
|