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.
Files changed (41) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/branding.py +3 -2
  3. glaip_sdk/cli/commands/__init__.py +1 -1
  4. glaip_sdk/cli/commands/agents.py +444 -268
  5. glaip_sdk/cli/commands/configure.py +12 -11
  6. glaip_sdk/cli/commands/mcps.py +28 -16
  7. glaip_sdk/cli/commands/models.py +5 -3
  8. glaip_sdk/cli/commands/tools.py +109 -102
  9. glaip_sdk/cli/display.py +38 -16
  10. glaip_sdk/cli/io.py +1 -1
  11. glaip_sdk/cli/main.py +26 -5
  12. glaip_sdk/cli/resolution.py +5 -4
  13. glaip_sdk/cli/utils.py +376 -157
  14. glaip_sdk/cli/validators.py +7 -2
  15. glaip_sdk/client/agents.py +184 -89
  16. glaip_sdk/client/base.py +24 -13
  17. glaip_sdk/client/validators.py +154 -94
  18. glaip_sdk/config/constants.py +0 -2
  19. glaip_sdk/models.py +4 -4
  20. glaip_sdk/utils/__init__.py +7 -7
  21. glaip_sdk/utils/client_utils.py +144 -78
  22. glaip_sdk/utils/display.py +4 -2
  23. glaip_sdk/utils/general.py +8 -6
  24. glaip_sdk/utils/import_export.py +55 -24
  25. glaip_sdk/utils/rendering/formatting.py +12 -6
  26. glaip_sdk/utils/rendering/models.py +1 -1
  27. glaip_sdk/utils/rendering/renderer/base.py +412 -248
  28. glaip_sdk/utils/rendering/renderer/console.py +6 -5
  29. glaip_sdk/utils/rendering/renderer/debug.py +94 -52
  30. glaip_sdk/utils/rendering/renderer/stream.py +93 -48
  31. glaip_sdk/utils/rendering/steps.py +103 -39
  32. glaip_sdk/utils/rich_utils.py +1 -1
  33. glaip_sdk/utils/run_renderer.py +1 -1
  34. glaip_sdk/utils/serialization.py +3 -1
  35. glaip_sdk/utils/validation.py +2 -2
  36. glaip_sdk-0.0.6a0.dist-info/METADATA +183 -0
  37. glaip_sdk-0.0.6a0.dist-info/RECORD +55 -0
  38. glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
  39. glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
  40. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/WHEEL +0 -0
  41. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/entry_points.txt +0 -0
@@ -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
- if isinstance(tool, str):
41
- # Check if it's already a UUID
42
- try:
43
- UUID(tool)
44
- tool_ids.append(tool) # Already a UUID string
45
- except ValueError:
46
- # It's a name, try to find the tool and get its ID
47
- try:
48
- found_tools = client.find_tools(name=tool)
49
- if len(found_tools) == 1:
50
- tool_ids.append(str(found_tools[0].id))
51
- elif len(found_tools) > 1:
52
- raise AmbiguousResourceError(
53
- f"Multiple tools found with name '{tool}': {[t.id for t in found_tools]}"
54
- )
55
- else:
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"Invalid tool reference: {tool} - must have 'id' or 'name' attribute"
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
- if isinstance(agent, str):
102
- # Check if it's already a UUID
103
- try:
104
- UUID(agent)
105
- agent_ids.append(agent) # Already a UUID string
106
- except ValueError:
107
- # It's a name, try to find the agent and get its ID
108
- try:
109
- found_agents = client.find_agents(name=agent)
110
- if len(found_agents) == 1:
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"Invalid agent reference: {agent} - must have 'id' or 'name' attribute"
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:
@@ -13,9 +13,7 @@ DEFAULT_TIMEOUT = 30.0
13
13
  DEFAULT_AGENT_RUN_TIMEOUT = 300
14
14
 
15
15
  # User agent and version
16
-
17
16
  SDK_NAME = "glaip-sdk"
18
- SDK_VERSION = "0.1.0"
19
17
 
20
18
  # Reserved names that cannot be used for agents/tools
21
19
  RESERVED_NAMES = {
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)
@@ -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
- "progress_bar",
36
- "print_agent_output",
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
  ]
@@ -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__(self, _exc_type, _exc_val, _exc_tb):
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 _parse_sse_line(line: str, buf: list, event_type: str = None, event_id: str = None):
119
- """Parse a single SSE line and return updated buffer and event metadata."""
120
- # Normalize CRLF and treat whitespace-only as blank
121
- line = line.rstrip("\r")
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
- if not line.strip(): # blank line
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
- False,
136
- ) # no completion
137
- return buf, event_type, event_id, None, False
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
- data_line = line[5:].lstrip()
144
- if data_line.strip() == "[DONE]": # sentinel end marker
145
- if buf:
146
- data = "\n".join(buf)
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[3:].strip() or None
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, timeout_seconds: float = None, agent_name: str = None
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, timeout_seconds: float = None, agent_name: str = None
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
- file_list = []
323
-
324
- with ExitStack() as stack:
325
- multipart_data = MultipartData(form_data, [])
326
- multipart_data._exit_stack = stack
327
-
328
- for file_item in files:
329
- if isinstance(file_item, str):
330
- # File path - let httpx stream the file handle
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
- # File-like object
349
- if hasattr(file_item, "name"):
350
- filename = getattr(file_item, "name", "file")
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
@@ -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: