glaip-sdk 0.7.12__py3-none-any.whl → 0.7.14__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 +79 -12
- glaip_sdk/cli/commands/agents/__init__.py +14 -17
- glaip_sdk/cli/commands/agents/_common.py +6 -5
- glaip_sdk/cli/commands/agents/create.py +9 -5
- glaip_sdk/cli/slash/tui/accounts.tcss +12 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +56 -15
- glaip_sdk/cli/slash/tui/context.py +7 -2
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +35 -11
- glaip_sdk/cli/slash/tui/remote_runs_app.py +47 -10
- glaip_sdk/cli/slash/tui/toast.py +14 -0
- glaip_sdk/client/agents.py +247 -15
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/main.py +0 -4
- glaip_sdk/client/payloads/agent/requests.py +6 -1
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/models/__init__.py +30 -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/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/runner/langgraph.py +105 -14
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/runtime_config.py +3 -2
- {glaip_sdk-0.7.12.dist-info → glaip_sdk-0.7.14.dist-info}/METADATA +1 -1
- {glaip_sdk-0.7.12.dist-info → glaip_sdk-0.7.14.dist-info}/RECORD +29 -25
- {glaip_sdk-0.7.12.dist-info → glaip_sdk-0.7.14.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.7.12.dist-info → glaip_sdk-0.7.14.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.7.12.dist-info → glaip_sdk-0.7.14.dist-info}/top_level.txt +0 -0
|
@@ -17,18 +17,35 @@ from dataclasses import dataclass
|
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
19
|
from rich.text import Text
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from textual.
|
|
23
|
-
from textual.
|
|
24
|
-
from textual.
|
|
25
|
-
from textual.
|
|
26
|
-
from textual.
|
|
20
|
+
|
|
21
|
+
try: # pragma: no cover - optional dependency
|
|
22
|
+
from textual.app import App, ComposeResult
|
|
23
|
+
from textual.binding import Binding
|
|
24
|
+
from textual.containers import Horizontal, Vertical
|
|
25
|
+
from textual.coordinate import Coordinate
|
|
26
|
+
from textual.reactive import ReactiveError
|
|
27
|
+
from textual.screen import ModalScreen
|
|
28
|
+
from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
|
|
29
|
+
except Exception: # pragma: no cover - optional dependency
|
|
30
|
+
App = None # type: ignore[assignment]
|
|
31
|
+
ComposeResult = None # type: ignore[assignment]
|
|
32
|
+
Binding = None # type: ignore[assignment]
|
|
33
|
+
Horizontal = None # type: ignore[assignment]
|
|
34
|
+
Vertical = None # type: ignore[assignment]
|
|
35
|
+
Coordinate = None # type: ignore[assignment]
|
|
36
|
+
ReactiveError = Exception # type: ignore[assignment, misc]
|
|
37
|
+
ModalScreen = object # type: ignore[assignment, misc]
|
|
38
|
+
DataTable = None # type: ignore[assignment]
|
|
39
|
+
Footer = None # type: ignore[assignment]
|
|
40
|
+
Header = None # type: ignore[assignment]
|
|
41
|
+
LoadingIndicator = None # type: ignore[assignment]
|
|
42
|
+
RichLog = None # type: ignore[assignment]
|
|
43
|
+
Static = None # type: ignore[assignment]
|
|
27
44
|
|
|
28
45
|
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
29
46
|
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
30
47
|
from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
|
|
31
|
-
from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastHandlerMixin
|
|
48
|
+
from glaip_sdk.cli.slash.tui.toast import ClipboardToastMixin, Toast, ToastBus, ToastContainer, ToastHandlerMixin
|
|
32
49
|
|
|
33
50
|
logger = logging.getLogger(__name__)
|
|
34
51
|
|
|
@@ -149,7 +166,7 @@ class RunDetailScreen(ToastHandlerMixin, ClipboardToastMixin, ModalScreen[None])
|
|
|
149
166
|
RichLog(id="detail-events", wrap=False),
|
|
150
167
|
)
|
|
151
168
|
yield main_content
|
|
152
|
-
yield
|
|
169
|
+
yield ToastContainer(Toast(), id="toast-container")
|
|
153
170
|
yield Footer()
|
|
154
171
|
|
|
155
172
|
def on_mount(self) -> None:
|
|
@@ -369,7 +386,7 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
|
|
|
369
386
|
def compose(self) -> ComposeResult:
|
|
370
387
|
"""Build layout."""
|
|
371
388
|
yield Header()
|
|
372
|
-
yield
|
|
389
|
+
yield ToastContainer(Toast(), id="toast-container")
|
|
373
390
|
table = DataTable(id=RUNS_TABLE_ID) # pragma: no cover - mocked in tests
|
|
374
391
|
table.cursor_type = "row" # pragma: no cover - mocked in tests
|
|
375
392
|
table.add_columns( # pragma: no cover - mocked in tests
|
|
@@ -478,6 +495,26 @@ class RemoteRunsTextualApp(ToastHandlerMixin, App[None]):
|
|
|
478
495
|
"""Track cursor position when DataTable selection changes."""
|
|
479
496
|
self.cursor_index = getattr(event, "cursor_row", self.cursor_index)
|
|
480
497
|
|
|
498
|
+
def _handle_table_click(self, row: int | None) -> None:
|
|
499
|
+
if row is None:
|
|
500
|
+
return
|
|
501
|
+
table = self.query_one(RUNS_TABLE_SELECTOR, DataTable)
|
|
502
|
+
self.cursor_index = row
|
|
503
|
+
try:
|
|
504
|
+
table.cursor_coordinate = Coordinate(row, 0)
|
|
505
|
+
except Exception:
|
|
506
|
+
return
|
|
507
|
+
self.action_open_detail()
|
|
508
|
+
|
|
509
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: # pragma: no cover - UI hook
|
|
510
|
+
"""Handle row selection event from DataTable."""
|
|
511
|
+
self._handle_table_click(getattr(event, "cursor_row", None))
|
|
512
|
+
|
|
513
|
+
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: # pragma: no cover - UI hook
|
|
514
|
+
"""Handle cell selection event from DataTable."""
|
|
515
|
+
row = getattr(event.coordinate, "row", None) if event.coordinate else None
|
|
516
|
+
self._handle_table_click(row)
|
|
517
|
+
|
|
481
518
|
def action_page_left(self) -> None:
|
|
482
519
|
"""Navigate to the previous page."""
|
|
483
520
|
if not self.current_page.has_prev:
|
glaip_sdk/cli/slash/tui/toast.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Any, cast
|
|
|
10
10
|
|
|
11
11
|
from rich.text import Text
|
|
12
12
|
from textual.message import Message
|
|
13
|
+
from textual.widget import Widget
|
|
13
14
|
from textual.widgets import Static
|
|
14
15
|
|
|
15
16
|
|
|
@@ -290,6 +291,19 @@ class ClipboardToastMixin:
|
|
|
290
291
|
self._append_copy_fallback(text)
|
|
291
292
|
|
|
292
293
|
|
|
294
|
+
class ToastContainer(Widget):
|
|
295
|
+
"""Simple wrapper for docking toast widgets without relying on containers.
|
|
296
|
+
|
|
297
|
+
This class exists to provide a lightweight widget wrapper for toast containers
|
|
298
|
+
that avoids direct dependency on Textual's Container class. It allows the toast
|
|
299
|
+
system to work consistently across different Textual versions and provides a
|
|
300
|
+
stable API for toast container composition.
|
|
301
|
+
|
|
302
|
+
Usage:
|
|
303
|
+
yield ToastContainer(Toast(), id="toast-container")
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
|
|
293
307
|
class Toast(Static):
|
|
294
308
|
"""A Textual widget that displays toast notifications at the top-right of the screen.
|
|
295
309
|
|
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,6 +9,7 @@ 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
|
|
@@ -21,15 +22,15 @@ if TYPE_CHECKING:
|
|
|
21
22
|
|
|
22
23
|
import httpx
|
|
23
24
|
from glaip_sdk.agents import Agent
|
|
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
|
|
24
28
|
from glaip_sdk.client.payloads.agent import (
|
|
25
29
|
AgentCreateRequest,
|
|
26
30
|
AgentListParams,
|
|
27
31
|
AgentListResult,
|
|
28
32
|
AgentUpdateRequest,
|
|
29
33
|
)
|
|
30
|
-
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
31
|
-
from glaip_sdk.client.base import BaseClient
|
|
32
|
-
from glaip_sdk.client.mcps import MCPClient
|
|
33
34
|
from glaip_sdk.client.run_rendering import (
|
|
34
35
|
AgentRunRenderingManager,
|
|
35
36
|
compute_timeout_seconds,
|
|
@@ -42,10 +43,10 @@ from glaip_sdk.config.constants import (
|
|
|
42
43
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
43
44
|
DEFAULT_AGENT_TYPE,
|
|
44
45
|
DEFAULT_AGENT_VERSION,
|
|
45
|
-
DEFAULT_MODEL,
|
|
46
46
|
)
|
|
47
47
|
from glaip_sdk.exceptions import NotFoundError, ValidationError
|
|
48
48
|
from glaip_sdk.models import AgentResponse
|
|
49
|
+
from glaip_sdk.models.constants import DEFAULT_MODEL
|
|
49
50
|
from glaip_sdk.payload_schemas.agent import list_server_only_fields
|
|
50
51
|
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
51
52
|
from glaip_sdk.utils.client_utils import (
|
|
@@ -255,13 +256,16 @@ class AgentClient(BaseClient):
|
|
|
255
256
|
self,
|
|
256
257
|
*,
|
|
257
258
|
parent_client: BaseClient | None = None,
|
|
259
|
+
lm_cache_ttl: float = 3600.0,
|
|
258
260
|
**kwargs: Any,
|
|
259
261
|
) -> None:
|
|
260
262
|
"""Initialize the agent client.
|
|
261
263
|
|
|
262
264
|
Args:
|
|
263
|
-
parent_client: Parent client to adopt session/config from
|
|
264
|
-
|
|
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.
|
|
265
269
|
"""
|
|
266
270
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
267
271
|
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
@@ -270,6 +274,117 @@ class AgentClient(BaseClient):
|
|
|
270
274
|
self._runs_client: AgentRunsClient | None = None
|
|
271
275
|
self._schedule_client: ScheduleClient | None = None
|
|
272
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
|
|
387
|
+
|
|
273
388
|
def list_agents(
|
|
274
389
|
self,
|
|
275
390
|
query: AgentListParams | None = None,
|
|
@@ -461,11 +576,18 @@ class AgentClient(BaseClient):
|
|
|
461
576
|
Returns:
|
|
462
577
|
Final text string.
|
|
463
578
|
"""
|
|
464
|
-
from glaip_sdk.client.run_rendering import
|
|
579
|
+
from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
|
|
580
|
+
finalize_render_manager,
|
|
581
|
+
)
|
|
465
582
|
|
|
466
583
|
manager = self._get_renderer_manager()
|
|
467
584
|
return finalize_render_manager(
|
|
468
|
-
manager,
|
|
585
|
+
manager,
|
|
586
|
+
renderer,
|
|
587
|
+
final_text,
|
|
588
|
+
stats_usage,
|
|
589
|
+
started_monotonic,
|
|
590
|
+
finished_monotonic,
|
|
469
591
|
)
|
|
470
592
|
|
|
471
593
|
def _get_tool_client(self) -> ToolClient:
|
|
@@ -769,10 +891,18 @@ class AgentClient(BaseClient):
|
|
|
769
891
|
plural_label="MCPs",
|
|
770
892
|
)
|
|
771
893
|
|
|
772
|
-
def
|
|
773
|
-
"""
|
|
774
|
-
|
|
894
|
+
def _validate_agent_basics(self, known: dict[str, Any]) -> tuple[str, str]:
|
|
895
|
+
"""Validate and extract basic agent fields.
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
known: Known fields dictionary.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Tuple of (name, validated_instruction).
|
|
775
902
|
|
|
903
|
+
Raises:
|
|
904
|
+
ValueError: If name or instruction is empty/whitespace.
|
|
905
|
+
"""
|
|
776
906
|
name = known.pop("name", None)
|
|
777
907
|
instruction = known.pop("instruction", None)
|
|
778
908
|
if not name or not str(name).strip():
|
|
@@ -781,9 +911,20 @@ class AgentClient(BaseClient):
|
|
|
781
911
|
raise ValueError("Agent instruction cannot be empty or whitespace")
|
|
782
912
|
|
|
783
913
|
validated_instruction = validate_agent_instruction(str(instruction))
|
|
784
|
-
|
|
914
|
+
return str(name).strip(), validated_instruction
|
|
785
915
|
|
|
786
|
-
|
|
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
|
+
"""
|
|
787
928
|
tool_refs = extras.pop("_tool_refs", None)
|
|
788
929
|
agent_refs = extras.pop("_agent_refs", None)
|
|
789
930
|
mcp_refs = extras.pop("_mcp_refs", None)
|
|
@@ -796,10 +937,56 @@ class AgentClient(BaseClient):
|
|
|
796
937
|
resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
|
|
797
938
|
resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
|
|
798
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
|
+
|
|
799
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
|
+
|
|
800
976
|
provider = known.pop("provider", None)
|
|
801
977
|
model_name = known.pop("model_name", None)
|
|
802
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
|
+
"""
|
|
803
990
|
agent_type_value = known.pop("agent_type", None)
|
|
804
991
|
fallback_type_value = known.pop("type", None)
|
|
805
992
|
if agent_type_value is None:
|
|
@@ -807,6 +994,26 @@ class AgentClient(BaseClient):
|
|
|
807
994
|
|
|
808
995
|
framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
|
|
809
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)
|
|
810
1017
|
account_id = known.pop("account_id", None)
|
|
811
1018
|
description = known.pop("description", None)
|
|
812
1019
|
metadata = _prepare_agent_metadata(known.pop("metadata", None))
|
|
@@ -819,7 +1026,7 @@ class AgentClient(BaseClient):
|
|
|
819
1026
|
final_extras.setdefault("model", resolved_model)
|
|
820
1027
|
|
|
821
1028
|
request = AgentCreateRequest(
|
|
822
|
-
name=
|
|
1029
|
+
name=name,
|
|
823
1030
|
instruction=validated_instruction,
|
|
824
1031
|
model=resolved_model,
|
|
825
1032
|
language_model_id=language_model_id,
|
|
@@ -897,6 +1104,29 @@ class AgentClient(BaseClient):
|
|
|
897
1104
|
"""Backward-compatible helper to create an agent from a configuration file."""
|
|
898
1105
|
return self.create_agent(file=file_path, **overrides)
|
|
899
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
|
+
|
|
900
1130
|
def _update_agent_from_payload(
|
|
901
1131
|
self,
|
|
902
1132
|
agent_id: str,
|
|
@@ -922,6 +1152,8 @@ class AgentClient(BaseClient):
|
|
|
922
1152
|
if mcps_value is not None:
|
|
923
1153
|
mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
|
|
924
1154
|
|
|
1155
|
+
self._resolve_update_model_fields(known)
|
|
1156
|
+
|
|
925
1157
|
request = AgentUpdateRequest(
|
|
926
1158
|
name=known.pop("name", None),
|
|
927
1159
|
instruction=known.pop("instruction", None),
|
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/main.py
CHANGED
|
@@ -212,10 +212,6 @@ class Client(BaseClient):
|
|
|
212
212
|
return self.mcps.get_mcp_tools_from_config(config)
|
|
213
213
|
|
|
214
214
|
# Language Models
|
|
215
|
-
def list_language_models(self) -> list[dict]:
|
|
216
|
-
"""List available language models."""
|
|
217
|
-
data = self._request("GET", "/language-models")
|
|
218
|
-
return data or []
|
|
219
215
|
|
|
220
216
|
# ---- Timeout propagation ----
|
|
221
217
|
@property
|
|
@@ -17,8 +17,8 @@ from glaip_sdk.config.constants import (
|
|
|
17
17
|
DEFAULT_AGENT_PROVIDER,
|
|
18
18
|
DEFAULT_AGENT_TYPE,
|
|
19
19
|
DEFAULT_AGENT_VERSION,
|
|
20
|
-
DEFAULT_MODEL,
|
|
21
20
|
)
|
|
21
|
+
from glaip_sdk.models.constants import DEFAULT_MODEL
|
|
22
22
|
from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
|
|
23
23
|
from glaip_sdk.utils.client_utils import extract_ids
|
|
24
24
|
|
|
@@ -100,6 +100,11 @@ def resolve_language_model_fields(
|
|
|
100
100
|
resolved_model = model_name or model or default_model
|
|
101
101
|
resolved_provider = provider if provider is not None else default_provider
|
|
102
102
|
|
|
103
|
+
if resolved_model and isinstance(resolved_model, str) and "/" in resolved_model:
|
|
104
|
+
parts = resolved_model.split("/", 1)
|
|
105
|
+
resolved_provider = parts[0]
|
|
106
|
+
resolved_model = parts[1]
|
|
107
|
+
|
|
103
108
|
result: dict[str, Any] = {}
|
|
104
109
|
if resolved_model is not None:
|
|
105
110
|
result["model_name"] = resolved_model
|
glaip_sdk/config/constants.py
CHANGED
|
@@ -4,8 +4,28 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
|
|
7
|
+
# Lazy import cache for DEFAULT_MODEL to avoid circular dependency
|
|
8
|
+
_DEFAULT_MODEL: str | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> str:
|
|
12
|
+
"""Lazy import DEFAULT_MODEL from models.constants to avoid circular dependency.
|
|
13
|
+
|
|
14
|
+
Note: Prefer importing DEFAULT_MODEL directly from glaip_sdk.models.constants
|
|
15
|
+
as it is the canonical source. This re-export exists for backward compatibility.
|
|
16
|
+
"""
|
|
17
|
+
if name in ("DEFAULT_MODEL", "SDK_DEFAULT_MODEL"):
|
|
18
|
+
global _DEFAULT_MODEL
|
|
19
|
+
if _DEFAULT_MODEL is None:
|
|
20
|
+
from glaip_sdk.models.constants import ( # noqa: PLC0415
|
|
21
|
+
DEFAULT_MODEL as _MODEL,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DEFAULT_MODEL = _MODEL
|
|
25
|
+
return _DEFAULT_MODEL
|
|
26
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
27
|
+
|
|
28
|
+
|
|
9
29
|
DEFAULT_AGENT_RUN_TIMEOUT = 300
|
|
10
30
|
|
|
11
31
|
# User agent and version
|
glaip_sdk/models/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
1
2
|
"""Models package for AIP SDK.
|
|
2
3
|
|
|
3
4
|
This package provides Pydantic models for API responses.
|
|
@@ -17,7 +18,7 @@ Authors:
|
|
|
17
18
|
|
|
18
19
|
import warnings
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
from glaip_sdk.models._validation import _validate_model, convert_model_for_local_execution
|
|
21
22
|
from glaip_sdk.models.agent import AgentResponse
|
|
22
23
|
from glaip_sdk.models.agent_runs import (
|
|
23
24
|
RunOutputChunk,
|
|
@@ -26,7 +27,22 @@ from glaip_sdk.models.agent_runs import (
|
|
|
26
27
|
RunWithOutput,
|
|
27
28
|
)
|
|
28
29
|
from glaip_sdk.models.common import LanguageModelResponse, TTYRenderer
|
|
30
|
+
|
|
31
|
+
# Pure Pydantic models for API responses (no runtime methods)
|
|
32
|
+
# Import model constants and validation (absolute imports)
|
|
33
|
+
from glaip_sdk.models.constants import (
|
|
34
|
+
DEFAULT_MODEL,
|
|
35
|
+
Anthropic,
|
|
36
|
+
AzureOpenAI,
|
|
37
|
+
Bedrock,
|
|
38
|
+
DeepInfra,
|
|
39
|
+
DeepSeek,
|
|
40
|
+
Google,
|
|
41
|
+
ModelProvider,
|
|
42
|
+
OpenAI,
|
|
43
|
+
)
|
|
29
44
|
from glaip_sdk.models.mcp import MCPResponse
|
|
45
|
+
from glaip_sdk.models.model import Model
|
|
30
46
|
|
|
31
47
|
# Export schedule models
|
|
32
48
|
from glaip_sdk.models.schedule import ( # noqa: F401
|
|
@@ -82,6 +98,19 @@ def __getattr__(name: str) -> type:
|
|
|
82
98
|
|
|
83
99
|
|
|
84
100
|
__all__ = [
|
|
101
|
+
# Model constants and validation
|
|
102
|
+
"OpenAI",
|
|
103
|
+
"Anthropic",
|
|
104
|
+
"Google",
|
|
105
|
+
"AzureOpenAI",
|
|
106
|
+
"DeepInfra",
|
|
107
|
+
"DeepSeek",
|
|
108
|
+
"Bedrock",
|
|
109
|
+
"Model",
|
|
110
|
+
"ModelProvider",
|
|
111
|
+
"DEFAULT_MODEL",
|
|
112
|
+
"_validate_model",
|
|
113
|
+
"convert_model_for_local_execution",
|
|
85
114
|
# Pure Pydantic response models (recommended for type hints)
|
|
86
115
|
"AgentResponse",
|
|
87
116
|
"ToolResponse",
|