glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5__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 +5 -5
- glaip_sdk/branding.py +18 -17
- glaip_sdk/cli/__init__.py +1 -1
- glaip_sdk/cli/agent_config.py +82 -0
- glaip_sdk/cli/commands/__init__.py +3 -3
- glaip_sdk/cli/commands/agents.py +570 -673
- glaip_sdk/cli/commands/configure.py +2 -2
- glaip_sdk/cli/commands/mcps.py +148 -143
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +250 -179
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +14 -18
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +305 -264
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +631 -191
- glaip_sdk/client/base.py +66 -4
- glaip_sdk/client/main.py +226 -0
- glaip_sdk/client/mcps.py +143 -18
- glaip_sdk/client/tools.py +146 -11
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +42 -2
- glaip_sdk/rich_components.py +29 -0
- glaip_sdk/utils/__init__.py +18 -171
- glaip_sdk/utils/agent_config.py +181 -0
- glaip_sdk/utils/client_utils.py +159 -79
- glaip_sdk/utils/display.py +100 -0
- glaip_sdk/utils/general.py +94 -0
- glaip_sdk/utils/import_export.py +140 -0
- glaip_sdk/utils/rendering/formatting.py +6 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
- glaip_sdk/utils/rendering/renderer/base.py +340 -247
- glaip_sdk/utils/rendering/renderer/debug.py +3 -2
- glaip_sdk/utils/rendering/renderer/panels.py +11 -10
- glaip_sdk/utils/rendering/steps.py +1 -1
- glaip_sdk/utils/resource_refs.py +192 -0
- glaip_sdk/utils/rich_utils.py +29 -0
- glaip_sdk/utils/serialization.py +285 -0
- glaip_sdk/utils/validation.py +273 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
- glaip_sdk-0.0.5.dist-info/RECORD +55 -0
- glaip_sdk/cli/commands/init.py +0 -93
- glaip_sdk-0.0.4.dist-info/RECORD +0 -41
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/agents.py
CHANGED
|
@@ -5,11 +5,14 @@ Authors:
|
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import io
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
11
|
+
from collections.abc import AsyncGenerator
|
|
10
12
|
from time import monotonic
|
|
11
13
|
from typing import Any, BinaryIO
|
|
12
14
|
|
|
15
|
+
import httpx
|
|
13
16
|
from rich.console import Console as _Console
|
|
14
17
|
|
|
15
18
|
from glaip_sdk.client.base import BaseClient
|
|
@@ -21,8 +24,10 @@ from glaip_sdk.config.constants import (
|
|
|
21
24
|
DEFAULT_AGENT_VERSION,
|
|
22
25
|
DEFAULT_MODEL,
|
|
23
26
|
)
|
|
27
|
+
from glaip_sdk.exceptions import NotFoundError
|
|
24
28
|
from glaip_sdk.models import Agent
|
|
25
29
|
from glaip_sdk.utils.client_utils import (
|
|
30
|
+
aiter_sse_events,
|
|
26
31
|
create_model_instances,
|
|
27
32
|
extract_ids,
|
|
28
33
|
find_by_name,
|
|
@@ -32,6 +37,10 @@ from glaip_sdk.utils.client_utils import (
|
|
|
32
37
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
33
38
|
from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
34
39
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
40
|
+
from glaip_sdk.utils.validation import validate_agent_instruction
|
|
41
|
+
|
|
42
|
+
# API endpoints
|
|
43
|
+
AGENTS_ENDPOINT = "/agents/"
|
|
35
44
|
|
|
36
45
|
# Set up module-level logger
|
|
37
46
|
logger = logging.getLogger("glaip_sdk.agents")
|
|
@@ -49,14 +58,87 @@ class AgentClient(BaseClient):
|
|
|
49
58
|
"""
|
|
50
59
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
51
60
|
|
|
52
|
-
def list_agents(
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
def list_agents(
|
|
62
|
+
self,
|
|
63
|
+
agent_type: str | None = None,
|
|
64
|
+
framework: str | None = None,
|
|
65
|
+
name: str | None = None,
|
|
66
|
+
version: str | None = None,
|
|
67
|
+
sync_langflow_agents: bool = False,
|
|
68
|
+
) -> list[Agent]:
|
|
69
|
+
"""List agents with optional filtering.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
agent_type: Filter by agent type (config, code, a2a)
|
|
73
|
+
framework: Filter by framework (langchain, langgraph, google_adk)
|
|
74
|
+
name: Filter by partial name match (case-insensitive)
|
|
75
|
+
version: Filter by exact version match
|
|
76
|
+
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of agents matching the filters
|
|
80
|
+
"""
|
|
81
|
+
params = {}
|
|
82
|
+
if agent_type is not None:
|
|
83
|
+
params["agent_type"] = agent_type
|
|
84
|
+
if framework is not None:
|
|
85
|
+
params["framework"] = framework
|
|
86
|
+
if name is not None:
|
|
87
|
+
params["name"] = name
|
|
88
|
+
if version is not None:
|
|
89
|
+
params["version"] = version
|
|
90
|
+
if sync_langflow_agents:
|
|
91
|
+
params["sync_langflow_agents"] = "true"
|
|
92
|
+
|
|
93
|
+
if params:
|
|
94
|
+
data = self._request("GET", AGENTS_ENDPOINT, params=params)
|
|
95
|
+
else:
|
|
96
|
+
data = self._request("GET", AGENTS_ENDPOINT)
|
|
55
97
|
return create_model_instances(data, Agent, self)
|
|
56
98
|
|
|
99
|
+
def sync_langflow_agents(
|
|
100
|
+
self,
|
|
101
|
+
base_url: str | None = None,
|
|
102
|
+
api_key: str | None = None,
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""Sync LangFlow agents by fetching flows from the LangFlow server.
|
|
105
|
+
|
|
106
|
+
This method synchronizes agents with LangFlow flows. It fetches all flows
|
|
107
|
+
from the configured LangFlow server and creates/updates corresponding agents.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
base_url: Custom LangFlow server base URL. If not provided, uses LANGFLOW_BASE_URL env var.
|
|
111
|
+
api_key: Custom LangFlow API key. If not provided, uses LANGFLOW_API_KEY env var.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Response containing sync results and statistics
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If LangFlow server configuration is missing
|
|
118
|
+
"""
|
|
119
|
+
payload = {}
|
|
120
|
+
if base_url is not None:
|
|
121
|
+
payload["base_url"] = base_url
|
|
122
|
+
if api_key is not None:
|
|
123
|
+
payload["api_key"] = api_key
|
|
124
|
+
|
|
125
|
+
return self._request("POST", "/agents/langflow/sync", json=payload)
|
|
126
|
+
|
|
57
127
|
def get_agent_by_id(self, agent_id: str) -> Agent:
|
|
58
128
|
"""Get agent by ID."""
|
|
59
129
|
data = self._request("GET", f"/agents/{agent_id}")
|
|
130
|
+
|
|
131
|
+
if isinstance(data, str):
|
|
132
|
+
# Some backends may respond with plain text for missing agents.
|
|
133
|
+
message = data.strip() or f"Agent '{agent_id}' not found"
|
|
134
|
+
raise NotFoundError(message, status_code=404)
|
|
135
|
+
|
|
136
|
+
if not isinstance(data, dict):
|
|
137
|
+
raise NotFoundError(
|
|
138
|
+
f"Agent '{agent_id}' not found (unexpected response type)",
|
|
139
|
+
status_code=404,
|
|
140
|
+
)
|
|
141
|
+
|
|
60
142
|
return Agent(**data)._set_client(self)
|
|
61
143
|
|
|
62
144
|
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
@@ -65,13 +147,13 @@ class AgentClient(BaseClient):
|
|
|
65
147
|
if name:
|
|
66
148
|
params["name"] = name
|
|
67
149
|
|
|
68
|
-
data = self._request("GET",
|
|
150
|
+
data = self._request("GET", AGENTS_ENDPOINT, params=params)
|
|
69
151
|
agents = create_model_instances(data, Agent, self)
|
|
70
152
|
if name is None:
|
|
71
153
|
return agents
|
|
72
154
|
return find_by_name(agents, name, case_sensitive=False)
|
|
73
155
|
|
|
74
|
-
def
|
|
156
|
+
def _build_create_payload(
|
|
75
157
|
self,
|
|
76
158
|
name: str,
|
|
77
159
|
instruction: str,
|
|
@@ -80,38 +162,53 @@ class AgentClient(BaseClient):
|
|
|
80
162
|
agents: list[str | Any] | None = None,
|
|
81
163
|
timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
|
|
82
164
|
**kwargs,
|
|
83
|
-
) ->
|
|
84
|
-
"""
|
|
85
|
-
# Client-side validation
|
|
86
|
-
if not name or not name.strip():
|
|
87
|
-
raise ValueError("Agent name cannot be empty or whitespace")
|
|
88
|
-
|
|
89
|
-
if not instruction or not instruction.strip():
|
|
90
|
-
raise ValueError("Agent instruction cannot be empty or whitespace")
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Build payload for agent creation with proper LM selection and metadata handling.
|
|
91
167
|
|
|
92
|
-
|
|
93
|
-
|
|
168
|
+
CENTRALIZED PAYLOAD BUILDING LOGIC:
|
|
169
|
+
- LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
|
|
170
|
+
- Always includes required backend metadata
|
|
171
|
+
- Preserves mem0 keys in agent_config
|
|
172
|
+
- Handles tool/agent ID extraction from objects
|
|
94
173
|
|
|
95
|
-
|
|
174
|
+
Args:
|
|
175
|
+
name: Agent name
|
|
176
|
+
instruction: Agent instruction
|
|
177
|
+
model: Language model name (used when language_model_id not provided)
|
|
178
|
+
tools: List of tools to attach
|
|
179
|
+
agents: List of sub-agents to attach
|
|
180
|
+
timeout: Agent execution timeout
|
|
181
|
+
**kwargs: Additional parameters (language_model_id, agent_config, etc.)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Complete payload dictionary for agent creation
|
|
185
|
+
"""
|
|
186
|
+
# Prepare the creation payload with required fields
|
|
96
187
|
payload: dict[str, Any] = {
|
|
97
188
|
"name": name.strip(),
|
|
98
189
|
"instruction": instruction.strip(),
|
|
99
190
|
"type": DEFAULT_AGENT_TYPE,
|
|
100
191
|
"framework": DEFAULT_AGENT_FRAMEWORK,
|
|
101
192
|
"version": DEFAULT_AGENT_VERSION,
|
|
102
|
-
"provider": DEFAULT_AGENT_PROVIDER,
|
|
103
|
-
"model_name": model or DEFAULT_MODEL, # Ensure model_name is never None
|
|
104
193
|
}
|
|
105
194
|
|
|
106
|
-
#
|
|
195
|
+
# Language model selection with exclusivity:
|
|
196
|
+
# Priority: language_model_id (if provided) > provider/model_name (fallback)
|
|
197
|
+
if kwargs.get("language_model_id"):
|
|
198
|
+
# Use language_model_id - defer to kwargs update below
|
|
199
|
+
pass
|
|
200
|
+
else:
|
|
201
|
+
# Use provider/model_name fallback
|
|
202
|
+
payload["provider"] = DEFAULT_AGENT_PROVIDER
|
|
203
|
+
payload["model_name"] = model or DEFAULT_MODEL
|
|
204
|
+
|
|
205
|
+
# Include execution timeout if provided
|
|
107
206
|
if timeout is not None:
|
|
108
207
|
payload["timeout"] = str(timeout)
|
|
109
208
|
|
|
110
209
|
# Ensure minimum required metadata for visibility
|
|
111
210
|
if "metadata" not in kwargs:
|
|
112
211
|
kwargs["metadata"] = {}
|
|
113
|
-
|
|
114
|
-
# Always include the minimum required metadata for visibility
|
|
115
212
|
if "type" not in kwargs["metadata"]:
|
|
116
213
|
kwargs["metadata"]["type"] = "custom"
|
|
117
214
|
|
|
@@ -125,32 +222,16 @@ class AgentClient(BaseClient):
|
|
|
125
222
|
if agent_ids:
|
|
126
223
|
payload["agents"] = agent_ids
|
|
127
224
|
|
|
128
|
-
# Add any additional kwargs
|
|
225
|
+
# Add any additional kwargs (including language_model_id, agent_config, etc.)
|
|
129
226
|
payload.update(kwargs)
|
|
130
227
|
|
|
131
|
-
|
|
132
|
-
full_agent_data = self._post_then_fetch(
|
|
133
|
-
id_key="id",
|
|
134
|
-
post_endpoint="/agents/",
|
|
135
|
-
get_endpoint_fmt="/agents/{id}",
|
|
136
|
-
json=payload,
|
|
137
|
-
)
|
|
138
|
-
return Agent(**full_agent_data)._set_client(self)
|
|
139
|
-
|
|
140
|
-
def update_agent(
|
|
141
|
-
self,
|
|
142
|
-
agent_id: str,
|
|
143
|
-
name: str | None = None,
|
|
144
|
-
instruction: str | None = None,
|
|
145
|
-
model: str | None = None,
|
|
146
|
-
**kwargs,
|
|
147
|
-
) -> "Agent":
|
|
148
|
-
"""Update an existing agent."""
|
|
149
|
-
# First, get the current agent data
|
|
150
|
-
current_agent = self.get_agent_by_id(agent_id)
|
|
228
|
+
return payload
|
|
151
229
|
|
|
152
|
-
|
|
153
|
-
|
|
230
|
+
def _build_basic_update_payload(
|
|
231
|
+
self, current_agent: "Agent", name: str | None, instruction: str | None
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
"""Build the basic update payload with required fields."""
|
|
234
|
+
return {
|
|
154
235
|
"name": name if name is not None else current_agent.name,
|
|
155
236
|
"instruction": instruction
|
|
156
237
|
if instruction is not None
|
|
@@ -160,48 +241,250 @@ class AgentClient(BaseClient):
|
|
|
160
241
|
"version": DEFAULT_AGENT_VERSION, # Required by backend
|
|
161
242
|
}
|
|
162
243
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
244
|
+
def _handle_language_model_selection(
|
|
245
|
+
self,
|
|
246
|
+
update_data: dict[str, Any],
|
|
247
|
+
current_agent: "Agent",
|
|
248
|
+
model: str | None,
|
|
249
|
+
language_model_id: str | None,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Handle language model selection with proper priority and fallbacks."""
|
|
252
|
+
if language_model_id:
|
|
253
|
+
# Use language_model_id if provided
|
|
254
|
+
update_data["language_model_id"] = language_model_id
|
|
255
|
+
elif model is not None:
|
|
256
|
+
# Use explicit model parameter
|
|
257
|
+
update_data["provider"] = DEFAULT_AGENT_PROVIDER
|
|
166
258
|
update_data["model_name"] = model
|
|
167
259
|
else:
|
|
168
|
-
# Use current
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
260
|
+
# Use current agent config or fallbacks
|
|
261
|
+
self._set_language_model_from_current_agent(update_data, current_agent)
|
|
262
|
+
|
|
263
|
+
def _set_language_model_from_current_agent(
|
|
264
|
+
self, update_data: dict[str, Any], current_agent: "Agent"
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Set language model from current agent config or use defaults."""
|
|
267
|
+
if hasattr(current_agent, "agent_config") and current_agent.agent_config:
|
|
268
|
+
agent_config = current_agent.agent_config
|
|
269
|
+
if "lm_provider" in agent_config:
|
|
270
|
+
update_data["provider"] = agent_config["lm_provider"]
|
|
271
|
+
if "lm_name" in agent_config:
|
|
272
|
+
update_data["model_name"] = agent_config["lm_name"]
|
|
273
|
+
else:
|
|
274
|
+
# Default fallback values
|
|
275
|
+
update_data["provider"] = DEFAULT_AGENT_PROVIDER
|
|
276
|
+
update_data["model_name"] = DEFAULT_MODEL
|
|
178
277
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
278
|
+
def _handle_tools_and_agents(
|
|
279
|
+
self,
|
|
280
|
+
update_data: dict[str, Any],
|
|
281
|
+
current_agent: "Agent",
|
|
282
|
+
tools: list | None,
|
|
283
|
+
agents: list | None,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Handle tools and agents with proper ID extraction."""
|
|
286
|
+
# Handle tools
|
|
287
|
+
if tools is not None:
|
|
288
|
+
tool_ids = extract_ids(tools)
|
|
289
|
+
update_data["tools"] = tool_ids if tool_ids else []
|
|
290
|
+
else:
|
|
291
|
+
update_data["tools"] = self._extract_current_tool_ids(current_agent)
|
|
292
|
+
|
|
293
|
+
# Handle agents
|
|
294
|
+
if agents is not None:
|
|
295
|
+
agent_ids = extract_ids(agents)
|
|
296
|
+
update_data["agents"] = agent_ids if agent_ids else []
|
|
297
|
+
else:
|
|
298
|
+
update_data["agents"] = self._extract_current_agent_ids(current_agent)
|
|
299
|
+
|
|
300
|
+
def _extract_current_tool_ids(self, current_agent: "Agent") -> list[str]:
|
|
301
|
+
"""Extract tool IDs from current agent."""
|
|
302
|
+
if current_agent.tools:
|
|
303
|
+
return [
|
|
186
304
|
tool["id"] if isinstance(tool, dict) else tool
|
|
187
305
|
for tool in current_agent.tools
|
|
188
306
|
]
|
|
307
|
+
return []
|
|
189
308
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
elif current_agent.agents:
|
|
195
|
-
update_data["agents"] = [
|
|
309
|
+
def _extract_current_agent_ids(self, current_agent: "Agent") -> list[str]:
|
|
310
|
+
"""Extract agent IDs from current agent."""
|
|
311
|
+
if current_agent.agents:
|
|
312
|
+
return [
|
|
196
313
|
agent["id"] if isinstance(agent, dict) else agent
|
|
197
314
|
for agent in current_agent.agents
|
|
198
315
|
]
|
|
316
|
+
return []
|
|
199
317
|
|
|
200
|
-
|
|
318
|
+
def _handle_agent_config(
|
|
319
|
+
self,
|
|
320
|
+
update_data: dict[str, Any],
|
|
321
|
+
current_agent: "Agent",
|
|
322
|
+
agent_config: dict | None,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Handle agent_config with proper merging and cleanup."""
|
|
325
|
+
if agent_config is not None:
|
|
326
|
+
# Use provided agent_config, merging with current if needed
|
|
327
|
+
update_data["agent_config"] = self._merge_agent_configs(
|
|
328
|
+
current_agent, agent_config
|
|
329
|
+
)
|
|
330
|
+
elif hasattr(current_agent, "agent_config") and current_agent.agent_config:
|
|
331
|
+
# Preserve existing agent_config
|
|
332
|
+
update_data["agent_config"] = current_agent.agent_config.copy()
|
|
333
|
+
else:
|
|
334
|
+
# Default agent_config
|
|
335
|
+
update_data["agent_config"] = {
|
|
336
|
+
"lm_provider": DEFAULT_AGENT_PROVIDER,
|
|
337
|
+
"lm_name": DEFAULT_MODEL,
|
|
338
|
+
"lm_hyperparameters": {"temperature": 0.0},
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Clean LM keys from agent_config to prevent conflicts
|
|
342
|
+
self._clean_agent_config_lm_keys(update_data)
|
|
343
|
+
|
|
344
|
+
def _merge_agent_configs(self, current_agent: "Agent", new_config: dict) -> dict:
|
|
345
|
+
"""Merge current agent config with new config."""
|
|
346
|
+
if hasattr(current_agent, "agent_config") and current_agent.agent_config:
|
|
347
|
+
merged_config = current_agent.agent_config.copy()
|
|
348
|
+
merged_config.update(new_config)
|
|
349
|
+
return merged_config
|
|
350
|
+
return new_config
|
|
351
|
+
|
|
352
|
+
def _clean_agent_config_lm_keys(self, update_data: dict[str, Any]) -> None:
|
|
353
|
+
"""Remove LM keys from agent_config to prevent conflicts."""
|
|
354
|
+
if "agent_config" in update_data and isinstance(
|
|
355
|
+
update_data["agent_config"], dict
|
|
356
|
+
):
|
|
357
|
+
agent_config = update_data["agent_config"]
|
|
358
|
+
lm_keys_to_remove = {
|
|
359
|
+
"lm_provider",
|
|
360
|
+
"lm_name",
|
|
361
|
+
"lm_base_url",
|
|
362
|
+
"lm_hyperparameters",
|
|
363
|
+
}
|
|
364
|
+
for key in lm_keys_to_remove:
|
|
365
|
+
agent_config.pop(key, None)
|
|
366
|
+
|
|
367
|
+
def _finalize_update_payload(
|
|
368
|
+
self, update_data: dict[str, Any], current_agent: "Agent", **kwargs
|
|
369
|
+
) -> dict[str, Any]:
|
|
370
|
+
"""Finalize the update payload with metadata and additional kwargs."""
|
|
371
|
+
# Handle metadata preservation
|
|
372
|
+
if hasattr(current_agent, "metadata") and current_agent.metadata:
|
|
373
|
+
update_data["metadata"] = current_agent.metadata.copy()
|
|
374
|
+
|
|
375
|
+
# Add any other kwargs (excluding already handled ones)
|
|
376
|
+
excluded_keys = {"tools", "agents", "agent_config", "language_model_id"}
|
|
201
377
|
for key, value in kwargs.items():
|
|
202
|
-
if key not in
|
|
378
|
+
if key not in excluded_keys:
|
|
203
379
|
update_data[key] = value
|
|
204
380
|
|
|
381
|
+
return update_data
|
|
382
|
+
|
|
383
|
+
def _build_update_payload(
|
|
384
|
+
self,
|
|
385
|
+
current_agent: "Agent",
|
|
386
|
+
name: str | None = None,
|
|
387
|
+
instruction: str | None = None,
|
|
388
|
+
model: str | None = None,
|
|
389
|
+
**kwargs,
|
|
390
|
+
) -> dict[str, Any]:
|
|
391
|
+
"""Build payload for agent update with proper LM selection and current state preservation.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
current_agent: Current agent object to update
|
|
395
|
+
name: New agent name (None to keep current)
|
|
396
|
+
instruction: New instruction (None to keep current)
|
|
397
|
+
model: New language model name (None to use current or fallback)
|
|
398
|
+
**kwargs: Additional parameters including language_model_id, agent_config, etc.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Complete payload dictionary for agent update
|
|
402
|
+
|
|
403
|
+
Notes:
|
|
404
|
+
- LM exclusivity: Uses language_model_id if provided, otherwise provider/model_name
|
|
405
|
+
- Preserves current values as defaults when new values not provided
|
|
406
|
+
- Handles tools/agents updates with proper ID extraction
|
|
407
|
+
"""
|
|
408
|
+
# Build basic payload
|
|
409
|
+
update_data = self._build_basic_update_payload(current_agent, name, instruction)
|
|
410
|
+
|
|
411
|
+
# Handle language model selection
|
|
412
|
+
language_model_id = kwargs.get("language_model_id")
|
|
413
|
+
self._handle_language_model_selection(
|
|
414
|
+
update_data, current_agent, model, language_model_id
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Handle tools and agents
|
|
418
|
+
tools = kwargs.get("tools")
|
|
419
|
+
agents = kwargs.get("agents")
|
|
420
|
+
self._handle_tools_and_agents(update_data, current_agent, tools, agents)
|
|
421
|
+
|
|
422
|
+
# Handle agent config
|
|
423
|
+
agent_config = kwargs.get("agent_config")
|
|
424
|
+
self._handle_agent_config(update_data, current_agent, agent_config)
|
|
425
|
+
|
|
426
|
+
# Finalize payload
|
|
427
|
+
return self._finalize_update_payload(update_data, current_agent, **kwargs)
|
|
428
|
+
|
|
429
|
+
def create_agent(
|
|
430
|
+
self,
|
|
431
|
+
name: str,
|
|
432
|
+
instruction: str,
|
|
433
|
+
model: str = DEFAULT_MODEL,
|
|
434
|
+
tools: list[str | Any] | None = None,
|
|
435
|
+
agents: list[str | Any] | None = None,
|
|
436
|
+
timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
|
|
437
|
+
**kwargs,
|
|
438
|
+
) -> "Agent":
|
|
439
|
+
"""Create a new agent."""
|
|
440
|
+
# Client-side validation
|
|
441
|
+
if not name or not name.strip():
|
|
442
|
+
raise ValueError("Agent name cannot be empty or whitespace")
|
|
443
|
+
|
|
444
|
+
# Validate instruction using centralized validation
|
|
445
|
+
instruction = validate_agent_instruction(instruction)
|
|
446
|
+
|
|
447
|
+
# Build payload using centralized builder
|
|
448
|
+
payload = self._build_create_payload(
|
|
449
|
+
name=name,
|
|
450
|
+
instruction=instruction,
|
|
451
|
+
model=model,
|
|
452
|
+
tools=tools,
|
|
453
|
+
agents=agents,
|
|
454
|
+
timeout=timeout,
|
|
455
|
+
**kwargs,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Create the agent and fetch full details
|
|
459
|
+
full_agent_data = self._post_then_fetch(
|
|
460
|
+
id_key="id",
|
|
461
|
+
post_endpoint=AGENTS_ENDPOINT,
|
|
462
|
+
get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
|
|
463
|
+
json=payload,
|
|
464
|
+
)
|
|
465
|
+
return Agent(**full_agent_data)._set_client(self)
|
|
466
|
+
|
|
467
|
+
def update_agent(
|
|
468
|
+
self,
|
|
469
|
+
agent_id: str,
|
|
470
|
+
name: str | None = None,
|
|
471
|
+
instruction: str | None = None,
|
|
472
|
+
model: str | None = None,
|
|
473
|
+
**kwargs,
|
|
474
|
+
) -> "Agent":
|
|
475
|
+
"""Update an existing agent."""
|
|
476
|
+
# First, get the current agent data
|
|
477
|
+
current_agent = self.get_agent_by_id(agent_id)
|
|
478
|
+
|
|
479
|
+
# Build payload using centralized builder
|
|
480
|
+
update_data = self._build_update_payload(
|
|
481
|
+
current_agent=current_agent,
|
|
482
|
+
name=name,
|
|
483
|
+
instruction=instruction,
|
|
484
|
+
model=model,
|
|
485
|
+
**kwargs,
|
|
486
|
+
)
|
|
487
|
+
|
|
205
488
|
# Send the complete payload
|
|
206
489
|
data = self._request("PUT", f"/agents/{agent_id}", json=update_data)
|
|
207
490
|
return Agent(**data)._set_client(self)
|
|
@@ -210,20 +493,12 @@ class AgentClient(BaseClient):
|
|
|
210
493
|
"""Delete an agent."""
|
|
211
494
|
self._request("DELETE", f"/agents/{agent_id}")
|
|
212
495
|
|
|
213
|
-
def
|
|
214
|
-
self,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
files: list[str | BinaryIO] | None = None,
|
|
218
|
-
tty: bool = False,
|
|
219
|
-
*,
|
|
220
|
-
renderer: RichStreamRenderer | str | None = "auto",
|
|
221
|
-
**kwargs,
|
|
222
|
-
) -> str:
|
|
223
|
-
"""Run an agent with a message, streaming via a renderer."""
|
|
224
|
-
# Prepare multipart data if files are provided
|
|
496
|
+
def _prepare_payload_and_headers(
|
|
497
|
+
self, message: str, files: list[str | BinaryIO] | None, tty: bool, **kwargs
|
|
498
|
+
):
|
|
499
|
+
"""Prepare payload and headers for agent run request."""
|
|
225
500
|
multipart_data = None
|
|
226
|
-
headers = None
|
|
501
|
+
headers = None
|
|
227
502
|
|
|
228
503
|
if files:
|
|
229
504
|
multipart_data = prepare_multipart_data(message, files)
|
|
@@ -232,69 +507,195 @@ class AgentClient(BaseClient):
|
|
|
232
507
|
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
233
508
|
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
234
509
|
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
235
|
-
headers = None # Let httpx set proper multipart boundaries
|
|
236
510
|
|
|
237
511
|
# When streaming, explicitly prefer SSE
|
|
238
512
|
headers = {**(headers or {}), "Accept": "text/event-stream"}
|
|
239
513
|
|
|
240
514
|
if files:
|
|
241
515
|
payload = None
|
|
242
|
-
# Use multipart data
|
|
243
516
|
data_payload = multipart_data.data
|
|
244
517
|
files_payload = multipart_data.files
|
|
245
518
|
else:
|
|
246
519
|
payload = {"input": message, **kwargs}
|
|
247
520
|
if tty:
|
|
248
521
|
payload["tty"] = True
|
|
249
|
-
# Explicitly send stream intent both ways
|
|
250
522
|
payload["stream"] = True
|
|
251
523
|
data_payload = None
|
|
252
524
|
files_payload = None
|
|
253
525
|
|
|
254
|
-
|
|
526
|
+
return payload, data_payload, files_payload, headers, multipart_data
|
|
527
|
+
|
|
528
|
+
def _create_renderer(self, renderer, **kwargs):
|
|
529
|
+
"""Create appropriate renderer based on configuration."""
|
|
255
530
|
if isinstance(renderer, RichStreamRenderer):
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
live=False, # CLI disables live updates for verbose
|
|
266
|
-
show_delegate_tool_panels=True, # CLI always shows tool panels
|
|
267
|
-
append_finished_snapshots=False,
|
|
268
|
-
)
|
|
269
|
-
r = RichStreamRenderer(
|
|
270
|
-
console=_Console(),
|
|
271
|
-
cfg=verbose_config,
|
|
272
|
-
verbose=True,
|
|
273
|
-
)
|
|
531
|
+
return renderer
|
|
532
|
+
|
|
533
|
+
verbose = kwargs.get("verbose", False)
|
|
534
|
+
|
|
535
|
+
if isinstance(renderer, str):
|
|
536
|
+
if renderer == "silent":
|
|
537
|
+
return self._create_silent_renderer()
|
|
538
|
+
elif renderer == "minimal":
|
|
539
|
+
return self._create_minimal_renderer()
|
|
274
540
|
else:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
541
|
+
return self._create_default_renderer(verbose)
|
|
542
|
+
elif verbose:
|
|
543
|
+
return self._create_verbose_renderer()
|
|
544
|
+
else:
|
|
545
|
+
return self._create_default_renderer(verbose)
|
|
546
|
+
|
|
547
|
+
def _create_silent_renderer(self):
|
|
548
|
+
"""Create a silent renderer that suppresses all output."""
|
|
549
|
+
silent_config = RendererConfig(
|
|
550
|
+
live=False,
|
|
551
|
+
persist_live=False,
|
|
552
|
+
show_delegate_tool_panels=False,
|
|
553
|
+
render_thinking=False,
|
|
554
|
+
)
|
|
555
|
+
return RichStreamRenderer(
|
|
556
|
+
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
557
|
+
cfg=silent_config,
|
|
558
|
+
verbose=False,
|
|
559
|
+
)
|
|
280
560
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
561
|
+
def _create_minimal_renderer(self):
|
|
562
|
+
"""Create a minimal renderer with basic output."""
|
|
563
|
+
minimal_config = RendererConfig(
|
|
564
|
+
live=False,
|
|
565
|
+
persist_live=False,
|
|
566
|
+
show_delegate_tool_panels=False,
|
|
567
|
+
render_thinking=False,
|
|
568
|
+
)
|
|
569
|
+
return RichStreamRenderer(
|
|
570
|
+
console=_Console(),
|
|
571
|
+
cfg=minimal_config,
|
|
572
|
+
verbose=False,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
def _create_verbose_renderer(self):
|
|
576
|
+
"""Create a verbose renderer for detailed output."""
|
|
577
|
+
verbose_config = RendererConfig(
|
|
578
|
+
theme="dark",
|
|
579
|
+
style="debug",
|
|
580
|
+
live=False,
|
|
581
|
+
show_delegate_tool_panels=True,
|
|
582
|
+
append_finished_snapshots=False,
|
|
583
|
+
)
|
|
584
|
+
return RichStreamRenderer(
|
|
585
|
+
console=_Console(),
|
|
586
|
+
cfg=verbose_config,
|
|
587
|
+
verbose=True,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def _create_default_renderer(self, verbose: bool):
|
|
591
|
+
"""Create the default renderer."""
|
|
592
|
+
if verbose:
|
|
593
|
+
return self._create_verbose_renderer()
|
|
594
|
+
else:
|
|
595
|
+
default_config = RendererConfig(show_delegate_tool_panels=True)
|
|
596
|
+
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
289
597
|
|
|
598
|
+
def _process_stream_events(
|
|
599
|
+
self, stream_response, renderer, timeout_seconds, agent_name, kwargs
|
|
600
|
+
):
|
|
601
|
+
"""Process streaming events and accumulate response."""
|
|
290
602
|
final_text = ""
|
|
291
603
|
stats_usage = {}
|
|
292
604
|
started_monotonic = None
|
|
293
605
|
finished_monotonic = None
|
|
606
|
+
meta = {
|
|
607
|
+
"agent_name": kwargs.get("agent_name", ""),
|
|
608
|
+
"model": kwargs.get("model"),
|
|
609
|
+
"run_id": None,
|
|
610
|
+
"input_message": "", # Will be set from kwargs if available
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
# Capture request id if provided
|
|
614
|
+
req_id = stream_response.headers.get(
|
|
615
|
+
"x-request-id"
|
|
616
|
+
) or stream_response.headers.get("x-run-id")
|
|
617
|
+
if req_id:
|
|
618
|
+
meta["run_id"] = req_id
|
|
619
|
+
renderer.on_start(meta)
|
|
620
|
+
|
|
621
|
+
for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
622
|
+
try:
|
|
623
|
+
ev = json.loads(event["data"])
|
|
624
|
+
except json.JSONDecodeError:
|
|
625
|
+
logger.debug("Non-JSON SSE fragment skipped")
|
|
626
|
+
continue
|
|
627
|
+
|
|
628
|
+
# Start timer at first meaningful event
|
|
629
|
+
if started_monotonic is None and (
|
|
630
|
+
"content" in ev or "status" in ev or ev.get("metadata")
|
|
631
|
+
):
|
|
632
|
+
started_monotonic = monotonic()
|
|
633
|
+
|
|
634
|
+
kind = (ev.get("metadata") or {}).get("kind")
|
|
635
|
+
renderer.on_event(ev)
|
|
636
|
+
|
|
637
|
+
# Skip artifacts from content accumulation
|
|
638
|
+
if kind == "artifact":
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
# Accumulate assistant content
|
|
642
|
+
if ev.get("content"):
|
|
643
|
+
if not ev["content"].startswith("Artifact received:"):
|
|
644
|
+
final_text = ev["content"]
|
|
645
|
+
continue
|
|
646
|
+
|
|
647
|
+
# Handle final response
|
|
648
|
+
if kind == "final_response" and ev.get("content"):
|
|
649
|
+
final_text = ev["content"]
|
|
650
|
+
continue
|
|
651
|
+
|
|
652
|
+
# Handle usage stats
|
|
653
|
+
if kind == "usage":
|
|
654
|
+
stats_usage.update(ev.get("usage") or {})
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
# Handle run info updates
|
|
658
|
+
if kind == "run_info":
|
|
659
|
+
if ev.get("model"):
|
|
660
|
+
meta["model"] = ev["model"]
|
|
661
|
+
renderer.on_start(meta)
|
|
662
|
+
if ev.get("run_id"):
|
|
663
|
+
meta["run_id"] = ev["run_id"]
|
|
664
|
+
renderer.on_start(meta)
|
|
665
|
+
|
|
666
|
+
finished_monotonic = monotonic()
|
|
667
|
+
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
294
668
|
|
|
295
|
-
|
|
669
|
+
def run_agent(
|
|
670
|
+
self,
|
|
671
|
+
agent_id: str,
|
|
672
|
+
message: str,
|
|
673
|
+
files: list[str | BinaryIO] | None = None,
|
|
674
|
+
tty: bool = False,
|
|
675
|
+
*,
|
|
676
|
+
renderer: RichStreamRenderer | str | None = "auto",
|
|
677
|
+
**kwargs,
|
|
678
|
+
) -> str:
|
|
679
|
+
"""Run an agent with a message, streaming via a renderer."""
|
|
680
|
+
# Prepare request payload and headers
|
|
681
|
+
payload, data_payload, files_payload, headers, multipart_data = (
|
|
682
|
+
self._prepare_payload_and_headers(message, files, tty, **kwargs)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Create renderer
|
|
686
|
+
r = self._create_renderer(renderer, **kwargs)
|
|
687
|
+
|
|
688
|
+
# Initialize renderer
|
|
689
|
+
meta = {
|
|
690
|
+
"agent_name": kwargs.get("agent_name", agent_id),
|
|
691
|
+
"model": kwargs.get("model"),
|
|
692
|
+
"run_id": None,
|
|
693
|
+
"input_message": message,
|
|
694
|
+
}
|
|
695
|
+
r.on_start(meta)
|
|
296
696
|
|
|
297
697
|
try:
|
|
698
|
+
# Make streaming request
|
|
298
699
|
response = self.http_client.stream(
|
|
299
700
|
"POST",
|
|
300
701
|
f"/agents/{agent_id}/run",
|
|
@@ -307,71 +708,16 @@ class AgentClient(BaseClient):
|
|
|
307
708
|
with response as stream_response:
|
|
308
709
|
stream_response.raise_for_status()
|
|
309
710
|
|
|
310
|
-
#
|
|
311
|
-
req_id = stream_response.headers.get(
|
|
312
|
-
"x-request-id"
|
|
313
|
-
) or stream_response.headers.get("x-run-id")
|
|
314
|
-
if req_id:
|
|
315
|
-
meta["run_id"] = req_id
|
|
316
|
-
r.on_start(meta) # refresh header with run_id
|
|
317
|
-
|
|
318
|
-
# Get agent run timeout for execution control
|
|
319
|
-
# Prefer CLI-provided timeout, otherwise use default
|
|
711
|
+
# Process streaming events
|
|
320
712
|
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
321
|
-
|
|
322
713
|
agent_name = kwargs.get("agent_name")
|
|
323
714
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
logger.debug("Non-JSON SSE fragment skipped")
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
# Start timer at first meaningful event
|
|
334
|
-
if started_monotonic is None and (
|
|
335
|
-
"content" in ev or "status" in ev or ev.get("metadata")
|
|
336
|
-
):
|
|
337
|
-
started_monotonic = monotonic()
|
|
338
|
-
|
|
339
|
-
kind = (ev.get("metadata") or {}).get("kind")
|
|
340
|
-
|
|
341
|
-
# Pass event to the renderer (always, don't filter)
|
|
342
|
-
r.on_event(ev)
|
|
343
|
-
|
|
344
|
-
# Hide "artifact" chatter from content accumulation only
|
|
345
|
-
if kind == "artifact":
|
|
346
|
-
continue
|
|
347
|
-
|
|
348
|
-
# Accumulate assistant content, but do not print here
|
|
349
|
-
if "content" in ev and ev["content"]:
|
|
350
|
-
# Filter weird backend text like "Artifact received: ..."
|
|
351
|
-
if not ev["content"].startswith("Artifact received:"):
|
|
352
|
-
final_text = ev["content"] # replace with latest
|
|
353
|
-
continue
|
|
354
|
-
|
|
355
|
-
# Also treat final_response like content for CLI return value
|
|
356
|
-
if kind == "final_response" and ev.get("content"):
|
|
357
|
-
final_text = ev["content"] # ensure CLI non-empty
|
|
358
|
-
continue
|
|
359
|
-
|
|
360
|
-
# Usage/cost event (if your backend emits it)
|
|
361
|
-
if kind == "usage":
|
|
362
|
-
stats_usage.update(ev.get("usage") or {})
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
# Model/run info (if emitted mid-stream)
|
|
366
|
-
if kind == "run_info":
|
|
367
|
-
if ev.get("model"):
|
|
368
|
-
meta["model"] = ev["model"]
|
|
369
|
-
r.on_start(meta)
|
|
370
|
-
if ev.get("run_id"):
|
|
371
|
-
meta["run_id"] = ev["run_id"]
|
|
372
|
-
r.on_start(meta)
|
|
373
|
-
|
|
374
|
-
finished_monotonic = monotonic()
|
|
715
|
+
final_text, stats_usage, started_monotonic, finished_monotonic = (
|
|
716
|
+
self._process_stream_events(
|
|
717
|
+
stream_response, r, timeout_seconds, agent_name, kwargs
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
|
|
375
721
|
except KeyboardInterrupt:
|
|
376
722
|
try:
|
|
377
723
|
r.close()
|
|
@@ -383,26 +729,120 @@ class AgentClient(BaseClient):
|
|
|
383
729
|
finally:
|
|
384
730
|
raise
|
|
385
731
|
finally:
|
|
386
|
-
# Ensure
|
|
732
|
+
# Ensure cleanup
|
|
387
733
|
if multipart_data:
|
|
388
734
|
multipart_data.close()
|
|
389
735
|
|
|
390
|
-
# Finalize
|
|
736
|
+
# Finalize and return result
|
|
391
737
|
st = RunStats()
|
|
392
|
-
# Ensure monotonic order (avoid negative -0.0s)
|
|
393
|
-
if started_monotonic is None:
|
|
394
|
-
started_monotonic = finished_monotonic
|
|
395
|
-
|
|
396
738
|
st.started_at = started_monotonic or st.started_at
|
|
397
739
|
st.finished_at = finished_monotonic or st.started_at
|
|
398
740
|
st.usage = stats_usage
|
|
399
741
|
|
|
400
|
-
#
|
|
742
|
+
# Get final content
|
|
401
743
|
if hasattr(r, "state") and hasattr(r.state, "buffer"):
|
|
402
744
|
rendered_text = "".join(r.state.buffer)
|
|
403
745
|
else:
|
|
404
746
|
rendered_text = ""
|
|
405
|
-
final_payload = final_text or rendered_text or "No response content received."
|
|
406
747
|
|
|
748
|
+
final_payload = final_text or rendered_text or "No response content received."
|
|
407
749
|
r.on_complete(st)
|
|
408
750
|
return final_payload
|
|
751
|
+
|
|
752
|
+
async def arun_agent(
|
|
753
|
+
self,
|
|
754
|
+
agent_id: str,
|
|
755
|
+
message: str,
|
|
756
|
+
files: list[str | BinaryIO] | None = None,
|
|
757
|
+
*,
|
|
758
|
+
timeout: float | None = None,
|
|
759
|
+
**kwargs,
|
|
760
|
+
) -> AsyncGenerator[dict, None]:
|
|
761
|
+
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
agent_id: ID of the agent to run
|
|
765
|
+
message: Message to send to the agent
|
|
766
|
+
files: Optional list of files to include
|
|
767
|
+
timeout: Request timeout in seconds
|
|
768
|
+
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
769
|
+
|
|
770
|
+
Yields:
|
|
771
|
+
Dictionary containing parsed JSON chunks from the streaming response
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
AgentTimeoutError: When agent execution times out
|
|
775
|
+
httpx.TimeoutException: When general timeout occurs
|
|
776
|
+
Exception: For other unexpected errors
|
|
777
|
+
"""
|
|
778
|
+
# Prepare multipart data if files are provided
|
|
779
|
+
multipart_data = None
|
|
780
|
+
headers = None # None means "don't override client defaults"
|
|
781
|
+
|
|
782
|
+
if files:
|
|
783
|
+
multipart_data = prepare_multipart_data(message, files)
|
|
784
|
+
# Inject optional multipart extras expected by backend
|
|
785
|
+
if "chat_history" in kwargs and kwargs["chat_history"] is not None:
|
|
786
|
+
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
787
|
+
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
788
|
+
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
789
|
+
headers = None # Let httpx set proper multipart boundaries
|
|
790
|
+
|
|
791
|
+
# When streaming, explicitly prefer SSE
|
|
792
|
+
headers = {**(headers or {}), "Accept": "text/event-stream"}
|
|
793
|
+
|
|
794
|
+
if files:
|
|
795
|
+
payload = None
|
|
796
|
+
# Use multipart data
|
|
797
|
+
data_payload = multipart_data.data
|
|
798
|
+
files_payload = multipart_data.files
|
|
799
|
+
else:
|
|
800
|
+
payload = {"input": message, **kwargs}
|
|
801
|
+
# Explicitly send stream intent both ways
|
|
802
|
+
payload["stream"] = True
|
|
803
|
+
data_payload = None
|
|
804
|
+
files_payload = None
|
|
805
|
+
|
|
806
|
+
# Use timeout from parameter or instance default
|
|
807
|
+
request_timeout = timeout or self.timeout
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
async_client_config = self._build_async_client(request_timeout)
|
|
811
|
+
if headers:
|
|
812
|
+
async_client_config["headers"] = {
|
|
813
|
+
**async_client_config["headers"],
|
|
814
|
+
**headers,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
# Create async client for this request
|
|
818
|
+
async with httpx.AsyncClient(**async_client_config) as async_client:
|
|
819
|
+
async with async_client.stream(
|
|
820
|
+
"POST",
|
|
821
|
+
f"/agents/{agent_id}/run",
|
|
822
|
+
json=payload,
|
|
823
|
+
data=data_payload,
|
|
824
|
+
files=files_payload,
|
|
825
|
+
headers=headers,
|
|
826
|
+
) as stream_response:
|
|
827
|
+
stream_response.raise_for_status()
|
|
828
|
+
|
|
829
|
+
# Get agent run timeout for execution control
|
|
830
|
+
# Prefer parameter timeout, otherwise use default
|
|
831
|
+
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
832
|
+
|
|
833
|
+
agent_name = kwargs.get("agent_name")
|
|
834
|
+
|
|
835
|
+
async for event in aiter_sse_events(
|
|
836
|
+
stream_response, timeout_seconds, agent_name
|
|
837
|
+
):
|
|
838
|
+
try:
|
|
839
|
+
chunk = json.loads(event["data"])
|
|
840
|
+
yield chunk
|
|
841
|
+
except json.JSONDecodeError:
|
|
842
|
+
logger.debug("Non-JSON SSE fragment skipped")
|
|
843
|
+
continue
|
|
844
|
+
|
|
845
|
+
finally:
|
|
846
|
+
# Ensure we close any opened file handles from multipart
|
|
847
|
+
if multipart_data:
|
|
848
|
+
multipart_data.close()
|