glaip-sdk 0.0.1b10__py3-none-any.whl → 0.0.3__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 +2 -2
- glaip_sdk/_version.py +51 -0
- glaip_sdk/cli/commands/agents.py +201 -109
- glaip_sdk/cli/commands/configure.py +29 -87
- glaip_sdk/cli/commands/init.py +16 -7
- glaip_sdk/cli/commands/mcps.py +73 -153
- glaip_sdk/cli/commands/tools.py +185 -49
- glaip_sdk/cli/main.py +30 -27
- glaip_sdk/cli/utils.py +126 -13
- glaip_sdk/client/__init__.py +54 -2
- glaip_sdk/client/agents.py +175 -237
- glaip_sdk/client/base.py +62 -2
- glaip_sdk/client/mcps.py +63 -20
- glaip_sdk/client/tools.py +95 -28
- glaip_sdk/config/constants.py +10 -3
- glaip_sdk/exceptions.py +13 -0
- glaip_sdk/models.py +20 -4
- glaip_sdk/utils/__init__.py +116 -18
- glaip_sdk/utils/client_utils.py +284 -0
- glaip_sdk/utils/rendering/__init__.py +1 -0
- glaip_sdk/utils/rendering/formatting.py +211 -0
- glaip_sdk/utils/rendering/models.py +53 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
- glaip_sdk/utils/rendering/renderer/base.py +827 -0
- glaip_sdk/utils/rendering/renderer/config.py +33 -0
- glaip_sdk/utils/rendering/renderer/console.py +54 -0
- glaip_sdk/utils/rendering/renderer/debug.py +82 -0
- glaip_sdk/utils/rendering/renderer/panels.py +123 -0
- glaip_sdk/utils/rendering/renderer/progress.py +118 -0
- glaip_sdk/utils/rendering/renderer/stream.py +198 -0
- glaip_sdk/utils/rendering/steps.py +168 -0
- glaip_sdk/utils/run_renderer.py +22 -1086
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/METADATA +9 -37
- glaip_sdk-0.0.3.dist-info/RECORD +40 -0
- glaip_sdk/cli/config.py +0 -592
- glaip_sdk/utils.py +0 -167
- glaip_sdk-0.0.1b10.dist-info/RECORD +0 -28
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/__init__.py
CHANGED
|
@@ -5,6 +5,8 @@ Authors:
|
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
8
10
|
from glaip_sdk.client.agents import AgentClient
|
|
9
11
|
from glaip_sdk.client.base import BaseClient
|
|
10
12
|
from glaip_sdk.client.mcps import MCPClient
|
|
@@ -104,10 +106,18 @@ class Client(BaseClient):
|
|
|
104
106
|
return tool
|
|
105
107
|
|
|
106
108
|
def create_tool_from_code(
|
|
107
|
-
self,
|
|
109
|
+
self,
|
|
110
|
+
name: str,
|
|
111
|
+
code: str,
|
|
112
|
+
framework: str = "langchain",
|
|
113
|
+
*,
|
|
114
|
+
description: str | None = None,
|
|
115
|
+
tags: list[str] | None = None,
|
|
108
116
|
) -> Tool:
|
|
109
117
|
"""Create a new tool plugin from code string."""
|
|
110
|
-
tool = self.tools.create_tool_from_code(
|
|
118
|
+
tool = self.tools.create_tool_from_code(
|
|
119
|
+
name, code, framework, description=description, tags=tags
|
|
120
|
+
)
|
|
111
121
|
tool._set_client(self)
|
|
112
122
|
return tool
|
|
113
123
|
|
|
@@ -119,6 +129,16 @@ class Client(BaseClient):
|
|
|
119
129
|
"""Delete a tool by ID."""
|
|
120
130
|
return self.tools.delete_tool(tool_id)
|
|
121
131
|
|
|
132
|
+
def get_tool_script(self, tool_id: str) -> str:
|
|
133
|
+
"""Get tool script content."""
|
|
134
|
+
return self.tools.get_tool_script(tool_id)
|
|
135
|
+
|
|
136
|
+
def update_tool_via_file(self, tool_id: str, file_path: str, **kwargs) -> Tool:
|
|
137
|
+
"""Update a tool plugin via file upload."""
|
|
138
|
+
tool = self.tools.update_tool_via_file(tool_id, file_path, **kwargs)
|
|
139
|
+
tool._set_client(self)
|
|
140
|
+
return tool
|
|
141
|
+
|
|
122
142
|
# ---- MCPs
|
|
123
143
|
def list_mcps(self) -> list[MCP]:
|
|
124
144
|
mcps = self.mcps.list_mcps()
|
|
@@ -150,6 +170,18 @@ class Client(BaseClient):
|
|
|
150
170
|
"""Update an MCP by ID."""
|
|
151
171
|
return self.mcps.update_mcp(mcp_id, **kwargs)
|
|
152
172
|
|
|
173
|
+
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
"""Test MCP connection using configuration."""
|
|
175
|
+
return self.mcps.test_mcp_connection(config)
|
|
176
|
+
|
|
177
|
+
def test_mcp_connection_from_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
+
"""Test MCP connection using configuration (alias)."""
|
|
179
|
+
return self.mcps.test_mcp_connection_from_config(config)
|
|
180
|
+
|
|
181
|
+
def get_mcp_tools_from_config(self, config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
182
|
+
"""Fetch tools from MCP configuration without saving."""
|
|
183
|
+
return self.mcps.get_mcp_tools_from_config(config)
|
|
184
|
+
|
|
153
185
|
def run_agent(self, agent_id: str, message: str, **kwargs) -> str:
|
|
154
186
|
"""Run an agent with a message."""
|
|
155
187
|
return self.agents.run_agent(agent_id, message, **kwargs)
|
|
@@ -160,6 +192,26 @@ class Client(BaseClient):
|
|
|
160
192
|
data = self._request("GET", "/language-models")
|
|
161
193
|
return data or []
|
|
162
194
|
|
|
195
|
+
# ---- Timeout propagation ----
|
|
196
|
+
@property
|
|
197
|
+
def timeout(self) -> float: # type: ignore[override]
|
|
198
|
+
return super().timeout
|
|
199
|
+
|
|
200
|
+
@timeout.setter
|
|
201
|
+
def timeout(self, value: float) -> None: # type: ignore[override]
|
|
202
|
+
# Rebuild the root http client
|
|
203
|
+
BaseClient.timeout.fset(self, value) # call parent setter
|
|
204
|
+
# Propagate the new session to sub-clients so they don't hold a closed client
|
|
205
|
+
try:
|
|
206
|
+
if hasattr(self, "agents"):
|
|
207
|
+
self.agents.http_client = self.http_client
|
|
208
|
+
if hasattr(self, "tools"):
|
|
209
|
+
self.tools.http_client = self.http_client
|
|
210
|
+
if hasattr(self, "mcps"):
|
|
211
|
+
self.mcps.http_client = self.http_client
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
163
215
|
# ---- Aliases (back-compat)
|
|
164
216
|
def get_agent(self, agent_id: str) -> Agent:
|
|
165
217
|
return self.get_agent_by_id(agent_id)
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -7,46 +7,35 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
|
-
import
|
|
10
|
+
from time import monotonic
|
|
11
11
|
from typing import Any, BinaryIO
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
from rich.console import Console
|
|
13
|
+
from rich.console import Console as _Console
|
|
15
14
|
|
|
16
15
|
from glaip_sdk.client.base import BaseClient
|
|
16
|
+
from glaip_sdk.config.constants import (
|
|
17
|
+
DEFAULT_AGENT_FRAMEWORK,
|
|
18
|
+
DEFAULT_AGENT_PROVIDER,
|
|
19
|
+
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
20
|
+
DEFAULT_AGENT_TYPE,
|
|
21
|
+
DEFAULT_AGENT_VERSION,
|
|
22
|
+
DEFAULT_MODEL,
|
|
23
|
+
)
|
|
17
24
|
from glaip_sdk.models import Agent
|
|
18
|
-
from glaip_sdk.utils.
|
|
19
|
-
|
|
25
|
+
from glaip_sdk.utils.client_utils import (
|
|
26
|
+
create_model_instances,
|
|
27
|
+
extract_ids,
|
|
28
|
+
find_by_name,
|
|
29
|
+
iter_sse_events,
|
|
30
|
+
prepare_multipart_data,
|
|
20
31
|
)
|
|
32
|
+
from glaip_sdk.utils.rendering.models import RunStats
|
|
33
|
+
from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
21
34
|
|
|
22
35
|
# Set up module-level logger
|
|
23
36
|
logger = logging.getLogger("glaip_sdk.agents")
|
|
24
37
|
|
|
25
38
|
|
|
26
|
-
def _select_renderer(
|
|
27
|
-
renderer: RichStreamRenderer | str | None, *, verbose: bool = False
|
|
28
|
-
) -> RichStreamRenderer:
|
|
29
|
-
"""Select the appropriate renderer based on input."""
|
|
30
|
-
if isinstance(renderer, RichStreamRenderer):
|
|
31
|
-
return renderer
|
|
32
|
-
|
|
33
|
-
console = Console(file=sys.stdout, force_terminal=sys.stdout.isatty())
|
|
34
|
-
|
|
35
|
-
if renderer in (None, "auto"):
|
|
36
|
-
return RichStreamRenderer(console=console, verbose=verbose)
|
|
37
|
-
if renderer == "json":
|
|
38
|
-
# JSON output is handled by the renderer itself
|
|
39
|
-
return RichStreamRenderer(console=console, verbose=verbose)
|
|
40
|
-
if renderer == "markdown":
|
|
41
|
-
# Markdown output is handled by the renderer itself
|
|
42
|
-
return RichStreamRenderer(console=console, verbose=verbose)
|
|
43
|
-
if renderer == "plain":
|
|
44
|
-
# Plain output is handled by the renderer itself
|
|
45
|
-
return RichStreamRenderer(console=console, verbose=verbose)
|
|
46
|
-
|
|
47
|
-
raise ValueError(f"Unknown renderer: {renderer}")
|
|
48
|
-
|
|
49
|
-
|
|
50
39
|
class AgentClient(BaseClient):
|
|
51
40
|
"""Client for agent operations."""
|
|
52
41
|
|
|
@@ -59,27 +48,10 @@ class AgentClient(BaseClient):
|
|
|
59
48
|
"""
|
|
60
49
|
super().__init__(parent_client=parent_client, **kwargs)
|
|
61
50
|
|
|
62
|
-
def _extract_ids(self, items: list[str | Any] | None) -> list[str] | None:
|
|
63
|
-
"""Extract IDs from a list of objects or strings."""
|
|
64
|
-
if not items:
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
ids = []
|
|
68
|
-
for item in items:
|
|
69
|
-
if isinstance(item, str):
|
|
70
|
-
ids.append(item)
|
|
71
|
-
elif hasattr(item, "id"):
|
|
72
|
-
ids.append(item.id)
|
|
73
|
-
else:
|
|
74
|
-
# Fallback: convert to string
|
|
75
|
-
ids.append(str(item))
|
|
76
|
-
|
|
77
|
-
return ids
|
|
78
|
-
|
|
79
51
|
def list_agents(self) -> list[Agent]:
|
|
80
52
|
"""List all agents."""
|
|
81
53
|
data = self._request("GET", "/agents/")
|
|
82
|
-
return
|
|
54
|
+
return create_model_instances(data, Agent, self)
|
|
83
55
|
|
|
84
56
|
def get_agent_by_id(self, agent_id: str) -> Agent:
|
|
85
57
|
"""Get agent by ID."""
|
|
@@ -93,16 +65,19 @@ class AgentClient(BaseClient):
|
|
|
93
65
|
params["name"] = name
|
|
94
66
|
|
|
95
67
|
data = self._request("GET", "/agents/", params=params)
|
|
96
|
-
|
|
68
|
+
agents = create_model_instances(data, Agent, self)
|
|
69
|
+
if name is None:
|
|
70
|
+
return agents
|
|
71
|
+
return find_by_name(agents, name, case_sensitive=False)
|
|
97
72
|
|
|
98
73
|
def create_agent(
|
|
99
74
|
self,
|
|
100
75
|
name: str,
|
|
101
76
|
instruction: str,
|
|
102
|
-
model: str =
|
|
77
|
+
model: str = DEFAULT_MODEL,
|
|
103
78
|
tools: list[str | Any] | None = None,
|
|
104
79
|
agents: list[str | Any] | None = None,
|
|
105
|
-
timeout: int =
|
|
80
|
+
timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
|
|
106
81
|
**kwargs,
|
|
107
82
|
) -> "Agent":
|
|
108
83
|
"""Create a new agent."""
|
|
@@ -117,16 +92,20 @@ class AgentClient(BaseClient):
|
|
|
117
92
|
raise ValueError("Agent instruction must be at least 10 characters long")
|
|
118
93
|
|
|
119
94
|
# Prepare the creation payload
|
|
120
|
-
payload = {
|
|
95
|
+
payload: dict[str, Any] = {
|
|
121
96
|
"name": name.strip(),
|
|
122
97
|
"instruction": instruction.strip(),
|
|
123
|
-
"type":
|
|
124
|
-
"framework":
|
|
125
|
-
"version":
|
|
126
|
-
"provider":
|
|
127
|
-
"model_name": model or
|
|
98
|
+
"type": DEFAULT_AGENT_TYPE,
|
|
99
|
+
"framework": DEFAULT_AGENT_FRAMEWORK,
|
|
100
|
+
"version": DEFAULT_AGENT_VERSION,
|
|
101
|
+
"provider": DEFAULT_AGENT_PROVIDER,
|
|
102
|
+
"model_name": model or DEFAULT_MODEL, # Ensure model_name is never None
|
|
128
103
|
}
|
|
129
104
|
|
|
105
|
+
# Include default execution timeout if provided
|
|
106
|
+
if timeout is not None:
|
|
107
|
+
payload["timeout"] = str(timeout)
|
|
108
|
+
|
|
130
109
|
# Ensure minimum required metadata for visibility
|
|
131
110
|
if "metadata" not in kwargs:
|
|
132
111
|
kwargs["metadata"] = {}
|
|
@@ -136,8 +115,8 @@ class AgentClient(BaseClient):
|
|
|
136
115
|
kwargs["metadata"]["type"] = "custom"
|
|
137
116
|
|
|
138
117
|
# Extract IDs from tool and agent objects
|
|
139
|
-
tool_ids =
|
|
140
|
-
agent_ids =
|
|
118
|
+
tool_ids = extract_ids(tools)
|
|
119
|
+
agent_ids = extract_ids(agents)
|
|
141
120
|
|
|
142
121
|
# Add tools and agents if provided
|
|
143
122
|
if tool_ids:
|
|
@@ -148,16 +127,13 @@ class AgentClient(BaseClient):
|
|
|
148
127
|
# Add any additional kwargs
|
|
149
128
|
payload.update(kwargs)
|
|
150
129
|
|
|
151
|
-
# Create the agent
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# Fetch the full agent details
|
|
160
|
-
full_agent_data = self._request("GET", f"/agents/{agent_id}")
|
|
130
|
+
# Create the agent and fetch full details
|
|
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
|
+
)
|
|
161
137
|
return Agent(**full_agent_data)._set_client(self)
|
|
162
138
|
|
|
163
139
|
def update_agent(
|
|
@@ -178,14 +154,14 @@ class AgentClient(BaseClient):
|
|
|
178
154
|
"instruction": instruction
|
|
179
155
|
if instruction is not None
|
|
180
156
|
else current_agent.instruction,
|
|
181
|
-
"type":
|
|
182
|
-
"framework":
|
|
183
|
-
"version":
|
|
157
|
+
"type": DEFAULT_AGENT_TYPE, # Required by backend
|
|
158
|
+
"framework": DEFAULT_AGENT_FRAMEWORK, # Required by backend
|
|
159
|
+
"version": DEFAULT_AGENT_VERSION, # Required by backend
|
|
184
160
|
}
|
|
185
161
|
|
|
186
162
|
# Handle model specification
|
|
187
163
|
if model is not None:
|
|
188
|
-
update_data["provider"] =
|
|
164
|
+
update_data["provider"] = DEFAULT_AGENT_PROVIDER # Default provider
|
|
189
165
|
update_data["model_name"] = model
|
|
190
166
|
else:
|
|
191
167
|
# Use current model if available
|
|
@@ -196,12 +172,12 @@ class AgentClient(BaseClient):
|
|
|
196
172
|
update_data["model_name"] = current_agent.agent_config["lm_name"]
|
|
197
173
|
else:
|
|
198
174
|
# Default values
|
|
199
|
-
update_data["provider"] =
|
|
200
|
-
update_data["model_name"] =
|
|
175
|
+
update_data["provider"] = DEFAULT_AGENT_PROVIDER
|
|
176
|
+
update_data["model_name"] = DEFAULT_MODEL
|
|
201
177
|
|
|
202
178
|
# Handle tools and agents
|
|
203
179
|
if "tools" in kwargs:
|
|
204
|
-
tool_ids =
|
|
180
|
+
tool_ids = extract_ids(kwargs["tools"])
|
|
205
181
|
if tool_ids:
|
|
206
182
|
update_data["tools"] = tool_ids
|
|
207
183
|
elif current_agent.tools:
|
|
@@ -211,7 +187,7 @@ class AgentClient(BaseClient):
|
|
|
211
187
|
]
|
|
212
188
|
|
|
213
189
|
if "agents" in kwargs:
|
|
214
|
-
agent_ids =
|
|
190
|
+
agent_ids = extract_ids(kwargs["agents"])
|
|
215
191
|
if agent_ids:
|
|
216
192
|
update_data["agents"] = agent_ids
|
|
217
193
|
elif current_agent.agents:
|
|
@@ -239,30 +215,47 @@ class AgentClient(BaseClient):
|
|
|
239
215
|
message: str,
|
|
240
216
|
files: list[str | BinaryIO] | None = None,
|
|
241
217
|
tty: bool = False,
|
|
242
|
-
stream: bool = True,
|
|
243
218
|
*,
|
|
244
219
|
renderer: RichStreamRenderer | str | None = "auto",
|
|
245
|
-
verbose: bool = False,
|
|
246
220
|
**kwargs,
|
|
247
221
|
) -> str:
|
|
248
222
|
"""Run an agent with a message, streaming via a renderer."""
|
|
249
223
|
# Prepare multipart data if files are provided
|
|
250
|
-
|
|
251
|
-
headers =
|
|
224
|
+
multipart_data = None
|
|
225
|
+
headers = None # None means "don't override client defaults"
|
|
226
|
+
|
|
227
|
+
if files:
|
|
228
|
+
multipart_data = prepare_multipart_data(message, files)
|
|
229
|
+
# Inject optional multipart extras expected by backend
|
|
230
|
+
if "chat_history" in kwargs and kwargs["chat_history"] is not None:
|
|
231
|
+
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
232
|
+
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
233
|
+
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
234
|
+
headers = None # Let httpx set proper multipart boundaries
|
|
235
|
+
|
|
236
|
+
# When streaming, explicitly prefer SSE
|
|
237
|
+
headers = {**(headers or {}), "Accept": "text/event-stream"}
|
|
252
238
|
|
|
253
239
|
if files:
|
|
254
|
-
form_data = self._prepare_multipart_data(message, files)
|
|
255
|
-
headers["Content-Type"] = "multipart/form-data"
|
|
256
240
|
payload = None
|
|
241
|
+
# Use multipart data
|
|
242
|
+
data_payload = multipart_data.data
|
|
243
|
+
files_payload = multipart_data.files
|
|
257
244
|
else:
|
|
258
245
|
payload = {"input": message, **kwargs}
|
|
259
246
|
if tty:
|
|
260
247
|
payload["tty"] = True
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
248
|
+
# Explicitly send stream intent both ways
|
|
249
|
+
payload["stream"] = True
|
|
250
|
+
data_payload = None
|
|
251
|
+
files_payload = None
|
|
252
|
+
|
|
253
|
+
# Choose renderer: use provided instance or create a default
|
|
254
|
+
if isinstance(renderer, RichStreamRenderer):
|
|
255
|
+
r = renderer
|
|
256
|
+
else:
|
|
257
|
+
# Default to a standard rich renderer
|
|
258
|
+
r = RichStreamRenderer(console=_Console())
|
|
266
259
|
|
|
267
260
|
# Try to set some meta early; refine as we receive events
|
|
268
261
|
meta = {
|
|
@@ -278,172 +271,117 @@ class AgentClient(BaseClient):
|
|
|
278
271
|
started_monotonic = None
|
|
279
272
|
finished_monotonic = None
|
|
280
273
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# capture request id if provided
|
|
292
|
-
req_id = response.headers.get("x-request-id") or response.headers.get(
|
|
293
|
-
"x-run-id"
|
|
274
|
+
# MultipartData handles file cleanup automatically
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
response = self.http_client.stream(
|
|
278
|
+
"POST",
|
|
279
|
+
f"/agents/{agent_id}/run",
|
|
280
|
+
json=payload,
|
|
281
|
+
data=data_payload,
|
|
282
|
+
files=files_payload,
|
|
283
|
+
headers=headers,
|
|
294
284
|
)
|
|
295
|
-
if req_id:
|
|
296
|
-
meta["run_id"] = req_id
|
|
297
|
-
r.on_start(meta) # refresh header with run_id
|
|
298
|
-
|
|
299
|
-
for event in self._iter_sse_events(response):
|
|
300
|
-
try:
|
|
301
|
-
ev = json.loads(event["data"])
|
|
302
|
-
except json.JSONDecodeError:
|
|
303
|
-
logger.debug("Non-JSON SSE fragment skipped")
|
|
304
|
-
continue
|
|
305
|
-
|
|
306
|
-
# Start timer at first meaningful event
|
|
307
|
-
if started_monotonic is None and (
|
|
308
|
-
"content" in ev or "status" in ev or ev.get("metadata")
|
|
309
|
-
):
|
|
310
|
-
from time import monotonic
|
|
311
285
|
|
|
312
|
-
|
|
286
|
+
with response as stream_response:
|
|
287
|
+
stream_response.raise_for_status()
|
|
313
288
|
|
|
314
|
-
|
|
289
|
+
# capture request id if provided
|
|
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
|
|
315
296
|
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
|
|
297
|
+
# Get agent run timeout for execution control
|
|
298
|
+
# Prefer CLI-provided timeout, otherwise use default
|
|
299
|
+
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
319
300
|
|
|
320
|
-
|
|
321
|
-
if "content" in ev and ev["content"]:
|
|
322
|
-
# Filter weird backend text like "Artifact received: ..."
|
|
323
|
-
if not ev["content"].startswith("Artifact received:"):
|
|
324
|
-
final_text = ev["content"] # replace with latest
|
|
325
|
-
r.on_event(ev)
|
|
326
|
-
continue
|
|
301
|
+
agent_name = kwargs.get("agent_name")
|
|
327
302
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
303
|
+
for event in iter_sse_events(
|
|
304
|
+
stream_response, timeout_seconds, agent_name
|
|
305
|
+
):
|
|
306
|
+
try:
|
|
307
|
+
ev = json.loads(event["data"])
|
|
308
|
+
except json.JSONDecodeError:
|
|
309
|
+
logger.debug("Non-JSON SSE fragment skipped")
|
|
310
|
+
continue
|
|
332
311
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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()
|
|
337
317
|
|
|
338
|
-
|
|
339
|
-
if kind == "usage":
|
|
340
|
-
stats_usage.update(ev.get("usage") or {})
|
|
341
|
-
continue
|
|
318
|
+
kind = (ev.get("metadata") or {}).get("kind")
|
|
342
319
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if ev.get("model"):
|
|
346
|
-
meta["model"] = ev["model"]
|
|
347
|
-
r.on_start(meta)
|
|
348
|
-
if ev.get("run_id"):
|
|
349
|
-
meta["run_id"] = ev["run_id"]
|
|
350
|
-
r.on_start(meta)
|
|
320
|
+
# Pass event to the renderer (always, don't filter)
|
|
321
|
+
r.on_event(ev)
|
|
351
322
|
|
|
352
|
-
|
|
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)
|
|
353
352
|
|
|
354
353
|
finished_monotonic = monotonic()
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
try:
|
|
356
|
+
r.close()
|
|
357
|
+
finally:
|
|
358
|
+
raise
|
|
359
|
+
except Exception:
|
|
360
|
+
try:
|
|
361
|
+
r.close()
|
|
362
|
+
finally:
|
|
363
|
+
raise
|
|
364
|
+
finally:
|
|
365
|
+
# Ensure we close any opened file handles from multipart
|
|
366
|
+
if multipart_data:
|
|
367
|
+
multipart_data.close()
|
|
355
368
|
|
|
356
369
|
# Finalize stats
|
|
357
|
-
from glaip_sdk.utils.run_renderer import RunStats
|
|
358
|
-
|
|
359
370
|
st = RunStats()
|
|
371
|
+
# Ensure monotonic order (avoid negative -0.0s)
|
|
372
|
+
if started_monotonic is None:
|
|
373
|
+
started_monotonic = finished_monotonic
|
|
374
|
+
|
|
360
375
|
st.started_at = started_monotonic or st.started_at
|
|
361
376
|
st.finished_at = finished_monotonic or st.started_at
|
|
362
377
|
st.usage = stats_usage
|
|
363
378
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for raw in response.iter_lines():
|
|
374
|
-
line = raw.decode("utf-8") if isinstance(raw, bytes) else raw
|
|
375
|
-
|
|
376
|
-
if line == "":
|
|
377
|
-
if buf:
|
|
378
|
-
data = "\n".join(buf)
|
|
379
|
-
yield {
|
|
380
|
-
"event": event_type or "message",
|
|
381
|
-
"id": event_id,
|
|
382
|
-
"data": data,
|
|
383
|
-
}
|
|
384
|
-
buf, event_type, event_id = [], None, None
|
|
385
|
-
continue
|
|
386
|
-
|
|
387
|
-
if line.startswith(":"): # comment
|
|
388
|
-
continue
|
|
389
|
-
if line.startswith("data:"):
|
|
390
|
-
buf.append(line[5:].lstrip())
|
|
391
|
-
elif line.startswith("event:"):
|
|
392
|
-
event_type = line[6:].strip() or None
|
|
393
|
-
elif line.startswith("id:"):
|
|
394
|
-
event_id = line[3:].strip() or None
|
|
395
|
-
|
|
396
|
-
# Flush any remaining data
|
|
397
|
-
if buf:
|
|
398
|
-
yield {
|
|
399
|
-
"event": event_type or "message",
|
|
400
|
-
"id": event_id,
|
|
401
|
-
"data": "\n".join(buf),
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
def _prepare_multipart_data(
|
|
405
|
-
self, message: str, files: list[str | BinaryIO]
|
|
406
|
-
) -> dict[str, Any]:
|
|
407
|
-
"""Prepare multipart form data for file uploads."""
|
|
408
|
-
from pathlib import Path
|
|
409
|
-
|
|
410
|
-
form_data = {"data": {"message": message}}
|
|
411
|
-
file_list = []
|
|
412
|
-
|
|
413
|
-
for file_item in files:
|
|
414
|
-
if isinstance(file_item, str):
|
|
415
|
-
# File path - let httpx stream the file handle
|
|
416
|
-
file_path = Path(file_item)
|
|
417
|
-
if not file_path.exists():
|
|
418
|
-
raise FileNotFoundError(f"File not found: {file_item}")
|
|
419
|
-
|
|
420
|
-
file_list.append(
|
|
421
|
-
(
|
|
422
|
-
"files",
|
|
423
|
-
(
|
|
424
|
-
file_path.name,
|
|
425
|
-
open(file_path, "rb"),
|
|
426
|
-
"application/octet-stream",
|
|
427
|
-
),
|
|
428
|
-
)
|
|
429
|
-
)
|
|
430
|
-
else:
|
|
431
|
-
# File-like object
|
|
432
|
-
if hasattr(file_item, "name"):
|
|
433
|
-
filename = getattr(file_item, "name", "file")
|
|
434
|
-
else:
|
|
435
|
-
filename = "file"
|
|
436
|
-
|
|
437
|
-
if hasattr(file_item, "read"):
|
|
438
|
-
# For file-like objects, we need to read them since httpx expects bytes
|
|
439
|
-
file_content = file_item.read()
|
|
440
|
-
file_list.append(
|
|
441
|
-
("files", (filename, file_content, "application/octet-stream"))
|
|
442
|
-
)
|
|
443
|
-
else:
|
|
444
|
-
raise ValueError(f"Invalid file object: {file_item}")
|
|
445
|
-
|
|
446
|
-
if file_list:
|
|
447
|
-
form_data["files"] = file_list
|
|
448
|
-
|
|
449
|
-
return form_data
|
|
379
|
+
# Prefer explicit content, otherwise fall back to what the renderer saw
|
|
380
|
+
if hasattr(r, "state") and hasattr(r.state, "buffer"):
|
|
381
|
+
rendered_text = "".join(r.state.buffer)
|
|
382
|
+
else:
|
|
383
|
+
rendered_text = ""
|
|
384
|
+
final_payload = final_text or rendered_text or "No response content received."
|
|
385
|
+
|
|
386
|
+
r.on_complete(st)
|
|
387
|
+
return final_payload
|