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.
Files changed (135) hide show
  1. glaip_sdk/agents/base.py +283 -30
  2. glaip_sdk/agents/component.py +233 -0
  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 +1 -1
  17. glaip_sdk/cli/commands/configure.py +1 -2
  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/entrypoint.py +20 -0
  46. glaip_sdk/cli/main.py +112 -35
  47. glaip_sdk/cli/pager.py +3 -3
  48. glaip_sdk/cli/resolution.py +2 -1
  49. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  50. glaip_sdk/cli/slash/agent_session.py +1 -1
  51. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  52. glaip_sdk/cli/slash/session.py +343 -20
  53. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  54. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  55. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  56. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  57. glaip_sdk/cli/slash/tui/context.py +92 -0
  58. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  59. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  60. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  61. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  62. glaip_sdk/cli/slash/tui/loading.py +43 -21
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +388 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +1 -1
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +293 -17
  78. glaip_sdk/client/base.py +25 -0
  79. glaip_sdk/client/hitl.py +136 -0
  80. glaip_sdk/client/main.py +7 -5
  81. glaip_sdk/client/mcps.py +44 -13
  82. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  83. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  84. glaip_sdk/client/payloads/agent/responses.py +43 -0
  85. glaip_sdk/client/run_rendering.py +109 -30
  86. glaip_sdk/client/schedules.py +439 -0
  87. glaip_sdk/client/tools.py +52 -23
  88. glaip_sdk/config/constants.py +22 -2
  89. glaip_sdk/guardrails/__init__.py +80 -0
  90. glaip_sdk/guardrails/serializer.py +91 -0
  91. glaip_sdk/hitl/__init__.py +35 -2
  92. glaip_sdk/hitl/base.py +64 -0
  93. glaip_sdk/hitl/callback.py +43 -0
  94. glaip_sdk/hitl/local.py +1 -31
  95. glaip_sdk/hitl/remote.py +523 -0
  96. glaip_sdk/models/__init__.py +47 -1
  97. glaip_sdk/models/_provider_mappings.py +101 -0
  98. glaip_sdk/models/_validation.py +97 -0
  99. glaip_sdk/models/agent.py +2 -1
  100. glaip_sdk/models/agent_runs.py +2 -1
  101. glaip_sdk/models/constants.py +141 -0
  102. glaip_sdk/models/model.py +170 -0
  103. glaip_sdk/models/schedule.py +224 -0
  104. glaip_sdk/payload_schemas/agent.py +1 -0
  105. glaip_sdk/payload_schemas/guardrails.py +34 -0
  106. glaip_sdk/ptc.py +145 -0
  107. glaip_sdk/registry/tool.py +270 -57
  108. glaip_sdk/runner/__init__.py +20 -3
  109. glaip_sdk/runner/deps.py +4 -1
  110. glaip_sdk/runner/langgraph.py +251 -27
  111. glaip_sdk/runner/logging_config.py +77 -0
  112. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  113. glaip_sdk/runner/ptc_adapter.py +98 -0
  114. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  115. glaip_sdk/schedules/__init__.py +22 -0
  116. glaip_sdk/schedules/base.py +291 -0
  117. glaip_sdk/tools/base.py +67 -14
  118. glaip_sdk/utils/__init__.py +1 -0
  119. glaip_sdk/utils/agent_config.py +8 -2
  120. glaip_sdk/utils/bundler.py +138 -2
  121. glaip_sdk/utils/import_resolver.py +427 -49
  122. glaip_sdk/utils/runtime_config.py +3 -2
  123. glaip_sdk/utils/sync.py +31 -11
  124. glaip_sdk/utils/tool_detection.py +274 -6
  125. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
  126. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  127. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
  128. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  129. glaip_sdk/cli/commands/agents.py +0 -1509
  130. glaip_sdk/cli/commands/mcps.py +0 -1356
  131. glaip_sdk/cli/commands/tools.py +0 -576
  132. glaip_sdk/cli/utils.py +0 -263
  133. glaip_sdk-0.6.19.dist-info/RECORD +0 -163
  134. glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
  135. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ # pylint: disable=duplicate-code
