glaip-sdk 0.6.12__py3-none-any.whl → 0.6.15__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 +42 -5
- {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.15.dist-info}/METADATA +32 -37
- glaip_sdk-0.6.15.dist-info/RECORD +12 -0
- {glaip_sdk-0.6.12.dist-info → glaip_sdk-0.6.15.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.15.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.15.dist-info/top_level.txt +1 -0
- glaip_sdk/agents/__init__.py +0 -27
- glaip_sdk/agents/base.py +0 -1191
- glaip_sdk/cli/__init__.py +0 -9
- glaip_sdk/cli/account_store.py +0 -540
- glaip_sdk/cli/agent_config.py +0 -78
- glaip_sdk/cli/auth.py +0 -699
- glaip_sdk/cli/commands/__init__.py +0 -5
- glaip_sdk/cli/commands/accounts.py +0 -746
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/common_config.py +0 -101
- glaip_sdk/cli/commands/configure.py +0 -896
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/models.py +0 -69
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/commands/transcripts.py +0 -755
- glaip_sdk/cli/commands/update.py +0 -61
- glaip_sdk/cli/config.py +0 -95
- glaip_sdk/cli/constants.py +0 -38
- glaip_sdk/cli/context.py +0 -150
- glaip_sdk/cli/core/__init__.py +0 -79
- glaip_sdk/cli/core/context.py +0 -124
- glaip_sdk/cli/core/output.py +0 -846
- glaip_sdk/cli/core/prompting.py +0 -649
- glaip_sdk/cli/core/rendering.py +0 -187
- glaip_sdk/cli/display.py +0 -355
- glaip_sdk/cli/hints.py +0 -57
- glaip_sdk/cli/io.py +0 -112
- glaip_sdk/cli/main.py +0 -604
- glaip_sdk/cli/masking.py +0 -136
- glaip_sdk/cli/mcp_validators.py +0 -287
- glaip_sdk/cli/pager.py +0 -266
- glaip_sdk/cli/parsers/__init__.py +0 -7
- glaip_sdk/cli/parsers/json_input.py +0 -177
- glaip_sdk/cli/resolution.py +0 -67
- glaip_sdk/cli/rich_helpers.py +0 -27
- glaip_sdk/cli/slash/__init__.py +0 -15
- glaip_sdk/cli/slash/accounts_controller.py +0 -578
- glaip_sdk/cli/slash/accounts_shared.py +0 -75
- glaip_sdk/cli/slash/agent_session.py +0 -285
- glaip_sdk/cli/slash/prompt.py +0 -256
- glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
- glaip_sdk/cli/slash/session.py +0 -1708
- glaip_sdk/cli/slash/tui/__init__.py +0 -9
- glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
- glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
- glaip_sdk/cli/slash/tui/loading.py +0 -58
- glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
- glaip_sdk/cli/transcript/__init__.py +0 -31
- glaip_sdk/cli/transcript/cache.py +0 -536
- glaip_sdk/cli/transcript/capture.py +0 -329
- glaip_sdk/cli/transcript/export.py +0 -38
- glaip_sdk/cli/transcript/history.py +0 -815
- glaip_sdk/cli/transcript/launcher.py +0 -77
- glaip_sdk/cli/transcript/viewer.py +0 -374
- glaip_sdk/cli/update_notifier.py +0 -290
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk/cli/validators.py +0 -238
- glaip_sdk/client/__init__.py +0 -11
- glaip_sdk/client/_agent_payloads.py +0 -520
- glaip_sdk/client/agent_runs.py +0 -147
- glaip_sdk/client/agents.py +0 -1335
- glaip_sdk/client/base.py +0 -502
- glaip_sdk/client/main.py +0 -249
- glaip_sdk/client/mcps.py +0 -370
- glaip_sdk/client/run_rendering.py +0 -700
- glaip_sdk/client/shared.py +0 -21
- glaip_sdk/client/tools.py +0 -661
- glaip_sdk/client/validators.py +0 -198
- glaip_sdk/config/constants.py +0 -52
- glaip_sdk/mcps/__init__.py +0 -21
- glaip_sdk/mcps/base.py +0 -345
- glaip_sdk/models/__init__.py +0 -90
- glaip_sdk/models/agent.py +0 -47
- glaip_sdk/models/agent_runs.py +0 -116
- glaip_sdk/models/common.py +0 -42
- glaip_sdk/models/mcp.py +0 -33
- glaip_sdk/models/tool.py +0 -33
- glaip_sdk/payload_schemas/__init__.py +0 -7
- glaip_sdk/payload_schemas/agent.py +0 -85
- glaip_sdk/registry/__init__.py +0 -55
- glaip_sdk/registry/agent.py +0 -164
- glaip_sdk/registry/base.py +0 -139
- glaip_sdk/registry/mcp.py +0 -253
- glaip_sdk/registry/tool.py +0 -232
- glaip_sdk/runner/__init__.py +0 -59
- glaip_sdk/runner/base.py +0 -84
- glaip_sdk/runner/deps.py +0 -115
- glaip_sdk/runner/langgraph.py +0 -782
- glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
- glaip_sdk/runner/tool_adapter/__init__.py +0 -18
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
- glaip_sdk/tools/__init__.py +0 -22
- glaip_sdk/tools/base.py +0 -435
- glaip_sdk/utils/__init__.py +0 -86
- glaip_sdk/utils/a2a/__init__.py +0 -34
- glaip_sdk/utils/a2a/event_processor.py +0 -188
- glaip_sdk/utils/agent_config.py +0 -194
- glaip_sdk/utils/bundler.py +0 -267
- glaip_sdk/utils/client.py +0 -111
- glaip_sdk/utils/client_utils.py +0 -486
- glaip_sdk/utils/datetime_helpers.py +0 -58
- glaip_sdk/utils/discovery.py +0 -78
- glaip_sdk/utils/display.py +0 -135
- glaip_sdk/utils/export.py +0 -143
- glaip_sdk/utils/general.py +0 -61
- glaip_sdk/utils/import_export.py +0 -168
- glaip_sdk/utils/import_resolver.py +0 -492
- glaip_sdk/utils/instructions.py +0 -101
- glaip_sdk/utils/rendering/__init__.py +0 -115
- glaip_sdk/utils/rendering/formatting.py +0 -264
- glaip_sdk/utils/rendering/layout/__init__.py +0 -64
- glaip_sdk/utils/rendering/layout/panels.py +0 -156
- glaip_sdk/utils/rendering/layout/progress.py +0 -202
- glaip_sdk/utils/rendering/layout/summary.py +0 -74
- glaip_sdk/utils/rendering/layout/transcript.py +0 -606
- glaip_sdk/utils/rendering/models.py +0 -85
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
- glaip_sdk/utils/rendering/renderer/base.py +0 -1024
- glaip_sdk/utils/rendering/renderer/config.py +0 -27
- glaip_sdk/utils/rendering/renderer/console.py +0 -55
- glaip_sdk/utils/rendering/renderer/debug.py +0 -178
- glaip_sdk/utils/rendering/renderer/factory.py +0 -138
- glaip_sdk/utils/rendering/renderer/stream.py +0 -202
- glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
- glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
- glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
- glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
- glaip_sdk/utils/rendering/state.py +0 -204
- glaip_sdk/utils/rendering/step_tree_state.py +0 -100
- glaip_sdk/utils/rendering/steps/__init__.py +0 -34
- glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
- glaip_sdk/utils/rendering/steps/format.py +0 -176
- glaip_sdk/utils/rendering/steps/manager.py +0 -387
- glaip_sdk/utils/rendering/timing.py +0 -36
- glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
- glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
- glaip_sdk/utils/resource_refs.py +0 -195
- glaip_sdk/utils/run_renderer.py +0 -41
- glaip_sdk/utils/runtime_config.py +0 -425
- glaip_sdk/utils/serialization.py +0 -424
- glaip_sdk/utils/sync.py +0 -142
- glaip_sdk/utils/tool_detection.py +0 -33
- glaip_sdk/utils/validation.py +0 -264
- glaip_sdk-0.6.12.dist-info/RECORD +0 -159
- glaip_sdk-0.6.12.dist-info/entry_points.txt +0 -3
glaip_sdk/utils/client.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"""Client singleton management for GLAIP SDK.
|
|
2
|
-
|
|
3
|
-
This module provides a singleton pattern for the GLAIP SDK client instance
|
|
4
|
-
used by the agents runtime. Uses a class-based singleton pattern consistent
|
|
5
|
-
with the registry implementations.
|
|
6
|
-
|
|
7
|
-
Thread Safety:
|
|
8
|
-
The singleton is created lazily on first access. In Python, the GIL ensures
|
|
9
|
-
that class attribute assignment is atomic, making this pattern safe for
|
|
10
|
-
multi-threaded access. For multiprocessing, each process gets its own
|
|
11
|
-
client instance (no shared state across processes).
|
|
12
|
-
|
|
13
|
-
Authors:
|
|
14
|
-
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from dotenv import load_dotenv
|
|
20
|
-
from glaip_sdk.client import Client
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class _ClientSingleton:
|
|
24
|
-
"""Singleton holder for GLAIP SDK Client.
|
|
25
|
-
|
|
26
|
-
This class follows the same pattern as registry singletons
|
|
27
|
-
(_ToolRegistrySingleton, _MCPRegistrySingleton, _AgentRegistrySingleton).
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
_instance: Client | None = None
|
|
31
|
-
|
|
32
|
-
@classmethod
|
|
33
|
-
def get_instance(cls) -> Client:
|
|
34
|
-
"""Get or create the singleton client instance.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
The singleton client instance.
|
|
38
|
-
|
|
39
|
-
Example:
|
|
40
|
-
>>> from glaip_sdk.utils.client import get_client
|
|
41
|
-
>>> client = get_client()
|
|
42
|
-
>>> agents = client.list_agents()
|
|
43
|
-
"""
|
|
44
|
-
if cls._instance is None:
|
|
45
|
-
load_dotenv()
|
|
46
|
-
cls._instance = Client()
|
|
47
|
-
return cls._instance
|
|
48
|
-
|
|
49
|
-
@classmethod
|
|
50
|
-
def set_instance(cls, client: Client) -> None:
|
|
51
|
-
"""Set the singleton client instance.
|
|
52
|
-
|
|
53
|
-
Useful for testing or when you need to configure the client manually.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
client: The client instance to use.
|
|
57
|
-
|
|
58
|
-
Example:
|
|
59
|
-
>>> from glaip_sdk import Client
|
|
60
|
-
>>> from glaip_sdk.utils.client import set_client
|
|
61
|
-
>>> client = Client(api_key="my-key")
|
|
62
|
-
>>> set_client(client)
|
|
63
|
-
"""
|
|
64
|
-
cls._instance = client
|
|
65
|
-
|
|
66
|
-
@classmethod
|
|
67
|
-
def reset(cls) -> None:
|
|
68
|
-
"""Reset the singleton client instance.
|
|
69
|
-
|
|
70
|
-
Useful for testing to ensure a fresh client is created.
|
|
71
|
-
"""
|
|
72
|
-
cls._instance = None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_client() -> Client:
|
|
76
|
-
"""Get or create singleton client instance.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
The singleton client instance.
|
|
80
|
-
|
|
81
|
-
Example:
|
|
82
|
-
>>> from glaip_sdk.utils.client import get_client
|
|
83
|
-
>>> client = get_client()
|
|
84
|
-
>>> agents = client.list_agents()
|
|
85
|
-
"""
|
|
86
|
-
return _ClientSingleton.get_instance()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def set_client(client: Client) -> None:
|
|
90
|
-
"""Set the singleton client instance.
|
|
91
|
-
|
|
92
|
-
Useful for testing or when you need to configure the client manually.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
client: The client instance to use.
|
|
96
|
-
|
|
97
|
-
Example:
|
|
98
|
-
>>> from glaip_sdk import Client
|
|
99
|
-
>>> from glaip_sdk.utils.client import set_client
|
|
100
|
-
>>> client = Client(api_key="my-key")
|
|
101
|
-
>>> set_client(client)
|
|
102
|
-
"""
|
|
103
|
-
_ClientSingleton.set_instance(client)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def reset_client() -> None:
|
|
107
|
-
"""Reset the singleton client instance.
|
|
108
|
-
|
|
109
|
-
Useful for testing to ensure a fresh client is created.
|
|
110
|
-
"""
|
|
111
|
-
_ClientSingleton.reset()
|
glaip_sdk/utils/client_utils.py
DELETED
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Utility functions for AIP SDK clients.
|
|
3
|
-
|
|
4
|
-
This module contains generic utility functions that can be reused across
|
|
5
|
-
different client types (agents, tools, etc.).
|
|
6
|
-
|
|
7
|
-
Authors:
|
|
8
|
-
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import logging
|
|
12
|
-
from collections.abc import AsyncGenerator, Iterator
|
|
13
|
-
from contextlib import ExitStack
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any, BinaryIO, NoReturn
|
|
16
|
-
|
|
17
|
-
import httpx
|
|
18
|
-
from glaip_sdk.exceptions import AgentTimeoutError
|
|
19
|
-
from glaip_sdk.models import AgentResponse, MCPResponse, ToolResponse
|
|
20
|
-
from glaip_sdk.utils.resource_refs import extract_ids as extract_ids_new
|
|
21
|
-
from glaip_sdk.utils.resource_refs import find_by_name as find_by_name_new
|
|
22
|
-
|
|
23
|
-
# Set up module-level logger
|
|
24
|
-
logger = logging.getLogger("glaip_sdk.client_utils")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class MultipartData:
|
|
28
|
-
"""Container for multipart form data with automatic file handle cleanup."""
|
|
29
|
-
|
|
30
|
-
def __init__(self, data: dict[str, Any], files: list[tuple[str, Any]]):
|
|
31
|
-
"""Initialize multipart data container.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
data: Form data dictionary
|
|
35
|
-
files: List of file tuples for multipart form
|
|
36
|
-
"""
|
|
37
|
-
self.data = data
|
|
38
|
-
self.files = files
|
|
39
|
-
self._exit_stack = ExitStack()
|
|
40
|
-
|
|
41
|
-
def close(self) -> None:
|
|
42
|
-
"""Close all opened file handles."""
|
|
43
|
-
self._exit_stack.close()
|
|
44
|
-
|
|
45
|
-
def __enter__(self) -> "MultipartData":
|
|
46
|
-
"""Enter context manager.
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
Self instance for context manager protocol
|
|
50
|
-
"""
|
|
51
|
-
return self
|
|
52
|
-
|
|
53
|
-
def __exit__(
|
|
54
|
-
self,
|
|
55
|
-
_exc_type: type[BaseException] | None,
|
|
56
|
-
_exc_val: BaseException | None,
|
|
57
|
-
_exc_tb: Any,
|
|
58
|
-
) -> None:
|
|
59
|
-
"""Exit context manager and close all file handles.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
_exc_type: Exception type (unused)
|
|
63
|
-
_exc_val: Exception value (unused)
|
|
64
|
-
_exc_tb: Exception traceback (unused)
|
|
65
|
-
"""
|
|
66
|
-
self.close()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def extract_ids(items: list[str | Any] | None) -> list[str] | None:
|
|
70
|
-
"""Extract IDs from a list of objects or strings.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
items: List of items that may be strings, objects with .id, or other types
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
List of extracted IDs, or None if items is empty/None
|
|
77
|
-
|
|
78
|
-
Note:
|
|
79
|
-
This function maintains backward compatibility by returning None for empty input.
|
|
80
|
-
New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
|
|
81
|
-
"""
|
|
82
|
-
if not items:
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
result = extract_ids_new(items)
|
|
86
|
-
return result if result else None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def create_model_instances(data: list[dict] | None, model_class: type, client: Any) -> list[Any]:
|
|
90
|
-
"""Create model instances from API data with client association.
|
|
91
|
-
|
|
92
|
-
This is a common pattern used across different clients (agents, tools, mcps)
|
|
93
|
-
to create model instances and associate them with the client.
|
|
94
|
-
|
|
95
|
-
For runtime classes (Agent, Tool, MCP) that have a from_response method,
|
|
96
|
-
this function will use the corresponding Response model to parse the API data
|
|
97
|
-
and then create the runtime instance using from_response.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
data: List of dictionaries from API response
|
|
101
|
-
model_class: The model class to instantiate
|
|
102
|
-
client: The client instance to associate with models
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
List of model instances with client association
|
|
106
|
-
"""
|
|
107
|
-
if not data:
|
|
108
|
-
return []
|
|
109
|
-
|
|
110
|
-
# Check if the model_class has a from_response method (runtime class pattern)
|
|
111
|
-
if hasattr(model_class, "from_response"):
|
|
112
|
-
# Map runtime classes to their response models
|
|
113
|
-
response_model_map = {
|
|
114
|
-
"Agent": AgentResponse,
|
|
115
|
-
"Tool": ToolResponse,
|
|
116
|
-
"MCP": MCPResponse,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
response_model = response_model_map.get(model_class.__name__)
|
|
120
|
-
if response_model:
|
|
121
|
-
instances = []
|
|
122
|
-
for item_data in data:
|
|
123
|
-
response = response_model(**item_data)
|
|
124
|
-
instance = model_class.from_response(response, client=client)
|
|
125
|
-
instances.append(instance)
|
|
126
|
-
return instances
|
|
127
|
-
|
|
128
|
-
# Fallback to direct instantiation for other classes
|
|
129
|
-
return [model_class(**item_data)._set_client(client) for item_data in data]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def find_by_name(items: list[Any], name: str, case_sensitive: bool = False) -> list[Any]:
|
|
133
|
-
"""Filter items by name with optional case sensitivity.
|
|
134
|
-
|
|
135
|
-
This is a common pattern used across different clients for client-side
|
|
136
|
-
filtering when the backend doesn't support name query parameters.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
items: List of items to filter
|
|
140
|
-
name: Name to search for
|
|
141
|
-
case_sensitive: Whether the search should be case sensitive
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
Filtered list of items matching the name
|
|
145
|
-
|
|
146
|
-
Note:
|
|
147
|
-
This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
|
|
148
|
-
"""
|
|
149
|
-
return find_by_name_new(items, name, case_sensitive)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _handle_blank_line(
|
|
153
|
-
buf: list[str],
|
|
154
|
-
event_type: str | None,
|
|
155
|
-
event_id: str | None,
|
|
156
|
-
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
157
|
-
"""Handle blank SSE lines by returning accumulated data if buffer exists."""
|
|
158
|
-
if buf:
|
|
159
|
-
data = "\n".join(buf)
|
|
160
|
-
return (
|
|
161
|
-
[],
|
|
162
|
-
None,
|
|
163
|
-
None,
|
|
164
|
-
{
|
|
165
|
-
"event": event_type or "message",
|
|
166
|
-
"id": event_id,
|
|
167
|
-
"data": data,
|
|
168
|
-
},
|
|
169
|
-
False,
|
|
170
|
-
)
|
|
171
|
-
return buf, event_type, event_id, None, False
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _handle_data_line(
|
|
175
|
-
line: str,
|
|
176
|
-
buf: list[str],
|
|
177
|
-
event_type: str | None,
|
|
178
|
-
event_id: str | None,
|
|
179
|
-
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
180
|
-
"""Handle data: lines, including [DONE] sentinel marker."""
|
|
181
|
-
data_line = line[5:].lstrip()
|
|
182
|
-
|
|
183
|
-
if data_line.strip() == "[DONE]":
|
|
184
|
-
if buf:
|
|
185
|
-
data = "\n".join(buf)
|
|
186
|
-
return (
|
|
187
|
-
[],
|
|
188
|
-
None,
|
|
189
|
-
None,
|
|
190
|
-
{
|
|
191
|
-
"event": event_type or "message",
|
|
192
|
-
"id": event_id,
|
|
193
|
-
"data": data,
|
|
194
|
-
},
|
|
195
|
-
True,
|
|
196
|
-
)
|
|
197
|
-
return buf, event_type, event_id, None, True
|
|
198
|
-
|
|
199
|
-
buf.append(data_line)
|
|
200
|
-
return buf, event_type, event_id, None, False
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _handle_field_line(
|
|
204
|
-
line: str,
|
|
205
|
-
field_type: str,
|
|
206
|
-
current_value: str | None,
|
|
207
|
-
) -> str | None:
|
|
208
|
-
"""Handle event: or id: field lines."""
|
|
209
|
-
if field_type == "event":
|
|
210
|
-
return line[6:].strip() or None
|
|
211
|
-
elif field_type == "id":
|
|
212
|
-
return line[3:].strip() or None
|
|
213
|
-
return current_value
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _parse_sse_line(
|
|
217
|
-
line: str,
|
|
218
|
-
buf: list[str],
|
|
219
|
-
event_type: str | None = None,
|
|
220
|
-
event_id: str | None = None,
|
|
221
|
-
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
222
|
-
"""Parse a single SSE line and return updated buffer and event metadata."""
|
|
223
|
-
# Normalize CRLF and treat whitespace-only as blank
|
|
224
|
-
line = line.rstrip("\r")
|
|
225
|
-
|
|
226
|
-
if not line.strip(): # blank line
|
|
227
|
-
return _handle_blank_line(buf, event_type, event_id)
|
|
228
|
-
|
|
229
|
-
if line.startswith(":"): # comment
|
|
230
|
-
return buf, event_type, event_id, None, False
|
|
231
|
-
|
|
232
|
-
if line.startswith("data:"):
|
|
233
|
-
return _handle_data_line(line, buf, event_type, event_id)
|
|
234
|
-
|
|
235
|
-
if line.startswith("event:"):
|
|
236
|
-
event_type = _handle_field_line(line, "event", event_type)
|
|
237
|
-
elif line.startswith("id:"):
|
|
238
|
-
event_id = _handle_field_line(line, "id", event_id)
|
|
239
|
-
|
|
240
|
-
return buf, event_type, event_id, None, False
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _handle_streaming_error(
|
|
244
|
-
e: Exception,
|
|
245
|
-
timeout_seconds: float | None = None,
|
|
246
|
-
agent_name: str | None = None,
|
|
247
|
-
) -> NoReturn:
|
|
248
|
-
"""Handle different types of streaming errors with appropriate logging and exceptions."""
|
|
249
|
-
if isinstance(e, httpx.ReadTimeout):
|
|
250
|
-
logger.error(f"Read timeout during streaming: {e}")
|
|
251
|
-
logger.error("This usually indicates the backend is taking too long to respond")
|
|
252
|
-
logger.error("Consider increasing the timeout value or checking backend performance")
|
|
253
|
-
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
254
|
-
|
|
255
|
-
elif isinstance(e, httpx.TimeoutException):
|
|
256
|
-
logger.error(f"General timeout during streaming: {e}")
|
|
257
|
-
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
258
|
-
|
|
259
|
-
elif isinstance(e, httpx.StreamClosed):
|
|
260
|
-
logger.error(f"Stream closed unexpectedly during streaming: {e}")
|
|
261
|
-
logger.error("This may indicate a backend issue or network problem")
|
|
262
|
-
logger.error("The response stream was closed before all data could be read")
|
|
263
|
-
raise
|
|
264
|
-
|
|
265
|
-
elif isinstance(e, httpx.ConnectError):
|
|
266
|
-
logger.error(f"Connection error during streaming: {e}")
|
|
267
|
-
logger.error("Check your network connection and backend availability")
|
|
268
|
-
raise
|
|
269
|
-
|
|
270
|
-
else:
|
|
271
|
-
logger.error(f"Unexpected error during streaming: {e}")
|
|
272
|
-
logger.error(f"Error type: {type(e).__name__}")
|
|
273
|
-
raise
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _process_sse_line(
|
|
277
|
-
line: str, buf: list[str], event_type: str | None, event_id: str | None
|
|
278
|
-
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
279
|
-
"""Process a single SSE line and return updated state."""
|
|
280
|
-
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
281
|
-
buf, event_type, event_id, event_data, completed = result
|
|
282
|
-
return buf, event_type, event_id, event_data, completed
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def _yield_event_data(event_data: dict[str, Any] | None) -> Iterator[dict[str, Any]]:
|
|
286
|
-
"""Yield event data if available."""
|
|
287
|
-
if event_data:
|
|
288
|
-
yield event_data
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _flush_remaining_buffer(buf: list[str], event_type: str | None, event_id: str | None) -> Iterator[dict[str, Any]]:
|
|
292
|
-
"""Flush any remaining data in buffer."""
|
|
293
|
-
if buf:
|
|
294
|
-
yield {
|
|
295
|
-
"event": event_type or "message",
|
|
296
|
-
"id": event_id,
|
|
297
|
-
"data": "\n".join(buf),
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def iter_sse_events(
|
|
302
|
-
response: httpx.Response,
|
|
303
|
-
timeout_seconds: float | None = None,
|
|
304
|
-
agent_name: str | None = None,
|
|
305
|
-
) -> Iterator[dict[str, Any]]:
|
|
306
|
-
"""Iterate over Server-Sent Events with proper parsing.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
response: HTTP response object with streaming content
|
|
310
|
-
timeout_seconds: Timeout duration in seconds (for error messages)
|
|
311
|
-
agent_name: Agent name (for error messages)
|
|
312
|
-
|
|
313
|
-
Yields:
|
|
314
|
-
Dictionary with event data, type, and ID
|
|
315
|
-
|
|
316
|
-
Raises:
|
|
317
|
-
AgentTimeoutError: When agent execution times out
|
|
318
|
-
httpx.TimeoutException: When general timeout occurs
|
|
319
|
-
Exception: For other unexpected errors
|
|
320
|
-
"""
|
|
321
|
-
buf = []
|
|
322
|
-
event_type = None
|
|
323
|
-
event_id = None
|
|
324
|
-
|
|
325
|
-
try:
|
|
326
|
-
for raw in response.iter_lines():
|
|
327
|
-
line = raw.decode("utf-8") if isinstance(raw, bytes) else raw
|
|
328
|
-
if line is None:
|
|
329
|
-
continue
|
|
330
|
-
|
|
331
|
-
buf, event_type, event_id, event_data, completed = _process_sse_line(line, buf, event_type, event_id)
|
|
332
|
-
|
|
333
|
-
yield from _yield_event_data(event_data)
|
|
334
|
-
if completed:
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
# Flush any remaining data
|
|
338
|
-
yield from _flush_remaining_buffer(buf, event_type, event_id)
|
|
339
|
-
|
|
340
|
-
except Exception as e:
|
|
341
|
-
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
async def aiter_sse_events(
|
|
345
|
-
response: httpx.Response, timeout_seconds: float = None, agent_name: str = None
|
|
346
|
-
) -> AsyncGenerator[dict, None]:
|
|
347
|
-
"""Async iterate over Server-Sent Events with proper parsing.
|
|
348
|
-
|
|
349
|
-
Args:
|
|
350
|
-
response: HTTP response object with streaming content
|
|
351
|
-
timeout_seconds: Timeout duration in seconds (for error messages)
|
|
352
|
-
agent_name: Agent name (for error messages)
|
|
353
|
-
|
|
354
|
-
Yields:
|
|
355
|
-
Dictionary with event data, type, and ID
|
|
356
|
-
|
|
357
|
-
Raises:
|
|
358
|
-
AgentTimeoutError: When agent execution times out
|
|
359
|
-
httpx.TimeoutException: When general timeout occurs
|
|
360
|
-
Exception: For other unexpected errors
|
|
361
|
-
"""
|
|
362
|
-
buf = []
|
|
363
|
-
event_type = None
|
|
364
|
-
event_id = None
|
|
365
|
-
|
|
366
|
-
try:
|
|
367
|
-
async for raw in response.aiter_lines():
|
|
368
|
-
line = raw
|
|
369
|
-
if line is None:
|
|
370
|
-
continue
|
|
371
|
-
|
|
372
|
-
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
373
|
-
buf, event_type, event_id, event_data, completed = result
|
|
374
|
-
|
|
375
|
-
if event_data:
|
|
376
|
-
yield event_data
|
|
377
|
-
if completed:
|
|
378
|
-
return
|
|
379
|
-
|
|
380
|
-
# Flush any remaining data
|
|
381
|
-
if buf:
|
|
382
|
-
yield {
|
|
383
|
-
"event": event_type or "message",
|
|
384
|
-
"id": event_id,
|
|
385
|
-
"data": "\n".join(buf),
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
except Exception as e:
|
|
389
|
-
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def _create_form_data(message: str) -> dict[str, Any]:
|
|
393
|
-
"""Create form data with message and stream flag."""
|
|
394
|
-
return {"input": message, "message": message, "stream": True}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _prepare_file_entry(item: str | BinaryIO, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
398
|
-
"""Prepare a single file entry for multipart data."""
|
|
399
|
-
if isinstance(item, str):
|
|
400
|
-
return _prepare_path_entry(item, stack)
|
|
401
|
-
else:
|
|
402
|
-
return _prepare_stream_entry(item)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def _prepare_path_entry(path_str: str, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
406
|
-
"""Prepare a file path entry."""
|
|
407
|
-
file_path = Path(path_str)
|
|
408
|
-
if not file_path.exists():
|
|
409
|
-
raise FileNotFoundError(f"File not found: {path_str}")
|
|
410
|
-
|
|
411
|
-
handle = stack.enter_context(open(file_path, "rb"))
|
|
412
|
-
return (
|
|
413
|
-
"files",
|
|
414
|
-
(
|
|
415
|
-
file_path.name,
|
|
416
|
-
handle,
|
|
417
|
-
"application/octet-stream",
|
|
418
|
-
),
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def _prepare_stream_entry(
|
|
423
|
-
file_obj: BinaryIO,
|
|
424
|
-
) -> tuple[str, tuple[str, BinaryIO, str]]:
|
|
425
|
-
"""Prepare a file object entry."""
|
|
426
|
-
if not hasattr(file_obj, "read"):
|
|
427
|
-
raise ValueError(f"Invalid file object: {file_obj}")
|
|
428
|
-
|
|
429
|
-
raw_name = getattr(file_obj, "name", "file")
|
|
430
|
-
filename = Path(raw_name).name if raw_name else "file"
|
|
431
|
-
|
|
432
|
-
try:
|
|
433
|
-
if hasattr(file_obj, "seek"):
|
|
434
|
-
file_obj.seek(0)
|
|
435
|
-
except (OSError, ValueError):
|
|
436
|
-
pass
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
"files",
|
|
440
|
-
(
|
|
441
|
-
filename,
|
|
442
|
-
file_obj,
|
|
443
|
-
"application/octet-stream",
|
|
444
|
-
),
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def add_kwargs_to_payload(payload: dict[str, Any], kwargs: dict[str, Any], excluded_keys: set[str]) -> None:
|
|
449
|
-
"""Add kwargs to payload excluding specified keys.
|
|
450
|
-
|
|
451
|
-
Args:
|
|
452
|
-
payload: Payload dictionary to update.
|
|
453
|
-
kwargs: Keyword arguments to add.
|
|
454
|
-
excluded_keys: Keys to exclude from kwargs.
|
|
455
|
-
"""
|
|
456
|
-
for key, value in kwargs.items():
|
|
457
|
-
if key not in excluded_keys:
|
|
458
|
-
payload[key] = value
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
|
|
462
|
-
"""Prepare multipart form data for file uploads.
|
|
463
|
-
|
|
464
|
-
Args:
|
|
465
|
-
message: Text message to include with the upload
|
|
466
|
-
files: List of file paths or file-like objects
|
|
467
|
-
|
|
468
|
-
Returns:
|
|
469
|
-
MultipartData object with automatic file handle cleanup
|
|
470
|
-
|
|
471
|
-
Raises:
|
|
472
|
-
FileNotFoundError: When a file path doesn't exist
|
|
473
|
-
ValueError: When a file object is invalid
|
|
474
|
-
"""
|
|
475
|
-
form_data = _create_form_data(message)
|
|
476
|
-
stack = ExitStack()
|
|
477
|
-
multipart_data = MultipartData(form_data, [])
|
|
478
|
-
multipart_data._exit_stack = stack
|
|
479
|
-
|
|
480
|
-
try:
|
|
481
|
-
file_entries = [_prepare_file_entry(item, stack) for item in files]
|
|
482
|
-
multipart_data.files = file_entries
|
|
483
|
-
return multipart_data
|
|
484
|
-
except Exception:
|
|
485
|
-
stack.close()
|
|
486
|
-
raise
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Shared datetime parsing helpers used across CLI and rendering modules."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
__all__ = ["coerce_datetime", "from_numeric_timestamp"]
|
|
9
|
-
|
|
10
|
-
_Z_SUFFIX = "+00:00"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def from_numeric_timestamp(raw_value: Any) -> datetime | None:
|
|
14
|
-
"""Convert unix timestamp-like values to datetime with sanity checks."""
|
|
15
|
-
try:
|
|
16
|
-
candidate = float(raw_value)
|
|
17
|
-
except Exception:
|
|
18
|
-
return None
|
|
19
|
-
|
|
20
|
-
if candidate < 1_000_000_000:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
return datetime.fromtimestamp(candidate, tz=timezone.utc)
|
|
25
|
-
except Exception:
|
|
26
|
-
return None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _parse_iso(value: str | None) -> datetime | None:
|
|
30
|
-
"""Parse ISO8601 strings while tolerating legacy 'Z' suffixes."""
|
|
31
|
-
if not value:
|
|
32
|
-
return None
|
|
33
|
-
try:
|
|
34
|
-
return datetime.fromisoformat(value.replace("Z", _Z_SUFFIX))
|
|
35
|
-
except Exception:
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def coerce_datetime(value: Any) -> datetime | None:
|
|
40
|
-
"""Best-effort conversion of assorted timestamp inputs to aware UTC datetimes."""
|
|
41
|
-
if value is None:
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
if isinstance(value, datetime):
|
|
45
|
-
dt = value
|
|
46
|
-
elif isinstance(value, (int, float)):
|
|
47
|
-
dt = from_numeric_timestamp(value)
|
|
48
|
-
elif isinstance(value, str):
|
|
49
|
-
dt = _parse_iso(value) or from_numeric_timestamp(value)
|
|
50
|
-
else:
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
if dt is None:
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
if dt.tzinfo is None:
|
|
57
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
58
|
-
return dt.astimezone(timezone.utc)
|