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.
- glaip_sdk/__init__.py +42 -5
- glaip_sdk/agents/base.py +362 -39
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +375 -25
- glaip_sdk/cli/slash/tui/__init__.py +28 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +43 -21
- glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +290 -16
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -5
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +414 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +47 -1
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +2 -1
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/tool.py +273 -66
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +1055 -0
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +120 -0
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
- glaip_sdk-0.7.17.dist-info/RECORD +224 -0
- {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
- glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
2
|
+
"""LangGraph-based runner for local agent execution.
|
|
3
|
+
|
|
4
|
+
This module provides the LangGraphRunner which executes glaip-sdk agents
|
|
5
|
+
locally via the aip-agents LangGraphReactAgent, without requiring the AIP server.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from glaip_sdk.runner import LangGraphRunner
|
|
12
|
+
>>> from glaip_sdk.agents import Agent
|
|
13
|
+
>>>
|
|
14
|
+
>>> runner = LangGraphRunner()
|
|
15
|
+
>>> agent = Agent(name="my-agent", instruction="You are helpful.")
|
|
16
|
+
>>> result = runner.run(agent, "Hello, world!")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import inspect
|
|
23
|
+
import logging
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
from glaip_sdk.client.run_rendering import AgentRunRenderingManager
|
|
28
|
+
from glaip_sdk.hitl import PauseResumeCallback
|
|
29
|
+
from glaip_sdk.models import DEFAULT_MODEL
|
|
30
|
+
from glaip_sdk.runner.base import BaseRunner
|
|
31
|
+
from glaip_sdk.runner.deps import (
|
|
32
|
+
check_local_runtime_available,
|
|
33
|
+
get_local_runtime_missing_message,
|
|
34
|
+
)
|
|
35
|
+
from glaip_sdk.utils.tool_storage_provider import build_tool_output_manager
|
|
36
|
+
from gllm_core.utils import LoggerManager
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from glaip_sdk.agents.base import Agent
|
|
40
|
+
from langchain_core.messages import BaseMessage
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_AIP_LOGS_SWALLOWED = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _swallow_aip_logs(level: int = logging.ERROR) -> None:
|
|
47
|
+
"""Consume noisy AIPAgents logs once (opt-in via runner flag)."""
|
|
48
|
+
global _AIP_LOGS_SWALLOWED
|
|
49
|
+
if _AIP_LOGS_SWALLOWED:
|
|
50
|
+
return
|
|
51
|
+
prefixes = ("aip_agents.",)
|
|
52
|
+
|
|
53
|
+
def _silence(name: str) -> None:
|
|
54
|
+
lg = logging.getLogger(name)
|
|
55
|
+
lg.handlers = [logging.NullHandler()]
|
|
56
|
+
lg.propagate = False
|
|
57
|
+
lg.setLevel(level)
|
|
58
|
+
|
|
59
|
+
# Silence any already-registered loggers under the given prefixes
|
|
60
|
+
for logger_name in logging.root.manager.loggerDict:
|
|
61
|
+
if any(logger_name.startswith(prefix) for prefix in prefixes):
|
|
62
|
+
_silence(logger_name)
|
|
63
|
+
|
|
64
|
+
# Also set the base prefix loggers so future children inherit silence
|
|
65
|
+
for prefix in prefixes:
|
|
66
|
+
_silence(prefix.rstrip("."))
|
|
67
|
+
_AIP_LOGS_SWALLOWED = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
logger = LoggerManager().get_logger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Constants for MCP configuration validation
|
|
74
|
+
_MCP_TRANSPORT_KEYS = {"url", "command", "args", "env", "timeout", "headers"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _convert_chat_history_to_messages(
|
|
78
|
+
chat_history: list[dict[str, str]] | None,
|
|
79
|
+
) -> list[BaseMessage]:
|
|
80
|
+
"""Convert chat history dicts to LangChain messages.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
chat_history: List of dicts with "role" and "content" keys.
|
|
84
|
+
Supported roles: "user"/"human", "assistant"/"ai", "system".
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of LangChain BaseMessage instances.
|
|
88
|
+
"""
|
|
89
|
+
if not chat_history:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
from langchain_core.messages import ( # noqa: PLC0415
|
|
93
|
+
AIMessage,
|
|
94
|
+
HumanMessage,
|
|
95
|
+
SystemMessage,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
messages: list[BaseMessage] = []
|
|
99
|
+
for msg in chat_history:
|
|
100
|
+
role = msg.get("role", "").lower()
|
|
101
|
+
content = msg.get("content", "")
|
|
102
|
+
|
|
103
|
+
if role in ("user", "human"):
|
|
104
|
+
messages.append(HumanMessage(content=content))
|
|
105
|
+
elif role in ("assistant", "ai"):
|
|
106
|
+
messages.append(AIMessage(content=content))
|
|
107
|
+
elif role == "system":
|
|
108
|
+
messages.append(SystemMessage(content=content))
|
|
109
|
+
else:
|
|
110
|
+
# Default to human message for unknown roles
|
|
111
|
+
logger.warning("Unknown chat history role '%s', treating as user message", role)
|
|
112
|
+
messages.append(HumanMessage(content=content))
|
|
113
|
+
|
|
114
|
+
return messages
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class LangGraphRunner(BaseRunner):
|
|
119
|
+
"""Runner implementation using aip-agents LangGraphReactAgent.
|
|
120
|
+
|
|
121
|
+
Current behavior:
|
|
122
|
+
- Execute via `LangGraphReactAgent.arun_sse_stream()` (normalized SSE-compatible stream)
|
|
123
|
+
- Route all events through `AgentRunRenderingManager.async_process_stream_events`
|
|
124
|
+
for unified rendering between local and remote agents
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
default_model: Model name to use when agent.model is not set.
|
|
128
|
+
Defaults to "gpt-4o-mini".
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
default_model: str = DEFAULT_MODEL
|
|
132
|
+
|
|
133
|
+
def run(
|
|
134
|
+
self,
|
|
135
|
+
agent: Agent,
|
|
136
|
+
message: str,
|
|
137
|
+
verbose: bool = False,
|
|
138
|
+
runtime_config: dict[str, Any] | None = None,
|
|
139
|
+
chat_history: list[dict[str, str]] | None = None,
|
|
140
|
+
*,
|
|
141
|
+
swallow_aip_logs: bool = True,
|
|
142
|
+
**kwargs: Any,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Execute agent synchronously and return final response text.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
agent: The glaip_sdk Agent to execute.
|
|
148
|
+
message: The user message to send to the agent.
|
|
149
|
+
verbose: If True, emit debug trace output during execution.
|
|
150
|
+
Defaults to False.
|
|
151
|
+
runtime_config: Optional runtime configuration for tools, MCPs, etc.
|
|
152
|
+
Defaults to None. (Implemented in PR-04+)
|
|
153
|
+
chat_history: Optional list of prior conversation messages.
|
|
154
|
+
Each message is a dict with "role" and "content" keys.
|
|
155
|
+
Defaults to None.
|
|
156
|
+
swallow_aip_logs: When True (default), silence noisy logs from aip-agents,
|
|
157
|
+
gllm_inference, OpenAILMInvoker, and httpx. Set to False to honor user
|
|
158
|
+
logging configuration.
|
|
159
|
+
**kwargs: Additional keyword arguments passed to the backend.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The final response text from the agent.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
RuntimeError: If the local runtime dependencies are not available.
|
|
166
|
+
RuntimeError: If no final response is received from the agent.
|
|
167
|
+
"""
|
|
168
|
+
if not check_local_runtime_available():
|
|
169
|
+
raise RuntimeError(get_local_runtime_missing_message())
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
asyncio.get_running_loop()
|
|
173
|
+
except RuntimeError:
|
|
174
|
+
pass
|
|
175
|
+
else:
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
"LangGraphRunner.run() cannot be called from a running event loop. "
|
|
178
|
+
"Use 'await LangGraphRunner.arun(...)' instead."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
coro = self._arun_internal(
|
|
182
|
+
agent=agent,
|
|
183
|
+
message=message,
|
|
184
|
+
verbose=verbose,
|
|
185
|
+
runtime_config=runtime_config,
|
|
186
|
+
chat_history=chat_history,
|
|
187
|
+
swallow_aip_logs=swallow_aip_logs,
|
|
188
|
+
**kwargs,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return asyncio.run(coro)
|
|
192
|
+
|
|
193
|
+
async def arun(
|
|
194
|
+
self,
|
|
195
|
+
agent: Agent,
|
|
196
|
+
message: str,
|
|
197
|
+
verbose: bool = False,
|
|
198
|
+
runtime_config: dict[str, Any] | None = None,
|
|
199
|
+
chat_history: list[dict[str, str]] | None = None,
|
|
200
|
+
*,
|
|
201
|
+
swallow_aip_logs: bool = True,
|
|
202
|
+
**kwargs: Any,
|
|
203
|
+
) -> str:
|
|
204
|
+
"""Execute agent asynchronously and return final response text.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
agent: The glaip_sdk Agent to execute.
|
|
208
|
+
message: The user message to send to the agent.
|
|
209
|
+
verbose: If True, emit debug trace output during execution.
|
|
210
|
+
Defaults to False.
|
|
211
|
+
runtime_config: Optional runtime configuration for tools, MCPs, etc.
|
|
212
|
+
Defaults to None. (Implemented in PR-04+)
|
|
213
|
+
chat_history: Optional list of prior conversation messages.
|
|
214
|
+
Each message is a dict with "role" and "content" keys.
|
|
215
|
+
Defaults to None.
|
|
216
|
+
swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
|
|
217
|
+
**kwargs: Additional keyword arguments passed to the backend.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The final response text from the agent.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
RuntimeError: If no final response is received from the agent.
|
|
224
|
+
"""
|
|
225
|
+
return await self._arun_internal(
|
|
226
|
+
agent=agent,
|
|
227
|
+
message=message,
|
|
228
|
+
verbose=verbose,
|
|
229
|
+
runtime_config=runtime_config,
|
|
230
|
+
chat_history=chat_history,
|
|
231
|
+
swallow_aip_logs=swallow_aip_logs,
|
|
232
|
+
**kwargs,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def _arun_internal(
|
|
236
|
+
self,
|
|
237
|
+
agent: Agent,
|
|
238
|
+
message: str,
|
|
239
|
+
verbose: bool = False,
|
|
240
|
+
runtime_config: dict[str, Any] | None = None,
|
|
241
|
+
chat_history: list[dict[str, str]] | None = None,
|
|
242
|
+
*,
|
|
243
|
+
swallow_aip_logs: bool = True,
|
|
244
|
+
**kwargs: Any,
|
|
245
|
+
) -> str:
|
|
246
|
+
"""Internal async implementation of agent execution.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
agent: The glaip_sdk Agent to execute.
|
|
250
|
+
message: The user message to send to the agent.
|
|
251
|
+
verbose: If True, emit debug trace output during execution.
|
|
252
|
+
runtime_config: Optional runtime configuration for tools, MCPs, etc.
|
|
253
|
+
chat_history: Optional list of prior conversation messages.
|
|
254
|
+
swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
|
|
255
|
+
**kwargs: Additional keyword arguments passed to the backend.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The final response text from the agent.
|
|
259
|
+
"""
|
|
260
|
+
# Optionally swallow noisy AIPAgents logs
|
|
261
|
+
if swallow_aip_logs:
|
|
262
|
+
_swallow_aip_logs()
|
|
263
|
+
|
|
264
|
+
# POC/MVP: Create pause/resume callback for interactive HITL input
|
|
265
|
+
pause_resume_callback = PauseResumeCallback()
|
|
266
|
+
|
|
267
|
+
# Build the local LangGraphReactAgent from the glaip_sdk Agent
|
|
268
|
+
local_agent = self.build_langgraph_agent(
|
|
269
|
+
agent,
|
|
270
|
+
runtime_config=runtime_config,
|
|
271
|
+
pause_resume_callback=pause_resume_callback,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Convert chat history to LangChain messages for the agent
|
|
275
|
+
langchain_messages = _convert_chat_history_to_messages(chat_history)
|
|
276
|
+
if langchain_messages:
|
|
277
|
+
kwargs["messages"] = langchain_messages
|
|
278
|
+
logger.debug(
|
|
279
|
+
"Passing %d chat history messages to agent '%s'",
|
|
280
|
+
len(langchain_messages),
|
|
281
|
+
agent.name,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Use shared render manager for unified processing
|
|
285
|
+
render_manager = AgentRunRenderingManager(logger)
|
|
286
|
+
renderer = render_manager.create_renderer(kwargs.get("renderer"), verbose=verbose)
|
|
287
|
+
|
|
288
|
+
# POC/MVP: Set renderer on callback so LocalPromptHandler can pause/resume Live
|
|
289
|
+
pause_resume_callback.set_renderer(renderer)
|
|
290
|
+
|
|
291
|
+
meta = render_manager.build_initial_metadata(agent.name, message, kwargs)
|
|
292
|
+
render_manager.start_renderer(renderer, meta)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Use shared async stream processor for unified event handling
|
|
296
|
+
(
|
|
297
|
+
final_text,
|
|
298
|
+
stats_usage,
|
|
299
|
+
started_monotonic,
|
|
300
|
+
finished_monotonic,
|
|
301
|
+
) = await render_manager.async_process_stream_events(
|
|
302
|
+
local_agent.arun_sse_stream(message, **kwargs),
|
|
303
|
+
renderer,
|
|
304
|
+
meta,
|
|
305
|
+
skip_final_render=True,
|
|
306
|
+
)
|
|
307
|
+
except KeyboardInterrupt:
|
|
308
|
+
try:
|
|
309
|
+
renderer.close()
|
|
310
|
+
finally:
|
|
311
|
+
raise
|
|
312
|
+
except Exception:
|
|
313
|
+
try:
|
|
314
|
+
renderer.close()
|
|
315
|
+
finally:
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
# Use shared finalizer to avoid code duplication
|
|
319
|
+
from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
|
|
320
|
+
finalize_render_manager,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return finalize_render_manager(
|
|
324
|
+
render_manager,
|
|
325
|
+
renderer,
|
|
326
|
+
final_text,
|
|
327
|
+
stats_usage,
|
|
328
|
+
started_monotonic,
|
|
329
|
+
finished_monotonic,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def build_langgraph_agent(
|
|
333
|
+
self,
|
|
334
|
+
agent: Agent,
|
|
335
|
+
runtime_config: dict[str, Any] | None = None,
|
|
336
|
+
shared_tool_output_manager: Any | None = None,
|
|
337
|
+
*,
|
|
338
|
+
pause_resume_callback: Any | None = None,
|
|
339
|
+
) -> Any:
|
|
340
|
+
"""Build a LangGraphReactAgent from a glaip_sdk Agent definition.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
agent: The glaip_sdk Agent to convert.
|
|
344
|
+
runtime_config: Optional runtime configuration with tool_configs,
|
|
345
|
+
mcp_configs, agent_config, and agent-specific overrides.
|
|
346
|
+
shared_tool_output_manager: Optional ToolOutputManager to reuse across
|
|
347
|
+
agents with tool_output_sharing enabled.
|
|
348
|
+
pause_resume_callback: Optional callback used to pause/resume the renderer
|
|
349
|
+
during interactive HITL prompts.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
A configured LangGraphReactAgent instance.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ImportError: If aip-agents is not installed.
|
|
356
|
+
ValueError: If agent has unsupported tools, MCPs, or sub-agents for local mode.
|
|
357
|
+
"""
|
|
358
|
+
from aip_agents.agent import LangGraphReactAgent # noqa: PLC0415
|
|
359
|
+
from glaip_sdk.runner.tool_adapter import LangChainToolAdapter # noqa: PLC0415
|
|
360
|
+
|
|
361
|
+
# Adapt tools for local execution
|
|
362
|
+
# NOTE: CLI parity waiver - local tool execution is SDK-only for MVP.
|
|
363
|
+
# See specs/f/local-agent-runtime/plan.md: "CLI parity is explicitly deferred
|
|
364
|
+
# and will require SDK Technical Lead sign-off per constitution principle IV."
|
|
365
|
+
langchain_tools: list[Any] = []
|
|
366
|
+
if agent.tools:
|
|
367
|
+
adapter = LangChainToolAdapter()
|
|
368
|
+
langchain_tools = adapter.adapt_tools(agent.tools)
|
|
369
|
+
|
|
370
|
+
# Normalize runtime config: merge global and agent-specific configs
|
|
371
|
+
normalized_config = self._normalize_runtime_config(runtime_config, agent)
|
|
372
|
+
|
|
373
|
+
# Merge tool_configs: agent definition < runtime config
|
|
374
|
+
tool_configs = self._merge_tool_configs(agent, normalized_config)
|
|
375
|
+
|
|
376
|
+
# Merge mcp_configs: agent definition < runtime config
|
|
377
|
+
mcp_configs = self._merge_mcp_configs(agent, normalized_config)
|
|
378
|
+
|
|
379
|
+
# Merge agent_config: agent definition < runtime config
|
|
380
|
+
merged_agent_config = self._merge_agent_config(agent, normalized_config)
|
|
381
|
+
agent_config_params, agent_config_kwargs = self._apply_agent_config(merged_agent_config)
|
|
382
|
+
|
|
383
|
+
# Resolve model and merge its configuration into agent kwargs
|
|
384
|
+
model_string = self._resolve_local_model(agent, agent_config_kwargs)
|
|
385
|
+
|
|
386
|
+
tool_output_manager = self._resolve_tool_output_manager(
|
|
387
|
+
agent,
|
|
388
|
+
merged_agent_config,
|
|
389
|
+
shared_tool_output_manager,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Build sub-agents recursively, sharing tool output manager when enabled.
|
|
393
|
+
sub_agent_instances = self._build_sub_agents(
|
|
394
|
+
agent.agents,
|
|
395
|
+
runtime_config,
|
|
396
|
+
shared_tool_output_manager=tool_output_manager,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Build the LangGraphReactAgent with tools, sub-agents, and configs
|
|
400
|
+
local_agent = LangGraphReactAgent(
|
|
401
|
+
name=agent.name,
|
|
402
|
+
instruction=agent.instruction,
|
|
403
|
+
description=agent.description,
|
|
404
|
+
model=model_string,
|
|
405
|
+
tools=langchain_tools,
|
|
406
|
+
agents=sub_agent_instances if sub_agent_instances else None,
|
|
407
|
+
tool_configs=tool_configs if tool_configs else None,
|
|
408
|
+
tool_output_manager=tool_output_manager,
|
|
409
|
+
guardrail=agent.guardrail,
|
|
410
|
+
**agent_config_params,
|
|
411
|
+
**agent_config_kwargs,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Add MCP servers if configured
|
|
415
|
+
self._add_mcp_servers(local_agent, agent, mcp_configs)
|
|
416
|
+
|
|
417
|
+
# Inject local HITL manager only if hitl_enabled is True (master switch).
|
|
418
|
+
# This matches remote behavior: hitl_enabled gates the HITL plumbing.
|
|
419
|
+
# Tool-level HITL configs are only enforced when hitl_enabled=True.
|
|
420
|
+
self._inject_hitl_manager(local_agent, merged_agent_config, agent.name, pause_resume_callback)
|
|
421
|
+
|
|
422
|
+
logger.debug(
|
|
423
|
+
"Built local LangGraphReactAgent for agent '%s' with %d tools, %d sub-agents, and %d MCPs",
|
|
424
|
+
agent.name,
|
|
425
|
+
len(langchain_tools),
|
|
426
|
+
len(sub_agent_instances),
|
|
427
|
+
len(agent.mcps) if agent.mcps else 0,
|
|
428
|
+
)
|
|
429
|
+
return local_agent
|
|
430
|
+
|
|
431
|
+
def _resolve_tool_output_manager(
|
|
432
|
+
self,
|
|
433
|
+
agent: Agent,
|
|
434
|
+
merged_agent_config: dict[str, Any],
|
|
435
|
+
shared_tool_output_manager: Any | None,
|
|
436
|
+
) -> Any | None:
|
|
437
|
+
"""Resolve tool output manager for local agent execution."""
|
|
438
|
+
tool_output_sharing_enabled = merged_agent_config.get("tool_output_sharing", False)
|
|
439
|
+
if not tool_output_sharing_enabled:
|
|
440
|
+
return None
|
|
441
|
+
if shared_tool_output_manager is not None:
|
|
442
|
+
return shared_tool_output_manager
|
|
443
|
+
return build_tool_output_manager(agent.name, merged_agent_config)
|
|
444
|
+
|
|
445
|
+
def _inject_hitl_manager(
|
|
446
|
+
self,
|
|
447
|
+
local_agent: Any,
|
|
448
|
+
merged_agent_config: dict[str, Any],
|
|
449
|
+
agent_name: str,
|
|
450
|
+
pause_resume_callback: Any | None,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Inject HITL manager when enabled, mirroring remote gating behavior."""
|
|
453
|
+
hitl_enabled = merged_agent_config.get("hitl_enabled", False)
|
|
454
|
+
if hitl_enabled:
|
|
455
|
+
try:
|
|
456
|
+
from aip_agents.agent.hitl.manager import ( # noqa: PLC0415
|
|
457
|
+
ApprovalManager,
|
|
458
|
+
)
|
|
459
|
+
from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
|
|
460
|
+
|
|
461
|
+
local_agent.hitl_manager = ApprovalManager(
|
|
462
|
+
prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
|
|
463
|
+
)
|
|
464
|
+
# Store callback reference for setting renderer later
|
|
465
|
+
if pause_resume_callback:
|
|
466
|
+
local_agent._pause_resume_callback = pause_resume_callback
|
|
467
|
+
logger.debug(
|
|
468
|
+
"HITL manager injected for agent '%s' (hitl_enabled=True)",
|
|
469
|
+
agent_name,
|
|
470
|
+
)
|
|
471
|
+
except ImportError as e:
|
|
472
|
+
# Missing dependencies - fail fast
|
|
473
|
+
raise ImportError("Local HITL requires aip_agents. Install with: pip install 'glaip-sdk[local]'") from e
|
|
474
|
+
except Exception as e:
|
|
475
|
+
# Other errors during HITL setup - fail fast
|
|
476
|
+
raise RuntimeError(f"Failed to initialize HITL manager for agent '{agent_name}'") from e
|
|
477
|
+
else:
|
|
478
|
+
logger.debug(
|
|
479
|
+
"HITL manager not injected for agent '%s' (hitl_enabled=False)",
|
|
480
|
+
agent_name,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def _build_sub_agents(
|
|
484
|
+
self,
|
|
485
|
+
sub_agents: list[Any] | None,
|
|
486
|
+
runtime_config: dict[str, Any] | None,
|
|
487
|
+
shared_tool_output_manager: Any | None = None,
|
|
488
|
+
) -> list[Any]:
|
|
489
|
+
"""Build sub-agent instances recursively.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
sub_agents: List of sub-agent definitions.
|
|
493
|
+
runtime_config: Runtime config to pass to sub-agents.
|
|
494
|
+
shared_tool_output_manager: Optional ToolOutputManager to reuse across
|
|
495
|
+
agents with tool_output_sharing enabled.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
List of built sub-agent instances.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
ValueError: If sub-agent is platform-only.
|
|
502
|
+
"""
|
|
503
|
+
if not sub_agents:
|
|
504
|
+
return []
|
|
505
|
+
|
|
506
|
+
sub_agent_instances = []
|
|
507
|
+
for sub_agent in sub_agents:
|
|
508
|
+
self._validate_sub_agent_for_local_mode(sub_agent)
|
|
509
|
+
sub_agent_instances.append(
|
|
510
|
+
self.build_langgraph_agent(
|
|
511
|
+
sub_agent,
|
|
512
|
+
runtime_config,
|
|
513
|
+
shared_tool_output_manager=shared_tool_output_manager,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return sub_agent_instances
|
|
517
|
+
|
|
518
|
+
def _add_mcp_servers(
|
|
519
|
+
self,
|
|
520
|
+
local_agent: Any,
|
|
521
|
+
agent: Agent,
|
|
522
|
+
merged_mcp_configs: dict[str, Any],
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Add MCP servers to a built agent.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
local_agent: The LangGraphReactAgent to add MCPs to.
|
|
528
|
+
agent: The glaip_sdk Agent with MCP definitions.
|
|
529
|
+
merged_mcp_configs: Merged mcp_configs (agent definition + runtime).
|
|
530
|
+
"""
|
|
531
|
+
if not agent.mcps:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
from glaip_sdk.runner.mcp_adapter import LangChainMCPAdapter # noqa: PLC0415
|
|
535
|
+
|
|
536
|
+
mcp_adapter = LangChainMCPAdapter()
|
|
537
|
+
base_mcp_configs = mcp_adapter.adapt_mcps(agent.mcps)
|
|
538
|
+
|
|
539
|
+
# Apply merged mcp_configs overrides (agent definition + runtime)
|
|
540
|
+
if merged_mcp_configs:
|
|
541
|
+
base_mcp_configs = self._apply_runtime_mcp_configs(base_mcp_configs, merged_mcp_configs)
|
|
542
|
+
|
|
543
|
+
if base_mcp_configs:
|
|
544
|
+
local_agent.add_mcp_server(base_mcp_configs)
|
|
545
|
+
logger.debug(
|
|
546
|
+
"Registered %d MCP server(s) for agent '%s'",
|
|
547
|
+
len(base_mcp_configs),
|
|
548
|
+
agent.name,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def _normalize_runtime_config(
|
|
552
|
+
self,
|
|
553
|
+
runtime_config: dict[str, Any] | None,
|
|
554
|
+
agent: Agent,
|
|
555
|
+
) -> dict[str, Any]:
|
|
556
|
+
"""Normalize runtime_config for local execution.
|
|
557
|
+
|
|
558
|
+
Merges global and agent-specific configs with proper priority.
|
|
559
|
+
Keys are resolved from instances/classes to string names.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
runtime_config: Raw runtime config from user.
|
|
563
|
+
agent: The agent being built (for resolving agent-specific overrides).
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Normalized config with string keys and merged priorities.
|
|
567
|
+
"""
|
|
568
|
+
from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
|
|
569
|
+
merge_configs,
|
|
570
|
+
normalize_local_config_keys,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
if not runtime_config:
|
|
574
|
+
return {}
|
|
575
|
+
|
|
576
|
+
# 1. Extract global configs and normalize keys
|
|
577
|
+
global_tool_configs = normalize_local_config_keys(runtime_config.get("tool_configs", {}))
|
|
578
|
+
global_mcp_configs = normalize_local_config_keys(runtime_config.get("mcp_configs", {}))
|
|
579
|
+
global_agent_config = runtime_config.get("agent_config", {})
|
|
580
|
+
|
|
581
|
+
# 2. Extract agent-specific overrides (highest priority)
|
|
582
|
+
agent_specific = self._get_agent_specific_config(runtime_config, agent)
|
|
583
|
+
agent_tool_configs = normalize_local_config_keys(agent_specific.get("tool_configs", {}))
|
|
584
|
+
agent_mcp_configs = normalize_local_config_keys(agent_specific.get("mcp_configs", {}))
|
|
585
|
+
agent_config_override = agent_specific.get("agent_config", {})
|
|
586
|
+
|
|
587
|
+
# 3. Merge with priority: global < agent-specific
|
|
588
|
+
merged_result = {
|
|
589
|
+
"tool_configs": merge_configs(global_tool_configs, agent_tool_configs),
|
|
590
|
+
"mcp_configs": merge_configs(global_mcp_configs, agent_mcp_configs),
|
|
591
|
+
"agent_config": merge_configs(global_agent_config, agent_config_override),
|
|
592
|
+
}
|
|
593
|
+
return merged_result
|
|
594
|
+
|
|
595
|
+
def _get_agent_specific_config(
|
|
596
|
+
self,
|
|
597
|
+
runtime_config: dict[str, Any],
|
|
598
|
+
agent: Agent,
|
|
599
|
+
) -> dict[str, Any]:
|
|
600
|
+
"""Extract agent-specific config from runtime_config.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
runtime_config: Runtime config that may contain agent-specific overrides.
|
|
604
|
+
agent: The agent to find config for.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
Agent-specific config dict, or empty dict if not found.
|
|
608
|
+
"""
|
|
609
|
+
from glaip_sdk.utils.resource_refs import is_uuid # noqa: PLC0415
|
|
610
|
+
from glaip_sdk.utils.runtime_config import get_name_from_key # noqa: PLC0415
|
|
611
|
+
|
|
612
|
+
# Reserved keys at the top level
|
|
613
|
+
reserved_keys = {"tool_configs", "mcp_configs", "agent_config"}
|
|
614
|
+
|
|
615
|
+
# Try finding agent by instance, class, or name
|
|
616
|
+
for key, value in runtime_config.items():
|
|
617
|
+
if key in reserved_keys:
|
|
618
|
+
continue # Skip global configs
|
|
619
|
+
|
|
620
|
+
if isinstance(key, str) and is_uuid(key):
|
|
621
|
+
logger.warning(
|
|
622
|
+
"UUID agent override key '%s' is not supported in local mode; skipping. "
|
|
623
|
+
"Use agent name string or Agent instance as the key instead.",
|
|
624
|
+
key,
|
|
625
|
+
)
|
|
626
|
+
continue
|
|
627
|
+
|
|
628
|
+
# Check if this key matches the agent
|
|
629
|
+
try:
|
|
630
|
+
key_name = get_name_from_key(key)
|
|
631
|
+
except ValueError:
|
|
632
|
+
continue # Skip invalid keys
|
|
633
|
+
|
|
634
|
+
if key_name and key_name == agent.name:
|
|
635
|
+
return value if isinstance(value, dict) else {}
|
|
636
|
+
|
|
637
|
+
return {}
|
|
638
|
+
|
|
639
|
+
def _merge_tool_configs(
|
|
640
|
+
self,
|
|
641
|
+
agent: Agent,
|
|
642
|
+
normalized_config: dict[str, Any],
|
|
643
|
+
) -> dict[str, Any]:
|
|
644
|
+
"""Merge agent.tool_configs with runtime tool_configs.
|
|
645
|
+
|
|
646
|
+
Priority (lowest to highest):
|
|
647
|
+
1. Agent definition (agent.tool_configs)
|
|
648
|
+
2. Runtime config (normalized_config["tool_configs"])
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
agent: The agent with optional tool_configs property.
|
|
652
|
+
normalized_config: Normalized runtime config.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Merged tool_configs dict.
|
|
656
|
+
"""
|
|
657
|
+
from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
|
|
658
|
+
merge_configs,
|
|
659
|
+
normalize_local_config_keys,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# Get agent's tool_configs if defined
|
|
663
|
+
agent_tool_configs = {}
|
|
664
|
+
if hasattr(agent, "tool_configs") and agent.tool_configs:
|
|
665
|
+
agent_tool_configs = normalize_local_config_keys(agent.tool_configs)
|
|
666
|
+
|
|
667
|
+
# Get runtime tool_configs
|
|
668
|
+
runtime_tool_configs = normalized_config.get("tool_configs", {})
|
|
669
|
+
|
|
670
|
+
# Merge: agent definition < runtime config
|
|
671
|
+
return merge_configs(agent_tool_configs, runtime_tool_configs)
|
|
672
|
+
|
|
673
|
+
def _merge_mcp_configs(
|
|
674
|
+
self,
|
|
675
|
+
agent: Agent,
|
|
676
|
+
normalized_config: dict[str, Any],
|
|
677
|
+
) -> dict[str, Any]:
|
|
678
|
+
"""Merge agent.mcp_configs with runtime mcp_configs.
|
|
679
|
+
|
|
680
|
+
Priority (lowest to highest):
|
|
681
|
+
1. Agent definition (agent.mcp_configs)
|
|
682
|
+
2. Runtime config (normalized_config["mcp_configs"])
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
agent: The agent with optional mcp_configs property.
|
|
686
|
+
normalized_config: Normalized runtime config.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Merged mcp_configs dict.
|
|
690
|
+
"""
|
|
691
|
+
from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
|
|
692
|
+
merge_configs,
|
|
693
|
+
normalize_local_config_keys,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Get agent's mcp_configs if defined
|
|
697
|
+
agent_mcp_configs = {}
|
|
698
|
+
if hasattr(agent, "mcp_configs") and agent.mcp_configs:
|
|
699
|
+
agent_mcp_configs = normalize_local_config_keys(agent.mcp_configs)
|
|
700
|
+
|
|
701
|
+
# Get runtime mcp_configs
|
|
702
|
+
runtime_mcp_configs = normalized_config.get("mcp_configs", {})
|
|
703
|
+
|
|
704
|
+
# Merge: agent definition < runtime config
|
|
705
|
+
return merge_configs(agent_mcp_configs, runtime_mcp_configs)
|
|
706
|
+
|
|
707
|
+
def _merge_agent_config(
|
|
708
|
+
self,
|
|
709
|
+
agent: Agent,
|
|
710
|
+
normalized_config: dict[str, Any],
|
|
711
|
+
) -> dict[str, Any]:
|
|
712
|
+
"""Merge agent.agent_config with runtime agent_config.
|
|
713
|
+
|
|
714
|
+
Priority (lowest to highest):
|
|
715
|
+
1. Agent definition (agent.agent_config)
|
|
716
|
+
2. Runtime config (normalized_config["agent_config"])
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
agent: The agent with optional agent_config property.
|
|
720
|
+
normalized_config: Normalized runtime config.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Merged agent_config dict.
|
|
724
|
+
"""
|
|
725
|
+
from glaip_sdk.utils.runtime_config import merge_configs # noqa: PLC0415
|
|
726
|
+
|
|
727
|
+
# Get agent's agent_config if defined
|
|
728
|
+
agent_agent_config = {}
|
|
729
|
+
if hasattr(agent, "agent_config") and agent.agent_config:
|
|
730
|
+
agent_agent_config = agent.agent_config
|
|
731
|
+
|
|
732
|
+
# Get runtime agent_config
|
|
733
|
+
runtime_agent_config = normalized_config.get("agent_config", {})
|
|
734
|
+
|
|
735
|
+
# Merge: agent definition < runtime config
|
|
736
|
+
return merge_configs(agent_agent_config, runtime_agent_config)
|
|
737
|
+
|
|
738
|
+
def _apply_agent_config(
|
|
739
|
+
self,
|
|
740
|
+
agent_config: dict[str, Any],
|
|
741
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
742
|
+
"""Extract and separate agent_config into direct params and kwargs.
|
|
743
|
+
|
|
744
|
+
Separates agent_config into parameters that go directly to LangGraphReactAgent
|
|
745
|
+
constructor vs those that go through **kwargs.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
agent_config: Runtime agent configuration dict.
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Tuple of (direct_params, kwargs_params):
|
|
752
|
+
- direct_params: Parameters passed directly to LangGraphReactAgent.__init__()
|
|
753
|
+
- kwargs_params: Parameters passed via **kwargs to BaseAgent
|
|
754
|
+
"""
|
|
755
|
+
direct_params = {}
|
|
756
|
+
kwargs_params = {}
|
|
757
|
+
config_dict = {}
|
|
758
|
+
|
|
759
|
+
# Direct constructor parameters
|
|
760
|
+
if "planning" in agent_config:
|
|
761
|
+
direct_params["planning"] = agent_config["planning"]
|
|
762
|
+
|
|
763
|
+
if "enable_a2a_token_streaming" in agent_config:
|
|
764
|
+
direct_params["enable_a2a_token_streaming"] = agent_config["enable_a2a_token_streaming"]
|
|
765
|
+
|
|
766
|
+
# Kwargs parameters (passed through **kwargs to BaseAgent)
|
|
767
|
+
if "enable_pii" in agent_config:
|
|
768
|
+
kwargs_params["enable_pii"] = agent_config["enable_pii"]
|
|
769
|
+
config_dict["enable_pii"] = agent_config["enable_pii"]
|
|
770
|
+
|
|
771
|
+
if "memory" in agent_config:
|
|
772
|
+
# Map "memory" to "memory_backend" for aip-agents compatibility
|
|
773
|
+
kwargs_params["memory_backend"] = agent_config["memory"]
|
|
774
|
+
config_dict["memory_backend"] = agent_config["memory"]
|
|
775
|
+
|
|
776
|
+
# Additional memory-related settings
|
|
777
|
+
memory_settings = ["agent_id", "memory_namespace", "save_interaction_to_memory"]
|
|
778
|
+
for key in memory_settings:
|
|
779
|
+
if key in agent_config:
|
|
780
|
+
kwargs_params[key] = agent_config[key]
|
|
781
|
+
config_dict[key] = agent_config[key]
|
|
782
|
+
|
|
783
|
+
# Ensure we pass a config dictionary to BaseAgent, which uses it for
|
|
784
|
+
# LM configuration (api keys, etc.) and other settings.
|
|
785
|
+
if config_dict:
|
|
786
|
+
kwargs_params["config"] = config_dict
|
|
787
|
+
|
|
788
|
+
return direct_params, kwargs_params
|
|
789
|
+
|
|
790
|
+
def _convert_model_for_local(self, model: Any) -> tuple[str, dict[str, Any]]:
|
|
791
|
+
"""Convert model to aip_agents format for local execution.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
model: Model object or string identifier.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Tuple of (model_string, config_dict).
|
|
798
|
+
"""
|
|
799
|
+
from glaip_sdk.models._validation import ( # noqa: PLC0415
|
|
800
|
+
convert_model_for_local_execution,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
return convert_model_for_local_execution(model)
|
|
804
|
+
|
|
805
|
+
def _resolve_local_model(self, agent: Agent, agent_config_kwargs: dict[str, Any]) -> str:
|
|
806
|
+
"""Resolve model string and merge its configuration into agent kwargs.
|
|
807
|
+
|
|
808
|
+
This method extracts model-specific credentials and hyperparameters from a Model
|
|
809
|
+
object and merges them into the 'config' dictionary within agent_config_kwargs.
|
|
810
|
+
This is required because BaseAgent expects LM settings (api keys, etc.) to be
|
|
811
|
+
inside the 'config' parameter, not top-level kwargs.
|
|
812
|
+
|
|
813
|
+
Example:
|
|
814
|
+
If agent has:
|
|
815
|
+
- model = Model(id="deepinfra/model", credentials="key-123")
|
|
816
|
+
- agent_config_kwargs = {"enable_pii": True, "config": {"enable_pii": True}}
|
|
817
|
+
|
|
818
|
+
_resolve_local_model will:
|
|
819
|
+
1. Resolve model_string to "openai-compatible/model"
|
|
820
|
+
2. Extract model_config as {"lm_api_key": "key-123"}
|
|
821
|
+
3. Update agent_config_kwargs["config"] to:
|
|
822
|
+
{"enable_pii": True, "lm_api_key": "key-123"}
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
agent: The glaip_sdk Agent.
|
|
826
|
+
agent_config_kwargs: Agent config kwargs to update (modified in-place).
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
The model identifier string for local execution.
|
|
830
|
+
"""
|
|
831
|
+
model_to_use = agent.model or self.default_model
|
|
832
|
+
model_string, model_config = self._convert_model_for_local(model_to_use)
|
|
833
|
+
|
|
834
|
+
if model_config:
|
|
835
|
+
# Normalize config to a dict early to simplify merging
|
|
836
|
+
config_val = agent_config_kwargs.get("config", {})
|
|
837
|
+
if hasattr(config_val, "model_dump"):
|
|
838
|
+
config_val = config_val.model_dump()
|
|
839
|
+
|
|
840
|
+
if not isinstance(config_val, dict):
|
|
841
|
+
config_val = {}
|
|
842
|
+
|
|
843
|
+
# Use a single merge path for model configuration
|
|
844
|
+
config_val.update(model_config)
|
|
845
|
+
agent_config_kwargs["config"] = config_val
|
|
846
|
+
|
|
847
|
+
return model_string
|
|
848
|
+
|
|
849
|
+
def _apply_runtime_mcp_configs(
|
|
850
|
+
self,
|
|
851
|
+
base_configs: dict[str, Any],
|
|
852
|
+
runtime_overrides: dict[str, Any],
|
|
853
|
+
) -> dict[str, Any]:
|
|
854
|
+
"""Apply runtime mcp_configs overrides to base MCP configurations.
|
|
855
|
+
|
|
856
|
+
Merges runtime overrides into the base configs, handling authentication
|
|
857
|
+
conversion to headers using MCPConfigBuilder.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
base_configs: Base MCP configs from adapter (server_name -> config).
|
|
861
|
+
runtime_overrides: Runtime mcp_configs overrides (server_name -> config).
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Merged MCP configs with authentication converted to headers.
|
|
865
|
+
"""
|
|
866
|
+
return {
|
|
867
|
+
server_name: self._merge_single_mcp_config(server_name, base_config, runtime_overrides.get(server_name))
|
|
868
|
+
for server_name, base_config in base_configs.items()
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
def _merge_single_mcp_config(
|
|
872
|
+
self,
|
|
873
|
+
server_name: str,
|
|
874
|
+
base_config: dict[str, Any],
|
|
875
|
+
override: dict[str, Any] | None,
|
|
876
|
+
) -> dict[str, Any]:
|
|
877
|
+
"""Merge a single MCP config with a runtime override, handling normalization and parity fixes.
|
|
878
|
+
|
|
879
|
+
This method orchestrates the merging of base MCP settings (from the object definition)
|
|
880
|
+
with runtime overrides. It enforces Platform parity by prioritizing the nested 'config'
|
|
881
|
+
block while maintaining robustness for local development by auto-fixing flat transport keys.
|
|
882
|
+
|
|
883
|
+
The merge follows these priority rules (highest to lowest):
|
|
884
|
+
1. Misplaced flat keys in the override (e.g., 'url' at top level) - Auto-fixed with warning.
|
|
885
|
+
2. Nested 'config' block in the override (Matches Platform/Constructor schema).
|
|
886
|
+
3. Authentication objects in the override (Converted to HTTP headers).
|
|
887
|
+
4. Structural settings in the override (e.g., 'allowed_tools').
|
|
888
|
+
5. Base configuration from the MCP object definition.
|
|
889
|
+
|
|
890
|
+
Examples:
|
|
891
|
+
>>> # 1. Strict Nested Style (Recommended)
|
|
892
|
+
>>> override = {"config": {"url": "https://new.api"}, "allowed_tools": ["t1"]}
|
|
893
|
+
>>> self._merge_single_mcp_config("mcp", base, override)
|
|
894
|
+
>>> # Result: {"url": "https://new.api", "allowed_tools": ["t1"], ...}
|
|
895
|
+
|
|
896
|
+
>>> # 2. Flat Legacy Style (Auto-fixed with warning)
|
|
897
|
+
>>> override = {"url": "https://new.api"}
|
|
898
|
+
>>> self._merge_single_mcp_config("mcp", base, override)
|
|
899
|
+
>>> # Result: {"url": "https://new.api", ...}
|
|
900
|
+
|
|
901
|
+
>>> # 3. Header Merging (Preserves Auth)
|
|
902
|
+
>>> base = {"headers": {"Authorization": "Bearer token"}}
|
|
903
|
+
>>> override = {"headers": {"X-Custom": "val"}}
|
|
904
|
+
>>> self._merge_single_mcp_config("mcp", base, override)
|
|
905
|
+
>>> # Result: {"headers": {"Authorization": "Bearer token", "X-Custom": "val"}, ...}
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
server_name: Name of the MCP server being configured.
|
|
909
|
+
base_config: Base configuration dictionary derived from the MCP object.
|
|
910
|
+
override: Optional dictionary of runtime overrides.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
A fully merged and normalized configuration dictionary ready for the local runner.
|
|
914
|
+
"""
|
|
915
|
+
merged = base_config.copy()
|
|
916
|
+
|
|
917
|
+
if not override:
|
|
918
|
+
return merged
|
|
919
|
+
|
|
920
|
+
# 1. Check for misplaced keys and warn (DX/Parity guidance)
|
|
921
|
+
self._warn_if_mcp_override_misplaced(server_name, override)
|
|
922
|
+
|
|
923
|
+
# 2. Apply Authentication (Converted to headers)
|
|
924
|
+
self._apply_mcp_auth_override(server_name, merged, override)
|
|
925
|
+
|
|
926
|
+
# 3. Apply Transport Settings (Nested 'config')
|
|
927
|
+
if "config" in override and isinstance(override["config"], dict):
|
|
928
|
+
merged.update(override["config"])
|
|
929
|
+
|
|
930
|
+
# 4. Apply Structural Settings (e.g., allowed_tools)
|
|
931
|
+
if "allowed_tools" in override:
|
|
932
|
+
merged["allowed_tools"] = override["allowed_tools"]
|
|
933
|
+
|
|
934
|
+
# 5. Preserve unknown top-level keys (backward compatibility)
|
|
935
|
+
known_keys = _MCP_TRANSPORT_KEYS | {"config", "authentication", "allowed_tools"}
|
|
936
|
+
for key, value in override.items():
|
|
937
|
+
if key not in known_keys:
|
|
938
|
+
merged[key] = value
|
|
939
|
+
|
|
940
|
+
# 6. Apply Auto-fix for misplaced keys (Local Success)
|
|
941
|
+
for key in [k for k in override if k in _MCP_TRANSPORT_KEYS]:
|
|
942
|
+
val = override[key]
|
|
943
|
+
# Special case: Merge headers instead of overwriting to preserve auth
|
|
944
|
+
if key == "headers" and isinstance(val, dict) and isinstance(merged.get("headers"), dict):
|
|
945
|
+
merged["headers"].update(val)
|
|
946
|
+
else:
|
|
947
|
+
merged[key] = val
|
|
948
|
+
|
|
949
|
+
return merged
|
|
950
|
+
|
|
951
|
+
def _warn_if_mcp_override_misplaced(self, server_name: str, override: dict[str, Any]) -> None:
|
|
952
|
+
"""Log a warning if transport keys are found at the top level of an override.
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
server_name: Name of the MCP server.
|
|
956
|
+
override: The raw override dictionary.
|
|
957
|
+
"""
|
|
958
|
+
misplaced = [k for k in override if k in _MCP_TRANSPORT_KEYS]
|
|
959
|
+
if misplaced:
|
|
960
|
+
logger.warning(
|
|
961
|
+
"MCP '%s' override contains transport keys at the top level: %s. "
|
|
962
|
+
"This structure is inconsistent with the Platform and MCP constructor. "
|
|
963
|
+
"Transport settings should be nested within a 'config' dictionary. "
|
|
964
|
+
"Example: mcp_configs={'%s': {'config': {'%s': '...'}}}. "
|
|
965
|
+
"Automatically merging top-level keys for local execution parity.",
|
|
966
|
+
server_name,
|
|
967
|
+
misplaced,
|
|
968
|
+
server_name,
|
|
969
|
+
misplaced[0],
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
def _apply_mcp_auth_override(
|
|
973
|
+
self,
|
|
974
|
+
server_name: str,
|
|
975
|
+
merged_config: dict[str, Any],
|
|
976
|
+
override: dict[str, Any],
|
|
977
|
+
) -> None:
|
|
978
|
+
"""Convert authentication override to headers and apply to config.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
server_name: Name of the MCP server.
|
|
982
|
+
merged_config: The configuration being built (mutated in place).
|
|
983
|
+
override: The raw override dictionary.
|
|
984
|
+
"""
|
|
985
|
+
if "authentication" not in override:
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
|
|
989
|
+
MCPConfigBuilder,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
|
|
993
|
+
if headers:
|
|
994
|
+
merged_config["headers"] = headers
|
|
995
|
+
logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
|
|
996
|
+
|
|
997
|
+
def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
|
|
998
|
+
"""Validate that a sub-agent reference is supported for local execution.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
sub_agent: The sub-agent reference to validate.
|
|
1002
|
+
|
|
1003
|
+
Raises:
|
|
1004
|
+
ValueError: If the sub-agent is not supported in local mode.
|
|
1005
|
+
"""
|
|
1006
|
+
# String references are allowed by SDK API but not for local mode
|
|
1007
|
+
if isinstance(sub_agent, str):
|
|
1008
|
+
raise ValueError(
|
|
1009
|
+
f"Sub-agent '{sub_agent}' is a string reference and cannot be used in local mode. "
|
|
1010
|
+
"String sub-agent references are only supported for server execution. "
|
|
1011
|
+
"For local mode, define the sub-agent with Agent(name=..., instruction=...)."
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Validate sub-agent is not a class
|
|
1015
|
+
if inspect.isclass(sub_agent):
|
|
1016
|
+
raise ValueError(
|
|
1017
|
+
f"Sub-agent '{sub_agent.__name__}' is a class, not an instance. "
|
|
1018
|
+
"Local mode requires Agent INSTANCES. "
|
|
1019
|
+
"Did you forget to instantiate it? e.g., Agent(...), not Agent"
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Validate sub-agent is an Agent-like object (has required attributes)
|
|
1023
|
+
if not hasattr(sub_agent, "name") or not hasattr(sub_agent, "instruction"):
|
|
1024
|
+
raise ValueError(
|
|
1025
|
+
f"Sub-agent {type(sub_agent).__name__} is not supported in local mode. "
|
|
1026
|
+
"Local mode requires Agent instances with 'name' and 'instruction' attributes. "
|
|
1027
|
+
"Define the sub-agent with Agent(name=..., instruction=...)."
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# Validate sub-agent is not platform-only (from_id, from_native)
|
|
1031
|
+
if getattr(sub_agent, "_lookup_only", False):
|
|
1032
|
+
agent_name = getattr(sub_agent, "name", "<unknown>")
|
|
1033
|
+
raise ValueError(
|
|
1034
|
+
f"Sub-agent '{agent_name}' is not supported in local mode. "
|
|
1035
|
+
"Platform agents (from_id, from_native) cannot be used as "
|
|
1036
|
+
"sub-agents in local execution. "
|
|
1037
|
+
"Define the sub-agent locally with Agent(name=..., instruction=...) instead."
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
def _log_event(self, event: dict[str, Any]) -> None:
|
|
1041
|
+
"""Log an A2AEvent for verbose debug output.
|
|
1042
|
+
|
|
1043
|
+
Args:
|
|
1044
|
+
event: The A2AEvent dictionary to log.
|
|
1045
|
+
"""
|
|
1046
|
+
event_type = event.get("event_type", "unknown")
|
|
1047
|
+
content = event.get("content", "")
|
|
1048
|
+
is_final = event.get("is_final", False)
|
|
1049
|
+
|
|
1050
|
+
# Truncate long content for readability
|
|
1051
|
+
content_str = str(content) if content else ""
|
|
1052
|
+
content_preview = content_str[:100] + "..." if len(content_str) > 100 else content_str
|
|
1053
|
+
|
|
1054
|
+
final_marker = "(final)" if is_final else ""
|
|
1055
|
+
logger.info("[%s] %s %s", event_type, final_marker, content_preview)
|