1
2
  """LangGraph-based runner for local agent execution.
2
3
 
3
4
  This module provides the LangGraphRunner which executes glaip-sdk agents
@@ -23,16 +24,20 @@ import logging
23
24
  from dataclasses import dataclass
24
25
  from typing import TYPE_CHECKING, Any
25
26
 
26
- from aip_agents.agent.hitl.manager import ApprovalManager # noqa: PLC0415
27
27
  from gllm_core.utils import LoggerManager
28
28
 
29
29
  from glaip_sdk.client.run_rendering import AgentRunRenderingManager
30
- from glaip_sdk.hitl import LocalPromptHandler, PauseResumeCallback
30
+ from glaip_sdk.hitl import PauseResumeCallback
31
+ from glaip_sdk.models import DEFAULT_MODEL
31
32
  from glaip_sdk.runner.base import BaseRunner
32
33
  from glaip_sdk.runner.deps import (
33
34
  check_local_runtime_available,
34
35
  get_local_runtime_missing_message,
35
36
  )
37
+ from glaip_sdk.runner.ptc_adapter import (
38
+ normalize_ptc_for_aip_agents,
39
+ validate_ptc_for_local_run,
40
+ )
36
41
  from glaip_sdk.utils.tool_storage_provider import build_tool_output_manager
37
42
 
38
43
  if TYPE_CHECKING:
@@ -71,6 +76,10 @@ def _swallow_aip_logs(level: int = logging.ERROR) -> None:
71
76
  logger = LoggerManager().get_logger(__name__)
72
77
 
73
78
 
79
+ # Constants for MCP configuration validation
80
+ _MCP_TRANSPORT_KEYS = {"url", "command", "args", "env", "timeout", "headers"}
81
+
82
+
74
83
  def _convert_chat_history_to_messages(
75
84
  chat_history: list[dict[str, str]] | None,
76
85
  ) -> list[BaseMessage]:
@@ -86,7 +95,11 @@ def _convert_chat_history_to_messages(
86
95
  if not chat_history:
87
96
  return []
88
97
 
89
- from langchain_core.messages import AIMessage, HumanMessage, SystemMessage # noqa: PLC0415
98
+ from langchain_core.messages import ( # noqa: PLC0415
99
+ AIMessage,
100
+ HumanMessage,
101
+ SystemMessage,
102
+ )
90
103
 
91
104
  messages: list[BaseMessage] = []
92
105
  for msg in chat_history:
@@ -121,7 +134,7 @@ class LangGraphRunner(BaseRunner):
121
134
  Defaults to "gpt-4o-mini".
122
135
  """
123
136
 
124
- default_model: str = "openai/gpt-4o-mini"
137
+ default_model: str = DEFAULT_MODEL
125
138
 
126
139
  def run(
127
140
  self,
@@ -259,7 +272,9 @@ class LangGraphRunner(BaseRunner):
259
272
 
260
273
  # Build the local LangGraphReactAgent from the glaip_sdk Agent
261
274
  local_agent = self.build_langgraph_agent(
262
- agent, runtime_config=runtime_config, pause_resume_callback=pause_resume_callback
275
+ agent,
276
+ runtime_config=runtime_config,
277
+ pause_resume_callback=pause_resume_callback,
263
278
  )
264
279
 
265
280
  # Convert chat history to LangChain messages for the agent
@@ -305,12 +320,26 @@ class LangGraphRunner(BaseRunner):
305
320
  renderer.close()
306
321
  finally:
307
322
  raise
323
+ finally:
324
+ # Cleanup PTC sandbox and MCP sessions
325
+ # Isolated cleanup steps so one failure doesn't skip the other
326
+ try:
327
+ await local_agent.cleanup()
328
+ except Exception as e:
329
+ logger.warning("Failed to cleanup agent resources: %s", e)
308
330
 
309
331
  # Use shared finalizer to avoid code duplication
310
- from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
332
+ from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
333
+ finalize_render_manager,
334
+ )
311
335
 
312
336
  return finalize_render_manager(
313
- render_manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
337
+ render_manager,
338
+ renderer,
339
+ final_text,
340
+ stats_usage,
341
+ started_monotonic,
342
+ finished_monotonic,
314
343
  )
315
344
 
