glaip-sdk 0.0.6a0__py3-none-any.whl → 0.0.7__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/_version.py +42 -19
- glaip_sdk/cli/commands/agents.py +17 -26
- glaip_sdk/cli/commands/configure.py +2 -2
- glaip_sdk/cli/commands/mcps.py +2 -4
- glaip_sdk/cli/commands/tools.py +2 -4
- glaip_sdk/cli/display.py +11 -12
- glaip_sdk/cli/utils.py +63 -33
- glaip_sdk/client/agents.py +101 -73
- glaip_sdk/client/base.py +45 -14
- glaip_sdk/client/tools.py +44 -26
- glaip_sdk/models.py +1 -0
- glaip_sdk/utils/client_utils.py +95 -71
- glaip_sdk/utils/import_export.py +3 -1
- glaip_sdk/utils/rendering/renderer/base.py +152 -125
- glaip_sdk/utils/serialization.py +6 -2
- {glaip_sdk-0.0.6a0.dist-info → glaip_sdk-0.0.7.dist-info}/METADATA +1 -1
- {glaip_sdk-0.0.6a0.dist-info → glaip_sdk-0.0.7.dist-info}/RECORD +19 -19
- {glaip_sdk-0.0.6a0.dist-info → glaip_sdk-0.0.7.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.6a0.dist-info → glaip_sdk-0.0.7.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/agents.py
CHANGED
|
@@ -42,6 +42,9 @@ from glaip_sdk.utils.validation import validate_agent_instruction
|
|
|
42
42
|
# API endpoints
|
|
43
43
|
AGENTS_ENDPOINT = "/agents/"
|
|
44
44
|
|
|
45
|
+
# SSE content type
|
|
46
|
+
SSE_CONTENT_TYPE = "text/event-stream"
|
|
47
|
+
|
|
45
48
|
# Set up module-level logger
|
|
46
49
|
logger = logging.getLogger("glaip_sdk.agents")
|
|
47
50
|
|
|
@@ -519,7 +522,7 @@ class AgentClient(BaseClient):
|
|
|
519
522
|
Returns:
|
|
520
523
|
Tuple of (payload, data_payload, files_payload, headers, multipart_data)
|
|
521
524
|
"""
|
|
522
|
-
headers = {"Accept":
|
|
525
|
+
headers = {"Accept": SSE_CONTENT_TYPE}
|
|
523
526
|
|
|
524
527
|
if files:
|
|
525
528
|
# Handle multipart data for file uploads
|
|
@@ -665,10 +668,6 @@ class AgentClient(BaseClient):
|
|
|
665
668
|
return content
|
|
666
669
|
return final_text
|
|
667
670
|
|
|
668
|
-
def _handle_final_response_event(self, ev: dict[str, Any]) -> str:
|
|
669
|
-
"""Handle final response events."""
|
|
670
|
-
return ev.get("content", "")
|
|
671
|
-
|
|
672
671
|
def _handle_usage_event(
|
|
673
672
|
self, ev: dict[str, Any], stats_usage: dict[str, Any]
|
|
674
673
|
) -> None:
|
|
@@ -708,11 +707,11 @@ class AgentClient(BaseClient):
|
|
|
708
707
|
if kind == "artifact":
|
|
709
708
|
return final_text, stats_usage
|
|
710
709
|
|
|
711
|
-
#
|
|
712
|
-
if ev.get("content"):
|
|
710
|
+
# Handle different event types
|
|
711
|
+
if kind == "final_response" and ev.get("content"):
|
|
712
|
+
final_text = ev.get("content", "")
|
|
713
|
+
elif ev.get("content"):
|
|
713
714
|
final_text = self._handle_content_event(ev, final_text)
|
|
714
|
-
elif kind == "final_response" and ev.get("content"):
|
|
715
|
-
final_text = self._handle_final_response_event(ev)
|
|
716
715
|
elif kind == "usage":
|
|
717
716
|
self._handle_usage_event(ev, stats_usage)
|
|
718
717
|
elif kind == "run_info":
|
|
@@ -844,6 +843,75 @@ class AgentClient(BaseClient):
|
|
|
844
843
|
r.on_complete(st)
|
|
845
844
|
return final_payload
|
|
846
845
|
|
|
846
|
+
def _prepare_request_data(
|
|
847
|
+
self,
|
|
848
|
+
message: str,
|
|
849
|
+
files: list[str | BinaryIO] | None,
|
|
850
|
+
**kwargs,
|
|
851
|
+
) -> tuple[dict | None, dict | None, dict | None, dict | None]:
|
|
852
|
+
"""Prepare request data for async agent runs.
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
Tuple of (payload, data_payload, files_payload, headers)
|
|
856
|
+
"""
|
|
857
|
+
if files:
|
|
858
|
+
# Handle multipart data for file uploads
|
|
859
|
+
multipart_data = prepare_multipart_data(message, files)
|
|
860
|
+
# Inject optional multipart extras expected by backend
|
|
861
|
+
if "chat_history" in kwargs and kwargs["chat_history"] is not None:
|
|
862
|
+
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
863
|
+
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
864
|
+
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
865
|
+
|
|
866
|
+
headers = {"Accept": SSE_CONTENT_TYPE}
|
|
867
|
+
return None, multipart_data.data, multipart_data.files, headers
|
|
868
|
+
else:
|
|
869
|
+
# Simple JSON payload for text-only requests
|
|
870
|
+
payload = {"input": message, "stream": True, **kwargs}
|
|
871
|
+
headers = {"Accept": SSE_CONTENT_TYPE}
|
|
872
|
+
return payload, None, None, headers
|
|
873
|
+
|
|
874
|
+
def _create_async_client_config(
|
|
875
|
+
self, timeout: float | None, headers: dict | None
|
|
876
|
+
) -> dict:
|
|
877
|
+
"""Create async client configuration with proper headers and timeout."""
|
|
878
|
+
config = self._build_async_client(timeout or self.timeout)
|
|
879
|
+
if headers:
|
|
880
|
+
config["headers"] = {**config["headers"], **headers}
|
|
881
|
+
return config
|
|
882
|
+
|
|
883
|
+
async def _stream_agent_response(
|
|
884
|
+
self,
|
|
885
|
+
async_client: httpx.AsyncClient,
|
|
886
|
+
agent_id: str,
|
|
887
|
+
payload: dict | None,
|
|
888
|
+
data_payload: dict | None,
|
|
889
|
+
files_payload: dict | None,
|
|
890
|
+
headers: dict | None,
|
|
891
|
+
timeout_seconds: float,
|
|
892
|
+
agent_name: str | None,
|
|
893
|
+
) -> AsyncGenerator[dict, None]:
|
|
894
|
+
"""Stream the agent response and yield parsed JSON chunks."""
|
|
895
|
+
async with async_client.stream(
|
|
896
|
+
"POST",
|
|
897
|
+
f"/agents/{agent_id}/run",
|
|
898
|
+
json=payload,
|
|
899
|
+
data=data_payload,
|
|
900
|
+
files=files_payload,
|
|
901
|
+
headers=headers,
|
|
902
|
+
) as stream_response:
|
|
903
|
+
stream_response.raise_for_status()
|
|
904
|
+
|
|
905
|
+
async for event in aiter_sse_events(
|
|
906
|
+
stream_response, timeout_seconds, agent_name
|
|
907
|
+
):
|
|
908
|
+
try:
|
|
909
|
+
chunk = json.loads(event["data"])
|
|
910
|
+
yield chunk
|
|
911
|
+
except json.JSONDecodeError:
|
|
912
|
+
logger.debug("Non-JSON SSE fragment skipped")
|
|
913
|
+
continue
|
|
914
|
+
|
|
847
915
|
async def arun_agent(
|
|
848
916
|
self,
|
|
849
917
|
agent_id: str,
|
|
@@ -870,74 +938,34 @@ class AgentClient(BaseClient):
|
|
|
870
938
|
httpx.TimeoutException: When general timeout occurs
|
|
871
939
|
Exception: For other unexpected errors
|
|
872
940
|
"""
|
|
873
|
-
# Prepare
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if files:
|
|
878
|
-
multipart_data = prepare_multipart_data(message, files)
|
|
879
|
-
# Inject optional multipart extras expected by backend
|
|
880
|
-
if "chat_history" in kwargs and kwargs["chat_history"] is not None:
|
|
881
|
-
multipart_data.data["chat_history"] = kwargs["chat_history"]
|
|
882
|
-
if "pii_mapping" in kwargs and kwargs["pii_mapping"] is not None:
|
|
883
|
-
multipart_data.data["pii_mapping"] = kwargs["pii_mapping"]
|
|
884
|
-
headers = None # Let httpx set proper multipart boundaries
|
|
941
|
+
# Prepare request data
|
|
942
|
+
payload, data_payload, files_payload, headers = self._prepare_request_data(
|
|
943
|
+
message, files, **kwargs
|
|
944
|
+
)
|
|
885
945
|
|
|
886
|
-
#
|
|
887
|
-
|
|
946
|
+
# Create async client configuration
|
|
947
|
+
async_client_config = self._create_async_client_config(timeout, headers)
|
|
888
948
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
data_payload = multipart_data.data
|
|
893
|
-
files_payload = multipart_data.files
|
|
894
|
-
else:
|
|
895
|
-
payload = {"input": message, **kwargs}
|
|
896
|
-
# Explicitly send stream intent both ways
|
|
897
|
-
payload["stream"] = True
|
|
898
|
-
data_payload = None
|
|
899
|
-
files_payload = None
|
|
900
|
-
|
|
901
|
-
# Use timeout from parameter or instance default
|
|
902
|
-
request_timeout = timeout or self.timeout
|
|
949
|
+
# Get execution timeout for streaming control
|
|
950
|
+
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
951
|
+
agent_name = kwargs.get("agent_name")
|
|
903
952
|
|
|
904
953
|
try:
|
|
905
|
-
|
|
906
|
-
if headers:
|
|
907
|
-
async_client_config["headers"] = {
|
|
908
|
-
**async_client_config["headers"],
|
|
909
|
-
**headers,
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
# Create async client for this request
|
|
954
|
+
# Create async client and stream response
|
|
913
955
|
async with httpx.AsyncClient(**async_client_config) as async_client:
|
|
914
|
-
async
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
headers
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
# Prefer parameter timeout, otherwise use default
|
|
926
|
-
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
927
|
-
|
|
928
|
-
agent_name = kwargs.get("agent_name")
|
|
929
|
-
|
|
930
|
-
async for event in aiter_sse_events(
|
|
931
|
-
stream_response, timeout_seconds, agent_name
|
|
932
|
-
):
|
|
933
|
-
try:
|
|
934
|
-
chunk = json.loads(event["data"])
|
|
935
|
-
yield chunk
|
|
936
|
-
except json.JSONDecodeError:
|
|
937
|
-
logger.debug("Non-JSON SSE fragment skipped")
|
|
938
|
-
continue
|
|
956
|
+
async for chunk in self._stream_agent_response(
|
|
957
|
+
async_client,
|
|
958
|
+
agent_id,
|
|
959
|
+
payload,
|
|
960
|
+
data_payload,
|
|
961
|
+
files_payload,
|
|
962
|
+
headers,
|
|
963
|
+
timeout_seconds,
|
|
964
|
+
agent_name,
|
|
965
|
+
):
|
|
966
|
+
yield chunk
|
|
939
967
|
|
|
940
968
|
finally:
|
|
941
|
-
# Ensure
|
|
942
|
-
|
|
943
|
-
|
|
969
|
+
# Ensure cleanup - this is handled by the calling context
|
|
970
|
+
# but we keep this for safety in case of future changes
|
|
971
|
+
pass
|
glaip_sdk/client/base.py
CHANGED
|
@@ -254,25 +254,25 @@ class BaseClient:
|
|
|
254
254
|
client_log.error(f"Retry failed for {method} {endpoint}: {e}")
|
|
255
255
|
raise e
|
|
256
256
|
|
|
257
|
-
def
|
|
258
|
-
"""
|
|
257
|
+
def _parse_response_content(self, response: httpx.Response) -> Any | None:
|
|
258
|
+
"""Parse response content based on content type."""
|
|
259
259
|
if response.status_code == 204:
|
|
260
260
|
return None
|
|
261
261
|
|
|
262
|
-
parsed = None
|
|
263
262
|
content_type = response.headers.get("content-type", "").lower()
|
|
264
263
|
if "json" in content_type:
|
|
265
264
|
try:
|
|
266
|
-
|
|
265
|
+
return response.json()
|
|
267
266
|
except ValueError:
|
|
268
267
|
pass
|
|
269
268
|
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
self._raise_api_error(response.status_code, response.text)
|
|
269
|
+
if 200 <= response.status_code < 300:
|
|
270
|
+
return response.text
|
|
271
|
+
else:
|
|
272
|
+
return None # Let _handle_response deal with error status codes
|
|
275
273
|
|
|
274
|
+
def _handle_success_response(self, parsed: Any) -> Any:
|
|
275
|
+
"""Handle successful response with success flag."""
|
|
276
276
|
if isinstance(parsed, dict) and "success" in parsed:
|
|
277
277
|
if parsed.get("success"):
|
|
278
278
|
return parsed.get("data", parsed)
|
|
@@ -280,14 +280,45 @@ class BaseClient:
|
|
|
280
280
|
error_type = parsed.get("error", "UnknownError")
|
|
281
281
|
message = parsed.get("message", "Unknown error")
|
|
282
282
|
self._raise_api_error(
|
|
283
|
-
|
|
283
|
+
400,
|
|
284
|
+
message,
|
|
285
|
+
error_type,
|
|
286
|
+
payload=parsed, # Using 400 as status since original response had error
|
|
284
287
|
)
|
|
285
288
|
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
return parsed
|
|
290
|
+
|
|
291
|
+
def _get_error_message(self, response: httpx.Response) -> str:
|
|
292
|
+
"""Extract error message from response, preferring parsed content."""
|
|
293
|
+
# Try to get error message from parsed content if available
|
|
294
|
+
error_message = response.text
|
|
295
|
+
try:
|
|
296
|
+
parsed = response.json()
|
|
297
|
+
if isinstance(parsed, dict) and "message" in parsed:
|
|
298
|
+
error_message = parsed["message"]
|
|
299
|
+
elif isinstance(parsed, str):
|
|
300
|
+
error_message = parsed
|
|
301
|
+
except (ValueError, TypeError):
|
|
302
|
+
pass # Use response.text as fallback
|
|
303
|
+
return error_message
|
|
304
|
+
|
|
305
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
306
|
+
"""Handle HTTP response with proper error handling."""
|
|
307
|
+
# Handle no-content success before general error handling
|
|
308
|
+
if response.status_code == 204:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# Handle error status codes
|
|
312
|
+
if not (200 <= response.status_code < 300):
|
|
313
|
+
error_message = self._get_error_message(response)
|
|
314
|
+
self._raise_api_error(response.status_code, error_message)
|
|
315
|
+
return None # Won't be reached but helps with type checking
|
|
316
|
+
|
|
317
|
+
parsed = self._parse_response_content(response)
|
|
318
|
+
if parsed is None:
|
|
319
|
+
return None
|
|
288
320
|
|
|
289
|
-
|
|
290
|
-
self._raise_api_error(response.status_code, message, payload=parsed)
|
|
321
|
+
return self._handle_success_response(parsed)
|
|
291
322
|
|
|
292
323
|
def _raise_api_error(
|
|
293
324
|
self,
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -208,6 +208,44 @@ class ToolClient(BaseClient):
|
|
|
208
208
|
|
|
209
209
|
return payload
|
|
210
210
|
|
|
211
|
+
def _handle_description_update(
|
|
212
|
+
self, update_data: dict[str, Any], description: str | None, current_tool: Tool
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Handle description field in update payload."""
|
|
215
|
+
if description is not None:
|
|
216
|
+
update_data["description"] = description.strip()
|
|
217
|
+
elif hasattr(current_tool, "description") and current_tool.description:
|
|
218
|
+
update_data["description"] = current_tool.description
|
|
219
|
+
|
|
220
|
+
def _handle_tags_update(
|
|
221
|
+
self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Handle tags field in update payload."""
|
|
224
|
+
if kwargs.get("tags"):
|
|
225
|
+
if isinstance(kwargs["tags"], list):
|
|
226
|
+
update_data["tags"] = ",".join(
|
|
227
|
+
str(tag).strip() for tag in kwargs["tags"]
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
update_data["tags"] = str(kwargs["tags"])
|
|
231
|
+
elif hasattr(current_tool, "tags") and current_tool.tags:
|
|
232
|
+
# Preserve existing tags if present
|
|
233
|
+
if isinstance(current_tool.tags, list):
|
|
234
|
+
update_data["tags"] = ",".join(
|
|
235
|
+
str(tag).strip() for tag in current_tool.tags
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
update_data["tags"] = str(current_tool.tags)
|
|
239
|
+
|
|
240
|
+
def _handle_additional_kwargs(
|
|
241
|
+
self, update_data: dict[str, Any], kwargs: dict[str, Any]
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Handle additional kwargs in update payload."""
|
|
244
|
+
excluded_keys = {"tags", "framework", "version"}
|
|
245
|
+
for key, value in kwargs.items():
|
|
246
|
+
if key not in excluded_keys:
|
|
247
|
+
update_data[key] = value
|
|
248
|
+
|
|
211
249
|
def _build_update_payload(
|
|
212
250
|
self,
|
|
213
251
|
current_tool: Tool,
|
|
@@ -242,34 +280,14 @@ class ToolClient(BaseClient):
|
|
|
242
280
|
),
|
|
243
281
|
}
|
|
244
282
|
|
|
245
|
-
# Handle description
|
|
246
|
-
|
|
247
|
-
update_data["description"] = description.strip()
|
|
248
|
-
elif hasattr(current_tool, "description") and current_tool.description:
|
|
249
|
-
update_data["description"] = current_tool.description
|
|
283
|
+
# Handle description update
|
|
284
|
+
self._handle_description_update(update_data, description, current_tool)
|
|
250
285
|
|
|
251
|
-
# Handle tags
|
|
252
|
-
|
|
253
|
-
if isinstance(kwargs["tags"], list):
|
|
254
|
-
update_data["tags"] = ",".join(
|
|
255
|
-
str(tag).strip() for tag in kwargs["tags"]
|
|
256
|
-
)
|
|
257
|
-
else:
|
|
258
|
-
update_data["tags"] = str(kwargs["tags"])
|
|
259
|
-
elif hasattr(current_tool, "tags") and current_tool.tags:
|
|
260
|
-
# Preserve existing tags if present
|
|
261
|
-
if isinstance(current_tool.tags, list):
|
|
262
|
-
update_data["tags"] = ",".join(
|
|
263
|
-
str(tag).strip() for tag in current_tool.tags
|
|
264
|
-
)
|
|
265
|
-
else:
|
|
266
|
-
update_data["tags"] = str(current_tool.tags)
|
|
286
|
+
# Handle tags update
|
|
287
|
+
self._handle_tags_update(update_data, kwargs, current_tool)
|
|
267
288
|
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
for key, value in kwargs.items():
|
|
271
|
-
if key not in excluded_keys:
|
|
272
|
-
update_data[key] = value
|
|
289
|
+
# Handle additional kwargs
|
|
290
|
+
self._handle_additional_kwargs(update_data, kwargs)
|
|
273
291
|
|
|
274
292
|
return update_data
|
|
275
293
|
|
glaip_sdk/models.py
CHANGED
|
@@ -119,6 +119,7 @@ class Tool(BaseModel):
|
|
|
119
119
|
version: str | None = None
|
|
120
120
|
tool_script: str | None = None
|
|
121
121
|
tool_file: str | None = None
|
|
122
|
+
tags: str | list[str] | None = None
|
|
122
123
|
_client: Any = None # Will hold client reference
|
|
123
124
|
|
|
124
125
|
def _set_client(self, client: Any) -> "Tool":
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -246,6 +246,33 @@ def _handle_streaming_error(
|
|
|
246
246
|
raise
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
def _process_sse_line(
|
|
250
|
+
line: str, buf: list[str], event_type: str | None, event_id: str | None
|
|
251
|
+
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
252
|
+
"""Process a single SSE line and return updated state."""
|
|
253
|
+
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
254
|
+
buf, event_type, event_id, event_data, completed = result
|
|
255
|
+
return buf, event_type, event_id, event_data, completed
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _yield_event_data(event_data: dict[str, Any] | None) -> Iterator[dict[str, Any]]:
|
|
259
|
+
"""Yield event data if available."""
|
|
260
|
+
if event_data:
|
|
261
|
+
yield event_data
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _flush_remaining_buffer(
|
|
265
|
+
buf: list[str], event_type: str | None, event_id: str | None
|
|
266
|
+
) -> Iterator[dict[str, Any]]:
|
|
267
|
+
"""Flush any remaining data in buffer."""
|
|
268
|
+
if buf:
|
|
269
|
+
yield {
|
|
270
|
+
"event": event_type or "message",
|
|
271
|
+
"id": event_id,
|
|
272
|
+
"data": "\n".join(buf),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
249
276
|
def iter_sse_events(
|
|
250
277
|
response: httpx.Response,
|
|
251
278
|
timeout_seconds: float | None = None,
|
|
@@ -276,25 +303,16 @@ def iter_sse_events(
|
|
|
276
303
|
if line is None:
|
|
277
304
|
continue
|
|
278
305
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
else: # normal case
|
|
283
|
-
buf, event_type, event_id, event_data = result
|
|
284
|
-
completed = False
|
|
306
|
+
buf, event_type, event_id, event_data, completed = _process_sse_line(
|
|
307
|
+
line, buf, event_type, event_id
|
|
308
|
+
)
|
|
285
309
|
|
|
286
|
-
|
|
287
|
-
yield event_data
|
|
310
|
+
yield from _yield_event_data(event_data)
|
|
288
311
|
if completed:
|
|
289
312
|
return
|
|
290
313
|
|
|
291
314
|
# Flush any remaining data
|
|
292
|
-
|
|
293
|
-
yield {
|
|
294
|
-
"event": event_type or "message",
|
|
295
|
-
"id": event_id,
|
|
296
|
-
"data": "\n".join(buf),
|
|
297
|
-
}
|
|
315
|
+
yield from _flush_remaining_buffer(buf, event_type, event_id)
|
|
298
316
|
|
|
299
317
|
except Exception as e:
|
|
300
318
|
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
@@ -329,11 +347,7 @@ async def aiter_sse_events(
|
|
|
329
347
|
continue
|
|
330
348
|
|
|
331
349
|
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
332
|
-
|
|
333
|
-
buf, event_type, event_id, event_data, completed = result
|
|
334
|
-
else: # normal case
|
|
335
|
-
buf, event_type, event_id, event_data = result
|
|
336
|
-
completed = False
|
|
350
|
+
buf, event_type, event_id, event_data, completed = result
|
|
337
351
|
|
|
338
352
|
if event_data:
|
|
339
353
|
yield event_data
|
|
@@ -352,6 +366,66 @@ async def aiter_sse_events(
|
|
|
352
366
|
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
353
367
|
|
|
354
368
|
|
|
369
|
+
def _create_form_data(message: str) -> dict[str, Any]:
|
|
370
|
+
"""Create form data with message and stream flag."""
|
|
371
|
+
return {"input": message, "message": message, "stream": True}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _prepare_file_entry(
|
|
375
|
+
item: str | BinaryIO, stack: ExitStack
|
|
376
|
+
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
377
|
+
"""Prepare a single file entry for multipart data."""
|
|
378
|
+
if isinstance(item, str):
|
|
379
|
+
return _prepare_path_entry(item, stack)
|
|
380
|
+
else:
|
|
381
|
+
return _prepare_stream_entry(item)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _prepare_path_entry(
|
|
385
|
+
path_str: str, stack: ExitStack
|
|
386
|
+
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
387
|
+
"""Prepare a file path entry."""
|
|
388
|
+
file_path = Path(path_str)
|
|
389
|
+
if not file_path.exists():
|
|
390
|
+
raise FileNotFoundError(f"File not found: {path_str}")
|
|
391
|
+
|
|
392
|
+
handle = stack.enter_context(open(file_path, "rb"))
|
|
393
|
+
return (
|
|
394
|
+
"files",
|
|
395
|
+
(
|
|
396
|
+
file_path.name,
|
|
397
|
+
handle,
|
|
398
|
+
"application/octet-stream",
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _prepare_stream_entry(
|
|
404
|
+
file_obj: BinaryIO,
|
|
405
|
+
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
406
|
+
"""Prepare a file object entry."""
|
|
407
|
+
if not hasattr(file_obj, "read"):
|
|
408
|
+
raise ValueError(f"Invalid file object: {file_obj}")
|
|
409
|
+
|
|
410
|
+
raw_name = getattr(file_obj, "name", "file")
|
|
411
|
+
filename = Path(raw_name).name if raw_name else "file"
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
if hasattr(file_obj, "seek"):
|
|
415
|
+
file_obj.seek(0)
|
|
416
|
+
except (OSError, ValueError):
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
"files",
|
|
421
|
+
(
|
|
422
|
+
filename,
|
|
423
|
+
file_obj,
|
|
424
|
+
"application/octet-stream",
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
355
429
|
def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
|
|
356
430
|
"""Prepare multipart form data for file uploads.
|
|
357
431
|
|
|
@@ -366,63 +440,13 @@ def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> Multipa
|
|
|
366
440
|
FileNotFoundError: When a file path doesn't exist
|
|
367
441
|
ValueError: When a file object is invalid
|
|
368
442
|
"""
|
|
369
|
-
|
|
370
|
-
def _prepare_path_entry(
|
|
371
|
-
path_str: str, stack: ExitStack
|
|
372
|
-
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
373
|
-
file_path = Path(path_str)
|
|
374
|
-
if not file_path.exists():
|
|
375
|
-
raise FileNotFoundError(f"File not found: {path_str}")
|
|
376
|
-
|
|
377
|
-
handle = stack.enter_context(open(file_path, "rb"))
|
|
378
|
-
return (
|
|
379
|
-
"files",
|
|
380
|
-
(
|
|
381
|
-
file_path.name,
|
|
382
|
-
handle,
|
|
383
|
-
"application/octet-stream",
|
|
384
|
-
),
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
def _prepare_stream_entry(
|
|
388
|
-
file_obj: BinaryIO,
|
|
389
|
-
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
390
|
-
if not hasattr(file_obj, "read"):
|
|
391
|
-
raise ValueError(f"Invalid file object: {file_obj}")
|
|
392
|
-
|
|
393
|
-
raw_name = getattr(file_obj, "name", "file")
|
|
394
|
-
filename = Path(raw_name).name if raw_name else "file"
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
if hasattr(file_obj, "seek"):
|
|
398
|
-
file_obj.seek(0)
|
|
399
|
-
except (OSError, ValueError):
|
|
400
|
-
pass
|
|
401
|
-
|
|
402
|
-
return (
|
|
403
|
-
"files",
|
|
404
|
-
(
|
|
405
|
-
filename,
|
|
406
|
-
file_obj,
|
|
407
|
-
"application/octet-stream",
|
|
408
|
-
),
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
# Backend expects 'input' for the main prompt. Keep 'message' for
|
|
412
|
-
# backward-compatibility with any legacy handlers.
|
|
413
|
-
form_data = {"input": message, "message": message, "stream": True}
|
|
443
|
+
form_data = _create_form_data(message)
|
|
414
444
|
stack = ExitStack()
|
|
415
445
|
multipart_data = MultipartData(form_data, [])
|
|
416
446
|
multipart_data._exit_stack = stack
|
|
417
447
|
|
|
418
448
|
try:
|
|
419
|
-
file_entries = []
|
|
420
|
-
for item in files:
|
|
421
|
-
if isinstance(item, str):
|
|
422
|
-
file_entries.append(_prepare_path_entry(item, stack))
|
|
423
|
-
else:
|
|
424
|
-
file_entries.append(_prepare_stream_entry(item))
|
|
425
|
-
|
|
449
|
+
file_entries = [_prepare_file_entry(item, stack) for item in files]
|
|
426
450
|
multipart_data.files = file_entries
|
|
427
451
|
return multipart_data
|
|
428
452
|
except Exception:
|
glaip_sdk/utils/import_export.py
CHANGED
|
@@ -40,7 +40,9 @@ def extract_ids_from_export(items: list[Any]) -> list[str]:
|
|
|
40
40
|
return ids
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def convert_export_to_import_format(
|
|
43
|
+
def convert_export_to_import_format(
|
|
44
|
+
data: dict[str, Any],
|
|
45
|
+
) -> dict[str, Any]:
|
|
44
46
|
"""Convert export format to import-compatible format (extract IDs from objects).
|
|
45
47
|
|
|
46
48
|
Args:
|