glaip-sdk 0.0.5b1__py3-none-any.whl → 0.0.6a0__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 +1 -1
- glaip_sdk/branding.py +3 -2
- glaip_sdk/cli/commands/__init__.py +1 -1
- glaip_sdk/cli/commands/agents.py +444 -268
- glaip_sdk/cli/commands/configure.py +12 -11
- glaip_sdk/cli/commands/mcps.py +28 -16
- glaip_sdk/cli/commands/models.py +5 -3
- glaip_sdk/cli/commands/tools.py +109 -102
- glaip_sdk/cli/display.py +38 -16
- glaip_sdk/cli/io.py +1 -1
- glaip_sdk/cli/main.py +26 -5
- glaip_sdk/cli/resolution.py +5 -4
- glaip_sdk/cli/utils.py +376 -157
- glaip_sdk/cli/validators.py +7 -2
- glaip_sdk/client/agents.py +184 -89
- glaip_sdk/client/base.py +24 -13
- glaip_sdk/client/validators.py +154 -94
- glaip_sdk/config/constants.py +0 -2
- glaip_sdk/models.py +4 -4
- glaip_sdk/utils/__init__.py +7 -7
- glaip_sdk/utils/client_utils.py +144 -78
- glaip_sdk/utils/display.py +4 -2
- glaip_sdk/utils/general.py +8 -6
- glaip_sdk/utils/import_export.py +55 -24
- glaip_sdk/utils/rendering/formatting.py +12 -6
- glaip_sdk/utils/rendering/models.py +1 -1
- glaip_sdk/utils/rendering/renderer/base.py +412 -248
- glaip_sdk/utils/rendering/renderer/console.py +6 -5
- glaip_sdk/utils/rendering/renderer/debug.py +94 -52
- glaip_sdk/utils/rendering/renderer/stream.py +93 -48
- glaip_sdk/utils/rendering/steps.py +103 -39
- glaip_sdk/utils/rich_utils.py +1 -1
- glaip_sdk/utils/run_renderer.py +1 -1
- glaip_sdk/utils/serialization.py +3 -1
- glaip_sdk/utils/validation.py +2 -2
- glaip_sdk-0.0.6a0.dist-info/METADATA +183 -0
- glaip_sdk-0.0.6a0.dist-info/RECORD +55 -0
- glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
- glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/validators.py
CHANGED
|
@@ -25,8 +25,67 @@ class ResourceValidator:
|
|
|
25
25
|
"""Check if a name is reserved."""
|
|
26
26
|
return name in cls.RESERVED_NAMES
|
|
27
27
|
|
|
28
|
+
def _is_uuid_string(self, value: str) -> bool:
|
|
29
|
+
"""Check if a string is a valid UUID."""
|
|
30
|
+
try:
|
|
31
|
+
UUID(value)
|
|
32
|
+
return True
|
|
33
|
+
except ValueError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def _resolve_tool_by_name(self, tool_name: str, client: Any) -> str:
|
|
37
|
+
"""Resolve tool name to ID."""
|
|
38
|
+
found_tools = client.find_tools(name=tool_name)
|
|
39
|
+
if len(found_tools) == 1:
|
|
40
|
+
return str(found_tools[0].id)
|
|
41
|
+
elif len(found_tools) > 1:
|
|
42
|
+
raise AmbiguousResourceError(
|
|
43
|
+
f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}"
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
raise NotFoundError(f"Tool not found: {tool_name}")
|
|
47
|
+
|
|
48
|
+
def _resolve_tool_by_name_attribute(self, tool: Tool, client: Any) -> str:
|
|
49
|
+
"""Resolve tool by name attribute."""
|
|
50
|
+
found_tools = client.find_tools(name=tool.name)
|
|
51
|
+
if len(found_tools) == 1:
|
|
52
|
+
return str(found_tools[0].id)
|
|
53
|
+
elif len(found_tools) > 1:
|
|
54
|
+
raise AmbiguousResourceError(
|
|
55
|
+
f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
raise NotFoundError(f"Tool not found: {tool.name}")
|
|
59
|
+
|
|
60
|
+
def _process_tool_string(self, tool: str, client: Any) -> str:
|
|
61
|
+
"""Process a string tool reference."""
|
|
62
|
+
if self._is_uuid_string(tool):
|
|
63
|
+
return tool # Already a UUID string
|
|
64
|
+
else:
|
|
65
|
+
return self._resolve_tool_by_name(tool, client)
|
|
66
|
+
|
|
67
|
+
def _process_tool_object(self, tool: Tool, client: Any) -> str:
|
|
68
|
+
"""Process a Tool object reference."""
|
|
69
|
+
if hasattr(tool, "id") and tool.id is not None:
|
|
70
|
+
return str(tool.id)
|
|
71
|
+
elif isinstance(tool, UUID):
|
|
72
|
+
return str(tool)
|
|
73
|
+
elif hasattr(tool, "name") and tool.name is not None:
|
|
74
|
+
return self._resolve_tool_by_name_attribute(tool, client)
|
|
75
|
+
else:
|
|
76
|
+
raise ValidationError(
|
|
77
|
+
f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _process_single_tool(self, tool: str | Tool, client: Any) -> str:
|
|
81
|
+
"""Process a single tool reference and return its ID."""
|
|
82
|
+
if isinstance(tool, str):
|
|
83
|
+
return self._process_tool_string(tool, client)
|
|
84
|
+
else:
|
|
85
|
+
return self._process_tool_object(tool, client)
|
|
86
|
+
|
|
28
87
|
@classmethod
|
|
29
|
-
def extract_tool_ids(cls, tools: list[str | Tool], client) -> list[str]:
|
|
88
|
+
def extract_tool_ids(cls, tools: list[str | Tool], client: Any) -> list[str]:
|
|
30
89
|
"""Extract tool IDs from a list of tool names, IDs, or Tool objects.
|
|
31
90
|
|
|
32
91
|
For agent creation, the backend expects tool IDs (UUIDs).
|
|
@@ -37,57 +96,81 @@ class ResourceValidator:
|
|
|
37
96
|
"""
|
|
38
97
|
tool_ids = []
|
|
39
98
|
for tool in tools:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
raise NotFoundError(f"Tool not found: {tool}")
|
|
57
|
-
except Exception as e:
|
|
58
|
-
raise ValidationError(
|
|
59
|
-
f"Failed to resolve tool name '{tool}' to ID: {e}"
|
|
60
|
-
)
|
|
61
|
-
elif hasattr(tool, "id") and tool.id is not None: # Tool object with ID
|
|
62
|
-
tool_ids.append(str(tool.id))
|
|
63
|
-
elif isinstance(tool, UUID): # UUID object
|
|
64
|
-
tool_ids.append(str(tool))
|
|
65
|
-
elif (
|
|
66
|
-
hasattr(tool, "name") and tool.name is not None
|
|
67
|
-
): # Tool object with name but no ID
|
|
68
|
-
# Try to find the tool by name and get its ID
|
|
69
|
-
try:
|
|
70
|
-
found_tools = client.find_tools(name=tool.name)
|
|
71
|
-
if len(found_tools) == 1:
|
|
72
|
-
tool_ids.append(str(found_tools[0].id))
|
|
73
|
-
elif len(found_tools) > 1:
|
|
74
|
-
raise AmbiguousResourceError(
|
|
75
|
-
f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}"
|
|
76
|
-
)
|
|
77
|
-
else:
|
|
78
|
-
raise NotFoundError(f"Tool not found: {tool.name}")
|
|
79
|
-
except Exception as e:
|
|
80
|
-
raise ValidationError(
|
|
81
|
-
f"Failed to resolve tool name '{tool.name}' to ID: {e}"
|
|
82
|
-
)
|
|
83
|
-
else:
|
|
99
|
+
try:
|
|
100
|
+
tool_id = cls()._process_single_tool(tool, client)
|
|
101
|
+
tool_ids.append(tool_id)
|
|
102
|
+
except (AmbiguousResourceError, NotFoundError) as e:
|
|
103
|
+
# Determine the tool name for the error message
|
|
104
|
+
tool_name = (
|
|
105
|
+
tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
106
|
+
)
|
|
107
|
+
raise ValidationError(
|
|
108
|
+
f"Failed to resolve tool name '{tool_name}' to ID: {e}"
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
# For other exceptions, wrap them appropriately
|
|
112
|
+
tool_name = (
|
|
113
|
+
tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
114
|
+
)
|
|
84
115
|
raise ValidationError(
|
|
85
|
-
f"
|
|
116
|
+
f"Failed to resolve tool name '{tool_name}' to ID: {e}"
|
|
86
117
|
)
|
|
118
|
+
|
|
87
119
|
return tool_ids
|
|
88
120
|
|
|
121
|
+
def _resolve_agent_by_name(self, agent_name: str, client: Any) -> str:
|
|
122
|
+
"""Resolve agent name to ID."""
|
|
123
|
+
found_agents = client.find_agents(name=agent_name)
|
|
124
|
+
if len(found_agents) == 1:
|
|
125
|
+
return str(found_agents[0].id)
|
|
126
|
+
elif len(found_agents) > 1:
|
|
127
|
+
raise AmbiguousResourceError(
|
|
128
|
+
f"Multiple agents found with name '{agent_name}': {[a.id for a in found_agents]}"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
raise NotFoundError(f"Agent not found: {agent_name}")
|
|
132
|
+
|
|
133
|
+
def _resolve_agent_by_name_attribute(self, agent: Any, client: Any) -> str:
|
|
134
|
+
"""Resolve agent by name attribute."""
|
|
135
|
+
found_agents = client.find_agents(name=agent.name)
|
|
136
|
+
if len(found_agents) == 1:
|
|
137
|
+
return str(found_agents[0].id)
|
|
138
|
+
elif len(found_agents) > 1:
|
|
139
|
+
raise AmbiguousResourceError(
|
|
140
|
+
f"Multiple agents found with name '{agent.name}': {[a.id for a in found_agents]}"
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
raise NotFoundError(f"Agent not found: {agent.name}")
|
|
144
|
+
|
|
145
|
+
def _process_agent_string(self, agent: str, client: Any) -> str:
|
|
146
|
+
"""Process a string agent reference."""
|
|
147
|
+
if self._is_uuid_string(agent):
|
|
148
|
+
return agent # Already a UUID string
|
|
149
|
+
else:
|
|
150
|
+
return self._resolve_agent_by_name(agent, client)
|
|
151
|
+
|
|
152
|
+
def _process_agent_object(self, agent: Any, client: Any) -> str:
|
|
153
|
+
"""Process an Agent object reference."""
|
|
154
|
+
if hasattr(agent, "id") and agent.id is not None:
|
|
155
|
+
return str(agent.id)
|
|
156
|
+
elif isinstance(agent, UUID):
|
|
157
|
+
return str(agent)
|
|
158
|
+
elif hasattr(agent, "name") and agent.name is not None:
|
|
159
|
+
return self._resolve_agent_by_name_attribute(agent, client)
|
|
160
|
+
else:
|
|
161
|
+
raise ValidationError(
|
|
162
|
+
f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _process_single_agent(self, agent: str | Any, client: Any) -> str:
|
|
166
|
+
"""Process a single agent reference and return its ID."""
|
|
167
|
+
if isinstance(agent, str):
|
|
168
|
+
return self._process_agent_string(agent, client)
|
|
169
|
+
else:
|
|
170
|
+
return self._process_agent_object(agent, client)
|
|
171
|
+
|
|
89
172
|
@classmethod
|
|
90
|
-
def extract_agent_ids(cls, agents: list[str | Any], client) -> list[str]:
|
|
173
|
+
def extract_agent_ids(cls, agents: list[str | Any], client: Any) -> list[str]:
|
|
91
174
|
"""Extract agent IDs from a list of agent names, IDs, or agent objects.
|
|
92
175
|
|
|
93
176
|
For agent creation, the backend expects agent IDs (UUIDs).
|
|
@@ -98,57 +181,34 @@ class ResourceValidator:
|
|
|
98
181
|
"""
|
|
99
182
|
agent_ids = []
|
|
100
183
|
for agent in agents:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
agent_ids.append(str(found_agents[0].id))
|
|
112
|
-
elif len(found_agents) > 1:
|
|
113
|
-
raise AmbiguousResourceError(
|
|
114
|
-
f"Multiple agents found with name '{agent}': {[a.id for a in found_agents]}"
|
|
115
|
-
)
|
|
116
|
-
else:
|
|
117
|
-
raise NotFoundError(f"Agent not found: {agent}")
|
|
118
|
-
except Exception as e:
|
|
119
|
-
raise ValidationError(
|
|
120
|
-
f"Failed to resolve agent name '{agent}' to ID: {e}"
|
|
121
|
-
)
|
|
122
|
-
elif hasattr(agent, "id") and agent.id is not None: # Agent object with ID
|
|
123
|
-
agent_ids.append(str(agent.id))
|
|
124
|
-
elif isinstance(agent, UUID): # UUID object
|
|
125
|
-
agent_ids.append(str(agent))
|
|
126
|
-
elif (
|
|
127
|
-
hasattr(agent, "name") and agent.name is not None
|
|
128
|
-
): # Agent object with name but no ID
|
|
129
|
-
# Try to find the agent by name and get its ID
|
|
130
|
-
try:
|
|
131
|
-
found_agents = client.find_agents(name=agent.name)
|
|
132
|
-
if len(found_agents) == 1:
|
|
133
|
-
agent_ids.append(str(found_agents[0].id))
|
|
134
|
-
elif len(found_agents) > 1:
|
|
135
|
-
raise AmbiguousResourceError(
|
|
136
|
-
f"Multiple agents found with name '{agent.name}': {[a.id for a in found_agents]}"
|
|
137
|
-
)
|
|
138
|
-
else:
|
|
139
|
-
raise NotFoundError(f"Agent not found: {agent.name}")
|
|
140
|
-
except Exception as e:
|
|
141
|
-
raise ValidationError(
|
|
142
|
-
f"Failed to resolve agent name '{agent.name}' to ID: {e}"
|
|
143
|
-
)
|
|
144
|
-
else:
|
|
184
|
+
try:
|
|
185
|
+
agent_id = cls()._process_single_agent(agent, client)
|
|
186
|
+
agent_ids.append(agent_id)
|
|
187
|
+
except (AmbiguousResourceError, NotFoundError) as e:
|
|
188
|
+
# Determine the agent name for the error message
|
|
189
|
+
agent_name = (
|
|
190
|
+
agent
|
|
191
|
+
if isinstance(agent, str)
|
|
192
|
+
else getattr(agent, "name", str(agent))
|
|
193
|
+
)
|
|
145
194
|
raise ValidationError(
|
|
146
|
-
f"
|
|
195
|
+
f"Failed to resolve agent name '{agent_name}' to ID: {e}"
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
# For other exceptions, wrap them appropriately
|
|
199
|
+
agent_name = (
|
|
200
|
+
agent
|
|
201
|
+
if isinstance(agent, str)
|
|
202
|
+
else getattr(agent, "name", str(agent))
|
|
147
203
|
)
|
|
204
|
+
raise ValidationError(
|
|
205
|
+
f"Failed to resolve agent name '{agent_name}' to ID: {e}"
|
|
206
|
+
)
|
|
207
|
+
|
|
148
208
|
return agent_ids
|
|
149
209
|
|
|
150
210
|
@classmethod
|
|
151
|
-
def validate_tools_exist(cls, tool_ids: list[str], client) -> None:
|
|
211
|
+
def validate_tools_exist(cls, tool_ids: list[str], client: Any) -> None:
|
|
152
212
|
"""Validate that all tool IDs exist."""
|
|
153
213
|
for tool_id in tool_ids:
|
|
154
214
|
try:
|
|
@@ -157,7 +217,7 @@ class ResourceValidator:
|
|
|
157
217
|
raise ValidationError(f"Tool not found: {tool_id}")
|
|
158
218
|
|
|
159
219
|
@classmethod
|
|
160
|
-
def validate_agents_exist(cls, agent_ids: list[str], client) -> None:
|
|
220
|
+
def validate_agents_exist(cls, agent_ids: list[str], client: Any) -> None:
|
|
161
221
|
"""Validate that all agent IDs exist."""
|
|
162
222
|
for agent_id in agent_ids:
|
|
163
223
|
try:
|
glaip_sdk/config/constants.py
CHANGED
glaip_sdk/models.py
CHANGED
|
@@ -39,7 +39,7 @@ class Agent(BaseModel):
|
|
|
39
39
|
updated_at: datetime | None = None # Backend returns last update timestamp
|
|
40
40
|
_client: Any = None
|
|
41
41
|
|
|
42
|
-
def _set_client(self, client):
|
|
42
|
+
def _set_client(self, client: Any) -> "Agent":
|
|
43
43
|
"""Set the client reference for this resource."""
|
|
44
44
|
self._client = client
|
|
45
45
|
return self
|
|
@@ -121,7 +121,7 @@ class Tool(BaseModel):
|
|
|
121
121
|
tool_file: str | None = None
|
|
122
122
|
_client: Any = None # Will hold client reference
|
|
123
123
|
|
|
124
|
-
def _set_client(self, client):
|
|
124
|
+
def _set_client(self, client: Any) -> "Tool":
|
|
125
125
|
"""Set the client reference for this resource."""
|
|
126
126
|
self._client = client
|
|
127
127
|
return self
|
|
@@ -185,7 +185,7 @@ class MCP(BaseModel):
|
|
|
185
185
|
metadata: dict[str, Any] | None = None
|
|
186
186
|
_client: Any = None # Will hold client reference
|
|
187
187
|
|
|
188
|
-
def _set_client(self, client):
|
|
188
|
+
def _set_client(self, client: Any) -> "MCP":
|
|
189
189
|
"""Set the client reference for this resource."""
|
|
190
190
|
self._client = client
|
|
191
191
|
return self
|
|
@@ -239,7 +239,7 @@ class TTYRenderer:
|
|
|
239
239
|
def __init__(self, use_color: bool = True):
|
|
240
240
|
self.use_color = use_color
|
|
241
241
|
|
|
242
|
-
def render_message(self, message: str, event_type: str = "message"):
|
|
242
|
+
def render_message(self, message: str, event_type: str = "message") -> None:
|
|
243
243
|
"""Render a message with optional color."""
|
|
244
244
|
if event_type == "error":
|
|
245
245
|
print(f"ERROR: {message}", flush=True)
|
glaip_sdk/utils/__init__.py
CHANGED
|
@@ -23,18 +23,18 @@ from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
|
|
|
23
23
|
from glaip_sdk.utils.run_renderer import RichStreamRenderer
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
+
"RICH_AVAILABLE",
|
|
26
27
|
"RichStreamRenderer",
|
|
27
28
|
"RunStats",
|
|
28
29
|
"Step",
|
|
29
30
|
"StepManager",
|
|
30
|
-
"RICH_AVAILABLE",
|
|
31
|
-
"is_uuid",
|
|
32
|
-
"sanitize_name",
|
|
33
|
-
"format_file_size",
|
|
34
31
|
"format_datetime",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
32
|
+
"format_file_size",
|
|
33
|
+
"is_uuid",
|
|
37
34
|
"print_agent_created",
|
|
38
|
-
"print_agent_updated",
|
|
39
35
|
"print_agent_deleted",
|
|
36
|
+
"print_agent_output",
|
|
37
|
+
"print_agent_updated",
|
|
38
|
+
"progress_bar",
|
|
39
|
+
"sanitize_name",
|
|
40
40
|
]
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -9,10 +9,10 @@ Authors:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
-
from collections.abc import AsyncGenerator
|
|
12
|
+
from collections.abc import AsyncGenerator, Iterator
|
|
13
13
|
from contextlib import ExitStack
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any, BinaryIO
|
|
15
|
+
from typing import Any, BinaryIO, NoReturn
|
|
16
16
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
@@ -36,14 +36,19 @@ class MultipartData:
|
|
|
36
36
|
self.files = files
|
|
37
37
|
self._exit_stack = ExitStack()
|
|
38
38
|
|
|
39
|
-
def close(self):
|
|
39
|
+
def close(self) -> None:
|
|
40
40
|
"""Close all opened file handles."""
|
|
41
41
|
self._exit_stack.close()
|
|
42
42
|
|
|
43
|
-
def __enter__(self):
|
|
43
|
+
def __enter__(self) -> "MultipartData":
|
|
44
44
|
return self
|
|
45
45
|
|
|
46
|
-
def __exit__(
|
|
46
|
+
def __exit__(
|
|
47
|
+
self,
|
|
48
|
+
_exc_type: type[BaseException] | None,
|
|
49
|
+
_exc_val: BaseException | None,
|
|
50
|
+
_exc_tb: Any,
|
|
51
|
+
) -> None:
|
|
47
52
|
self.close()
|
|
48
53
|
|
|
49
54
|
|
|
@@ -115,12 +120,38 @@ def find_by_name(
|
|
|
115
120
|
return find_by_name_new(items, name, case_sensitive)
|
|
116
121
|
|
|
117
122
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
def _handle_blank_line(
|
|
124
|
+
buf: list[str],
|
|
125
|
+
event_type: str | None,
|
|
126
|
+
event_id: str | None,
|
|
127
|
+
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
128
|
+
"""Handle blank SSE lines by returning accumulated data if buffer exists."""
|
|
129
|
+
if buf:
|
|
130
|
+
data = "\n".join(buf)
|
|
131
|
+
return (
|
|
132
|
+
[],
|
|
133
|
+
None,
|
|
134
|
+
None,
|
|
135
|
+
{
|
|
136
|
+
"event": event_type or "message",
|
|
137
|
+
"id": event_id,
|
|
138
|
+
"data": data,
|
|
139
|
+
},
|
|
140
|
+
False,
|
|
141
|
+
)
|
|
142
|
+
return buf, event_type, event_id, None, False
|
|
122
143
|
|
|
123
|
-
|
|
144
|
+
|
|
145
|
+
def _handle_data_line(
|
|
146
|
+
line: str,
|
|
147
|
+
buf: list[str],
|
|
148
|
+
event_type: str | None,
|
|
149
|
+
event_id: str | None,
|
|
150
|
+
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
151
|
+
"""Handle data: lines, including [DONE] sentinel marker."""
|
|
152
|
+
data_line = line[5:].lstrip()
|
|
153
|
+
|
|
154
|
+
if data_line.strip() == "[DONE]":
|
|
124
155
|
if buf:
|
|
125
156
|
data = "\n".join(buf)
|
|
126
157
|
return (
|
|
@@ -132,42 +163,59 @@ def _parse_sse_line(line: str, buf: list, event_type: str = None, event_id: str
|
|
|
132
163
|
"id": event_id,
|
|
133
164
|
"data": data,
|
|
134
165
|
},
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
return buf, event_type, event_id, None,
|
|
166
|
+
True,
|
|
167
|
+
)
|
|
168
|
+
return buf, event_type, event_id, None, True
|
|
169
|
+
|
|
170
|
+
buf.append(data_line)
|
|
171
|
+
return buf, event_type, event_id, None, False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _handle_field_line(
|
|
175
|
+
line: str,
|
|
176
|
+
field_type: str,
|
|
177
|
+
current_value: str | None,
|
|
178
|
+
) -> str | None:
|
|
179
|
+
"""Handle event: or id: field lines."""
|
|
180
|
+
if field_type == "event":
|
|
181
|
+
return line[6:].strip() or None
|
|
182
|
+
elif field_type == "id":
|
|
183
|
+
return line[3:].strip() or None
|
|
184
|
+
return current_value
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_sse_line(
|
|
188
|
+
line: str,
|
|
189
|
+
buf: list[str],
|
|
190
|
+
event_type: str | None = None,
|
|
191
|
+
event_id: str | None = None,
|
|
192
|
+
) -> tuple[list[str], str | None, str | None, dict[str, Any] | None, bool]:
|
|
193
|
+
"""Parse a single SSE line and return updated buffer and event metadata."""
|
|
194
|
+
# Normalize CRLF and treat whitespace-only as blank
|
|
195
|
+
line = line.rstrip("\r")
|
|
196
|
+
|
|
197
|
+
if not line.strip(): # blank line
|
|
198
|
+
return _handle_blank_line(buf, event_type, event_id)
|
|
138
199
|
|
|
139
200
|
if line.startswith(":"): # comment
|
|
140
201
|
return buf, event_type, event_id, None, False
|
|
141
202
|
|
|
142
203
|
if line.startswith("data:"):
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
[],
|
|
149
|
-
None,
|
|
150
|
-
None,
|
|
151
|
-
{
|
|
152
|
-
"event": event_type or "message",
|
|
153
|
-
"id": event_id,
|
|
154
|
-
"data": data,
|
|
155
|
-
},
|
|
156
|
-
True,
|
|
157
|
-
) # signal completion
|
|
158
|
-
return buf, event_type, event_id, None, True
|
|
159
|
-
buf.append(data_line)
|
|
160
|
-
elif line.startswith("event:"):
|
|
161
|
-
event_type = line[6:].strip() or None
|
|
204
|
+
return _handle_data_line(line, buf, event_type, event_id)
|
|
205
|
+
|
|
206
|
+
if line.startswith("event:"):
|
|
207
|
+
event_type = _handle_field_line(line, "event", event_type)
|
|
162
208
|
elif line.startswith("id:"):
|
|
163
|
-
event_id = line
|
|
209
|
+
event_id = _handle_field_line(line, "id", event_id)
|
|
164
210
|
|
|
165
211
|
return buf, event_type, event_id, None, False
|
|
166
212
|
|
|
167
213
|
|
|
168
214
|
def _handle_streaming_error(
|
|
169
|
-
e: Exception,
|
|
170
|
-
|
|
215
|
+
e: Exception,
|
|
216
|
+
timeout_seconds: float | None = None,
|
|
217
|
+
agent_name: str | None = None,
|
|
218
|
+
) -> NoReturn:
|
|
171
219
|
"""Handle different types of streaming errors with appropriate logging and exceptions."""
|
|
172
220
|
if isinstance(e, httpx.ReadTimeout):
|
|
173
221
|
logger.error(f"Read timeout during streaming: {e}")
|
|
@@ -199,8 +247,10 @@ def _handle_streaming_error(
|
|
|
199
247
|
|
|
200
248
|
|
|
201
249
|
def iter_sse_events(
|
|
202
|
-
response: httpx.Response,
|
|
203
|
-
|
|
250
|
+
response: httpx.Response,
|
|
251
|
+
timeout_seconds: float | None = None,
|
|
252
|
+
agent_name: str | None = None,
|
|
253
|
+
) -> Iterator[dict[str, Any]]:
|
|
204
254
|
"""Iterate over Server-Sent Events with proper parsing.
|
|
205
255
|
|
|
206
256
|
Args:
|
|
@@ -316,49 +366,65 @@ def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> Multipa
|
|
|
316
366
|
FileNotFoundError: When a file path doesn't exist
|
|
317
367
|
ValueError: When a file object is invalid
|
|
318
368
|
"""
|
|
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
|
+
|
|
319
411
|
# Backend expects 'input' for the main prompt. Keep 'message' for
|
|
320
412
|
# backward-compatibility with any legacy handlers.
|
|
321
413
|
form_data = {"input": message, "message": message, "stream": True}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for
|
|
329
|
-
if isinstance(
|
|
330
|
-
|
|
331
|
-
file_path = Path(file_item)
|
|
332
|
-
if not file_path.exists():
|
|
333
|
-
raise FileNotFoundError(f"File not found: {file_item}")
|
|
334
|
-
|
|
335
|
-
# Open file and register for cleanup
|
|
336
|
-
fh = stack.enter_context(open(file_path, "rb"))
|
|
337
|
-
file_list.append(
|
|
338
|
-
(
|
|
339
|
-
"files",
|
|
340
|
-
(
|
|
341
|
-
file_path.name,
|
|
342
|
-
fh,
|
|
343
|
-
"application/octet-stream",
|
|
344
|
-
),
|
|
345
|
-
)
|
|
346
|
-
)
|
|
414
|
+
stack = ExitStack()
|
|
415
|
+
multipart_data = MultipartData(form_data, [])
|
|
416
|
+
multipart_data._exit_stack = stack
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
file_entries = []
|
|
420
|
+
for item in files:
|
|
421
|
+
if isinstance(item, str):
|
|
422
|
+
file_entries.append(_prepare_path_entry(item, stack))
|
|
347
423
|
else:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
else:
|
|
352
|
-
filename = "file"
|
|
353
|
-
|
|
354
|
-
if hasattr(file_item, "read"):
|
|
355
|
-
# For file-like objects, we need to read them since httpx expects bytes
|
|
356
|
-
file_content = file_item.read()
|
|
357
|
-
file_list.append(
|
|
358
|
-
("files", (filename, file_content, "application/octet-stream"))
|
|
359
|
-
)
|
|
360
|
-
else:
|
|
361
|
-
raise ValueError(f"Invalid file object: {file_item}")
|
|
362
|
-
|
|
363
|
-
multipart_data.files = file_list
|
|
424
|
+
file_entries.append(_prepare_stream_entry(item))
|
|
425
|
+
|
|
426
|
+
multipart_data.files = file_entries
|
|
364
427
|
return multipart_data
|
|
428
|
+
except Exception:
|
|
429
|
+
stack.close()
|
|
430
|
+
raise
|
glaip_sdk/utils/display.py
CHANGED
|
@@ -4,6 +4,8 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
7
9
|
from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
|
|
8
10
|
|
|
9
11
|
|
|
@@ -34,7 +36,7 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
|
34
36
|
print("=" * (len(title) + 8))
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
def print_agent_created(agent, title: str = "🤖 Agent Created") -> None:
|
|
39
|
+
def print_agent_created(agent: Any, title: str = "🤖 Agent Created") -> None:
|
|
38
40
|
"""Print agent creation success with rich formatting.
|
|
39
41
|
|
|
40
42
|
Args:
|
|
@@ -68,7 +70,7 @@ def print_agent_created(agent, title: str = "🤖 Agent Created") -> None:
|
|
|
68
70
|
print(f"Version: {getattr(agent, 'version', '1.0')}")
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
def print_agent_updated(agent) -> None:
|
|
73
|
+
def print_agent_updated(agent: Any) -> None:
|
|
72
74
|
"""Print agent update success with rich formatting.
|
|
73
75
|
|
|
74
76
|
Args:
|