316
345
  def build_langgraph_agent(
@@ -365,6 +394,17 @@ class LangGraphRunner(BaseRunner):
365
394
  merged_agent_config = self._merge_agent_config(agent, normalized_config)
366
395
  agent_config_params, agent_config_kwargs = self._apply_agent_config(merged_agent_config)
367
396
 
397
+ # Validate and normalize PTC configuration for local runs
398
+ ptc_config = validate_ptc_for_local_run(
399
+ agent_ptc=agent.ptc if hasattr(agent, "ptc") else None,
400
+ agent_config_ptc=None, # Already validated in _merge_agent_config
401
+ runtime_config_ptc=None, # Already validated in _normalize_runtime_config
402
+ )
403
+ normalized_ptc = normalize_ptc_for_aip_agents(ptc_config)
404
+
405
+ # Resolve model and merge its configuration into agent kwargs
406
+ model_string = self._resolve_local_model(agent, agent_config_kwargs)
407
+
368
408
  tool_output_manager = self._resolve_tool_output_manager(
369
409
  agent,
370
410
  merged_agent_config,
@@ -378,16 +418,18 @@ class LangGraphRunner(BaseRunner):
378
418
  shared_tool_output_manager=tool_output_manager,
379
419
  )
380
420
 
381
- # Build the LangGraphReactAgent with tools, sub-agents, and configs
421
+ # Build the LangGraphReactAgent with tools, sub-agents, configs, and PTC
382
422
  local_agent = LangGraphReactAgent(
383
423
  name=agent.name,
384
424
  instruction=agent.instruction,
385
425
  description=agent.description,
386
- model=agent.model or self.default_model,
426
+ model=model_string,
387
427
  tools=langchain_tools,
388
428
  agents=sub_agent_instances if sub_agent_instances else None,
389
429
  tool_configs=tool_configs if tool_configs else None,
390
430
  tool_output_manager=tool_output_manager,
431
+ guardrail=agent.guardrail,
432
+ ptc_config=normalized_ptc,
391
433
  **agent_config_params,
392
434
  **agent_config_kwargs,
393
435
  )
@@ -434,13 +476,22 @@ class LangGraphRunner(BaseRunner):
434
476
  hitl_enabled = merged_agent_config.get("hitl_enabled", False)
435
477
  if hitl_enabled:
436
478
  try:
479
+ from aip_agents.agent.hitl.manager import ( # noqa: PLC0415
480
+ ApprovalManager,
481
+ )
482
+
483
+ from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
484
+
437
485
  local_agent.hitl_manager = ApprovalManager(
438
486
  prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
439
487
  )
440
488
  # Store callback reference for setting renderer later
441
489
  if pause_resume_callback:
442
490
  local_agent._pause_resume_callback = pause_resume_callback
443
- logger.debug("HITL manager injected for agent '%s' (hitl_enabled=True)", agent_name)
491
+ logger.debug(
492
+ "HITL manager injected for agent '%s' (hitl_enabled=True)",
493
+ agent_name,
494
+ )
444
495
  except ImportError as e:
445
496
  # Missing dependencies - fail fast
446
497
  raise ImportError("Local HITL requires aip_agents. Install with: pip install 'glaip-sdk[local]'") from e
@@ -448,7 +499,10 @@ class LangGraphRunner(BaseRunner):
448
499
  # Other errors during HITL setup - fail fast
449
500
  raise RuntimeError(f"Failed to initialize HITL manager for agent '{agent_name}'") from e
450
501
  else:
451
- logger.debug("HITL manager not injected for agent '%s' (hitl_enabled=False)", agent_name)
502
+ logger.debug(
503
+ "HITL manager not injected for agent '%s' (hitl_enabled=False)",
504
+ agent_name,
505
+ )
452
506
 
453
507
  def _build_sub_agents(
454
508
  self,
@@ -543,6 +597,14 @@ class LangGraphRunner(BaseRunner):
543
597
  if not runtime_config:
544
598
  return {}
545
599
 
600
+ # Check for unsupported runtime_config.ptc (v1 constraint)
601
+ if "ptc" in runtime_config:
602
+ validate_ptc_for_local_run(
603
+ agent_ptc=None,
604
+ agent_config_ptc=None,
605
+ runtime_config_ptc=runtime_config["ptc"],
606
+ )
607
+
546
608
  # 1. Extract global configs and normalize keys
547
609
  global_tool_configs = normalize_local_config_keys(runtime_config.get("tool_configs", {}))
548
610
  global_mcp_configs = normalize_local_config_keys(runtime_config.get("mcp_configs", {}))
@@ -702,6 +764,14 @@ class LangGraphRunner(BaseRunner):
702
764
  # Get runtime agent_config
703
765
  runtime_agent_config = normalized_config.get("agent_config", {})
704
766
 
767
+ # Check for unsupported agent_config.ptc (local runs constraint)
768
+ if "ptc" in agent_agent_config or "ptc" in runtime_agent_config:
769
+ validate_ptc_for_local_run(
770
+ agent_ptc=None,
771
+ agent_config_ptc=agent_agent_config.get("ptc") or runtime_agent_config.get("ptc"),
772
+ runtime_config_ptc=None,
773
+ )
774
+
705
775
  # Merge: agent definition < runtime config
706
776
  return merge_configs(agent_agent_config, runtime_agent_config)
707
777
 
@@ -724,6 +794,7 @@ class LangGraphRunner(BaseRunner):
724
794
  """
725
795
  direct_params = {}
726
796
  kwargs_params = {}
797
+ config_dict = {}
727
798
 
728
799
  # Direct constructor parameters
729
800
  if "planning" in agent_config:
@@ -735,6 +806,7 @@ class LangGraphRunner(BaseRunner):
735
806
  # Kwargs parameters (passed through **kwargs to BaseAgent)
736
807
  if "enable_pii" in agent_config:
737
808
  kwargs_params["enable_pii"] = agent_config["enable_pii"]
809
+ config_dict["enable_pii"] = agent_config["enable_pii"]
738
810
 
739
811
  if "memory" in agent_config:
740
812
  # Map "memory" to "memory_backend" for aip-agents compatibility
@@ -746,8 +818,73 @@ class LangGraphRunner(BaseRunner):
746
818
  if key in agent_config:
747
819
  kwargs_params[key] = agent_config[key]
748
820
 
821
+ # Ensure we pass a config dictionary to BaseAgent, which uses it for
822
+ # LM configuration (api keys, etc.). Memory settings are passed only
823
+ # via kwargs to avoid leaking into LM invoker config.
824
+ if config_dict:
825
+ kwargs_params["config"] = config_dict
826
+
749
827
  return direct_params, kwargs_params
750
828
 
829
+ def _convert_model_for_local(self, model: Any) -> tuple[str, dict[str, Any]]:
830
+ """Convert model to aip_agents format for local execution.
831
+
832
+ Args:
833
+ model: Model object or string identifier.
834
+
835
+ Returns:
836
+ Tuple of (model_string, config_dict).
837
+ """
838
+ from glaip_sdk.models._validation import ( # noqa: PLC0415
839
+ convert_model_for_local_execution,
840
+ )
841
+
842
+ return convert_model_for_local_execution(model)
843
+
844
+ def _resolve_local_model(self, agent: Agent, agent_config_kwargs: dict[str, Any]) -> str:
845
+ """Resolve model string and merge its configuration into agent kwargs.
846
+
847
+ This method extracts model-specific credentials and hyperparameters from a Model
848
+ object and merges them into the 'config' dictionary within agent_config_kwargs.
849
+ This is required because BaseAgent expects LM settings (api keys, etc.) to be
850
+ inside the 'config' parameter, not top-level kwargs.
851
+
852
+ Example:
853
+ If agent has:
854
+ - model = Model(id="deepinfra/model", credentials="key-123")
855
+ - agent_config_kwargs = {"enable_pii": True, "config": {"enable_pii": True}}
856
+
857
+ _resolve_local_model will:
858
+ 1. Resolve model_string to "openai-compatible/model"
859
+ 2. Extract model_config as {"lm_api_key": "key-123"}
860
+ 3. Update agent_config_kwargs["config"] to:
861
+ {"enable_pii": True, "lm_api_key": "key-123"}
862
+
863
+ Args:
864
+ agent: The glaip_sdk Agent.
865
+ agent_config_kwargs: Agent config kwargs to update (modified in-place).
866
+
867
+ Returns:
868
+ The model identifier string for local execution.
869
+ """
870
+ model_to_use = agent.model or self.default_model
871
+ model_string, model_config = self._convert_model_for_local(model_to_use)
872
+
873
+ if model_config:
874
+ # Normalize config to a dict early to simplify merging
875
+ config_val = agent_config_kwargs.get("config", {})
876
+ if hasattr(config_val, "model_dump"):
877
+ config_val = config_val.model_dump()
878
+
879
+ if not isinstance(config_val, dict):
880
+ config_val = {}
881
+
882
+ # Use a single merge path for model configuration
883
+ config_val.update(model_config)
884
+ agent_config_kwargs["config"] = config_val
885
+
886
+ return model_string
887
+
751
888
  def _apply_runtime_mcp_configs(
752
889
  self,
753
890
  base_configs: dict[str, Any],
@@ -776,39 +913,126 @@ class LangGraphRunner(BaseRunner):
776
913
  base_config: dict[str, Any],
777
914
  override: dict[str, Any] | None,
778
915
  ) -> dict[str, Any]:
779
- """Merge a single MCP config with runtime override.
916
+ """Merge a single MCP config with a runtime override, handling normalization and parity fixes.
917
+
918
+ This method orchestrates the merging of base MCP settings (from the object definition)
919
+ with runtime overrides. It enforces Platform parity by prioritizing the nested 'config'
920
+ block while maintaining robustness for local development by auto-fixing flat transport keys.
921
+
922
+ The merge follows these priority rules (highest to lowest):
923
+ 1. Misplaced flat keys in the override (e.g., 'url' at top level) - Auto-fixed with warning.
924
+ 2. Nested 'config' block in the override (Matches Platform/Constructor schema).
925
+ 3. Authentication objects in the override (Converted to HTTP headers).
926
+ 4. Structural settings in the override (e.g., 'allowed_tools').
927
+ 5. Base configuration from the MCP object definition.
928
+
929
+ Examples:
930
+ >>> # 1. Strict Nested Style (Recommended)
931
+ >>> override = {"config": {"url": "https://new.api"}, "allowed_tools": ["t1"]}
932
+ >>> self._merge_single_mcp_config("mcp", base, override)
933
+ >>> # Result: {"url": "https://new.api", "allowed_tools": ["t1"], ...}
934
+
935
+ >>> # 2. Flat Legacy Style (Auto-fixed with warning)
936
+ >>> override = {"url": "https://new.api"}
937
+ >>> self._merge_single_mcp_config("mcp", base, override)
938
+ >>> # Result: {"url": "https://new.api", ...}
939
+
940
+ >>> # 3. Header Merging (Preserves Auth)
941
+ >>> base = {"headers": {"Authorization": "Bearer token"}}
942
+ >>> override = {"headers": {"X-Custom": "val"}}
943
+ >>> self._merge_single_mcp_config("mcp", base, override)
944
+ >>> # Result: {"headers": {"Authorization": "Bearer token", "X-Custom": "val"}, ...}
780
945
 
781
946
  Args:
782
- server_name: Name of the MCP server.
783
- base_config: Base config from adapter.
784
- override: Optional runtime override config.
947
+ server_name: Name of the MCP server being configured.
948
+ base_config: Base configuration dictionary derived from the MCP object.
949
+ override: Optional dictionary of runtime overrides.
785
950
 
786
951
  Returns:
787
- Merged config dict.
952
+ A fully merged and normalized configuration dictionary ready for the local runner.
788
953
  """
789
954
  merged = base_config.copy()
790
955
 
791
956
  if not override:
792
957
  return merged
793
958
 
794
- from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
795
- MCPConfigBuilder,
796
- )
959
+ # 1. Check for misplaced keys and warn (DX/Parity guidance)
960
+ self._warn_if_mcp_override_misplaced(server_name, override)
961
+
962
+ # 2. Apply Authentication (Converted to headers)
963
+ self._apply_mcp_auth_override(server_name, merged, override)
964
+
965
+ # 3. Apply Transport Settings (Nested 'config')
966
+ if "config" in override and isinstance(override["config"], dict):
967
+ merged.update(override["config"])
797
968
 
798
- # Handle authentication override
799
- if "authentication" in override:
800
- headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
801
- if headers:
802
- merged["headers"] = headers
803
- logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
969
+ # 4. Apply Structural Settings (e.g., allowed_tools)
970
+ if "allowed_tools" in override:
971
+ merged["allowed_tools"] = override["allowed_tools"]
804
972
 
805
- # Merge other config keys (excluding authentication since we converted it)
973
+ # 5. Preserve unknown top-level keys (backward compatibility)
974
+ known_keys = _MCP_TRANSPORT_KEYS | {"config", "authentication", "allowed_tools"}
806
975
  for key, value in override.items():
807
- if key != "authentication":
976
+ if key not in known_keys:
808
977
  merged[key] = value
809
978
 
979
+ # 6. Apply Auto-fix for misplaced keys (Local Success)
980
+ for key in [k for k in override if k in _MCP_TRANSPORT_KEYS]:
981
+ val = override[key]
982
+ # Special case: Merge headers instead of overwriting to preserve auth
983
+ if key == "headers" and isinstance(val, dict) and isinstance(merged.get("headers"), dict):
984
+ merged["headers"].update(val)
985
+ else:
986
+ merged[key] = val
987
+
810
988
  return merged
811
989
 
990
+ def _warn_if_mcp_override_misplaced(self, server_name: str, override: dict[str, Any]) -> None:
991
+ """Log a warning if transport keys are found at the top level of an override.
992
+
993
+ Args:
994
+ server_name: Name of the MCP server.
995
+ override: The raw override dictionary.
996
+ """
997
+ misplaced = [k for k in override if k in _MCP_TRANSPORT_KEYS]
998
+ if misplaced:
999
+ logger.warning(
1000
+ "MCP '%s' override contains transport keys at the top level: %s. "
1001
+ "This structure is inconsistent with the Platform and MCP constructor. "
1002
+ "Transport settings should be nested within a 'config' dictionary. "
1003
+ "Example: mcp_configs={'%s': {'config': {'%s': '...'}}}. "
1004
+ "Automatically merging top-level keys for local execution parity.",
1005
+ server_name,
1006
+ misplaced,
1007
+ server_name,
1008
+ misplaced[0],
1009
+ )
1010
+
1011
+ def _apply_mcp_auth_override(
1012
+ self,
1013
+ server_name: str,
1014
+ merged_config: dict[str, Any],
1015
+ override: dict[str, Any],
1016
+ ) -> None:
1017
+ """Convert authentication override to headers and apply to config.
1018
+
1019
+ Args:
1020
+ server_name: Name of the MCP server.
1021
+ merged_config: The configuration being built (mutated in place).
1022
+ override: The raw override dictionary.
1023
+ """
1024
+ if "authentication" not in override:
1025
+ return
1026
+
1027
+ from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
1028
+ MCPConfigBuilder,
1029
+ )
1030
+
1031
+ headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
1032
+ if headers:
1033
+ merged_config["headers"] = headers
1034
+ logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
1035
+
812
1036
  def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
