glaip-sdk 0.0.3__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 +146 -0
- 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 +786 -271
- glaip_sdk/cli/commands/configure.py +19 -19
- glaip_sdk/cli/commands/mcps.py +151 -141
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +252 -178
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +27 -20
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +372 -213
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +632 -171
- 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 +327 -104
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +43 -3
- 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.3.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 -177
- glaip_sdk-0.0.3.dist-info/RECORD +0 -40
- {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.3.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,
|
|
@@ -31,6 +36,11 @@ from glaip_sdk.utils.client_utils import (
|
|
|
31
36
|
)
|
|
32
37
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
33
38
|
from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
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/"
|
|
34
44
|
|
|
35
45
|
# Set up module-level logger
|
|
36
46
|
logger = logging.getLogger("glaip_sdk.agents")
|
|
@@ -48,14 +58,87 @@ class AgentClient(BaseClient):
|
|
|
48
58
|
"""
|
|
49
59
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
50
60
|
|
|
51
|
-
def list_agents(
|
|
52
|
-
|
|
53
|
-
|
|
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)
|
|
54
97
|
return create_model_instances(data, Agent, self)
|
|
55
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
|
+
|
|
56
127
|
def get_agent_by_id(self, agent_id: str) -> Agent:
|
|
57
128
|
"""Get agent by ID."""
|
|
58
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
|
+
|
|
59
142
|
return Agent(**data)._set_client(self)
|
|
60
143
|
|
|
61
144
|
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
@@ -64,13 +147,13 @@ class AgentClient(BaseClient):
|
|
|
64
147
|
if name:
|
|
65
148
|
params["name"] = name
|
|
66
149
|
|
|
67
|
-
data = self._request("GET",
|
|
150
|
+
data = self._request("GET", AGENTS_ENDPOINT, params=params)
|
|
68
151
|
agents = create_model_instances(data, Agent, self)
|
|
69
152
|
if name is None:
|
|
70
153
|
return agents
|
|
71
154
|
return find_by_name(agents, name, case_sensitive=False)
|
|
72
155
|
|
|
73
|
-
def
|
|
156
|
+
def _build_create_payload(
|
|
74
157
|
self,
|
|
75
158
|
name: str,
|
|
76
159
|
instruction: str,
|
|
@@ -79,38 +162,53 @@ class AgentClient(BaseClient):
|
|
|
79
162
|
agents: list[str | Any] | None = None,
|
|
80
163
|
timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
|
|
81
164
|
**kwargs,
|
|
82
|
-
) ->
|
|
83
|
-
"""
|
|
84
|
-
# Client-side validation
|
|
85
|
-
if not name or not name.strip():
|
|
86
|
-
raise ValueError("Agent name cannot be empty or whitespace")
|
|
87
|
-
|
|
88
|
-
if not instruction or not instruction.strip():
|
|
89
|
-
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.
|
|
90
167
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
173
|
|
|
94
|
-
|
|
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
|
|
95
187
|
payload: dict[str, Any] = {
|
|
96
188
|
"name": name.strip(),
|
|
97
189
|
"instruction": instruction.strip(),
|
|
98
190
|
"type": DEFAULT_AGENT_TYPE,
|
|
99
191
|
"framework": DEFAULT_AGENT_FRAMEWORK,
|
|
100
192
|
"version": DEFAULT_AGENT_VERSION,
|
|
101
|
-
"provider": DEFAULT_AGENT_PROVIDER,
|
|
102
|
-
"model_name": model or DEFAULT_MODEL, # Ensure model_name is never None
|
|
103
193
|
}
|
|
104
194
|
|
|
105
|
-
#
|
|
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
|
|
106
206
|
if timeout is not None:
|
|
107
207
|
payload["timeout"] = str(timeout)
|
|
108
208
|
|
|
109
209
|
# Ensure minimum required metadata for visibility
|
|
110
210
|
if "metadata" not in kwargs:
|
|
111
211
|
kwargs["metadata"] = {}
|
|
112
|
-
|
|
113
|
-
# Always include the minimum required metadata for visibility
|
|
114
212
|
if "type" not in kwargs["metadata"]:
|
|
115
213
|
kwargs["metadata"]["type"] = "custom"
|
|
116
214
|
|
|
@@ -124,32 +222,16 @@ class AgentClient(BaseClient):
|
|
|
124
222
|
if agent_ids:
|
|
125
223
|
payload["agents"] = agent_ids
|
|
126
224
|
|
|
127
|
-
# Add any additional kwargs
|
|
225
|
+
# Add any additional kwargs (including language_model_id, agent_config, etc.)
|
|
128
226
|
payload.update(kwargs)
|
|
129
227
|
|
|
130
|
-
|
|
131
|
-
full_agent_data = self._post_then_fetch(
|
|
132
|
-
id_key="id",
|
|
133
|
-
post_endpoint="/agents/",
|
|
134
|
-
get_endpoint_fmt="/agents/{id}",
|
|
135
|
-
json=payload,
|
|
136
|
-
)
|
|
137
|
-
return Agent(**full_agent_data)._set_client(self)
|
|
228
|
+
return payload
|
|
138
229
|
|
|
139
|
-
def
|
|
140
|
-
self,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
model: str | None = None,
|
|
145
|
-
**kwargs,
|
|
146
|
-
) -> "Agent":
|
|
147
|
-
"""Update an existing agent."""
|
|
148
|
-
# First, get the current agent data
|
|
149
|
-
current_agent = self.get_agent_by_id(agent_id)
|
|
150
|
-
|
|
151
|
-
# Prepare the update payload with current values as defaults
|
|
152
|
-
update_data = {
|
|
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 {
|
|
153
235
|
"name": name if name is not None else current_agent.name,
|
|
154
236
|
"instruction": instruction
|
|
155
237
|
if instruction is not None
|
|
@@ -159,48 +241,250 @@ class AgentClient(BaseClient):
|
|
|
159
241
|
"version": DEFAULT_AGENT_VERSION, # Required by backend
|
|
160
242
|
}
|
|
161
243
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
165
258
|
update_data["model_name"] = model
|
|
166
259
|
else:
|
|
167
|
-
# Use current
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
177
277
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 [
|
|
185
304
|
tool["id"] if isinstance(tool, dict) else tool
|
|
186
305
|
for tool in current_agent.tools
|
|
187
306
|
]
|
|
307
|
+
return []
|
|
188
308
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
elif current_agent.agents:
|
|
194
|
-
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 [
|
|
195
313
|
agent["id"] if isinstance(agent, dict) else agent
|
|
196
314
|
for agent in current_agent.agents
|
|
197
315
|
]
|
|
316
|
+
return []
|
|
198
317
|
|
|
199
|
-
|
|
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"}
|
|
200
377
|
for key, value in kwargs.items():
|
|
201
|
-
if key not in
|
|
378
|
+
if key not in excluded_keys:
|
|
202
379
|
update_data[key] = value
|
|
203
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
|
+
|
|
204
488
|
# Send the complete payload
|
|
205
489
|
data = self._request("PUT", f"/agents/{agent_id}", json=update_data)
|
|
206
490
|
return Agent(**data)._set_client(self)
|
|
@@ -209,20 +493,12 @@ class AgentClient(BaseClient):
|
|
|
209
493
|
"""Delete an agent."""
|
|
210
494
|
self._request("DELETE", f"/agents/{agent_id}")
|
|
211
495
|
|
|
212
|
-
def
|
|
213
|
-
self,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
files: list[str | BinaryIO] | None = None,
|
|
217
|
-
tty: bool = False,
|
|
218
|
-
*,
|
|
219
|
-
renderer: RichStreamRenderer | str | None = "auto",
|
|
220
|
-
**kwargs,
|
|
221
|
-
) -> str:
|
|
222
|
-
"""Run an agent with a message, streaming via a renderer."""
|
|
223
|
-
# 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."""
|
|
224
500
|
multipart_data = None
|
|
225
|
-
headers = None
|
|
501
|
+
headers = None
|
|
226
502
|
|
|
227
503
|
if files:
|
|
228
504
|
multipart_data = prepare_multipart_data(message, files)
|
|
@@ -231,49 +507,195 @@ class AgentClient(BaseClient):
|
|
|
231
507
|
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
232
508
|
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
233
509
|
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
234
|
-
headers = None # Let httpx set proper multipart boundaries
|
|
235
510
|
|
|
236
511
|
# When streaming, explicitly prefer SSE
|
|
237
512
|
headers = {**(headers or {}), "Accept": "text/event-stream"}
|
|
238
513
|
|
|
239
514
|
if files:
|
|
240
515
|
payload = None
|
|
241
|
-
# Use multipart data
|
|
242
516
|
data_payload = multipart_data.data
|
|
243
517
|
files_payload = multipart_data.files
|
|
244
518
|
else:
|
|
245
519
|
payload = {"input": message, **kwargs}
|
|
246
520
|
if tty:
|
|
247
521
|
payload["tty"] = True
|
|
248
|
-
# Explicitly send stream intent both ways
|
|
249
522
|
payload["stream"] = True
|
|
250
523
|
data_payload = None
|
|
251
524
|
files_payload = None
|
|
252
525
|
|
|
253
|
-
|
|
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."""
|
|
254
530
|
if isinstance(renderer, RichStreamRenderer):
|
|
255
|
-
|
|
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()
|
|
540
|
+
else:
|
|
541
|
+
return self._create_default_renderer(verbose)
|
|
542
|
+
elif verbose:
|
|
543
|
+
return self._create_verbose_renderer()
|
|
256
544
|
else:
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
)
|
|
259
560
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
)
|
|
268
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)
|
|
597
|
+
|
|
598
|
+
def _process_stream_events(
|
|
599
|
+
self, stream_response, renderer, timeout_seconds, agent_name, kwargs
|
|
600
|
+
):
|
|
601
|
+
"""Process streaming events and accumulate response."""
|
|
269
602
|
final_text = ""
|
|
270
603
|
stats_usage = {}
|
|
271
604
|
started_monotonic = None
|
|
272
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
|
+
}
|
|
273
612
|
|
|
274
|
-
#
|
|
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
|
|
668
|
+
|
|
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)
|
|
275
696
|
|
|
276
697
|
try:
|
|
698
|
+
# Make streaming request
|
|
277
699
|
response = self.http_client.stream(
|
|
278
700
|
"POST",
|
|
279
701
|
f"/agents/{agent_id}/run",
|
|
@@ -286,71 +708,16 @@ class AgentClient(BaseClient):
|
|
|
286
708
|
with response as stream_response:
|
|
287
709
|
stream_response.raise_for_status()
|
|
288
710
|
|
|
289
|
-
#
|
|
290
|
-
req_id = stream_response.headers.get(
|
|
291
|
-
"x-request-id"
|
|
292
|
-
) or stream_response.headers.get("x-run-id")
|
|
293
|
-
if req_id:
|
|
294
|
-
meta["run_id"] = req_id
|
|
295
|
-
r.on_start(meta) # refresh header with run_id
|
|
296
|
-
|
|
297
|
-
# Get agent run timeout for execution control
|
|
298
|
-
# Prefer CLI-provided timeout, otherwise use default
|
|
711
|
+
# Process streaming events
|
|
299
712
|
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
300
|
-
|
|
301
713
|
agent_name = kwargs.get("agent_name")
|
|
302
714
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
logger.debug("Non-JSON SSE fragment skipped")
|
|
310
|
-
continue
|
|
311
|
-
|
|
312
|
-
# Start timer at first meaningful event
|
|
313
|
-
if started_monotonic is None and (
|
|
314
|
-
"content" in ev or "status" in ev or ev.get("metadata")
|
|
315
|
-
):
|
|
316
|
-
started_monotonic = monotonic()
|
|
317
|
-
|
|
318
|
-
kind = (ev.get("metadata") or {}).get("kind")
|
|
319
|
-
|
|
320
|
-
# Pass event to the renderer (always, don't filter)
|
|
321
|
-
r.on_event(ev)
|
|
322
|
-
|
|
323
|
-
# Hide "artifact" chatter from content accumulation only
|
|
324
|
-
if kind == "artifact":
|
|
325
|
-
continue
|
|
326
|
-
|
|
327
|
-
# Accumulate assistant content, but do not print here
|
|
328
|
-
if "content" in ev and ev["content"]:
|
|
329
|
-
# Filter weird backend text like "Artifact received: ..."
|
|
330
|
-
if not ev["content"].startswith("Artifact received:"):
|
|
331
|
-
final_text = ev["content"] # replace with latest
|
|
332
|
-
continue
|
|
333
|
-
|
|
334
|
-
# Also treat final_response like content for CLI return value
|
|
335
|
-
if kind == "final_response" and ev.get("content"):
|
|
336
|
-
final_text = ev["content"] # ensure CLI non-empty
|
|
337
|
-
continue
|
|
338
|
-
|
|
339
|
-
# Usage/cost event (if your backend emits it)
|
|
340
|
-
if kind == "usage":
|
|
341
|
-
stats_usage.update(ev.get("usage") or {})
|
|
342
|
-
continue
|
|
343
|
-
|
|
344
|
-
# Model/run info (if emitted mid-stream)
|
|
345
|
-
if kind == "run_info":
|
|
346
|
-
if ev.get("model"):
|
|
347
|
-
meta["model"] = ev["model"]
|
|
348
|
-
r.on_start(meta)
|
|
349
|
-
if ev.get("run_id"):
|
|
350
|
-
meta["run_id"] = ev["run_id"]
|
|
351
|
-
r.on_start(meta)
|
|
352
|
-
|
|
353
|
-
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
|
+
|
|
354
721
|
except KeyboardInterrupt:
|
|
355
722
|
try:
|
|
356
723
|
r.close()
|
|
@@ -362,26 +729,120 @@ class AgentClient(BaseClient):
|
|
|
362
729
|
finally:
|
|
363
730
|
raise
|
|
364
731
|
finally:
|
|
365
|
-
# Ensure
|
|
732
|
+
# Ensure cleanup
|
|
366
733
|
if multipart_data:
|
|
367
734
|
multipart_data.close()
|
|
368
735
|
|
|
369
|
-
# Finalize
|
|
736
|
+
# Finalize and return result
|
|
370
737
|
st = RunStats()
|
|
371
|
-
# Ensure monotonic order (avoid negative -0.0s)
|
|
372
|
-
if started_monotonic is None:
|
|
373
|
-
started_monotonic = finished_monotonic
|
|
374
|
-
|
|
375
738
|
st.started_at = started_monotonic or st.started_at
|
|
376
739
|
st.finished_at = finished_monotonic or st.started_at
|
|
377
740
|
st.usage = stats_usage
|
|
378
741
|
|
|
379
|
-
#
|
|
742
|
+
# Get final content
|
|
380
743
|
if hasattr(r, "state") and hasattr(r.state, "buffer"):
|
|
381
744
|
rendered_text = "".join(r.state.buffer)
|
|
382
745
|
else:
|
|
383
746
|
rendered_text = ""
|
|
384
|
-
final_payload = final_text or rendered_text or "No response content received."
|
|
385
747
|
|
|
748
|
+
final_payload = final_text or rendered_text or "No response content received."
|
|
386
749
|
r.on_complete(st)
|
|
387
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()
|