glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 +6 -3
- glaip_sdk/_version.py +12 -5
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1126 -0
- glaip_sdk/branding.py +79 -15
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +503 -183
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +774 -137
- glaip_sdk/cli/commands/mcps.py +1124 -181
- glaip_sdk/cli/commands/models.py +25 -10
- glaip_sdk/cli/commands/tools.py +144 -92
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +143 -53
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +24 -18
- glaip_sdk/cli/main.py +420 -145
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +28 -21
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +500 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +282 -0
- glaip_sdk/cli/slash/prompt.py +245 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1679 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +372 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +247 -1238
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +940 -574
- glaip_sdk/client/base.py +163 -48
- glaip_sdk/client/main.py +35 -12
- glaip_sdk/client/mcps.py +126 -18
- glaip_sdk/client/run_rendering.py +415 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +195 -37
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +15 -5
- glaip_sdk/exceptions.py +16 -9
- glaip_sdk/icons.py +25 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +231 -0
- glaip_sdk/rich_components.py +98 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +597 -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 +158 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -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 +177 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +59 -13
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +53 -40
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +58 -26
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +65 -32
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +20 -25
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +85 -43
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +39 -7
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +672 -759
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +75 -22
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +422 -0
- glaip_sdk/utils/serialization.py +184 -51
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +21 -30
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
- glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -250
- glaip_sdk/utils/rendering/renderer/progress.py +0 -118
- glaip_sdk/utils/rendering/steps.py +0 -232
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.7.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/agents.py
CHANGED
|
@@ -3,40 +3,60 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
import
|
|
9
|
+
import asyncio
|
|
9
10
|
import json
|
|
10
11
|
import logging
|
|
11
|
-
from collections.abc import AsyncGenerator
|
|
12
|
-
from
|
|
12
|
+
from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from os import PathLike
|
|
15
|
+
from pathlib import Path
|
|
13
16
|
from typing import Any, BinaryIO
|
|
14
17
|
|
|
15
18
|
import httpx
|
|
16
|
-
from
|
|
17
|
-
|
|
19
|
+
from glaip_sdk.agents import Agent
|
|
20
|
+
from glaip_sdk.client._agent_payloads import (
|
|
21
|
+
AgentCreateRequest,
|
|
22
|
+
AgentListParams,
|
|
23
|
+
AgentListResult,
|
|
24
|
+
AgentUpdateRequest,
|
|
25
|
+
)
|
|
26
|
+
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
18
27
|
from glaip_sdk.client.base import BaseClient
|
|
28
|
+
from glaip_sdk.client.mcps import MCPClient
|
|
29
|
+
from glaip_sdk.client.run_rendering import (
|
|
30
|
+
AgentRunRenderingManager,
|
|
31
|
+
compute_timeout_seconds,
|
|
32
|
+
)
|
|
33
|
+
from glaip_sdk.client.shared import build_shared_config
|
|
34
|
+
from glaip_sdk.client.tools import ToolClient
|
|
19
35
|
from glaip_sdk.config.constants import (
|
|
36
|
+
AGENT_CONFIG_FIELDS,
|
|
20
37
|
DEFAULT_AGENT_FRAMEWORK,
|
|
21
|
-
DEFAULT_AGENT_PROVIDER,
|
|
22
38
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
23
39
|
DEFAULT_AGENT_TYPE,
|
|
24
40
|
DEFAULT_AGENT_VERSION,
|
|
25
41
|
DEFAULT_MODEL,
|
|
26
42
|
)
|
|
27
|
-
from glaip_sdk.exceptions import NotFoundError
|
|
28
|
-
from glaip_sdk.models import
|
|
43
|
+
from glaip_sdk.exceptions import NotFoundError, ValidationError
|
|
44
|
+
from glaip_sdk.models import AgentResponse
|
|
45
|
+
from glaip_sdk.payload_schemas.agent import list_server_only_fields
|
|
46
|
+
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
29
47
|
from glaip_sdk.utils.client_utils import (
|
|
30
48
|
aiter_sse_events,
|
|
31
49
|
create_model_instances,
|
|
32
|
-
extract_ids,
|
|
33
50
|
find_by_name,
|
|
34
|
-
iter_sse_events,
|
|
35
51
|
prepare_multipart_data,
|
|
36
52
|
)
|
|
37
|
-
from glaip_sdk.utils.
|
|
53
|
+
from glaip_sdk.utils.import_export import (
|
|
54
|
+
convert_export_to_import_format,
|
|
55
|
+
merge_import_with_cli_args,
|
|
56
|
+
)
|
|
38
57
|
from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
39
|
-
from glaip_sdk.utils.
|
|
58
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
59
|
+
from glaip_sdk.utils.serialization import load_resource_from_file
|
|
40
60
|
from glaip_sdk.utils.validation import validate_agent_instruction
|
|
41
61
|
|
|
42
62
|
# API endpoints
|
|
@@ -48,6 +68,181 @@ SSE_CONTENT_TYPE = "text/event-stream"
|
|
|
48
68
|
# Set up module-level logger
|
|
49
69
|
logger = logging.getLogger("glaip_sdk.agents")
|
|
50
70
|
|
|
71
|
+
_SERVER_ONLY_IMPORT_FIELDS = set(list_server_only_fields()) | {"success", "message"}
|
|
72
|
+
_MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
|
|
73
|
+
_DEFAULT_METADATA_TYPE = "custom"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@asynccontextmanager
|
|
77
|
+
async def _async_timeout_guard(
|
|
78
|
+
timeout_seconds: float | None,
|
|
79
|
+
) -> AsyncGenerator[None, None]:
|
|
80
|
+
"""Apply an asyncio timeout when a custom timeout is provided."""
|
|
81
|
+
if timeout_seconds is None:
|
|
82
|
+
yield
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
async with asyncio.timeout(timeout_seconds):
|
|
86
|
+
yield
|
|
87
|
+
except asyncio.TimeoutError as exc:
|
|
88
|
+
raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _normalise_sequence(value: Any) -> list[Any] | None:
|
|
92
|
+
"""Normalise optional sequence inputs to plain lists."""
|
|
93
|
+
if value is None:
|
|
94
|
+
return None
|
|
95
|
+
if isinstance(value, list):
|
|
96
|
+
return value
|
|
97
|
+
if isinstance(value, (tuple, set)):
|
|
98
|
+
return list(value)
|
|
99
|
+
return [value]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalise_sequence_fields(mapping: dict[str, Any]) -> None:
|
|
103
|
+
"""Normalise merged sequence fields in-place."""
|
|
104
|
+
for field in _MERGED_SEQUENCE_FIELDS:
|
|
105
|
+
if field in mapping:
|
|
106
|
+
normalised = _normalise_sequence(mapping[field])
|
|
107
|
+
if normalised is not None:
|
|
108
|
+
mapping[field] = normalised
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _merge_override_maps(
|
|
112
|
+
base_values: Mapping[str, Any],
|
|
113
|
+
extra_values: Mapping[str, Any],
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Merge override mappings while normalising sequence fields."""
|
|
116
|
+
merged: dict[str, Any] = {}
|
|
117
|
+
for source in (base_values, extra_values):
|
|
118
|
+
for key, value in source.items():
|
|
119
|
+
if value is None:
|
|
120
|
+
continue
|
|
121
|
+
merged[key] = _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
|
|
122
|
+
return merged
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _split_known_and_extra(
|
|
126
|
+
payload: Mapping[str, Any],
|
|
127
|
+
known_fields: Mapping[str, Any],
|
|
128
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
129
|
+
"""Split payload mapping into known request fields and extras."""
|
|
130
|
+
known: dict[str, Any] = {}
|
|
131
|
+
extras: dict[str, Any] = {}
|
|
132
|
+
for key, value in payload.items():
|
|
133
|
+
if value is None:
|
|
134
|
+
continue
|
|
135
|
+
if key in known_fields:
|
|
136
|
+
known[key] = value
|
|
137
|
+
else:
|
|
138
|
+
extras[key] = value
|
|
139
|
+
return known, extras
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _prepare_agent_metadata(value: Any) -> dict[str, Any]:
|
|
143
|
+
"""Ensure agent metadata contains ``type: custom`` by default."""
|
|
144
|
+
if value is None:
|
|
145
|
+
return {"type": _DEFAULT_METADATA_TYPE}
|
|
146
|
+
if not isinstance(value, Mapping):
|
|
147
|
+
return {"type": _DEFAULT_METADATA_TYPE}
|
|
148
|
+
|
|
149
|
+
prepared = dict(value)
|
|
150
|
+
metadata_type = prepared.get("type")
|
|
151
|
+
if not metadata_type:
|
|
152
|
+
prepared["type"] = _DEFAULT_METADATA_TYPE
|
|
153
|
+
return prepared
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _load_agent_file_payload(file_path: Path, *, model_override: str | None) -> dict[str, Any]:
|
|
157
|
+
"""Load agent configuration from disk and normalise legacy fields."""
|
|
158
|
+
if not file_path.exists():
|
|
159
|
+
raise FileNotFoundError(f"Agent configuration file not found: {file_path}")
|
|
160
|
+
if not file_path.is_file():
|
|
161
|
+
raise ValueError(f"Agent configuration path must point to a file: {file_path}")
|
|
162
|
+
|
|
163
|
+
raw_data = load_resource_from_file(file_path)
|
|
164
|
+
if not isinstance(raw_data, Mapping):
|
|
165
|
+
raise ValueError("Agent configuration file must contain a mapping/object.")
|
|
166
|
+
|
|
167
|
+
payload = convert_export_to_import_format(dict(raw_data))
|
|
168
|
+
payload = normalize_agent_config_for_import(payload, model_override)
|
|
169
|
+
|
|
170
|
+
for field in _SERVER_ONLY_IMPORT_FIELDS:
|
|
171
|
+
payload.pop(field, None)
|
|
172
|
+
|
|
173
|
+
return payload
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _prepare_import_payload(
|
|
177
|
+
file_path: Path,
|
|
178
|
+
overrides: Mapping[str, Any],
|
|
179
|
+
*,
|
|
180
|
+
drop_model_fields: bool = False,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Prepare merged payload from file contents and explicit overrides."""
|
|
183
|
+
overrides_dict = dict(overrides)
|
|
184
|
+
|
|
185
|
+
raw_definition = load_resource_from_file(file_path)
|
|
186
|
+
original_refs = _extract_original_refs(raw_definition)
|
|
187
|
+
|
|
188
|
+
base_payload = _load_agent_file_payload(file_path, model_override=overrides_dict.get("model"))
|
|
189
|
+
|
|
190
|
+
cli_args = _build_cli_args(overrides_dict)
|
|
191
|
+
|
|
192
|
+
merged = merge_import_with_cli_args(base_payload, cli_args)
|
|
193
|
+
|
|
194
|
+
additional = _build_additional_args(overrides_dict, cli_args)
|
|
195
|
+
merged.update(additional)
|
|
196
|
+
|
|
197
|
+
if drop_model_fields:
|
|
198
|
+
_remove_model_fields_if_needed(merged, overrides_dict)
|
|
199
|
+
|
|
200
|
+
_set_default_refs(merged, original_refs)
|
|
201
|
+
|
|
202
|
+
_normalise_sequence_fields(merged)
|
|
203
|
+
return merged
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
|
|
207
|
+
"""Extract original tool/agent/mcp references from raw definition."""
|
|
208
|
+
return {
|
|
209
|
+
"tools": list(raw_definition.get("tools") or []),
|
|
210
|
+
"agents": list(raw_definition.get("agents") or []),
|
|
211
|
+
"mcps": list(raw_definition.get("mcps") or []),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
|
|
216
|
+
"""Build CLI args from overrides, filtering out None values."""
|
|
217
|
+
cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
|
|
218
|
+
|
|
219
|
+
# Normalize sequence fields
|
|
220
|
+
for field in _MERGED_SEQUENCE_FIELDS:
|
|
221
|
+
if field in cli_args:
|
|
222
|
+
cli_args[field] = tuple(_normalise_sequence(cli_args[field]) or [])
|
|
223
|
+
|
|
224
|
+
return cli_args
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _build_additional_args(overrides_dict: dict, cli_args: dict) -> dict[str, Any]:
|
|
228
|
+
"""Build additional args not already in CLI args."""
|
|
229
|
+
return {key: value for key, value in overrides_dict.items() if value is not None and key not in cli_args}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _remove_model_fields_if_needed(merged: dict, overrides_dict: dict) -> None:
|
|
233
|
+
"""Remove model fields if not explicitly overridden."""
|
|
234
|
+
if overrides_dict.get("language_model_id") is None:
|
|
235
|
+
merged.pop("language_model_id", None)
|
|
236
|
+
if overrides_dict.get("provider") is None:
|
|
237
|
+
merged.pop("provider", None)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _set_default_refs(merged: dict, original_refs: dict) -> None:
|
|
241
|
+
"""Set default references if not already present."""
|
|
242
|
+
merged.setdefault("_tool_refs", original_refs["tools"])
|
|
243
|
+
merged.setdefault("_agent_refs", original_refs["agents"])
|
|
244
|
+
merged.setdefault("_mcp_refs", original_refs["mcps"])
|
|
245
|
+
|
|
51
246
|
|
|
52
247
|
class AgentClient(BaseClient):
|
|
53
248
|
"""Client for agent operations."""
|
|
@@ -65,44 +260,52 @@ class AgentClient(BaseClient):
|
|
|
65
260
|
**kwargs: Additional arguments for standalone initialization
|
|
66
261
|
"""
|
|
67
262
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
263
|
+
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
264
|
+
self._tool_client: ToolClient | None = None
|
|
265
|
+
self._mcp_client: MCPClient | None = None
|
|
266
|
+
self._runs_client: AgentRunsClient | None = None
|
|
68
267
|
|
|
69
268
|
def list_agents(
|
|
70
269
|
self,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
sync_langflow_agents: bool = False,
|
|
76
|
-
) -> list[Agent]:
|
|
77
|
-
"""List agents with optional filtering.
|
|
270
|
+
query: AgentListParams | None = None,
|
|
271
|
+
**kwargs: Any,
|
|
272
|
+
) -> AgentListResult:
|
|
273
|
+
"""List agents with optional filtering and pagination support.
|
|
78
274
|
|
|
79
275
|
Args:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
name: Filter by partial name match (case-insensitive)
|
|
83
|
-
version: Filter by exact version match
|
|
84
|
-
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
List of agents matching the filters
|
|
276
|
+
query: Query parameters for filtering agents. If None, uses kwargs to create query.
|
|
277
|
+
**kwargs: Individual filter parameters for backward compatibility.
|
|
88
278
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
279
|
+
if query is not None and kwargs:
|
|
280
|
+
# Both query object and individual parameters provided
|
|
281
|
+
raise ValueError("Provide either `query` or individual filter arguments, not both.")
|
|
282
|
+
|
|
283
|
+
if query is None:
|
|
284
|
+
# Create query from individual parameters for backward compatibility
|
|
285
|
+
query = AgentListParams(**kwargs)
|
|
286
|
+
|
|
287
|
+
params = query.to_query_params()
|
|
288
|
+
envelope = self._request_with_envelope(
|
|
289
|
+
"GET",
|
|
290
|
+
AGENTS_ENDPOINT,
|
|
291
|
+
params=params if params else None,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if not isinstance(envelope, dict):
|
|
295
|
+
envelope = {"data": envelope}
|
|
296
|
+
|
|
297
|
+
data_payload = envelope.get("data") or []
|
|
298
|
+
items = create_model_instances(data_payload, Agent, self)
|
|
299
|
+
|
|
300
|
+
return AgentListResult(
|
|
301
|
+
items=items,
|
|
302
|
+
total=envelope.get("total"),
|
|
303
|
+
page=envelope.get("page"),
|
|
304
|
+
limit=envelope.get("limit"),
|
|
305
|
+
has_next=envelope.get("has_next"),
|
|
306
|
+
has_prev=envelope.get("has_prev"),
|
|
307
|
+
message=envelope.get("message"),
|
|
308
|
+
)
|
|
106
309
|
|
|
107
310
|
def sync_langflow_agents(
|
|
108
311
|
self,
|
|
@@ -134,7 +337,19 @@ class AgentClient(BaseClient):
|
|
|
134
337
|
|
|
135
338
|
def get_agent_by_id(self, agent_id: str) -> Agent:
|
|
136
339
|
"""Get agent by ID."""
|
|
137
|
-
|
|
340
|
+
try:
|
|
341
|
+
data = self._request("GET", f"/agents/{agent_id}")
|
|
342
|
+
except ValidationError as exc:
|
|
343
|
+
if exc.status_code == 422:
|
|
344
|
+
message = f"Agent '{agent_id}' not found"
|
|
345
|
+
raise NotFoundError(
|
|
346
|
+
message,
|
|
347
|
+
status_code=404,
|
|
348
|
+
error_type=exc.error_type,
|
|
349
|
+
payload=exc.payload,
|
|
350
|
+
request_id=exc.request_id,
|
|
351
|
+
) from exc
|
|
352
|
+
raise
|
|
138
353
|
|
|
139
354
|
if isinstance(data, str):
|
|
140
355
|
# Some backends may respond with plain text for missing agents.
|
|
@@ -147,333 +362,573 @@ class AgentClient(BaseClient):
|
|
|
147
362
|
status_code=404,
|
|
148
363
|
)
|
|
149
364
|
|
|
150
|
-
|
|
365
|
+
response = AgentResponse(**data)
|
|
366
|
+
return Agent.from_response(response, client=self)
|
|
151
367
|
|
|
152
368
|
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
153
369
|
"""Find agents by name."""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
params["name"] = name
|
|
157
|
-
|
|
158
|
-
data = self._request("GET", AGENTS_ENDPOINT, params=params)
|
|
159
|
-
agents = create_model_instances(data, Agent, self)
|
|
370
|
+
result = self.list_agents(name=name)
|
|
371
|
+
agents = list(result)
|
|
160
372
|
if name is None:
|
|
161
373
|
return agents
|
|
162
374
|
return find_by_name(agents, name, case_sensitive=False)
|
|
163
375
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
376
|
+
# ------------------------------------------------------------------ #
|
|
377
|
+
# Renderer delegation helpers
|
|
378
|
+
# ------------------------------------------------------------------ #
|
|
379
|
+
def _get_renderer_manager(self) -> AgentRunRenderingManager:
|
|
380
|
+
"""Get or create the renderer manager instance.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
AgentRunRenderingManager instance.
|
|
384
|
+
"""
|
|
385
|
+
manager = getattr(self, "_renderer_manager", None)
|
|
386
|
+
if manager is None:
|
|
387
|
+
manager = AgentRunRenderingManager(logger)
|
|
388
|
+
self._renderer_manager = manager
|
|
389
|
+
return manager
|
|
175
390
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- Always includes required backend metadata
|
|
179
|
-
- Preserves mem0 keys in agent_config
|
|
180
|
-
- Handles tool/agent ID extraction from objects
|
|
391
|
+
def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
|
|
392
|
+
"""Create or return a renderer instance.
|
|
181
393
|
|
|
182
394
|
Args:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
model: Language model name (used when language_model_id not provided)
|
|
186
|
-
tools: List of tools to attach
|
|
187
|
-
agents: List of sub-agents to attach
|
|
188
|
-
timeout: Agent execution timeout
|
|
189
|
-
**kwargs: Additional parameters (language_model_id, agent_config, etc.)
|
|
395
|
+
renderer: Renderer instance, string identifier, or None.
|
|
396
|
+
**kwargs: Additional keyword arguments (e.g., verbose).
|
|
190
397
|
|
|
191
398
|
Returns:
|
|
192
|
-
|
|
399
|
+
RichStreamRenderer instance.
|
|
193
400
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"framework": DEFAULT_AGENT_FRAMEWORK,
|
|
200
|
-
"version": DEFAULT_AGENT_VERSION,
|
|
201
|
-
}
|
|
401
|
+
manager = self._get_renderer_manager()
|
|
402
|
+
verbose = kwargs.get("verbose", False)
|
|
403
|
+
if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
|
|
404
|
+
return renderer # type: ignore[return-value]
|
|
405
|
+
return manager.create_renderer(renderer, verbose=verbose)
|
|
202
406
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
407
|
+
def _process_stream_events(
|
|
408
|
+
self,
|
|
409
|
+
stream_response: httpx.Response,
|
|
410
|
+
renderer: RichStreamRenderer,
|
|
411
|
+
timeout_seconds: float,
|
|
412
|
+
agent_name: str | None,
|
|
413
|
+
meta: dict[str, Any],
|
|
414
|
+
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
415
|
+
"""Process stream events from an HTTP response.
|
|
212
416
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
417
|
+
Args:
|
|
418
|
+
stream_response: HTTP response stream.
|
|
419
|
+
renderer: Renderer to use for displaying events.
|
|
420
|
+
timeout_seconds: Timeout in seconds.
|
|
421
|
+
agent_name: Optional agent name.
|
|
422
|
+
meta: Metadata dictionary.
|
|
216
423
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
424
|
+
Returns:
|
|
425
|
+
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
426
|
+
"""
|
|
427
|
+
manager = self._get_renderer_manager()
|
|
428
|
+
return manager.process_stream_events(
|
|
429
|
+
stream_response,
|
|
430
|
+
renderer,
|
|
431
|
+
timeout_seconds,
|
|
432
|
+
agent_name,
|
|
433
|
+
meta,
|
|
434
|
+
)
|
|
222
435
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
436
|
+
def _finalize_renderer(
|
|
437
|
+
self,
|
|
438
|
+
renderer: RichStreamRenderer,
|
|
439
|
+
final_text: str,
|
|
440
|
+
stats_usage: dict[str, Any],
|
|
441
|
+
started_monotonic: float | None,
|
|
442
|
+
finished_monotonic: float | None,
|
|
443
|
+
) -> str:
|
|
444
|
+
"""Finalize the renderer and return the final response text.
|
|
226
445
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
446
|
+
Args:
|
|
447
|
+
renderer: Renderer to finalize.
|
|
448
|
+
final_text: Final text content.
|
|
449
|
+
stats_usage: Usage statistics dictionary.
|
|
450
|
+
started_monotonic: Start time (monotonic).
|
|
451
|
+
finished_monotonic: Finish time (monotonic).
|
|
232
452
|
|
|
233
|
-
|
|
234
|
-
|
|
453
|
+
Returns:
|
|
454
|
+
Final text string.
|
|
455
|
+
"""
|
|
456
|
+
manager = self._get_renderer_manager()
|
|
457
|
+
return manager.finalize_renderer(
|
|
458
|
+
renderer,
|
|
459
|
+
final_text,
|
|
460
|
+
stats_usage,
|
|
461
|
+
started_monotonic,
|
|
462
|
+
finished_monotonic,
|
|
463
|
+
)
|
|
235
464
|
|
|
236
|
-
|
|
465
|
+
def _get_tool_client(self) -> ToolClient:
|
|
466
|
+
"""Get or create the tool client instance.
|
|
237
467
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"instruction": instruction
|
|
245
|
-
if instruction is not None
|
|
246
|
-
else current_agent.instruction,
|
|
247
|
-
"type": DEFAULT_AGENT_TYPE, # Required by backend
|
|
248
|
-
"framework": DEFAULT_AGENT_FRAMEWORK, # Required by backend
|
|
249
|
-
"version": DEFAULT_AGENT_VERSION, # Required by backend
|
|
250
|
-
}
|
|
468
|
+
Returns:
|
|
469
|
+
ToolClient instance.
|
|
470
|
+
"""
|
|
471
|
+
if self._tool_client is None:
|
|
472
|
+
self._tool_client = ToolClient(parent_client=self)
|
|
473
|
+
return self._tool_client
|
|
251
474
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
update_data: dict[str, Any],
|
|
255
|
-
current_agent: "Agent",
|
|
256
|
-
model: str | None,
|
|
257
|
-
language_model_id: str | None,
|
|
258
|
-
) -> None:
|
|
259
|
-
"""Handle language model selection with proper priority and fallbacks."""
|
|
260
|
-
if language_model_id:
|
|
261
|
-
# Use language_model_id if provided
|
|
262
|
-
update_data["language_model_id"] = language_model_id
|
|
263
|
-
elif model is not None:
|
|
264
|
-
# Use explicit model parameter
|
|
265
|
-
update_data["provider"] = DEFAULT_AGENT_PROVIDER
|
|
266
|
-
update_data["model_name"] = model
|
|
267
|
-
else:
|
|
268
|
-
# Use current agent config or fallbacks
|
|
269
|
-
self._set_language_model_from_current_agent(update_data, current_agent)
|
|
475
|
+
def _get_mcp_client(self) -> MCPClient:
|
|
476
|
+
"""Get or create the MCP client instance.
|
|
270
477
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if "lm_provider" in agent_config:
|
|
278
|
-
update_data["provider"] = agent_config["lm_provider"]
|
|
279
|
-
if "lm_name" in agent_config:
|
|
280
|
-
update_data["model_name"] = agent_config["lm_name"]
|
|
281
|
-
else:
|
|
282
|
-
# Default fallback values
|
|
283
|
-
update_data["provider"] = DEFAULT_AGENT_PROVIDER
|
|
284
|
-
update_data["model_name"] = DEFAULT_MODEL
|
|
478
|
+
Returns:
|
|
479
|
+
MCPClient instance.
|
|
480
|
+
"""
|
|
481
|
+
if self._mcp_client is None:
|
|
482
|
+
self._mcp_client = MCPClient(parent_client=self)
|
|
483
|
+
return self._mcp_client
|
|
285
484
|
|
|
286
|
-
def
|
|
485
|
+
def _normalise_reference_entry(
|
|
287
486
|
self,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
tool_ids = extract_ids(tools)
|
|
297
|
-
update_data["tools"] = tool_ids if tool_ids else []
|
|
298
|
-
else:
|
|
299
|
-
update_data["tools"] = self._extract_current_tool_ids(current_agent)
|
|
487
|
+
entry: Any,
|
|
488
|
+
fallback_iter: Iterator[Any] | None,
|
|
489
|
+
) -> tuple[str | None, str | None]:
|
|
490
|
+
"""Normalize a reference entry to extract ID and name.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
entry: Reference entry (string, dict, or other).
|
|
494
|
+
fallback_iter: Optional iterator for fallback values.
|
|
300
495
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
496
|
+
Returns:
|
|
497
|
+
Tuple of (entry_id, entry_name).
|
|
498
|
+
"""
|
|
499
|
+
entry_id: str | None = None
|
|
500
|
+
entry_name: str | None = None
|
|
501
|
+
|
|
502
|
+
if isinstance(entry, str):
|
|
503
|
+
if is_uuid(entry):
|
|
504
|
+
entry_id = entry
|
|
505
|
+
else:
|
|
506
|
+
entry_name = entry
|
|
507
|
+
elif isinstance(entry, dict):
|
|
508
|
+
entry_id = entry.get("id")
|
|
509
|
+
entry_name = entry.get("name")
|
|
305
510
|
else:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
agent["id"] if isinstance(agent, dict) else agent
|
|
322
|
-
for agent in current_agent.agents
|
|
323
|
-
]
|
|
324
|
-
return []
|
|
325
|
-
|
|
326
|
-
def _handle_agent_config(
|
|
511
|
+
entry_name = str(entry)
|
|
512
|
+
|
|
513
|
+
if entry_name or fallback_iter is None:
|
|
514
|
+
return entry_id, entry_name
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
ref = next(fallback_iter)
|
|
518
|
+
except StopIteration:
|
|
519
|
+
ref = None
|
|
520
|
+
if isinstance(ref, dict):
|
|
521
|
+
entry_name = ref.get("name") or entry_name
|
|
522
|
+
|
|
523
|
+
return entry_id, entry_name
|
|
524
|
+
|
|
525
|
+
def _resolve_resource_ids(
|
|
327
526
|
self,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
527
|
+
items: list[Any] | None,
|
|
528
|
+
references: list[Any] | None,
|
|
529
|
+
*,
|
|
530
|
+
fetch_by_id: Callable[[str], Any],
|
|
531
|
+
find_by_name: Callable[[str], list[Any]],
|
|
532
|
+
label: str,
|
|
533
|
+
plural_label: str | None = None,
|
|
534
|
+
) -> list[str] | None:
|
|
535
|
+
"""Resolve a list of resource references to IDs.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
items: List of resource references to resolve.
|
|
539
|
+
references: Optional list of reference objects for fallback.
|
|
540
|
+
fetch_by_id: Function to fetch resource by ID.
|
|
541
|
+
find_by_name: Function to find resources by name.
|
|
542
|
+
label: Singular label for error messages.
|
|
543
|
+
plural_label: Plural label for error messages.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
List of resolved resource IDs, or None if items is empty.
|
|
547
|
+
"""
|
|
548
|
+
if not items:
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
if references is None:
|
|
552
|
+
return [self._coerce_reference_value(entry) for entry in items]
|
|
553
|
+
|
|
554
|
+
singular = label
|
|
555
|
+
plural = plural_label or f"{label}s"
|
|
556
|
+
fallback_iter = iter(references or [])
|
|
557
|
+
|
|
558
|
+
return [
|
|
559
|
+
self._resolve_single_resource(
|
|
560
|
+
entry,
|
|
561
|
+
fallback_iter,
|
|
562
|
+
fetch_by_id,
|
|
563
|
+
find_by_name,
|
|
564
|
+
singular,
|
|
565
|
+
plural,
|
|
337
566
|
)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# Default agent_config
|
|
343
|
-
update_data["agent_config"] = {
|
|
344
|
-
"lm_provider": DEFAULT_AGENT_PROVIDER,
|
|
345
|
-
"lm_name": DEFAULT_MODEL,
|
|
346
|
-
"lm_hyperparameters": {"temperature": 0.0},
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
# Clean LM keys from agent_config to prevent conflicts
|
|
350
|
-
self._clean_agent_config_lm_keys(update_data)
|
|
351
|
-
|
|
352
|
-
def _merge_agent_configs(self, current_agent: "Agent", new_config: dict) -> dict:
|
|
353
|
-
"""Merge current agent config with new config."""
|
|
354
|
-
if hasattr(current_agent, "agent_config") and current_agent.agent_config:
|
|
355
|
-
merged_config = current_agent.agent_config.copy()
|
|
356
|
-
merged_config.update(new_config)
|
|
357
|
-
return merged_config
|
|
358
|
-
return new_config
|
|
359
|
-
|
|
360
|
-
def _clean_agent_config_lm_keys(self, update_data: dict[str, Any]) -> None:
|
|
361
|
-
"""Remove LM keys from agent_config to prevent conflicts."""
|
|
362
|
-
if "agent_config" in update_data and isinstance(
|
|
363
|
-
update_data["agent_config"], dict
|
|
364
|
-
):
|
|
365
|
-
agent_config = update_data["agent_config"]
|
|
366
|
-
lm_keys_to_remove = {
|
|
367
|
-
"lm_provider",
|
|
368
|
-
"lm_name",
|
|
369
|
-
"lm_base_url",
|
|
370
|
-
"lm_hyperparameters",
|
|
371
|
-
}
|
|
372
|
-
for key in lm_keys_to_remove:
|
|
373
|
-
agent_config.pop(key, None)
|
|
374
|
-
|
|
375
|
-
def _finalize_update_payload(
|
|
567
|
+
for entry in items
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
def _resolve_single_resource(
|
|
376
571
|
self,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
572
|
+
entry: Any,
|
|
573
|
+
fallback_iter: Iterator[Any] | None,
|
|
574
|
+
fetch_by_id: Callable[[str], Any],
|
|
575
|
+
find_by_name: Callable[[str], list[Any]],
|
|
576
|
+
singular: str,
|
|
577
|
+
plural: str,
|
|
578
|
+
) -> str:
|
|
579
|
+
"""Resolve a single resource reference to an ID.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
entry: Resource reference to resolve.
|
|
583
|
+
fallback_iter: Optional iterator for fallback values.
|
|
584
|
+
fetch_by_id: Function to fetch resource by ID.
|
|
585
|
+
find_by_name: Function to find resources by name.
|
|
586
|
+
singular: Singular label for error messages.
|
|
587
|
+
plural: Plural label for error messages.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Resolved resource ID string.
|
|
591
|
+
|
|
592
|
+
Raises:
|
|
593
|
+
ValueError: If the resource cannot be resolved.
|
|
594
|
+
"""
|
|
595
|
+
entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
|
|
596
|
+
|
|
597
|
+
validated_id = self._validate_resource_id(fetch_by_id, entry_id)
|
|
598
|
+
if validated_id:
|
|
599
|
+
return validated_id
|
|
600
|
+
if entry_id and entry_name is None:
|
|
601
|
+
return entry_id
|
|
602
|
+
|
|
603
|
+
if entry_name:
|
|
604
|
+
resolved, success = self._resolve_resource_by_name(find_by_name, entry_name, singular, plural)
|
|
605
|
+
return resolved if success else entry_name
|
|
606
|
+
|
|
607
|
+
raise ValueError(f"{singular} references must include a valid ID or name.")
|
|
385
608
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if key not in excluded_keys:
|
|
390
|
-
update_data[key] = value
|
|
609
|
+
@staticmethod
|
|
610
|
+
def _coerce_reference_value(entry: Any) -> str:
|
|
611
|
+
"""Coerce a reference entry to a string value.
|
|
391
612
|
|
|
392
|
-
|
|
613
|
+
Args:
|
|
614
|
+
entry: Reference entry (dict, string, or other).
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
String representation of the reference.
|
|
618
|
+
"""
|
|
619
|
+
if isinstance(entry, dict):
|
|
620
|
+
if entry.get("id"):
|
|
621
|
+
return str(entry["id"])
|
|
622
|
+
if entry.get("name"):
|
|
623
|
+
return str(entry["name"])
|
|
624
|
+
return str(entry)
|
|
625
|
+
|
|
626
|
+
@staticmethod
|
|
627
|
+
def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
|
|
628
|
+
"""Validate a resource ID by attempting to fetch it.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
fetch_by_id: Function to fetch resource by ID.
|
|
632
|
+
candidate_id: Candidate ID to validate.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Validated ID if found, None otherwise.
|
|
636
|
+
"""
|
|
637
|
+
if not candidate_id:
|
|
638
|
+
return None
|
|
639
|
+
try:
|
|
640
|
+
fetch_by_id(candidate_id)
|
|
641
|
+
except Exception:
|
|
642
|
+
return None
|
|
643
|
+
return candidate_id
|
|
644
|
+
|
|
645
|
+
@staticmethod
|
|
646
|
+
def _resolve_resource_by_name(
|
|
647
|
+
find_by_name: Callable[[str], list[Any]],
|
|
648
|
+
entry_name: str,
|
|
649
|
+
singular: str,
|
|
650
|
+
plural: str,
|
|
651
|
+
) -> tuple[str, bool]:
|
|
652
|
+
"""Resolve a resource by name to an ID.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
find_by_name: Function to find resources by name.
|
|
656
|
+
entry_name: Name of the resource to find.
|
|
657
|
+
singular: Singular label for error messages.
|
|
658
|
+
plural: Plural label for error messages.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Tuple of (resolved_id, success).
|
|
662
|
+
|
|
663
|
+
Raises:
|
|
664
|
+
ValueError: If resource not found or multiple matches exist.
|
|
665
|
+
"""
|
|
666
|
+
try:
|
|
667
|
+
matches = find_by_name(entry_name)
|
|
668
|
+
except Exception:
|
|
669
|
+
return entry_name, False
|
|
670
|
+
|
|
671
|
+
if not matches:
|
|
672
|
+
raise ValueError(f"{singular} '{entry_name}' not found in current workspace.")
|
|
673
|
+
if len(matches) > 1:
|
|
674
|
+
exact = [m for m in matches if getattr(m, "name", "").lower() == entry_name.lower()]
|
|
675
|
+
if len(exact) == 1:
|
|
676
|
+
matches = exact
|
|
677
|
+
else:
|
|
678
|
+
raise ValueError(f"Multiple {plural} named '{entry_name}'. Please disambiguate.")
|
|
679
|
+
return str(matches[0].id), True
|
|
393
680
|
|
|
394
|
-
def
|
|
681
|
+
def _resolve_tool_ids(
|
|
395
682
|
self,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
**kwargs: Any,
|
|
401
|
-
) -> dict[str, Any]:
|
|
402
|
-
"""Build payload for agent update with proper LM selection and current state preservation.
|
|
683
|
+
tools: list[Any] | None,
|
|
684
|
+
references: list[Any] | None = None,
|
|
685
|
+
) -> list[str] | None:
|
|
686
|
+
"""Resolve tool references to IDs.
|
|
403
687
|
|
|
404
688
|
Args:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
instruction: New instruction (None to keep current)
|
|
408
|
-
model: New language model name (None to use current or fallback)
|
|
409
|
-
**kwargs: Additional parameters including language_model_id, agent_config, etc.
|
|
689
|
+
tools: List of tool references to resolve.
|
|
690
|
+
references: Optional list of reference objects for fallback.
|
|
410
691
|
|
|
411
692
|
Returns:
|
|
412
|
-
|
|
693
|
+
List of resolved tool IDs, or None if tools is empty.
|
|
694
|
+
"""
|
|
695
|
+
tool_client = self._get_tool_client()
|
|
696
|
+
return self._resolve_resource_ids(
|
|
697
|
+
tools,
|
|
698
|
+
references,
|
|
699
|
+
fetch_by_id=tool_client.get_tool_by_id,
|
|
700
|
+
find_by_name=tool_client.find_tools,
|
|
701
|
+
label="Tool",
|
|
702
|
+
plural_label="tools",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def _resolve_agent_ids(
|
|
706
|
+
self,
|
|
707
|
+
agents: list[Any] | None,
|
|
708
|
+
references: list[Any] | None = None,
|
|
709
|
+
) -> list[str] | None:
|
|
710
|
+
"""Resolve agent references to IDs.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
agents: List of agent references to resolve.
|
|
714
|
+
references: Optional list of reference objects for fallback.
|
|
413
715
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
- Preserves current values as defaults when new values not provided
|
|
417
|
-
- Handles tools/agents updates with proper ID extraction
|
|
716
|
+
Returns:
|
|
717
|
+
List of resolved agent IDs, or None if agents is empty.
|
|
418
718
|
"""
|
|
419
|
-
|
|
420
|
-
|
|
719
|
+
return self._resolve_resource_ids(
|
|
720
|
+
agents,
|
|
721
|
+
references,
|
|
722
|
+
fetch_by_id=self.get_agent_by_id,
|
|
723
|
+
find_by_name=self.find_agents,
|
|
724
|
+
label="Agent",
|
|
725
|
+
plural_label="agents",
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def _resolve_mcp_ids(
|
|
729
|
+
self,
|
|
730
|
+
mcps: list[Any] | None,
|
|
731
|
+
references: list[Any] | None = None,
|
|
732
|
+
) -> list[str] | None:
|
|
733
|
+
"""Resolve MCP references to IDs.
|
|
421
734
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
735
|
+
Args:
|
|
736
|
+
mcps: List of MCP references to resolve.
|
|
737
|
+
references: Optional list of reference objects for fallback.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
List of resolved MCP IDs, or None if mcps is empty.
|
|
741
|
+
"""
|
|
742
|
+
mcp_client = self._get_mcp_client()
|
|
743
|
+
return self._resolve_resource_ids(
|
|
744
|
+
mcps,
|
|
745
|
+
references,
|
|
746
|
+
fetch_by_id=mcp_client.get_mcp_by_id,
|
|
747
|
+
find_by_name=mcp_client.find_mcps,
|
|
748
|
+
label="MCP",
|
|
749
|
+
plural_label="MCPs",
|
|
426
750
|
)
|
|
427
751
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
self._handle_tools_and_agents(update_data, current_agent, tools, agents)
|
|
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__)
|
|
432
755
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
756
|
+
name = known.pop("name", None)
|
|
757
|
+
instruction = known.pop("instruction", None)
|
|
758
|
+
if not name or not str(name).strip():
|
|
759
|
+
raise ValueError("Agent name cannot be empty or whitespace")
|
|
760
|
+
if not instruction or not str(instruction).strip():
|
|
761
|
+
raise ValueError("Agent instruction cannot be empty or whitespace")
|
|
762
|
+
|
|
763
|
+
validated_instruction = validate_agent_instruction(str(instruction))
|
|
764
|
+
_normalise_sequence_fields(known)
|
|
765
|
+
|
|
766
|
+
resolved_model = known.pop("model", None) or DEFAULT_MODEL
|
|
767
|
+
tool_refs = extras.pop("_tool_refs", None)
|
|
768
|
+
agent_refs = extras.pop("_agent_refs", None)
|
|
769
|
+
mcp_refs = extras.pop("_mcp_refs", None)
|
|
770
|
+
|
|
771
|
+
tools_raw = known.pop("tools", None)
|
|
772
|
+
agents_raw = known.pop("agents", None)
|
|
773
|
+
mcps_raw = known.pop("mcps", None)
|
|
774
|
+
|
|
775
|
+
resolved_tools = self._resolve_tool_ids(tools_raw, tool_refs)
|
|
776
|
+
resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
|
|
777
|
+
resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
|
|
778
|
+
|
|
779
|
+
language_model_id = known.pop("language_model_id", None)
|
|
780
|
+
provider = known.pop("provider", None)
|
|
781
|
+
model_name = known.pop("model_name", None)
|
|
782
|
+
|
|
783
|
+
agent_type_value = known.pop("agent_type", None)
|
|
784
|
+
fallback_type_value = known.pop("type", None)
|
|
785
|
+
if agent_type_value is None:
|
|
786
|
+
agent_type_value = fallback_type_value or DEFAULT_AGENT_TYPE
|
|
787
|
+
|
|
788
|
+
framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
|
|
789
|
+
version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
|
|
790
|
+
account_id = known.pop("account_id", None)
|
|
791
|
+
description = known.pop("description", None)
|
|
792
|
+
metadata = _prepare_agent_metadata(known.pop("metadata", None))
|
|
793
|
+
tool_configs = known.pop("tool_configs", None)
|
|
794
|
+
agent_config = known.pop("agent_config", None)
|
|
795
|
+
timeout_value = known.pop("timeout", None)
|
|
796
|
+
a2a_profile = known.pop("a2a_profile", None)
|
|
797
|
+
|
|
798
|
+
final_extras = {**known, **extras}
|
|
799
|
+
final_extras.setdefault("model", resolved_model)
|
|
800
|
+
|
|
801
|
+
request = AgentCreateRequest(
|
|
802
|
+
name=str(name).strip(),
|
|
803
|
+
instruction=validated_instruction,
|
|
804
|
+
model=resolved_model,
|
|
805
|
+
language_model_id=language_model_id,
|
|
806
|
+
provider=provider,
|
|
807
|
+
model_name=model_name,
|
|
808
|
+
agent_type=agent_type_value,
|
|
809
|
+
framework=framework_value,
|
|
810
|
+
version=version_value,
|
|
811
|
+
account_id=account_id,
|
|
812
|
+
description=description,
|
|
813
|
+
metadata=metadata,
|
|
814
|
+
tools=resolved_tools,
|
|
815
|
+
agents=resolved_agents,
|
|
816
|
+
mcps=resolved_mcps,
|
|
817
|
+
tool_configs=tool_configs,
|
|
818
|
+
agent_config=agent_config,
|
|
819
|
+
timeout=timeout_value or DEFAULT_AGENT_RUN_TIMEOUT,
|
|
820
|
+
a2a_profile=a2a_profile,
|
|
821
|
+
extras=final_extras,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
payload_dict = request.to_payload()
|
|
825
|
+
payload_dict.setdefault("model", resolved_model)
|
|
436
826
|
|
|
437
|
-
|
|
438
|
-
|
|
827
|
+
full_agent_data = self._post_then_fetch(
|
|
828
|
+
id_key="id",
|
|
829
|
+
post_endpoint=AGENTS_ENDPOINT,
|
|
830
|
+
get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
|
|
831
|
+
json=payload_dict,
|
|
832
|
+
)
|
|
833
|
+
response = AgentResponse(**full_agent_data)
|
|
834
|
+
return Agent.from_response(response, client=self)
|
|
439
835
|
|
|
440
836
|
def create_agent(
|
|
441
837
|
self,
|
|
442
|
-
name: str,
|
|
443
|
-
instruction: str,
|
|
444
|
-
model: str =
|
|
838
|
+
name: str | None = None,
|
|
839
|
+
instruction: str | None = None,
|
|
840
|
+
model: str | None = None,
|
|
445
841
|
tools: list[str | Any] | None = None,
|
|
446
842
|
agents: list[str | Any] | None = None,
|
|
447
|
-
timeout: int =
|
|
843
|
+
timeout: int | None = None,
|
|
844
|
+
*,
|
|
845
|
+
file: str | PathLike[str] | None = None,
|
|
846
|
+
mcps: list[str | Any] | None = None,
|
|
847
|
+
tool_configs: Mapping[str, Any] | None = None,
|
|
448
848
|
**kwargs: Any,
|
|
449
849
|
) -> "Agent":
|
|
450
|
-
"""Create a new agent."""
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
850
|
+
"""Create a new agent, optionally loading configuration from a file."""
|
|
851
|
+
base_overrides = {
|
|
852
|
+
"name": name,
|
|
853
|
+
"instruction": instruction,
|
|
854
|
+
"model": model,
|
|
855
|
+
"tools": tools,
|
|
856
|
+
"agents": agents,
|
|
857
|
+
"timeout": timeout,
|
|
858
|
+
"mcps": mcps,
|
|
859
|
+
"tool_configs": tool_configs,
|
|
860
|
+
}
|
|
861
|
+
overrides = _merge_override_maps(base_overrides, kwargs)
|
|
454
862
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
instruction=instruction,
|
|
462
|
-
model=model,
|
|
463
|
-
tools=tools,
|
|
464
|
-
agents=agents,
|
|
465
|
-
timeout=timeout,
|
|
466
|
-
**kwargs,
|
|
467
|
-
)
|
|
863
|
+
if file is not None:
|
|
864
|
+
payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
|
|
865
|
+
if overrides.get("model") is None:
|
|
866
|
+
payload.pop("model", None)
|
|
867
|
+
else:
|
|
868
|
+
payload = overrides
|
|
468
869
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
870
|
+
return self._create_agent_from_payload(payload)
|
|
871
|
+
|
|
872
|
+
def create_agent_from_file( # pragma: no cover - thin compatibility wrapper
|
|
873
|
+
self,
|
|
874
|
+
file_path: str | PathLike[str],
|
|
875
|
+
**overrides: Any,
|
|
876
|
+
) -> "Agent":
|
|
877
|
+
"""Backward-compatible helper to create an agent from a configuration file."""
|
|
878
|
+
return self.create_agent(file=file_path, **overrides)
|
|
879
|
+
|
|
880
|
+
def _update_agent_from_payload(
|
|
881
|
+
self,
|
|
882
|
+
agent_id: str,
|
|
883
|
+
current_agent: Agent,
|
|
884
|
+
payload: Mapping[str, Any],
|
|
885
|
+
) -> "Agent":
|
|
886
|
+
"""Update an agent using a prepared payload mapping."""
|
|
887
|
+
known, extras = _split_known_and_extra(payload, AgentUpdateRequest.__dataclass_fields__)
|
|
888
|
+
_normalise_sequence_fields(known)
|
|
889
|
+
|
|
890
|
+
tool_refs = extras.pop("_tool_refs", None)
|
|
891
|
+
agent_refs = extras.pop("_agent_refs", None)
|
|
892
|
+
mcp_refs = extras.pop("_mcp_refs", None)
|
|
893
|
+
|
|
894
|
+
tools_value = known.pop("tools", None)
|
|
895
|
+
agents_value = known.pop("agents", None)
|
|
896
|
+
mcps_value = known.pop("mcps", None)
|
|
897
|
+
|
|
898
|
+
if tools_value is not None:
|
|
899
|
+
tools_value = self._resolve_tool_ids(tools_value, tool_refs)
|
|
900
|
+
if agents_value is not None:
|
|
901
|
+
agents_value = self._resolve_agent_ids(agents_value, agent_refs)
|
|
902
|
+
if mcps_value is not None:
|
|
903
|
+
mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
|
|
904
|
+
|
|
905
|
+
request = AgentUpdateRequest(
|
|
906
|
+
name=known.pop("name", None),
|
|
907
|
+
instruction=known.pop("instruction", None),
|
|
908
|
+
description=known.pop("description", None),
|
|
909
|
+
model=known.pop("model", None),
|
|
910
|
+
language_model_id=known.pop("language_model_id", None),
|
|
911
|
+
provider=known.pop("provider", None),
|
|
912
|
+
model_name=known.pop("model_name", None),
|
|
913
|
+
agent_type=known.pop("agent_type", known.pop("type", None)),
|
|
914
|
+
framework=known.pop("framework", None),
|
|
915
|
+
version=known.pop("version", None),
|
|
916
|
+
account_id=known.pop("account_id", None),
|
|
917
|
+
metadata=known.pop("metadata", None),
|
|
918
|
+
tools=tools_value,
|
|
919
|
+
tool_configs=known.pop("tool_configs", None),
|
|
920
|
+
agents=agents_value,
|
|
921
|
+
mcps=mcps_value,
|
|
922
|
+
agent_config=known.pop("agent_config", None),
|
|
923
|
+
a2a_profile=known.pop("a2a_profile", None),
|
|
924
|
+
extras={**known, **extras},
|
|
475
925
|
)
|
|
476
|
-
|
|
926
|
+
|
|
927
|
+
payload_dict = request.to_payload(current_agent)
|
|
928
|
+
|
|
929
|
+
api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
|
|
930
|
+
response = AgentResponse(**api_response)
|
|
931
|
+
return Agent.from_response(response, client=self)
|
|
477
932
|
|
|
478
933
|
def update_agent(
|
|
479
934
|
self,
|
|
@@ -481,29 +936,101 @@ class AgentClient(BaseClient):
|
|
|
481
936
|
name: str | None = None,
|
|
482
937
|
instruction: str | None = None,
|
|
483
938
|
model: str | None = None,
|
|
939
|
+
*,
|
|
940
|
+
file: str | PathLike[str] | None = None,
|
|
941
|
+
tools: list[str | Any] | None = None,
|
|
942
|
+
agents: list[str | Any] | None = None,
|
|
943
|
+
mcps: list[str | Any] | None = None,
|
|
484
944
|
**kwargs: Any,
|
|
485
945
|
) -> "Agent":
|
|
486
946
|
"""Update an existing agent."""
|
|
487
|
-
|
|
488
|
-
|
|
947
|
+
base_overrides = {
|
|
948
|
+
"name": name,
|
|
949
|
+
"instruction": instruction,
|
|
950
|
+
"model": model,
|
|
951
|
+
"tools": tools,
|
|
952
|
+
"agents": agents,
|
|
953
|
+
"mcps": mcps,
|
|
954
|
+
}
|
|
955
|
+
overrides = _merge_override_maps(base_overrides, kwargs)
|
|
489
956
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
instruction=instruction,
|
|
495
|
-
model=model,
|
|
496
|
-
**kwargs,
|
|
497
|
-
)
|
|
957
|
+
if file is not None:
|
|
958
|
+
payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
|
|
959
|
+
else:
|
|
960
|
+
payload = overrides
|
|
498
961
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
962
|
+
current_agent = self.get_agent_by_id(agent_id)
|
|
963
|
+
return self._update_agent_from_payload(agent_id, current_agent, payload)
|
|
964
|
+
|
|
965
|
+
def update_agent_from_file( # pragma: no cover - thin compatibility wrapper
|
|
966
|
+
self,
|
|
967
|
+
agent_id: str,
|
|
968
|
+
file_path: str | PathLike[str],
|
|
969
|
+
**overrides: Any,
|
|
970
|
+
) -> "Agent":
|
|
971
|
+
"""Backward-compatible helper to update an agent from a configuration file."""
|
|
972
|
+
return self.update_agent(agent_id, file=file_path, **overrides)
|
|
502
973
|
|
|
503
974
|
def delete_agent(self, agent_id: str) -> None:
|
|
504
975
|
"""Delete an agent."""
|
|
505
976
|
self._request("DELETE", f"/agents/{agent_id}")
|
|
506
977
|
|
|
978
|
+
def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
|
|
979
|
+
"""Create or update an agent by instance, ID, or name.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
identifier: Agent instance, ID (UUID string), or name
|
|
983
|
+
**kwargs: Agent configuration (instruction, description, tools, etc.)
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
The created or updated agent.
|
|
987
|
+
|
|
988
|
+
Example:
|
|
989
|
+
>>> # By name (creates if not exists)
|
|
990
|
+
>>> agent = client.agents.upsert_agent(
|
|
991
|
+
... "hello_agent",
|
|
992
|
+
... instruction="You are a helpful assistant.",
|
|
993
|
+
... description="A friendly agent",
|
|
994
|
+
... )
|
|
995
|
+
>>> # By instance
|
|
996
|
+
>>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
|
|
997
|
+
>>> # By ID
|
|
998
|
+
>>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
|
|
999
|
+
"""
|
|
1000
|
+
# Handle Agent instance
|
|
1001
|
+
if isinstance(identifier, Agent):
|
|
1002
|
+
if identifier.id:
|
|
1003
|
+
logger.info("Updating agent by instance: %s", identifier.name)
|
|
1004
|
+
return self.update_agent(identifier.id, name=identifier.name, **kwargs)
|
|
1005
|
+
identifier = identifier.name
|
|
1006
|
+
|
|
1007
|
+
# Handle string (ID or name)
|
|
1008
|
+
if isinstance(identifier, str):
|
|
1009
|
+
# Check if it's a UUID
|
|
1010
|
+
if is_uuid(identifier):
|
|
1011
|
+
logger.info("Updating agent by ID: %s", identifier)
|
|
1012
|
+
return self.update_agent(identifier, **kwargs)
|
|
1013
|
+
|
|
1014
|
+
# It's a name - find or create
|
|
1015
|
+
return self._upsert_agent_by_name(identifier, **kwargs)
|
|
1016
|
+
|
|
1017
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
1018
|
+
|
|
1019
|
+
def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
|
|
1020
|
+
"""Find agent by name and update, or create if not found."""
|
|
1021
|
+
existing = self.find_agents(name)
|
|
1022
|
+
|
|
1023
|
+
if len(existing) == 1:
|
|
1024
|
+
logger.info("Updating existing agent: %s", name)
|
|
1025
|
+
return self.update_agent(existing[0].id, name=name, **kwargs)
|
|
1026
|
+
|
|
1027
|
+
if len(existing) > 1:
|
|
1028
|
+
raise ValueError(f"Multiple agents found with name '{name}'")
|
|
1029
|
+
|
|
1030
|
+
# Create new agent
|
|
1031
|
+
logger.info("Creating new agent: %s", name)
|
|
1032
|
+
return self.create_agent(name=name, **kwargs)
|
|
1033
|
+
|
|
507
1034
|
def _prepare_sync_request_data(
|
|
508
1035
|
self,
|
|
509
1036
|
message: str,
|
|
@@ -546,9 +1073,7 @@ class AgentClient(BaseClient):
|
|
|
546
1073
|
payload["tty"] = True
|
|
547
1074
|
return payload, None, None, headers, None
|
|
548
1075
|
|
|
549
|
-
def _get_timeout_values(
|
|
550
|
-
self, timeout: float | None, **kwargs: Any
|
|
551
|
-
) -> tuple[float, float]:
|
|
1076
|
+
def _get_timeout_values(self, timeout: float | None, **kwargs: Any) -> tuple[float, float]:
|
|
552
1077
|
"""Get request timeout and execution timeout values.
|
|
553
1078
|
|
|
554
1079
|
Args:
|
|
@@ -562,197 +1087,6 @@ class AgentClient(BaseClient):
|
|
|
562
1087
|
execution_timeout = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
563
1088
|
return request_timeout, execution_timeout
|
|
564
1089
|
|
|
565
|
-
def _create_renderer(
|
|
566
|
-
self, renderer: RichStreamRenderer | None, **kwargs: Any
|
|
567
|
-
) -> RichStreamRenderer:
|
|
568
|
-
"""Create appropriate renderer based on configuration."""
|
|
569
|
-
if isinstance(renderer, RichStreamRenderer):
|
|
570
|
-
return renderer
|
|
571
|
-
|
|
572
|
-
verbose = kwargs.get("verbose", False)
|
|
573
|
-
|
|
574
|
-
if isinstance(renderer, str):
|
|
575
|
-
if renderer == "silent":
|
|
576
|
-
return self._create_silent_renderer()
|
|
577
|
-
elif renderer == "minimal":
|
|
578
|
-
return self._create_minimal_renderer()
|
|
579
|
-
else:
|
|
580
|
-
return self._create_default_renderer(verbose)
|
|
581
|
-
elif verbose:
|
|
582
|
-
return self._create_verbose_renderer()
|
|
583
|
-
else:
|
|
584
|
-
return self._create_default_renderer(verbose)
|
|
585
|
-
|
|
586
|
-
def _create_silent_renderer(self) -> RichStreamRenderer:
|
|
587
|
-
"""Create a silent renderer that suppresses all output."""
|
|
588
|
-
silent_config = RendererConfig(
|
|
589
|
-
live=False,
|
|
590
|
-
persist_live=False,
|
|
591
|
-
show_delegate_tool_panels=False,
|
|
592
|
-
render_thinking=False,
|
|
593
|
-
)
|
|
594
|
-
return RichStreamRenderer(
|
|
595
|
-
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
596
|
-
cfg=silent_config,
|
|
597
|
-
verbose=False,
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
def _create_minimal_renderer(self) -> RichStreamRenderer:
|
|
601
|
-
"""Create a minimal renderer with basic output."""
|
|
602
|
-
minimal_config = RendererConfig(
|
|
603
|
-
live=False,
|
|
604
|
-
persist_live=False,
|
|
605
|
-
show_delegate_tool_panels=False,
|
|
606
|
-
render_thinking=False,
|
|
607
|
-
)
|
|
608
|
-
return RichStreamRenderer(
|
|
609
|
-
console=_Console(),
|
|
610
|
-
cfg=minimal_config,
|
|
611
|
-
verbose=False,
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
def _create_verbose_renderer(self) -> RichStreamRenderer:
|
|
615
|
-
"""Create a verbose renderer for detailed output."""
|
|
616
|
-
verbose_config = RendererConfig(
|
|
617
|
-
theme="dark",
|
|
618
|
-
style="debug",
|
|
619
|
-
live=False,
|
|
620
|
-
show_delegate_tool_panels=True,
|
|
621
|
-
append_finished_snapshots=False,
|
|
622
|
-
)
|
|
623
|
-
return RichStreamRenderer(
|
|
624
|
-
console=_Console(),
|
|
625
|
-
cfg=verbose_config,
|
|
626
|
-
verbose=True,
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
630
|
-
"""Create the default renderer."""
|
|
631
|
-
if verbose:
|
|
632
|
-
return self._create_verbose_renderer()
|
|
633
|
-
else:
|
|
634
|
-
default_config = RendererConfig(show_delegate_tool_panels=True)
|
|
635
|
-
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
636
|
-
|
|
637
|
-
def _initialize_stream_metadata(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
638
|
-
"""Initialize stream metadata."""
|
|
639
|
-
return {
|
|
640
|
-
"agent_name": kwargs.get("agent_name", ""),
|
|
641
|
-
"model": kwargs.get("model"),
|
|
642
|
-
"run_id": None,
|
|
643
|
-
"input_message": "", # Will be set from kwargs if available
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
def _capture_request_id(
|
|
647
|
-
self,
|
|
648
|
-
stream_response: httpx.Response,
|
|
649
|
-
meta: dict[str, Any],
|
|
650
|
-
renderer: RichStreamRenderer,
|
|
651
|
-
) -> None:
|
|
652
|
-
"""Capture request ID from response headers."""
|
|
653
|
-
req_id = stream_response.headers.get(
|
|
654
|
-
"x-request-id"
|
|
655
|
-
) or stream_response.headers.get("x-run-id")
|
|
656
|
-
if req_id:
|
|
657
|
-
meta["run_id"] = req_id
|
|
658
|
-
renderer.on_start(meta)
|
|
659
|
-
|
|
660
|
-
def _should_start_timer(self, ev: dict[str, Any]) -> bool:
|
|
661
|
-
"""Check if timer should be started for this event."""
|
|
662
|
-
return "content" in ev or "status" in ev or ev.get("metadata")
|
|
663
|
-
|
|
664
|
-
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
665
|
-
"""Handle content events."""
|
|
666
|
-
content = ev.get("content", "")
|
|
667
|
-
if not content.startswith("Artifact received:"):
|
|
668
|
-
return content
|
|
669
|
-
return final_text
|
|
670
|
-
|
|
671
|
-
def _handle_usage_event(
|
|
672
|
-
self, ev: dict[str, Any], stats_usage: dict[str, Any]
|
|
673
|
-
) -> None:
|
|
674
|
-
"""Handle usage events."""
|
|
675
|
-
stats_usage.update(ev.get("usage") or {})
|
|
676
|
-
|
|
677
|
-
def _handle_run_info_event(
|
|
678
|
-
self, ev: dict[str, Any], meta: dict[str, Any], renderer: RichStreamRenderer
|
|
679
|
-
) -> None:
|
|
680
|
-
"""Handle run info events."""
|
|
681
|
-
if ev.get("model"):
|
|
682
|
-
meta["model"] = ev["model"]
|
|
683
|
-
renderer.on_start(meta)
|
|
684
|
-
if ev.get("run_id"):
|
|
685
|
-
meta["run_id"] = ev["run_id"]
|
|
686
|
-
renderer.on_start(meta)
|
|
687
|
-
|
|
688
|
-
def _process_single_event(
|
|
689
|
-
self,
|
|
690
|
-
event: dict[str, Any],
|
|
691
|
-
renderer: RichStreamRenderer,
|
|
692
|
-
final_text: str,
|
|
693
|
-
stats_usage: dict[str, Any],
|
|
694
|
-
meta: dict[str, Any],
|
|
695
|
-
) -> tuple[str, dict[str, Any]]:
|
|
696
|
-
"""Process a single streaming event."""
|
|
697
|
-
try:
|
|
698
|
-
ev = json.loads(event["data"])
|
|
699
|
-
except json.JSONDecodeError:
|
|
700
|
-
logger.debug("Non-JSON SSE fragment skipped")
|
|
701
|
-
return final_text, stats_usage
|
|
702
|
-
|
|
703
|
-
kind = (ev.get("metadata") or {}).get("kind")
|
|
704
|
-
renderer.on_event(ev)
|
|
705
|
-
|
|
706
|
-
# Skip artifacts from content accumulation
|
|
707
|
-
if kind == "artifact":
|
|
708
|
-
return final_text, stats_usage
|
|
709
|
-
|
|
710
|
-
# Handle different event types
|
|
711
|
-
if kind == "final_response" and ev.get("content"):
|
|
712
|
-
final_text = ev.get("content", "")
|
|
713
|
-
elif ev.get("content"):
|
|
714
|
-
final_text = self._handle_content_event(ev, final_text)
|
|
715
|
-
elif kind == "usage":
|
|
716
|
-
self._handle_usage_event(ev, stats_usage)
|
|
717
|
-
elif kind == "run_info":
|
|
718
|
-
self._handle_run_info_event(ev, meta, renderer)
|
|
719
|
-
|
|
720
|
-
return final_text, stats_usage
|
|
721
|
-
|
|
722
|
-
def _process_stream_events(
|
|
723
|
-
self,
|
|
724
|
-
stream_response: httpx.Response,
|
|
725
|
-
renderer: RichStreamRenderer,
|
|
726
|
-
timeout_seconds: float,
|
|
727
|
-
agent_name: str | None,
|
|
728
|
-
kwargs: dict[str, Any],
|
|
729
|
-
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
730
|
-
"""Process streaming events and accumulate response."""
|
|
731
|
-
final_text = ""
|
|
732
|
-
stats_usage = {}
|
|
733
|
-
started_monotonic = None
|
|
734
|
-
finished_monotonic = None
|
|
735
|
-
|
|
736
|
-
meta = self._initialize_stream_metadata(kwargs)
|
|
737
|
-
self._capture_request_id(stream_response, meta, renderer)
|
|
738
|
-
|
|
739
|
-
for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
740
|
-
# Start timer at first meaningful event
|
|
741
|
-
if started_monotonic is None:
|
|
742
|
-
try:
|
|
743
|
-
ev = json.loads(event["data"])
|
|
744
|
-
if self._should_start_timer(ev):
|
|
745
|
-
started_monotonic = monotonic()
|
|
746
|
-
except json.JSONDecodeError:
|
|
747
|
-
pass
|
|
748
|
-
|
|
749
|
-
final_text, stats_usage = self._process_single_event(
|
|
750
|
-
event, renderer, final_text, stats_usage, meta
|
|
751
|
-
)
|
|
752
|
-
|
|
753
|
-
finished_monotonic = monotonic()
|
|
754
|
-
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
755
|
-
|
|
756
1090
|
def run_agent(
|
|
757
1091
|
self,
|
|
758
1092
|
agent_id: str,
|
|
@@ -761,10 +1095,32 @@ class AgentClient(BaseClient):
|
|
|
761
1095
|
tty: bool = False,
|
|
762
1096
|
*,
|
|
763
1097
|
renderer: RichStreamRenderer | str | None = "auto",
|
|
1098
|
+
runtime_config: dict[str, Any] | None = None,
|
|
764
1099
|
**kwargs,
|
|
765
1100
|
) -> str:
|
|
766
|
-
"""Run an agent with a message, streaming via a renderer.
|
|
767
|
-
|
|
1101
|
+
"""Run an agent with a message, streaming via a renderer.
|
|
1102
|
+
|
|
1103
|
+
Args:
|
|
1104
|
+
agent_id: The ID of the agent to run.
|
|
1105
|
+
message: The message to send to the agent.
|
|
1106
|
+
files: Optional list of files to include with the request.
|
|
1107
|
+
tty: Whether to enable TTY mode.
|
|
1108
|
+
renderer: Renderer for streaming output.
|
|
1109
|
+
runtime_config: Optional runtime configuration for tools, MCPs, and agents.
|
|
1110
|
+
Keys should be platform IDs. Example:
|
|
1111
|
+
{
|
|
1112
|
+
"tool_configs": {"tool-id": {"param": "value"}},
|
|
1113
|
+
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1114
|
+
"agent_config": {"planning": True},
|
|
1115
|
+
}
|
|
1116
|
+
**kwargs: Additional arguments to pass to the run API.
|
|
1117
|
+
|
|
1118
|
+
Returns:
|
|
1119
|
+
The agent's response as a string.
|
|
1120
|
+
"""
|
|
1121
|
+
# Include runtime_config in kwargs only when caller hasn't already provided it
|
|
1122
|
+
if runtime_config is not None and "runtime_config" not in kwargs:
|
|
1123
|
+
kwargs["runtime_config"] = runtime_config
|
|
768
1124
|
(
|
|
769
1125
|
payload,
|
|
770
1126
|
data_payload,
|
|
@@ -773,20 +1129,20 @@ class AgentClient(BaseClient):
|
|
|
773
1129
|
multipart_data,
|
|
774
1130
|
) = self._prepare_sync_request_data(message, files, tty, **kwargs)
|
|
775
1131
|
|
|
776
|
-
|
|
777
|
-
|
|
1132
|
+
render_manager = self._get_renderer_manager()
|
|
1133
|
+
verbose = kwargs.get("verbose", False)
|
|
1134
|
+
r = self._create_renderer(renderer, verbose=verbose)
|
|
1135
|
+
meta = render_manager.build_initial_metadata(agent_id, message, kwargs)
|
|
1136
|
+
render_manager.start_renderer(r, meta)
|
|
778
1137
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
r.on_start(meta)
|
|
1138
|
+
final_text = ""
|
|
1139
|
+
stats_usage: dict[str, Any] = {}
|
|
1140
|
+
started_monotonic: float | None = None
|
|
1141
|
+
finished_monotonic: float | None = None
|
|
1142
|
+
|
|
1143
|
+
timeout_seconds = compute_timeout_seconds(kwargs)
|
|
787
1144
|
|
|
788
1145
|
try:
|
|
789
|
-
# Make streaming request
|
|
790
1146
|
response = self.http_client.stream(
|
|
791
1147
|
"POST",
|
|
792
1148
|
f"/agents/{agent_id}/run",
|
|
@@ -794,13 +1150,12 @@ class AgentClient(BaseClient):
|
|
|
794
1150
|
data=data_payload,
|
|
795
1151
|
files=files_payload,
|
|
796
1152
|
headers=headers,
|
|
1153
|
+
timeout=timeout_seconds,
|
|
797
1154
|
)
|
|
798
1155
|
|
|
799
1156
|
with response as stream_response:
|
|
800
1157
|
stream_response.raise_for_status()
|
|
801
1158
|
|
|
802
|
-
# Process streaming events
|
|
803
|
-
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
804
1159
|
agent_name = kwargs.get("agent_name")
|
|
805
1160
|
|
|
806
1161
|
(
|
|
@@ -809,7 +1164,11 @@ class AgentClient(BaseClient):
|
|
|
809
1164
|
started_monotonic,
|
|
810
1165
|
finished_monotonic,
|
|
811
1166
|
) = self._process_stream_events(
|
|
812
|
-
stream_response,
|
|
1167
|
+
stream_response,
|
|
1168
|
+
r,
|
|
1169
|
+
timeout_seconds,
|
|
1170
|
+
agent_name,
|
|
1171
|
+
meta,
|
|
813
1172
|
)
|
|
814
1173
|
|
|
815
1174
|
except KeyboardInterrupt:
|
|
@@ -823,25 +1182,16 @@ class AgentClient(BaseClient):
|
|
|
823
1182
|
finally:
|
|
824
1183
|
raise
|
|
825
1184
|
finally:
|
|
826
|
-
# Ensure cleanup
|
|
827
1185
|
if multipart_data:
|
|
828
1186
|
multipart_data.close()
|
|
829
1187
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if hasattr(r, "state") and hasattr(r.state, "buffer"):
|
|
838
|
-
rendered_text = "".join(r.state.buffer)
|
|
839
|
-
else:
|
|
840
|
-
rendered_text = ""
|
|
841
|
-
|
|
842
|
-
final_payload = final_text or rendered_text or "No response content received."
|
|
843
|
-
r.on_complete(st)
|
|
844
|
-
return final_payload
|
|
1188
|
+
return self._finalize_renderer(
|
|
1189
|
+
r,
|
|
1190
|
+
final_text,
|
|
1191
|
+
stats_usage,
|
|
1192
|
+
started_monotonic,
|
|
1193
|
+
finished_monotonic,
|
|
1194
|
+
)
|
|
845
1195
|
|
|
846
1196
|
def _prepare_request_data(
|
|
847
1197
|
self,
|
|
@@ -871,9 +1221,7 @@ class AgentClient(BaseClient):
|
|
|
871
1221
|
headers = {"Accept": SSE_CONTENT_TYPE}
|
|
872
1222
|
return payload, None, None, headers
|
|
873
1223
|
|
|
874
|
-
def _create_async_client_config(
|
|
875
|
-
self, timeout: float | None, headers: dict | None
|
|
876
|
-
) -> dict:
|
|
1224
|
+
def _create_async_client_config(self, timeout: float | None, headers: dict | None) -> dict:
|
|
877
1225
|
"""Create async client configuration with proper headers and timeout."""
|
|
878
1226
|
config = self._build_async_client(timeout or self.timeout)
|
|
879
1227
|
if headers:
|
|
@@ -902,9 +1250,7 @@ class AgentClient(BaseClient):
|
|
|
902
1250
|
) as stream_response:
|
|
903
1251
|
stream_response.raise_for_status()
|
|
904
1252
|
|
|
905
|
-
async for event in aiter_sse_events(
|
|
906
|
-
stream_response, timeout_seconds, agent_name
|
|
907
|
-
):
|
|
1253
|
+
async for event in aiter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
908
1254
|
try:
|
|
909
1255
|
chunk = json.loads(event["data"])
|
|
910
1256
|
yield chunk
|
|
@@ -918,7 +1264,8 @@ class AgentClient(BaseClient):
|
|
|
918
1264
|
message: str,
|
|
919
1265
|
files: list[str | BinaryIO] | None = None,
|
|
920
1266
|
*,
|
|
921
|
-
|
|
1267
|
+
request_timeout: float | None = None,
|
|
1268
|
+
runtime_config: dict[str, Any] | None = None,
|
|
922
1269
|
**kwargs,
|
|
923
1270
|
) -> AsyncGenerator[dict, None]:
|
|
924
1271
|
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
@@ -927,7 +1274,14 @@ class AgentClient(BaseClient):
|
|
|
927
1274
|
agent_id: ID of the agent to run
|
|
928
1275
|
message: Message to send to the agent
|
|
929
1276
|
files: Optional list of files to include
|
|
930
|
-
|
|
1277
|
+
request_timeout: Optional request timeout in seconds (defaults to client timeout)
|
|
1278
|
+
runtime_config: Optional runtime configuration for tools, MCPs, and agents.
|
|
1279
|
+
Keys should be platform IDs. Example:
|
|
1280
|
+
{
|
|
1281
|
+
"tool_configs": {"tool-id": {"param": "value"}},
|
|
1282
|
+
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1283
|
+
"agent_config": {"planning": True},
|
|
1284
|
+
}
|
|
931
1285
|
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
932
1286
|
|
|
933
1287
|
Yields:
|
|
@@ -938,20 +1292,25 @@ class AgentClient(BaseClient):
|
|
|
938
1292
|
httpx.TimeoutException: When general timeout occurs
|
|
939
1293
|
Exception: For other unexpected errors
|
|
940
1294
|
"""
|
|
1295
|
+
# Include runtime_config in kwargs only when caller hasn't already provided it
|
|
1296
|
+
if runtime_config is not None and "runtime_config" not in kwargs:
|
|
1297
|
+
kwargs["runtime_config"] = runtime_config
|
|
1298
|
+
# Derive timeout values for request/control flow
|
|
1299
|
+
legacy_timeout = kwargs.get("timeout")
|
|
1300
|
+
http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
|
|
1301
|
+
http_timeout = http_timeout_override or self.timeout
|
|
1302
|
+
|
|
941
1303
|
# Prepare request data
|
|
942
|
-
payload, data_payload, files_payload, headers = self._prepare_request_data(
|
|
943
|
-
message, files, **kwargs
|
|
944
|
-
)
|
|
1304
|
+
payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
|
|
945
1305
|
|
|
946
1306
|
# Create async client configuration
|
|
947
|
-
async_client_config = self._create_async_client_config(
|
|
1307
|
+
async_client_config = self._create_async_client_config(http_timeout_override, headers)
|
|
948
1308
|
|
|
949
1309
|
# Get execution timeout for streaming control
|
|
950
1310
|
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
951
1311
|
agent_name = kwargs.get("agent_name")
|
|
952
1312
|
|
|
953
|
-
|
|
954
|
-
# Create async client and stream response
|
|
1313
|
+
async def _chunk_stream() -> AsyncGenerator[dict, None]:
|
|
955
1314
|
async with httpx.AsyncClient(**async_client_config) as async_client:
|
|
956
1315
|
async for chunk in self._stream_agent_response(
|
|
957
1316
|
async_client,
|
|
@@ -965,7 +1324,14 @@ class AgentClient(BaseClient):
|
|
|
965
1324
|
):
|
|
966
1325
|
yield chunk
|
|
967
1326
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1327
|
+
async with _async_timeout_guard(http_timeout):
|
|
1328
|
+
async for chunk in _chunk_stream():
|
|
1329
|
+
yield chunk
|
|
1330
|
+
|
|
1331
|
+
@property
|
|
1332
|
+
def runs(self) -> "AgentRunsClient":
|
|
1333
|
+
"""Get the agent runs client."""
|
|
1334
|
+
if self._runs_client is None:
|
|
1335
|
+
shared_config = build_shared_config(self)
|
|
1336
|
+
self._runs_client = AgentRunsClient(**shared_config)
|
|
1337
|
+
return self._runs_client
|