813
1037
  """Validate that a sub-agent reference is supported for local execution.
814
1038
 
@@ -0,0 +1,77 @@
1
+ """Logging configuration for CLI to suppress noisy dependency warnings.
2
+
3
+ This module provides centralized logging suppression for optional dependencies
4
+ that emit noisy warnings during CLI usage. Warnings are suppressed by default
5
+ but can be shown using GLAIP_LOG_LEVEL=DEBUG.
6
+
7
+ Authors:
8
+ Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import warnings
14
+
15
+ NOISY_LOGGERS = ["transformers", "gllm_privacy", "google.cloud.aiplatform"]
16
+
17
+
18
+ class NameFilter(logging.Filter):
19
+ """Filter logs by logger name prefix."""
20
+
21
+ def __init__(self, prefixes: list[str]) -> None:
22
+ """Initialize filter with logger name prefixes to suppress.
23
+
24
+ Args:
25
+ prefixes: List of logger name prefixes to filter out.
26
+ """
27
+ super().__init__()
28
+ self.prefixes = prefixes
29
+
30
+ def filter(self, record: logging.LogRecord) -> bool:
31
+ """Filter log records by name prefix.
32
+
33
+ Args:
34
+ record: Log record to filter.
35
+
36
+ Returns:
37
+ False if record should be suppressed, True otherwise.
38
+ """
39
+ return not any(record.name.startswith(p) for p in self.prefixes)
40
+
41
+
42
+ def setup_cli_logging() -> None:
43
+ """Suppress INFO from noisy third-party libraries.
44
+
45
+ Use GLAIP_LOG_LEVEL=DEBUG to see all warnings.
46
+ This function is idempotent - calling it multiple times is safe.
47
+ """
48
+ # Check env level FIRST before any suppression
49
+ env_level = os.getenv("GLAIP_LOG_LEVEL", "").upper()
50
+ is_debug = env_level == "DEBUG"
51
+
52
+ if is_debug:
53
+ # Debug mode: show everything, no suppression
54
+ if env_level and hasattr(logging, env_level):
55
+ logging.basicConfig(level=getattr(logging, env_level))
56
+ return
57
+
58
+ # Default mode: suppress noisy warnings
59
+ if env_level and hasattr(logging, env_level):
60
+ logging.basicConfig(level=getattr(logging, env_level))
61
+
62
+ # Add handler filter to suppress by name prefix (handles child loggers)
63
+ # Check if filter already exists to ensure idempotency
64
+ root_logger = logging.getLogger()
65
+ has_name_filter = any(isinstance(f, NameFilter) for h in root_logger.handlers for f in h.filters)
66
+
67
+ if not has_name_filter:
68
+ handler = logging.StreamHandler()
69
+ handler.addFilter(NameFilter(NOISY_LOGGERS))
70
+ root_logger.addHandler(handler)
71
+
72
+ # Suppress FutureWarning for GCS (idempotent - multiple calls are safe)
73
+ warnings.filterwarnings(
74
+ "ignore",
75
+ category=FutureWarning,
76
+ message=r".*google-cloud-storage.*",
77
+ )
@@ -60,19 +60,42 @@ class MCPConfigBuilder:
60
60
  def _handle_custom_header(authentication: dict[str, Any]) -> dict[str, str] | None:
61
61
  """Handle custom-header auth type."""
62
62
  headers = authentication.get("headers")
63
- if isinstance(headers, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in headers.items()):
64
- return headers
65
- logger.warning("custom-header auth requires 'headers' dict with string keys/values")
63
+ if isinstance(headers, dict):
64
+ cleaned_headers = MCPConfigBuilder._clean_headers(headers)
65
+ if cleaned_headers:
66
+ return cleaned_headers
67
+
68
+ logger.warning("custom-header auth requires 'headers' dict with at least one non-null key/value")
66
69
  return None
67
70
 
71
+ @staticmethod
72
+ def _clean_headers(headers: dict[str, Any]) -> dict[str, str] | None:
73
+ """Clean header dict by filtering None keys/values and stringifying values.
74
+
75
+ Args:
76
+ headers: Raw headers dict potentially containing None or non-string values.
77
+
78
+ Returns:
79
+ Cleaned headers dict with string keys/values, or None if empty after cleaning.
80
+ """
81
+ cleaned: dict[str, str] = {}
82
+ for key, value in headers.items():
83
+ if key is None:
84
+ logger.warning("Dropping header with null key")
85
+ continue
86
+ if value is None:
87
+ logger.warning("Dropping header '%s' with null value", key)
88
+ continue
89
+ cleaned[str(key)] = str(value)
90
+
91
+ return cleaned if cleaned else None
92
+
68
93
  @staticmethod
