glaip-sdk 0.6.12__py3-none-any.whl → 0.6.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
  3. glaip_sdk-0.6.14.dist-info/RECORD +12 -0
  4. {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
  5. glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
  6. glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
  7. glaip_sdk/agents/__init__.py +0 -27
  8. glaip_sdk/agents/base.py +0 -1191
  9. glaip_sdk/cli/__init__.py +0 -9
  10. glaip_sdk/cli/account_store.py +0 -540
  11. glaip_sdk/cli/agent_config.py +0 -78
  12. glaip_sdk/cli/auth.py +0 -699
  13. glaip_sdk/cli/commands/__init__.py +0 -5
  14. glaip_sdk/cli/commands/accounts.py +0 -746
  15. glaip_sdk/cli/commands/agents.py +0 -1509
  16. glaip_sdk/cli/commands/common_config.py +0 -101
  17. glaip_sdk/cli/commands/configure.py +0 -896
  18. glaip_sdk/cli/commands/mcps.py +0 -1356
  19. glaip_sdk/cli/commands/models.py +0 -69
  20. glaip_sdk/cli/commands/tools.py +0 -576
  21. glaip_sdk/cli/commands/transcripts.py +0 -755
  22. glaip_sdk/cli/commands/update.py +0 -61
  23. glaip_sdk/cli/config.py +0 -95
  24. glaip_sdk/cli/constants.py +0 -38
  25. glaip_sdk/cli/context.py +0 -150
  26. glaip_sdk/cli/core/__init__.py +0 -79
  27. glaip_sdk/cli/core/context.py +0 -124
  28. glaip_sdk/cli/core/output.py +0 -846
  29. glaip_sdk/cli/core/prompting.py +0 -649
  30. glaip_sdk/cli/core/rendering.py +0 -187
  31. glaip_sdk/cli/display.py +0 -355
  32. glaip_sdk/cli/hints.py +0 -57
  33. glaip_sdk/cli/io.py +0 -112
  34. glaip_sdk/cli/main.py +0 -604
  35. glaip_sdk/cli/masking.py +0 -136
  36. glaip_sdk/cli/mcp_validators.py +0 -287
  37. glaip_sdk/cli/pager.py +0 -266
  38. glaip_sdk/cli/parsers/__init__.py +0 -7
  39. glaip_sdk/cli/parsers/json_input.py +0 -177
  40. glaip_sdk/cli/resolution.py +0 -67
  41. glaip_sdk/cli/rich_helpers.py +0 -27
  42. glaip_sdk/cli/slash/__init__.py +0 -15
  43. glaip_sdk/cli/slash/accounts_controller.py +0 -578
  44. glaip_sdk/cli/slash/accounts_shared.py +0 -75
  45. glaip_sdk/cli/slash/agent_session.py +0 -285
  46. glaip_sdk/cli/slash/prompt.py +0 -256
  47. glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
  48. glaip_sdk/cli/slash/session.py +0 -1708
  49. glaip_sdk/cli/slash/tui/__init__.py +0 -9
  50. glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
  51. glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
  52. glaip_sdk/cli/slash/tui/loading.py +0 -58
  53. glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
  54. glaip_sdk/cli/transcript/__init__.py +0 -31
  55. glaip_sdk/cli/transcript/cache.py +0 -536
  56. glaip_sdk/cli/transcript/capture.py +0 -329
  57. glaip_sdk/cli/transcript/export.py +0 -38
  58. glaip_sdk/cli/transcript/history.py +0 -815
  59. glaip_sdk/cli/transcript/launcher.py +0 -77
  60. glaip_sdk/cli/transcript/viewer.py +0 -374
  61. glaip_sdk/cli/update_notifier.py +0 -290
  62. glaip_sdk/cli/utils.py +0 -263
  63. glaip_sdk/cli/validators.py +0 -238
  64. glaip_sdk/client/__init__.py +0 -11
  65. glaip_sdk/client/_agent_payloads.py +0 -520
  66. glaip_sdk/client/agent_runs.py +0 -147
  67. glaip_sdk/client/agents.py +0 -1335
  68. glaip_sdk/client/base.py +0 -502
  69. glaip_sdk/client/main.py +0 -249
  70. glaip_sdk/client/mcps.py +0 -370
  71. glaip_sdk/client/run_rendering.py +0 -700
  72. glaip_sdk/client/shared.py +0 -21
  73. glaip_sdk/client/tools.py +0 -661
  74. glaip_sdk/client/validators.py +0 -198
  75. glaip_sdk/config/constants.py +0 -52
  76. glaip_sdk/mcps/__init__.py +0 -21
  77. glaip_sdk/mcps/base.py +0 -345
  78. glaip_sdk/models/__init__.py +0 -90
  79. glaip_sdk/models/agent.py +0 -47
  80. glaip_sdk/models/agent_runs.py +0 -116
  81. glaip_sdk/models/common.py +0 -42
  82. glaip_sdk/models/mcp.py +0 -33
  83. glaip_sdk/models/tool.py +0 -33
  84. glaip_sdk/payload_schemas/__init__.py +0 -7
  85. glaip_sdk/payload_schemas/agent.py +0 -85
  86. glaip_sdk/registry/__init__.py +0 -55
  87. glaip_sdk/registry/agent.py +0 -164
  88. glaip_sdk/registry/base.py +0 -139
  89. glaip_sdk/registry/mcp.py +0 -253
  90. glaip_sdk/registry/tool.py +0 -232
  91. glaip_sdk/runner/__init__.py +0 -59
  92. glaip_sdk/runner/base.py +0 -84
  93. glaip_sdk/runner/deps.py +0 -115
  94. glaip_sdk/runner/langgraph.py +0 -782
  95. glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
  99. glaip_sdk/runner/tool_adapter/__init__.py +0 -18
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
  102. glaip_sdk/tools/__init__.py +0 -22
  103. glaip_sdk/tools/base.py +0 -435
  104. glaip_sdk/utils/__init__.py +0 -86
  105. glaip_sdk/utils/a2a/__init__.py +0 -34
  106. glaip_sdk/utils/a2a/event_processor.py +0 -188
  107. glaip_sdk/utils/agent_config.py +0 -194
  108. glaip_sdk/utils/bundler.py +0 -267
  109. glaip_sdk/utils/client.py +0 -111
  110. glaip_sdk/utils/client_utils.py +0 -486
  111. glaip_sdk/utils/datetime_helpers.py +0 -58
  112. glaip_sdk/utils/discovery.py +0 -78
  113. glaip_sdk/utils/display.py +0 -135
  114. glaip_sdk/utils/export.py +0 -143
  115. glaip_sdk/utils/general.py +0 -61
  116. glaip_sdk/utils/import_export.py +0 -168
  117. glaip_sdk/utils/import_resolver.py +0 -492
  118. glaip_sdk/utils/instructions.py +0 -101
  119. glaip_sdk/utils/rendering/__init__.py +0 -115
  120. glaip_sdk/utils/rendering/formatting.py +0 -264
  121. glaip_sdk/utils/rendering/layout/__init__.py +0 -64
  122. glaip_sdk/utils/rendering/layout/panels.py +0 -156
  123. glaip_sdk/utils/rendering/layout/progress.py +0 -202
  124. glaip_sdk/utils/rendering/layout/summary.py +0 -74
  125. glaip_sdk/utils/rendering/layout/transcript.py +0 -606
  126. glaip_sdk/utils/rendering/models.py +0 -85
  127. glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
  128. glaip_sdk/utils/rendering/renderer/base.py +0 -1024
  129. glaip_sdk/utils/rendering/renderer/config.py +0 -27
  130. glaip_sdk/utils/rendering/renderer/console.py +0 -55
  131. glaip_sdk/utils/rendering/renderer/debug.py +0 -178
  132. glaip_sdk/utils/rendering/renderer/factory.py +0 -138
  133. glaip_sdk/utils/rendering/renderer/stream.py +0 -202
  134. glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
  135. glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
  136. glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
  137. glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
  138. glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
  139. glaip_sdk/utils/rendering/state.py +0 -204
  140. glaip_sdk/utils/rendering/step_tree_state.py +0 -100
  141. glaip_sdk/utils/rendering/steps/__init__.py +0 -34
  142. glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
  143. glaip_sdk/utils/rendering/steps/format.py +0 -176
  144. glaip_sdk/utils/rendering/steps/manager.py +0 -387
  145. glaip_sdk/utils/rendering/timing.py +0 -36
  146. glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
  147. glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
  148. glaip_sdk/utils/resource_refs.py +0 -195
  149. glaip_sdk/utils/run_renderer.py +0 -41
  150. glaip_sdk/utils/runtime_config.py +0 -425
  151. glaip_sdk/utils/serialization.py +0 -424
  152. glaip_sdk/utils/sync.py +0 -142
  153. glaip_sdk/utils/tool_detection.py +0 -33
  154. glaip_sdk/utils/validation.py +0 -264
  155. glaip_sdk-0.6.12.dist-info/RECORD +0 -159
  156. glaip_sdk-0.6.12.dist-info/entry_points.txt +0 -3
@@ -1,1335 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Agent client for AIP SDK.
3
-
4
- Authors:
5
- Raymond Christopher (raymond.christopher@gdplabs.id)
6
- Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
7
- """
8
-
9
- import asyncio
10
- import json
11
- import logging
12
- from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
13
- from contextlib import asynccontextmanager
14
- from os import PathLike
15
- from pathlib import Path
16
- from typing import Any, BinaryIO
17
-
18
- import httpx
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
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
35
- from glaip_sdk.config.constants import (
36
- AGENT_CONFIG_FIELDS,
37
- DEFAULT_AGENT_FRAMEWORK,
38
- DEFAULT_AGENT_RUN_TIMEOUT,
39
- DEFAULT_AGENT_TYPE,
40
- DEFAULT_AGENT_VERSION,
41
- DEFAULT_MODEL,
42
- )
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
47
- from glaip_sdk.utils.client_utils import (
48
- aiter_sse_events,
49
- create_model_instances,
50
- find_by_name,
51
- prepare_multipart_data,
52
- )
53
- from glaip_sdk.utils.import_export import (
54
- convert_export_to_import_format,
55
- merge_import_with_cli_args,
56
- )
57
- from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
58
- from glaip_sdk.utils.resource_refs import is_uuid
59
- from glaip_sdk.utils.serialization import load_resource_from_file
60
- from glaip_sdk.utils.validation import validate_agent_instruction
61
-
62
- # API endpoints
63
- AGENTS_ENDPOINT = "/agents/"
64
-
65
- # SSE content type
66
- SSE_CONTENT_TYPE = "text/event-stream"
67
-
68
- # Set up module-level logger
69
- logger = logging.getLogger("glaip_sdk.agents")
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
-
246
-
247
- class AgentClient(BaseClient):
248
- """Client for agent operations."""
249
-
250
- def __init__(
251
- self,
252
- *,
253
- parent_client: BaseClient | None = None,
254
- **kwargs: Any,
255
- ) -> None:
256
- """Initialize the agent client.
257
-
258
- Args:
259
- parent_client: Parent client to adopt session/config from
260
- **kwargs: Additional arguments for standalone initialization
261
- """
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
267
-
268
- def list_agents(
269
- self,
270
- query: AgentListParams | None = None,
271
- **kwargs: Any,
272
- ) -> AgentListResult:
273
- """List agents with optional filtering and pagination support.
274
-
275
- Args:
276
- query: Query parameters for filtering agents. If None, uses kwargs to create query.
277
- **kwargs: Individual filter parameters for backward compatibility.
278
- """
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
- )
309
-
310
- def sync_langflow_agents(
311
- self,
312
- base_url: str | None = None,
313
- api_key: str | None = None,
314
- ) -> dict[str, Any]:
315
- """Sync LangFlow agents by fetching flows from the LangFlow server.
316
-
317
- This method synchronizes agents with LangFlow flows. It fetches all flows
318
- from the configured LangFlow server and creates/updates corresponding agents.
319
-
320
- Args:
321
- base_url: Custom LangFlow server base URL. If not provided, uses LANGFLOW_BASE_URL env var.
322
- api_key: Custom LangFlow API key. If not provided, uses LANGFLOW_API_KEY env var.
323
-
324
- Returns:
325
- Response containing sync results and statistics
326
-
327
- Raises:
328
- ValueError: If LangFlow server configuration is missing
329
- """
330
- payload = {}
331
- if base_url is not None:
332
- payload["base_url"] = base_url
333
- if api_key is not None:
334
- payload["api_key"] = api_key
335
-
336
- return self._request("POST", "/agents/langflow/sync", json=payload)
337
-
338
- def get_agent_by_id(self, agent_id: str) -> Agent:
339
- """Get agent by ID."""
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
353
-
354
- if isinstance(data, str):
355
- # Some backends may respond with plain text for missing agents.
356
- message = data.strip() or f"Agent '{agent_id}' not found"
357
- raise NotFoundError(message, status_code=404)
358
-
359
- if not isinstance(data, dict):
360
- raise NotFoundError(
361
- f"Agent '{agent_id}' not found (unexpected response type)",
362
- status_code=404,
363
- )
364
-
365
- response = AgentResponse(**data)
366
- return Agent.from_response(response, client=self)
367
-
368
- def find_agents(self, name: str | None = None) -> list[Agent]:
369
- """Find agents by name."""
370
- result = self.list_agents(name=name)
371
- agents = list(result)
372
- if name is None:
373
- return agents
374
- return find_by_name(agents, name, case_sensitive=False)
375
-
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
390
-
391
- def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
392
- """Create or return a renderer instance.
393
-
394
- Args:
395
- renderer: Renderer instance, string identifier, or None.
396
- **kwargs: Additional keyword arguments (e.g., verbose).
397
-
398
- Returns:
399
- RichStreamRenderer instance.
400
- """
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)
406
-
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.
416
-
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.
423
-
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
- )
435
-
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.
445
-
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).
452
-
453
- Returns:
454
- Final text string.
455
- """
456
- from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
457
-
458
- manager = self._get_renderer_manager()
459
- return finalize_render_manager(
460
- manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
461
- )
462
-
463
- def _get_tool_client(self) -> ToolClient:
464
- """Get or create the tool client instance.
465
-
466
- Returns:
467
- ToolClient instance.
468
- """
469
- if self._tool_client is None:
470
- self._tool_client = ToolClient(parent_client=self)
471
- return self._tool_client
472
-
473
- def _get_mcp_client(self) -> MCPClient:
474
- """Get or create the MCP client instance.
475
-
476
- Returns:
477
- MCPClient instance.
478
- """
479
- if self._mcp_client is None:
480
- self._mcp_client = MCPClient(parent_client=self)
481
- return self._mcp_client
482
-
483
- def _normalise_reference_entry(
484
- self,
485
- entry: Any,
486
- fallback_iter: Iterator[Any] | None,
487
- ) -> tuple[str | None, str | None]:
488
- """Normalize a reference entry to extract ID and name.
489
-
490
- Args:
491
- entry: Reference entry (string, dict, or other).
492
- fallback_iter: Optional iterator for fallback values.
493
-
494
- Returns:
495
- Tuple of (entry_id, entry_name).
496
- """
497
- entry_id: str | None = None
498
- entry_name: str | None = None
499
-
500
- if isinstance(entry, str):
501
- if is_uuid(entry):
502
- entry_id = entry
503
- else:
504
- entry_name = entry
505
- elif isinstance(entry, dict):
506
- entry_id = entry.get("id")
507
- entry_name = entry.get("name")
508
- else:
509
- entry_name = str(entry)
510
-
511
- if entry_name or fallback_iter is None:
512
- return entry_id, entry_name
513
-
514
- try:
515
- ref = next(fallback_iter)
516
- except StopIteration:
517
- ref = None
518
- if isinstance(ref, dict):
519
- entry_name = ref.get("name") or entry_name
520
-
521
- return entry_id, entry_name
522
-
523
- def _resolve_resource_ids(
524
- self,
525
- items: list[Any] | None,
526
- references: list[Any] | None,
527
- *,
528
- fetch_by_id: Callable[[str], Any],
529
- find_by_name: Callable[[str], list[Any]],
530
- label: str,
531
- plural_label: str | None = None,
532
- ) -> list[str] | None:
533
- """Resolve a list of resource references to IDs.
534
-
535
- Args:
536
- items: List of resource references to resolve.
537
- references: Optional list of reference objects for fallback.
538
- fetch_by_id: Function to fetch resource by ID.
539
- find_by_name: Function to find resources by name.
540
- label: Singular label for error messages.
541
- plural_label: Plural label for error messages.
542
-
543
- Returns:
544
- List of resolved resource IDs, or None if items is empty.
545
- """
546
- if not items:
547
- return None
548
-
549
- if references is None:
550
- return [self._coerce_reference_value(entry) for entry in items]
551
-
552
- singular = label
553
- plural = plural_label or f"{label}s"
554
- fallback_iter = iter(references or [])
555
-
556
- return [
557
- self._resolve_single_resource(
558
- entry,
559
- fallback_iter,
560
- fetch_by_id,
561
- find_by_name,
562
- singular,
563
- plural,
564
- )
565
- for entry in items
566
- ]
567
-
568
- def _resolve_single_resource(
569
- self,
570
- entry: Any,
571
- fallback_iter: Iterator[Any] | None,
572
- fetch_by_id: Callable[[str], Any],
573
- find_by_name: Callable[[str], list[Any]],
574
- singular: str,
575
- plural: str,
576
- ) -> str:
577
- """Resolve a single resource reference to an ID.
578
-
579
- Args:
580
- entry: Resource reference to resolve.
581
- fallback_iter: Optional iterator for fallback values.
582
- fetch_by_id: Function to fetch resource by ID.
583
- find_by_name: Function to find resources by name.
584
- singular: Singular label for error messages.
585
- plural: Plural label for error messages.
586
-
587
- Returns:
588
- Resolved resource ID string.
589
-
590
- Raises:
591
- ValueError: If the resource cannot be resolved.
592
- """
593
- entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
594
-
595
- validated_id = self._validate_resource_id(fetch_by_id, entry_id)
596
- if validated_id:
597
- return validated_id
598
- if entry_id and entry_name is None:
599
- return entry_id
600
-
601
- if entry_name:
602
- resolved, success = self._resolve_resource_by_name(find_by_name, entry_name, singular, plural)
603
- return resolved if success else entry_name
604
-
605
- raise ValueError(f"{singular} references must include a valid ID or name.")
606
-
607
- @staticmethod
608
- def _coerce_reference_value(entry: Any) -> str:
609
- """Coerce a reference entry to a string value.
610
-
611
- Args:
612
- entry: Reference entry (dict, string, or other).
613
-
614
- Returns:
615
- String representation of the reference.
616
- """
617
- if isinstance(entry, dict):
618
- if entry.get("id"):
619
- return str(entry["id"])
620
- if entry.get("name"):
621
- return str(entry["name"])
622
- return str(entry)
623
-
624
- @staticmethod
625
- def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
626
- """Validate a resource ID by attempting to fetch it.
627
-
628
- Args:
629
- fetch_by_id: Function to fetch resource by ID.
630
- candidate_id: Candidate ID to validate.
631
-
632
- Returns:
633
- Validated ID if found, None otherwise.
634
- """
635
- if not candidate_id:
636
- return None
637
- try:
638
- fetch_by_id(candidate_id)
639
- except Exception:
640
- return None
641
- return candidate_id
642
-
643
- @staticmethod
644
- def _resolve_resource_by_name(
645
- find_by_name: Callable[[str], list[Any]],
646
- entry_name: str,
647
- singular: str,
648
- plural: str,
649
- ) -> tuple[str, bool]:
650
- """Resolve a resource by name to an ID.
651
-
652
- Args:
653
- find_by_name: Function to find resources by name.
654
- entry_name: Name of the resource to find.
655
- singular: Singular label for error messages.
656
- plural: Plural label for error messages.
657
-
658
- Returns:
659
- Tuple of (resolved_id, success).
660
-
661
- Raises:
662
- ValueError: If resource not found or multiple matches exist.
663
- """
664
- try:
665
- matches = find_by_name(entry_name)
666
- except Exception:
667
- return entry_name, False
668
-
669
- if not matches:
670
- raise ValueError(f"{singular} '{entry_name}' not found in current workspace.")
671
- if len(matches) > 1:
672
- exact = [m for m in matches if getattr(m, "name", "").lower() == entry_name.lower()]
673
- if len(exact) == 1:
674
- matches = exact
675
- else:
676
- raise ValueError(f"Multiple {plural} named '{entry_name}'. Please disambiguate.")
677
- return str(matches[0].id), True
678
-
679
- def _resolve_tool_ids(
680
- self,
681
- tools: list[Any] | None,
682
- references: list[Any] | None = None,
683
- ) -> list[str] | None:
684
- """Resolve tool references to IDs.
685
-
686
- Args:
687
- tools: List of tool references to resolve.
688
- references: Optional list of reference objects for fallback.
689
-
690
- Returns:
691
- List of resolved tool IDs, or None if tools is empty.
692
- """
693
- tool_client = self._get_tool_client()
694
- return self._resolve_resource_ids(
695
- tools,
696
- references,
697
- fetch_by_id=tool_client.get_tool_by_id,
698
- find_by_name=tool_client.find_tools,
699
- label="Tool",
700
- plural_label="tools",
701
- )
702
-
703
- def _resolve_agent_ids(
704
- self,
705
- agents: list[Any] | None,
706
- references: list[Any] | None = None,
707
- ) -> list[str] | None:
708
- """Resolve agent references to IDs.
709
-
710
- Args:
711
- agents: List of agent references to resolve.
712
- references: Optional list of reference objects for fallback.
713
-
714
- Returns:
715
- List of resolved agent IDs, or None if agents is empty.
716
- """
717
- return self._resolve_resource_ids(
718
- agents,
719
- references,
720
- fetch_by_id=self.get_agent_by_id,
721
- find_by_name=self.find_agents,
722
- label="Agent",
723
- plural_label="agents",
724
- )
725
-
726
- def _resolve_mcp_ids(
727
- self,
728
- mcps: list[Any] | None,
729
- references: list[Any] | None = None,
730
- ) -> list[str] | None:
731
- """Resolve MCP references to IDs.
732
-
733
- Args:
734
- mcps: List of MCP references to resolve.
735
- references: Optional list of reference objects for fallback.
736
-
737
- Returns:
738
- List of resolved MCP IDs, or None if mcps is empty.
739
- """
740
- mcp_client = self._get_mcp_client()
741
- return self._resolve_resource_ids(
742
- mcps,
743
- references,
744
- fetch_by_id=mcp_client.get_mcp_by_id,
745
- find_by_name=mcp_client.find_mcps,
746
- label="MCP",
747
- plural_label="MCPs",
748
- )
749
-
750
- def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
751
- """Create an agent using a fully prepared payload mapping."""
752
- known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
753
-
754
- name = known.pop("name", None)
755
- instruction = known.pop("instruction", None)
756
- if not name or not str(name).strip():
757
- raise ValueError("Agent name cannot be empty or whitespace")
758
- if not instruction or not str(instruction).strip():
759
- raise ValueError("Agent instruction cannot be empty or whitespace")
760
-
761
- validated_instruction = validate_agent_instruction(str(instruction))
762
- _normalise_sequence_fields(known)
763
-
764
- resolved_model = known.pop("model", None) or DEFAULT_MODEL
765
- tool_refs = extras.pop("_tool_refs", None)
766
- agent_refs = extras.pop("_agent_refs", None)
767
- mcp_refs = extras.pop("_mcp_refs", None)
768
-
769
- tools_raw = known.pop("tools", None)
770
- agents_raw = known.pop("agents", None)
771
- mcps_raw = known.pop("mcps", None)
772
-
773
- resolved_tools = self._resolve_tool_ids(tools_raw, tool_refs)
774
- resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
775
- resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
776
-
777
- language_model_id = known.pop("language_model_id", None)
778
- provider = known.pop("provider", None)
779
- model_name = known.pop("model_name", None)
780
-
781
- agent_type_value = known.pop("agent_type", None)
782
- fallback_type_value = known.pop("type", None)
783
- if agent_type_value is None:
784
- agent_type_value = fallback_type_value or DEFAULT_AGENT_TYPE
785
-
786
- framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
787
- version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
788
- account_id = known.pop("account_id", None)
789
- description = known.pop("description", None)
790
- metadata = _prepare_agent_metadata(known.pop("metadata", None))
791
- tool_configs = known.pop("tool_configs", None)
792
- agent_config = known.pop("agent_config", None)
793
- timeout_value = known.pop("timeout", None)
794
- a2a_profile = known.pop("a2a_profile", None)
795
-
796
- final_extras = {**known, **extras}
797
- final_extras.setdefault("model", resolved_model)
798
-
799
- request = AgentCreateRequest(
800
- name=str(name).strip(),
801
- instruction=validated_instruction,
802
- model=resolved_model,
803
- language_model_id=language_model_id,
804
- provider=provider,
805
- model_name=model_name,
806
- agent_type=agent_type_value,
807
- framework=framework_value,
808
- version=version_value,
809
- account_id=account_id,
810
- description=description,
811
- metadata=metadata,
812
- tools=resolved_tools,
813
- agents=resolved_agents,
814
- mcps=resolved_mcps,
815
- tool_configs=tool_configs,
816
- agent_config=agent_config,
817
- timeout=timeout_value or DEFAULT_AGENT_RUN_TIMEOUT,
818
- a2a_profile=a2a_profile,
819
- extras=final_extras,
820
- )
821
-
822
- payload_dict = request.to_payload()
823
- payload_dict.setdefault("model", resolved_model)
824
-
825
- full_agent_data = self._post_then_fetch(
826
- id_key="id",
827
- post_endpoint=AGENTS_ENDPOINT,
828
- get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
829
- json=payload_dict,
830
- )
831
- response = AgentResponse(**full_agent_data)
832
- return Agent.from_response(response, client=self)
833
-
834
- def create_agent(
835
- self,
836
- name: str | None = None,
837
- instruction: str | None = None,
838
- model: str | None = None,
839
- tools: list[str | Any] | None = None,
840
- agents: list[str | Any] | None = None,
841
- timeout: int | None = None,
842
- *,
843
- file: str | PathLike[str] | None = None,
844
- mcps: list[str | Any] | None = None,
845
- tool_configs: Mapping[str, Any] | None = None,
846
- **kwargs: Any,
847
- ) -> "Agent":
848
- """Create a new agent, optionally loading configuration from a file."""
849
- base_overrides = {
850
- "name": name,
851
- "instruction": instruction,
852
- "model": model,
853
- "tools": tools,
854
- "agents": agents,
855
- "timeout": timeout,
856
- "mcps": mcps,
857
- "tool_configs": tool_configs,
858
- }
859
- overrides = _merge_override_maps(base_overrides, kwargs)
860
-
861
- if file is not None:
862
- payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
863
- if overrides.get("model") is None:
864
- payload.pop("model", None)
865
- else:
866
- payload = overrides
867
-
868
- return self._create_agent_from_payload(payload)
869
-
870
- def create_agent_from_file( # pragma: no cover - thin compatibility wrapper
871
- self,
872
- file_path: str | PathLike[str],
873
- **overrides: Any,
874
- ) -> "Agent":
875
- """Backward-compatible helper to create an agent from a configuration file."""
876
- return self.create_agent(file=file_path, **overrides)
877
-
878
- def _update_agent_from_payload(
879
- self,
880
- agent_id: str,
881
- current_agent: Agent,
882
- payload: Mapping[str, Any],
883
- ) -> "Agent":
884
- """Update an agent using a prepared payload mapping."""
885
- known, extras = _split_known_and_extra(payload, AgentUpdateRequest.__dataclass_fields__)
886
- _normalise_sequence_fields(known)
887
-
888
- tool_refs = extras.pop("_tool_refs", None)
889
- agent_refs = extras.pop("_agent_refs", None)
890
- mcp_refs = extras.pop("_mcp_refs", None)
891
-
892
- tools_value = known.pop("tools", None)
893
- agents_value = known.pop("agents", None)
894
- mcps_value = known.pop("mcps", None)
895
-
896
- if tools_value is not None:
897
- tools_value = self._resolve_tool_ids(tools_value, tool_refs)
898
- if agents_value is not None:
899
- agents_value = self._resolve_agent_ids(agents_value, agent_refs)
900
- if mcps_value is not None:
901
- mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
902
-
903
- request = AgentUpdateRequest(
904
- name=known.pop("name", None),
905
- instruction=known.pop("instruction", None),
906
- description=known.pop("description", None),
907
- model=known.pop("model", None),
908
- language_model_id=known.pop("language_model_id", None),
909
- provider=known.pop("provider", None),
910
- model_name=known.pop("model_name", None),
911
- agent_type=known.pop("agent_type", known.pop("type", None)),
912
- framework=known.pop("framework", None),
913
- version=known.pop("version", None),
914
- account_id=known.pop("account_id", None),
915
- metadata=known.pop("metadata", None),
916
- tools=tools_value,
917
- tool_configs=known.pop("tool_configs", None),
918
- agents=agents_value,
919
- mcps=mcps_value,
920
- agent_config=known.pop("agent_config", None),
921
- a2a_profile=known.pop("a2a_profile", None),
922
- extras={**known, **extras},
923
- )
924
-
925
- payload_dict = request.to_payload(current_agent)
926
-
927
- api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
928
- response = AgentResponse(**api_response)
929
- return Agent.from_response(response, client=self)
930
-
931
- def update_agent(
932
- self,
933
- agent_id: str,
934
- name: str | None = None,
935
- instruction: str | None = None,
936
- model: str | None = None,
937
- *,
938
- file: str | PathLike[str] | None = None,
939
- tools: list[str | Any] | None = None,
940
- agents: list[str | Any] | None = None,
941
- mcps: list[str | Any] | None = None,
942
- **kwargs: Any,
943
- ) -> "Agent":
944
- """Update an existing agent."""
945
- base_overrides = {
946
- "name": name,
947
- "instruction": instruction,
948
- "model": model,
949
- "tools": tools,
950
- "agents": agents,
951
- "mcps": mcps,
952
- }
953
- overrides = _merge_override_maps(base_overrides, kwargs)
954
-
955
- if file is not None:
956
- payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
957
- else:
958
- payload = overrides
959
-
960
- current_agent = self.get_agent_by_id(agent_id)
961
- return self._update_agent_from_payload(agent_id, current_agent, payload)
962
-
963
- def update_agent_from_file( # pragma: no cover - thin compatibility wrapper
964
- self,
965
- agent_id: str,
966
- file_path: str | PathLike[str],
967
- **overrides: Any,
968
- ) -> "Agent":
969
- """Backward-compatible helper to update an agent from a configuration file."""
970
- return self.update_agent(agent_id, file=file_path, **overrides)
971
-
972
- def delete_agent(self, agent_id: str) -> None:
973
- """Delete an agent."""
974
- self._request("DELETE", f"/agents/{agent_id}")
975
-
976
- def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
977
- """Create or update an agent by instance, ID, or name.
978
-
979
- Args:
980
- identifier: Agent instance, ID (UUID string), or name
981
- **kwargs: Agent configuration (instruction, description, tools, etc.)
982
-
983
- Returns:
984
- The created or updated agent.
985
-
986
- Example:
987
- >>> # By name (creates if not exists)
988
- >>> agent = client.agents.upsert_agent(
989
- ... "hello_agent",
990
- ... instruction="You are a helpful assistant.",
991
- ... description="A friendly agent",
992
- ... )
993
- >>> # By instance
994
- >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
995
- >>> # By ID
996
- >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
997
- """
998
- # Handle Agent instance
999
- if isinstance(identifier, Agent):
1000
- if identifier.id:
1001
- logger.info("Updating agent by instance: %s", identifier.name)
1002
- return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1003
- identifier = identifier.name
1004
-
1005
- # Handle string (ID or name)
1006
- if isinstance(identifier, str):
1007
- # Check if it's a UUID
1008
- if is_uuid(identifier):
1009
- logger.info("Updating agent by ID: %s", identifier)
1010
- return self.update_agent(identifier, **kwargs)
1011
-
1012
- # It's a name - find or create
1013
- return self._upsert_agent_by_name(identifier, **kwargs)
1014
-
1015
- raise ValueError(f"Invalid identifier type: {type(identifier)}")
1016
-
1017
- def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1018
- """Find agent by name and update, or create if not found."""
1019
- existing = self.find_agents(name)
1020
-
1021
- if len(existing) == 1:
1022
- logger.info("Updating existing agent: %s", name)
1023
- return self.update_agent(existing[0].id, name=name, **kwargs)
1024
-
1025
- if len(existing) > 1:
1026
- raise ValueError(f"Multiple agents found with name '{name}'")
1027
-
1028
- # Create new agent
1029
- logger.info("Creating new agent: %s", name)
1030
- return self.create_agent(name=name, **kwargs)
1031
-
1032
- def _prepare_sync_request_data(
1033
- self,
1034
- message: str,
1035
- files: list[str | BinaryIO] | None,
1036
- tty: bool,
1037
- **kwargs: Any,
1038
- ) -> tuple[dict | None, dict | None, list | None, dict, Any | None]:
1039
- """Prepare request data for synchronous agent runs with renderer support.
1040
-
1041
- Args:
1042
- message: Message to send
1043
- files: Optional files to include
1044
- tty: Whether to enable TTY mode
1045
- **kwargs: Additional request parameters
1046
-
1047
- Returns:
1048
- Tuple of (payload, data_payload, files_payload, headers, multipart_data)
1049
- """
1050
- headers = {"Accept": SSE_CONTENT_TYPE}
1051
-
1052
- if files:
1053
- # Handle multipart data for file uploads
1054
- multipart_data = prepare_multipart_data(message, files)
1055
- if "chat_history" in kwargs and kwargs["chat_history"] is not None:
1056
- multipart_data.data["chat_history"] = kwargs["chat_history"]
1057
- if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
1058
- multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
1059
-
1060
- return (
1061
- None,
1062
- multipart_data.data,
1063
- multipart_data.files,
1064
- headers,
1065
- multipart_data,
1066
- )
1067
- else:
1068
- # Simple JSON payload for text-only requests
1069
- payload = {"input": message, "stream": True, **kwargs}
1070
- if tty:
1071
- payload["tty"] = True
1072
- return payload, None, None, headers, None
1073
-
1074
- def _get_timeout_values(self, timeout: float | None, **kwargs: Any) -> tuple[float, float]:
1075
- """Get request timeout and execution timeout values.
1076
-
1077
- Args:
1078
- timeout: Request timeout (overrides instance timeout)
1079
- **kwargs: Additional parameters including execution timeout
1080
-
1081
- Returns:
1082
- Tuple of (request_timeout, execution_timeout)
1083
- """
1084
- request_timeout = timeout or self.timeout
1085
- execution_timeout = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1086
- return request_timeout, execution_timeout
1087
-
1088
- def run_agent(
1089
- self,
1090
- agent_id: str,
1091
- message: str,
1092
- files: list[str | BinaryIO] | None = None,
1093
- tty: bool = False,
1094
- *,
1095
- renderer: RichStreamRenderer | str | None = "auto",
1096
- runtime_config: dict[str, Any] | None = None,
1097
- **kwargs,
1098
- ) -> str:
1099
- """Run an agent with a message, streaming via a renderer.
1100
-
1101
- Args:
1102
- agent_id: The ID of the agent to run.
1103
- message: The message to send to the agent.
1104
- files: Optional list of files to include with the request.
1105
- tty: Whether to enable TTY mode.
1106
- renderer: Renderer for streaming output.
1107
- runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1108
- Keys should be platform IDs. Example:
1109
- {
1110
- "tool_configs": {"tool-id": {"param": "value"}},
1111
- "mcp_configs": {"mcp-id": {"setting": "on"}},
1112
- "agent_config": {"planning": True},
1113
- }
1114
- **kwargs: Additional arguments to pass to the run API.
1115
-
1116
- Returns:
1117
- The agent's response as a string.
1118
- """
1119
- # Include runtime_config in kwargs only when caller hasn't already provided it
1120
- if runtime_config is not None and "runtime_config" not in kwargs:
1121
- kwargs["runtime_config"] = runtime_config
1122
- (
1123
- payload,
1124
- data_payload,
1125
- files_payload,
1126
- headers,
1127
- multipart_data,
1128
- ) = self._prepare_sync_request_data(message, files, tty, **kwargs)
1129
-
1130
- render_manager = self._get_renderer_manager()
1131
- verbose = kwargs.get("verbose", False)
1132
- r = self._create_renderer(renderer, verbose=verbose)
1133
- meta = render_manager.build_initial_metadata(agent_id, message, kwargs)
1134
- render_manager.start_renderer(r, meta)
1135
-
1136
- final_text = ""
1137
- stats_usage: dict[str, Any] = {}
1138
- started_monotonic: float | None = None
1139
- finished_monotonic: float | None = None
1140
-
1141
- timeout_seconds = compute_timeout_seconds(kwargs)
1142
-
1143
- try:
1144
- response = self.http_client.stream(
1145
- "POST",
1146
- f"/agents/{agent_id}/run",
1147
- json=payload,
1148
- data=data_payload,
1149
- files=files_payload,
1150
- headers=headers,
1151
- timeout=timeout_seconds,
1152
- )
1153
-
1154
- with response as stream_response:
1155
- stream_response.raise_for_status()
1156
-
1157
- agent_name = kwargs.get("agent_name")
1158
-
1159
- (
1160
- final_text,
1161
- stats_usage,
1162
- started_monotonic,
1163
- finished_monotonic,
1164
- ) = self._process_stream_events(
1165
- stream_response,
1166
- r,
1167
- timeout_seconds,
1168
- agent_name,
1169
- meta,
1170
- )
1171
-
1172
- except KeyboardInterrupt:
1173
- try:
1174
- r.close()
1175
- finally:
1176
- raise
1177
- except Exception:
1178
- try:
1179
- r.close()
1180
- finally:
1181
- raise
1182
- finally:
1183
- if multipart_data:
1184
- multipart_data.close()
1185
-
1186
- return self._finalize_renderer(
1187
- r,
1188
- final_text,
1189
- stats_usage,
1190
- started_monotonic,
1191
- finished_monotonic,
1192
- )
1193
-
1194
- def _prepare_request_data(
1195
- self,
1196
- message: str,
1197
- files: list[str | BinaryIO] | None,
1198
- **kwargs,
1199
- ) -> tuple[dict | None, dict | None, dict | None, dict | None]:
1200
- """Prepare request data for async agent runs.
1201
-
1202
- Returns:
1203
- Tuple of (payload, data_payload, files_payload, headers)
1204
- """
1205
- if files:
1206
- # Handle multipart data for file uploads
1207
- multipart_data = prepare_multipart_data(message, files)
1208
- # Inject optional multipart extras expected by backend
1209
- if "chat_history" in kwargs and kwargs["chat_history"] is not None:
1210
- multipart_data.data["chat_history"] = kwargs["chat_history"]
1211
- if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
1212
- multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
1213
-
1214
- headers = {"Accept": SSE_CONTENT_TYPE}
1215
- return None, multipart_data.data, multipart_data.files, headers
1216
- else:
1217
- # Simple JSON payload for text-only requests
1218
- payload = {"input": message, "stream": True, **kwargs}
1219
- headers = {"Accept": SSE_CONTENT_TYPE}
1220
- return payload, None, None, headers
1221
-
1222
- def _create_async_client_config(self, timeout: float | None, headers: dict | None) -> dict:
1223
- """Create async client configuration with proper headers and timeout."""
1224
- config = self._build_async_client(timeout or self.timeout)
1225
- if headers:
1226
- config["headers"] = {**config["headers"], **headers}
1227
- return config
1228
-
1229
- async def _stream_agent_response(
1230
- self,
1231
- async_client: httpx.AsyncClient,
1232
- agent_id: str,
1233
- payload: dict | None,
1234
- data_payload: dict | None,
1235
- files_payload: dict | None,
1236
- headers: dict | None,
1237
- timeout_seconds: float,
1238
- agent_name: str | None,
1239
- ) -> AsyncGenerator[dict, None]:
1240
- """Stream the agent response and yield parsed JSON chunks."""
1241
- async with async_client.stream(
1242
- "POST",
1243
- f"/agents/{agent_id}/run",
1244
- json=payload,
1245
- data=data_payload,
1246
- files=files_payload,
1247
- headers=headers,
1248
- ) as stream_response:
1249
- stream_response.raise_for_status()
1250
-
1251
- async for event in aiter_sse_events(stream_response, timeout_seconds, agent_name):
1252
- try:
1253
- chunk = json.loads(event["data"])
1254
- yield chunk
1255
- except json.JSONDecodeError:
1256
- logger.debug("Non-JSON SSE fragment skipped")
1257
- continue
1258
-
1259
- async def arun_agent(
1260
- self,
1261
- agent_id: str,
1262
- message: str,
1263
- files: list[str | BinaryIO] | None = None,
1264
- *,
1265
- request_timeout: float | None = None,
1266
- runtime_config: dict[str, Any] | None = None,
1267
- **kwargs,
1268
- ) -> AsyncGenerator[dict, None]:
1269
- """Async run an agent with a message, yielding streaming JSON chunks.
1270
-
1271
- Args:
1272
- agent_id: ID of the agent to run
1273
- message: Message to send to the agent
1274
- files: Optional list of files to include
1275
- request_timeout: Optional request timeout in seconds (defaults to client timeout)
1276
- runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1277
- Keys should be platform IDs. Example:
1278
- {
1279
- "tool_configs": {"tool-id": {"param": "value"}},
1280
- "mcp_configs": {"mcp-id": {"setting": "on"}},
1281
- "agent_config": {"planning": True},
1282
- }
1283
- **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1284
-
1285
- Yields:
1286
- Dictionary containing parsed JSON chunks from the streaming response
1287
-
1288
- Raises:
1289
- AgentTimeoutError: When agent execution times out
1290
- httpx.TimeoutException: When general timeout occurs
1291
- Exception: For other unexpected errors
1292
- """
1293
- # Include runtime_config in kwargs only when caller hasn't already provided it
1294
- if runtime_config is not None and "runtime_config" not in kwargs:
1295
- kwargs["runtime_config"] = runtime_config
1296
- # Derive timeout values for request/control flow
1297
- legacy_timeout = kwargs.get("timeout")
1298
- http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1299
- http_timeout = http_timeout_override or self.timeout
1300
-
1301
- # Prepare request data
1302
- payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1303
-
1304
- # Create async client configuration
1305
- async_client_config = self._create_async_client_config(http_timeout_override, headers)
1306
-
1307
- # Get execution timeout for streaming control
1308
- timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1309
- agent_name = kwargs.get("agent_name")
1310
-
1311
- async def _chunk_stream() -> AsyncGenerator[dict, None]:
1312
- async with httpx.AsyncClient(**async_client_config) as async_client:
1313
- async for chunk in self._stream_agent_response(
1314
- async_client,
1315
- agent_id,
1316
- payload,
1317
- data_payload,
1318
- files_payload,
1319
- headers,
1320
- timeout_seconds,
1321
- agent_name,
1322
- ):
1323
- yield chunk
1324
-
1325
- async with _async_timeout_guard(http_timeout):
1326
- async for chunk in _chunk_stream():
1327
- yield chunk
1328
-
1329
- @property
1330
- def runs(self) -> "AgentRunsClient":
1331
- """Get the agent runs client."""
1332
- if self._runs_client is None:
1333
- shared_config = build_shared_config(self)
1334
- self._runs_client = AgentRunsClient(**shared_config)
1335
- return self._runs_client