glaip-sdk 0.6.5b3__py3-none-any.whl → 0.7.17__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.
Files changed (145) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +362 -39
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +375 -25
  55. glaip_sdk/cli/slash/tui/__init__.py +28 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +5 -3
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +57 -26
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +89 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/registry/tool.py +273 -66
  109. glaip_sdk/runner/__init__.py +76 -0
  110. glaip_sdk/runner/base.py +84 -0
  111. glaip_sdk/runner/deps.py +115 -0
  112. glaip_sdk/runner/langgraph.py +1055 -0
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  115. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  116. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  117. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  118. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  119. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  120. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  121. glaip_sdk/schedules/__init__.py +22 -0
  122. glaip_sdk/schedules/base.py +291 -0
  123. glaip_sdk/tools/base.py +67 -14
  124. glaip_sdk/utils/__init__.py +1 -0
  125. glaip_sdk/utils/a2a/__init__.py +34 -0
  126. glaip_sdk/utils/a2a/event_processor.py +188 -0
  127. glaip_sdk/utils/agent_config.py +8 -2
  128. glaip_sdk/utils/bundler.py +138 -2
  129. glaip_sdk/utils/import_resolver.py +43 -11
  130. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  131. glaip_sdk/utils/runtime_config.py +120 -0
  132. glaip_sdk/utils/sync.py +31 -11
  133. glaip_sdk/utils/tool_detection.py +301 -0
  134. glaip_sdk/utils/tool_storage_provider.py +140 -0
  135. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
  136. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  137. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  138. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  139. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  140. glaip_sdk/cli/commands/agents.py +0 -1509
  141. glaip_sdk/cli/commands/mcps.py +0 -1356
  142. glaip_sdk/cli/commands/tools.py +0 -576
  143. glaip_sdk/cli/utils.py +0 -263
  144. glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
  145. glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
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._agent_payloads import (
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
- **kwargs: Additional arguments for standalone initialization
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,8 +576,13 @@ class AgentClient(BaseClient):
453
576
  Returns:
454
577
  Final text string.
455
578
  """
579
+ from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
580
+ finalize_render_manager,
581
+ )
582
+
456
583
  manager = self._get_renderer_manager()
457
- return manager.finalize_renderer(
584
+ return finalize_render_manager(
585
+ manager,
458
586
  renderer,
459
587
  final_text,
460
588
  stats_usage,
@@ -482,6 +610,20 @@ class AgentClient(BaseClient):
482
610
  self._mcp_client = MCPClient(parent_client=self)
483
611
  return self._mcp_client
484
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
+
485
627
  def _normalise_reference_entry(
486
628
  self,
487
629
  entry: Any,
@@ -749,10 +891,18 @@ class AgentClient(BaseClient):
749
891
  plural_label="MCPs",
750
892
  )
751
893
 
752
- def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
753
- """Create an agent using a fully prepared payload mapping."""
754
- 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.
896
+
897
+ Args:
898
+ known: Known fields dictionary.
899
+
900
+ Returns:
901
+ Tuple of (name, validated_instruction).
755
902
 
903
+ Raises:
904
+ ValueError: If name or instruction is empty/whitespace.
905
+ """
756
906
  name = known.pop("name", None)
757
907
  instruction = known.pop("instruction", None)
758
908
  if not name or not str(name).strip():
@@ -761,9 +911,20 @@ class AgentClient(BaseClient):
761
911
  raise ValueError("Agent instruction cannot be empty or whitespace")
762
912
 
763
913
  validated_instruction = validate_agent_instruction(str(instruction))
764
- _normalise_sequence_fields(known)
914
+ return str(name).strip(), validated_instruction
765
915
 
766
- resolved_model = known.pop("model", None) or DEFAULT_MODEL
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
+ """
767
928
  tool_refs = extras.pop("_tool_refs", None)
768
929
  agent_refs = extras.pop("_agent_refs", None)
769
930
  mcp_refs = extras.pop("_mcp_refs", None)
@@ -776,10 +937,56 @@ class AgentClient(BaseClient):
776
937
  resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
777
938
  resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
778
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
+
779
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
+
780
976
  provider = known.pop("provider", None)
781
977
  model_name = known.pop("model_name", None)
782
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
+ """
783
990
  agent_type_value = known.pop("agent_type", None)
784
991
  fallback_type_value = known.pop("type", None)
785
992
  if agent_type_value is None:
@@ -787,6 +994,26 @@ class AgentClient(BaseClient):
787
994
 
788
995
  framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
789
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)
790
1017
  account_id = known.pop("account_id", None)
791
1018
  description = known.pop("description", None)
792
1019
  metadata = _prepare_agent_metadata(known.pop("metadata", None))
@@ -799,7 +1026,7 @@ class AgentClient(BaseClient):
799
1026
  final_extras.setdefault("model", resolved_model)
800
1027
 
801
1028
  request = AgentCreateRequest(
802
- name=str(name).strip(),
1029
+ name=name,
803
1030
  instruction=validated_instruction,
804
1031
  model=resolved_model,
805
1032
  language_model_id=language_model_id,
@@ -877,6 +1104,29 @@ class AgentClient(BaseClient):
877
1104
  """Backward-compatible helper to create an agent from a configuration file."""
878
1105
  return self.create_agent(file=file_path, **overrides)
879
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
+
880
1130
  def _update_agent_from_payload(
881
1131
  self,
882
1132
  agent_id: str,
@@ -902,6 +1152,8 @@ class AgentClient(BaseClient):
902
1152
  if mcps_value is not None:
903
1153
  mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
904
1154
 
1155
+ self._resolve_update_model_fields(known)
1156
+
905
1157
  request = AgentUpdateRequest(
906
1158
  name=known.pop("name", None),
907
1159
  instruction=known.pop("instruction", None),
@@ -1096,6 +1348,7 @@ class AgentClient(BaseClient):
1096
1348
  *,
1097
1349
  renderer: RichStreamRenderer | str | None = "auto",
1098
1350
  runtime_config: dict[str, Any] | None = None,
1351
+ hitl_handler: "RemoteHITLHandler | None" = None,
1099
1352
  **kwargs,
1100
1353
  ) -> str:
1101
1354
  """Run an agent with a message, streaming via a renderer.
@@ -1113,6 +1366,8 @@ class AgentClient(BaseClient):
1113
1366
  "mcp_configs": {"mcp-id": {"setting": "on"}},
1114
1367
  "agent_config": {"planning": True},
1115
1368
  }
1369
+ hitl_handler: Optional RemoteHITLHandler for approval callbacks.
1370
+ Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
1116
1371
  **kwargs: Additional arguments to pass to the run API.
1117
1372
 
1118
1373
  Returns:
@@ -1169,6 +1424,7 @@ class AgentClient(BaseClient):
1169
1424
  timeout_seconds,
1170
1425
  agent_name,
1171
1426
  meta,
1427
+ hitl_handler=hitl_handler,
1172
1428
  )
1173
1429
 
1174
1430
  except KeyboardInterrupt:
@@ -1185,6 +1441,13 @@ class AgentClient(BaseClient):
1185
1441
  if multipart_data:
1186
1442
  multipart_data.close()
1187
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
+
1188
1451
  return self._finalize_renderer(
1189
1452
  r,
1190
1453
  final_text,
@@ -1266,6 +1529,7 @@ class AgentClient(BaseClient):
1266
1529
  *,
1267
1530
  request_timeout: float | None = None,
1268
1531
  runtime_config: dict[str, Any] | None = None,
1532
+ hitl_handler: "RemoteHITLHandler | None" = None,
1269
1533
  **kwargs,
1270
1534
  ) -> AsyncGenerator[dict, None]:
1271
1535
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1282,16 +1546,26 @@ class AgentClient(BaseClient):
1282
1546
  "mcp_configs": {"mcp-id": {"setting": "on"}},
1283
1547
  "agent_config": {"planning": True},
1284
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.
1285
1553
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1286
1554
 
1287
1555
  Yields:
1288
1556
  Dictionary containing parsed JSON chunks from the streaming response
1289
1557
 
1290
1558
  Raises:
1559
+ NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
1291
1560
  AgentTimeoutError: When agent execution times out
1292
1561
  httpx.TimeoutException: When general timeout occurs
1293
1562
  Exception: For other unexpected errors
1294
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
+ )
1295
1569
  # Include runtime_config in kwargs only when caller hasn't already provided it
1296
1570
  if runtime_config is not None and "runtime_config" not in kwargs:
1297
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:
@@ -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")