69
94
  def _handle_bearer_token(authentication: dict[str, Any]) -> dict[str, str] | None:
70
95
  """Handle bearer-token auth type."""
71
- # Check if headers provided directly
72
96
  headers = authentication.get("headers")
73
97
  if isinstance(headers, dict):
74
- return headers
75
- # Otherwise build from token
98
+ return MCPConfigBuilder._clean_headers(headers)
76
99
  token = authentication.get("token")
77
100
  if token:
78
101
  return {"Authorization": f"Bearer {token}"}
@@ -82,11 +105,9 @@ class MCPConfigBuilder:
82
105
  @staticmethod
83
106
  def _handle_api_key(authentication: dict[str, Any]) -> dict[str, str] | None:
84
107
  """Handle api-key auth type."""
85
- # Check if headers provided directly
86
108
  headers = authentication.get("headers")
87
109
  if isinstance(headers, dict):
88
- return headers
89
- # Otherwise build from key/value
110
+ return MCPConfigBuilder._clean_headers(headers)
90
111
  key = authentication.get("key")
91
112
  value = authentication.get("value")
92
113
  if key and value:
@@ -0,0 +1,98 @@
1
+ """PTC adapter for local runner integration.
2
+
3
+ This module provides validation and normalization of PTC configuration
4
+ for use in the local LangGraph runner. It ensures PTC is configured
5
+ correctly and rejects unsupported configuration sources.
6
+
7
+ Authors:
8
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from glaip_sdk.exceptions import ValidationError
16
+
17
+ if TYPE_CHECKING:
18
+ from glaip_sdk.ptc import PTC
19
+
20
+
21
+ def validate_ptc_for_local_run(
22
+ agent_ptc: PTC | None,
23
+ agent_config_ptc: Any | None,
24
+ runtime_config_ptc: Any | None,
25
+ ) -> PTC | None:
26
+ """Validate PTC configuration for local runs.
27
+
28
+ Args:
29
+ agent_ptc: PTC object from Agent.ptc parameter.
30
+ agent_config_ptc: PTC from agent_config (should be None for local).
31
+ runtime_config_ptc: PTC from runtime_config (should be None for v1).
32
+
33
+ Returns:
34
+ Validated PTC object if enabled, None otherwise.
35
+
36
+ Raises:
37
+ ValidationError: If agent_config.ptc or runtime_config.ptc are provided,
38
+ or if agent_ptc is not a PTC instance when provided.
39
+ """
40
+ if agent_config_ptc is not None:
41
+ msg = (
42
+ "PTC configuration via agent_config.ptc is not supported for local runs. "
43
+ "Please configure PTC using the Agent.ptc parameter instead: "
44
+ "Agent(name='...', ptc=PTC(enabled=True), ...)"
45
+ )
46
+ raise ValidationError(msg)
47
+
48
+ if runtime_config_ptc is not None:
49
+ msg = (
50
+ "PTC configuration via runtime_config.ptc is not supported in v1. "
51
+ "PTC configuration must be set at Agent initialization time using "
52
+ "the Agent.ptc parameter and cannot be overridden at runtime. "
53
+ "This preserves local/remote parity and prevents ambiguous precedence."
54
+ )
55
+ raise ValidationError(msg)
56
+
57
+ if agent_ptc is None:
58
+ return None
59
+
60
+ from glaip_sdk.ptc import PTC # noqa: PLC0415
61
+
62
+ if not isinstance(agent_ptc, PTC):
63
+ msg = (
64
+ f"Agent.ptc must be a PTC instance, got {type(agent_ptc).__name__}. "
65
+ "Example: Agent(name='...', ptc=PTC(enabled=True), ...)"
66
+ )
67
+ raise ValidationError(msg)
68
+
69
+ if not agent_ptc.enabled:
70
+ return None
71
+
72
+ return agent_ptc
73
+
74
+
75
+ def normalize_ptc_for_aip_agents(ptc: PTC | None) -> Any:
76
+ """Normalize PTC config for aip-agents LangGraphReactAgent.
77
+
78
+ Args:
79
+ ptc: Validated PTC object or None.
80
+
81
+ Returns:
82
+ PTCSandboxConfig for aip-agents or None if PTC disabled.
83
+ """
84
+ if ptc is None or not ptc.enabled:
85
+ return None
86
+
87
+ from aip_agents.ptc import PromptConfig, PTCSandboxConfig # noqa: PLC0415
88
+
89
+ # Build PromptConfig if prompt dict is provided.
90
+ prompt_config = None
91
+ if ptc.prompt is not None:
92
+ prompt_config = PromptConfig(**ptc.prompt)
93
+
94
+ return PTCSandboxConfig(
95
+ enabled=ptc.enabled,
96
+ sandbox_timeout=ptc.sandbox_timeout,
97
+ prompt=prompt_config,
98
+ )