glaip-sdk 0.0.3__py3-none-any.whl → 0.0.5__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 (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +146 -0
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/tools.py CHANGED
@@ -8,14 +8,25 @@ Authors:
8
8
  import logging
9
9
  import os
10
10
  import tempfile
11
+ from typing import Any
11
12
 
12
13
  from glaip_sdk.client.base import BaseClient
14
+ from glaip_sdk.config.constants import (
15
+ DEFAULT_TOOL_FRAMEWORK,
16
+ DEFAULT_TOOL_TYPE,
17
+ DEFAULT_TOOL_VERSION,
18
+ )
13
19
  from glaip_sdk.models import Tool
14
20
  from glaip_sdk.utils.client_utils import (
15
21
  create_model_instances,
16
22
  find_by_name,
17
23
  )
18
24
 
25
+ # API endpoints
26
+ TOOLS_ENDPOINT = "/tools/"
27
+ TOOLS_UPLOAD_ENDPOINT = "/tools/upload"
28
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT = "/tools/{tool_id}/upload"
29
+
19
30
  # Set up module-level logger
20
31
  logger = logging.getLogger("glaip_sdk.tools")
21
32
 
@@ -32,84 +43,320 @@ class ToolClient(BaseClient):
32
43
  """
33
44
  super().__init__(parent_client=parent_client, **kwargs)
34
45
 
35
- def list_tools(self) -> list[Tool]:
36
- """List all tools."""
37
- data = self._request("GET", "/tools/")
46
+ def list_tools(self, tool_type: str | None = None) -> list[Tool]:
47
+ """List all tools, optionally filtered by type.
48
+
49
+ Args:
50
+ tool_type: Filter tools by type (e.g., "custom", "native")
51
+ """
52
+ endpoint = TOOLS_ENDPOINT
53
+ if tool_type:
54
+ endpoint += f"?type={tool_type}"
55
+ data = self._request("GET", endpoint)
38
56
  return create_model_instances(data, Tool, self)
39
57
 
40
58
  def get_tool_by_id(self, tool_id: str) -> Tool:
41
59
  """Get tool by ID."""
42
- data = self._request("GET", f"/tools/{tool_id}")
60
+ data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
43
61
  return Tool(**data)._set_client(self)
44
62
 
45
63
  def find_tools(self, name: str | None = None) -> list[Tool]:
46
64
  """Find tools by name."""
47
65
  # Backend doesn't support name query parameter, so we fetch all and filter client-side
48
- data = self._request("GET", "/tools/")
66
+ data = self._request("GET", TOOLS_ENDPOINT)
49
67
  tools = create_model_instances(data, Tool, self)
50
68
  return find_by_name(tools, name, case_sensitive=False)
51
69
 
52
- def create_tool(
70
+ def _validate_and_read_file(self, file_path: str) -> str:
71
+ """Validate file exists and read its content.
72
+
73
+ Args:
74
+ file_path: Path to the file to read
75
+
76
+ Returns:
77
+ str: File content
78
+
79
+ Raises:
80
+ FileNotFoundError: If file doesn't exist
81
+ """
82
+ if not os.path.exists(file_path):
83
+ raise FileNotFoundError(f"Tool file not found: {file_path}")
84
+
85
+ with open(file_path, encoding="utf-8") as f:
86
+ return f.read()
87
+
88
+ def _extract_name_from_file(self, file_path: str) -> str:
89
+ """Extract tool name from file path.
90
+
91
+ Args:
92
+ file_path: Path to the file
93
+
94
+ Returns:
95
+ str: Extracted name (filename without extension)
96
+ """
97
+ return os.path.splitext(os.path.basename(file_path))[0]
98
+
99
+ def _prepare_upload_data(
100
+ self, name: str, framework: str, description: str | None = None, **kwargs
101
+ ) -> dict:
102
+ """Prepare upload data dictionary.
103
+
104
+ Args:
105
+ name: Tool name
106
+ framework: Tool framework
107
+ description: Optional description
108
+ **kwargs: Additional parameters
109
+
110
+ Returns:
111
+ dict: Upload data dictionary
112
+ """
113
+ data = {
114
+ "name": name,
115
+ "framework": framework,
116
+ }
117
+
118
+ if description:
119
+ data["description"] = description
120
+
121
+ # Handle tags if provided in kwargs
122
+ if kwargs.get("tags"):
123
+ if isinstance(kwargs["tags"], list):
124
+ data["tags"] = ",".join(kwargs["tags"])
125
+ else:
126
+ data["tags"] = kwargs["tags"]
127
+
128
+ # Include any other kwargs in the upload data
129
+ for key, value in kwargs.items():
130
+ if key not in ["tags"]: # tags already handled above
131
+ data[key] = value
132
+
133
+ return data
134
+
135
+ def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
136
+ """Upload tool file to server.
137
+
138
+ Args:
139
+ file_path: Path to temporary file to upload
140
+ upload_data: Dictionary with upload metadata
141
+
142
+ Returns:
143
+ Tool: Created tool object
144
+ """
145
+ with open(file_path, "rb") as fb:
146
+ files = {
147
+ "file": (os.path.basename(file_path), fb, "application/octet-stream"),
148
+ }
149
+
150
+ response = self._request(
151
+ "POST",
152
+ TOOLS_UPLOAD_ENDPOINT,
153
+ files=files,
154
+ data=upload_data,
155
+ )
156
+
157
+ return Tool(**response)._set_client(self)
158
+
159
+ def _build_create_payload(
160
+ self,
161
+ name: str,
162
+ description: str | None = None,
163
+ framework: str = DEFAULT_TOOL_FRAMEWORK,
164
+ tool_type: str = DEFAULT_TOOL_TYPE,
165
+ **kwargs,
166
+ ) -> dict[str, Any]:
167
+ """Build payload for tool creation with proper metadata handling.
168
+
169
+ CENTRALIZED PAYLOAD BUILDING LOGIC:
170
+ - Handles file vs metadata-only tool creation
171
+ - Sets proper defaults and required fields
172
+ - Processes tags and other metadata consistently
173
+
174
+ Args:
175
+ name: Tool name
176
+ description: Tool description
177
+ framework: Tool framework (defaults to langchain)
178
+ tool_type: Tool type (defaults to custom)
179
+ **kwargs: Additional parameters (tags, version, etc.)
180
+
181
+ Returns:
182
+ Complete payload dictionary for tool creation
183
+ """
184
+ # Prepare the creation payload with required fields
185
+ payload: dict[str, any] = {
186
+ "name": name.strip(),
187
+ "type": tool_type,
188
+ "framework": framework,
189
+ "version": kwargs.get("version", DEFAULT_TOOL_VERSION),
190
+ }
191
+
192
+ # Add description if provided
193
+ if description:
194
+ payload["description"] = description.strip()
195
+
196
+ # Handle tags - convert list to comma-separated string for API
197
+ if kwargs.get("tags"):
198
+ if isinstance(kwargs["tags"], list):
199
+ payload["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
200
+ else:
201
+ payload["tags"] = str(kwargs["tags"])
202
+
203
+ # Add any other kwargs (excluding already handled ones)
204
+ excluded_keys = {"tags", "version"}
205
+ for key, value in kwargs.items():
206
+ if key not in excluded_keys:
207
+ payload[key] = value
208
+
209
+ return payload
210
+
211
+ def _build_update_payload(
53
212
  self,
213
+ current_tool: Tool,
214
+ name: str | None = None,
215
+ description: str | None = None,
216
+ **kwargs,
217
+ ) -> dict[str, Any]:
218
+ """Build payload for tool update with proper current state preservation.
219
+
220
+ Args:
221
+ current_tool: Current tool object to update
222
+ name: New tool name (None to keep current)
223
+ description: New description (None to keep current)
224
+ **kwargs: Additional parameters (tags, framework, etc.)
225
+
226
+ Returns:
227
+ Complete payload dictionary for tool update
228
+
229
+ Notes:
230
+ - Preserves current values as defaults when new values not provided
231
+ - Handles metadata updates properly
232
+ """
233
+ # Prepare the update payload with current values as defaults
234
+ update_data = {
235
+ "name": name if name is not None else current_tool.name,
236
+ "type": DEFAULT_TOOL_TYPE, # Required by backend
237
+ "framework": kwargs.get(
238
+ "framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)
239
+ ),
240
+ "version": kwargs.get(
241
+ "version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
242
+ ),
243
+ }
244
+
245
+ # Handle description with proper None handling
246
+ if description is not None:
247
+ update_data["description"] = description.strip()
248
+ elif hasattr(current_tool, "description") and current_tool.description:
249
+ update_data["description"] = current_tool.description
250
+
251
+ # Handle tags - convert list to comma-separated string for API
252
+ if kwargs.get("tags"):
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)
267
+
268
+ # Add any other kwargs (excluding already handled ones)
269
+ excluded_keys = {"tags", "framework", "version"}
270
+ for key, value in kwargs.items():
271
+ if key not in excluded_keys:
272
+ update_data[key] = value
273
+
274
+ return update_data
275
+
276
+ def _create_tool_from_file(
277
+ self,
278
+ file_path: str,
54
279
  name: str | None = None,
55
- tool_type: str = "custom",
56
280
  description: str | None = None,
57
- tool_script: str | None = None,
58
- tool_file: str | None = None,
59
- file_path: str | None = None,
60
- code: str | None = None,
61
281
  framework: str = "langchain",
62
282
  **kwargs,
63
283
  ) -> Tool:
64
- """Create a new tool.
284
+ """Create tool from file content using upload endpoint.
65
285
 
66
286
  Args:
67
- name: Tool name (required if not provided via file)
68
- tool_type: Tool type (defaults to "custom")
69
- description: Tool description (optional)
70
- tool_script: Tool script content (optional)
71
- tool_file: Tool file path (optional)
72
- file_path: Alternative to tool_file (for compatibility)
73
- code: Alternative to tool_script (for compatibility)
74
- framework: Tool framework (defaults to "langchain")
75
- **kwargs: Additional tool parameters
287
+ file_path: Path to tool file
288
+ name: Optional tool name (auto-detected if not provided)
289
+ description: Optional tool description
290
+ framework: Tool framework
291
+ **kwargs: Additional parameters
292
+
293
+ Returns:
294
+ Tool: Created tool object
76
295
  """
77
- # Handle compatibility parameters
78
- if file_path and not tool_file:
79
- tool_file = file_path
80
- if code and not tool_script:
81
- tool_script = code
296
+ # Read and validate file
297
+ file_content = self._validate_and_read_file(file_path)
82
298
 
83
- # Auto-detect name from file if not provided
84
- if not name and tool_file:
85
- import os
299
+ # Auto-detect name if not provided
300
+ if not name:
301
+ name = self._extract_name_from_file(file_path)
86
302
 
87
- name = os.path.splitext(os.path.basename(tool_file))[0]
303
+ # Handle description - generate default if not provided or empty
304
+ if description is None or description == "":
305
+ # Generate default description based on tool_type if available
306
+ tool_type = kwargs.get("tool_type", "custom")
307
+ description = f"A {tool_type} tool"
88
308
 
89
- if not name:
90
- raise ValueError(
91
- "Tool name is required (either explicitly or via file path)"
309
+ # Create temporary file for upload
310
+ with tempfile.NamedTemporaryFile(
311
+ mode="w",
312
+ suffix=".py",
313
+ prefix=f"{name}_",
314
+ delete=False,
315
+ encoding="utf-8",
316
+ ) as temp_file:
317
+ temp_file.write(file_content)
318
+ temp_file_path = temp_file.name
319
+
320
+ try:
321
+ # Prepare upload data
322
+ upload_data = self._prepare_upload_data(
323
+ name=name, framework=framework, description=description, **kwargs
92
324
  )
93
325
 
94
- # Auto-detect description if not provided
95
- if not description:
96
- description = f"A {tool_type} tool"
326
+ # Upload file
327
+ return self._upload_tool_file(temp_file_path, upload_data)
97
328
 
98
- payload = {
99
- "name": name,
100
- "tool_type": tool_type,
101
- "description": description,
102
- "framework": framework,
103
- **kwargs,
104
- }
329
+ finally:
330
+ # Clean up temporary file
331
+ try:
332
+ os.unlink(temp_file_path)
333
+ except OSError:
334
+ pass # Ignore cleanup errors
105
335
 
106
- if tool_script:
107
- payload["tool_script"] = tool_script
108
- if tool_file:
109
- payload["tool_file"] = tool_file
336
+ def create_tool(
337
+ self,
338
+ file_path: str,
339
+ name: str | None = None,
340
+ description: str | None = None,
341
+ framework: str = "langchain",
342
+ **kwargs,
343
+ ) -> Tool:
344
+ """Create a new tool from a file.
110
345
 
111
- data = self._request("POST", "/tools/", json=payload)
112
- return Tool(**data)._set_client(self)
346
+ Args:
347
+ file_path: File path to tool script (required) - file content will be read and processed as plugin
348
+ name: Tool name (auto-detected from file if not provided)
349
+ description: Tool description (auto-generated if not provided)
350
+ framework: Tool framework (defaults to "langchain")
351
+ **kwargs: Additional tool parameters
352
+ """
353
+ return self._create_tool_from_file(
354
+ file_path=file_path,
355
+ name=name,
356
+ description=description,
357
+ framework=framework,
358
+ **kwargs,
359
+ )
113
360
 
114
361
  def create_tool_from_code(
115
362
  self,
@@ -129,41 +376,35 @@ class ToolClient(BaseClient):
129
376
  name: Name for the tool (used for temporary file naming)
130
377
  code: Python code containing the tool plugin
131
378
  framework: Tool framework (defaults to "langchain")
379
+ description: Optional tool description
380
+ tags: Optional list of tags
132
381
 
133
382
  Returns:
134
383
  Tool: The created tool object
135
384
  """
136
385
  # Create a temporary file with the tool code
137
386
  with tempfile.NamedTemporaryFile(
138
- mode="w", suffix=".py", prefix=f"{name}_", delete=False, encoding="utf-8"
387
+ mode="w",
388
+ suffix=".py",
389
+ prefix=f"{name}_",
390
+ delete=False,
391
+ encoding="utf-8",
139
392
  ) as temp_file:
140
393
  temp_file.write(code)
141
394
  temp_file_path = temp_file.name
142
395
 
143
396
  try:
144
- # Prepare multipart upload
145
- filename = os.path.basename(temp_file_path)
146
- with open(temp_file_path, "rb") as fb:
147
- files = {
148
- "file": (filename, fb, "application/octet-stream"),
149
- }
150
- data = {
151
- "name": name,
152
- "framework": framework,
153
- }
154
- if description:
155
- data["description"] = description
156
- if tags:
157
- # Backend might expect comma-separated or JSON; start with comma-separated
158
- data["tags"] = ",".join(tags)
397
+ # Prepare upload data using shared helper
398
+ upload_data = self._prepare_upload_data(
399
+ name=name,
400
+ framework=framework,
401
+ description=description,
402
+ tags=tags if tags else None,
403
+ )
404
+
405
+ # Upload file using shared helper
406
+ return self._upload_tool_file(temp_file_path, upload_data)
159
407
 
160
- response = self._request(
161
- "POST",
162
- "/tools/upload",
163
- files=files,
164
- data=data,
165
- )
166
- return Tool(**response)._set_client(self)
167
408
  finally:
168
409
  # Clean up the temporary file
169
410
  try:
@@ -173,30 +414,12 @@ class ToolClient(BaseClient):
173
414
 
174
415
  def update_tool(self, tool_id: str, **kwargs) -> Tool:
175
416
  """Update an existing tool."""
176
- data = self._request("PUT", f"/tools/{tool_id}", json=kwargs)
417
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
177
418
  return Tool(**data)._set_client(self)
178
419
 
179
420
  def delete_tool(self, tool_id: str) -> None:
180
421
  """Delete a tool."""
181
- self._request("DELETE", f"/tools/{tool_id}")
182
-
183
- def install_tool(self, tool_id: str) -> bool:
184
- """Install a tool."""
185
- try:
186
- self._request("POST", f"/tools/{tool_id}/install")
187
- return True
188
- except Exception as e:
189
- logger.error(f"Failed to install tool {tool_id}: {e}")
190
- return False
191
-
192
- def uninstall_tool(self, tool_id: str) -> bool:
193
- """Uninstall a tool."""
194
- try:
195
- self._request("POST", f"/tools/{tool_id}/uninstall")
196
- return True
197
- except Exception as e:
198
- logger.error(f"Failed to install tool {tool_id}: {e}")
199
- return False
422
+ self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
200
423
 
201
424
  def get_tool_script(self, tool_id: str) -> str:
202
425
  """Get the tool script content.
@@ -211,7 +434,7 @@ class ToolClient(BaseClient):
211
434
  Exception: If the tool script cannot be retrieved
212
435
  """
213
436
  try:
214
- response = self._request("GET", f"/tools/{tool_id}/script")
437
+ response = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}/script")
215
438
  return response.get("script", "") or response.get("content", "")
216
439
  except Exception as e:
217
440
  logger.error(f"Failed to get tool script for {tool_id}: {e}")
@@ -232,29 +455,29 @@ class ToolClient(BaseClient):
232
455
  FileNotFoundError: If the file doesn't exist
233
456
  Exception: If the update fails
234
457
  """
235
- import os
236
-
237
- if not os.path.exists(file_path):
238
- raise FileNotFoundError(f"Tool file not found: {file_path}")
458
+ # Validate file exists
459
+ self._validate_and_read_file(file_path)
239
460
 
240
461
  try:
241
462
  # Prepare multipart upload
242
- filename = os.path.basename(file_path)
243
463
  with open(file_path, "rb") as fb:
244
464
  files = {
245
- "file": (filename, fb, "application/octet-stream"),
465
+ "file": (
466
+ os.path.basename(file_path),
467
+ fb,
468
+ "application/octet-stream",
469
+ ),
246
470
  }
247
471
 
248
- # Add any additional metadata
249
- data = kwargs.copy()
250
-
251
472
  response = self._request(
252
473
  "PUT",
253
- f"/tools/{tool_id}/upload",
474
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT.format(tool_id=tool_id),
254
475
  files=files,
255
- data=data,
476
+ data=kwargs, # Pass kwargs directly as data
256
477
  )
478
+
257
479
  return Tool(**response)._set_client(self)
480
+
258
481
  except Exception as e:
259
482
  logger.error(f"Failed to update tool {tool_id} via file: {e}")
260
483
  raise
@@ -10,7 +10,7 @@ DEFAULT_MODEL_PROVIDER = "openai"
10
10
 
11
11
  # Default timeout values
12
12
  DEFAULT_TIMEOUT = 30.0
13
- DEFAULT_AGENT_RUN_TIMEOUT = 300.0
13
+ DEFAULT_AGENT_RUN_TIMEOUT = 300
14
14
 
15
15
  # User agent and version
16
16
 
@@ -33,3 +33,12 @@ DEFAULT_AGENT_TYPE = "config"
33
33
  DEFAULT_AGENT_FRAMEWORK = "langchain"
34
34
  DEFAULT_AGENT_VERSION = "1.0"
35
35
  DEFAULT_AGENT_PROVIDER = "openai"
36
+
37
+ # Tool creation/update constants
38
+ DEFAULT_TOOL_TYPE = "custom"
39
+ DEFAULT_TOOL_FRAMEWORK = "langchain"
40
+ DEFAULT_TOOL_VERSION = "1.0"
41
+
42
+ # MCP creation/update constants
43
+ DEFAULT_MCP_TYPE = "server"
44
+ DEFAULT_MCP_TRANSPORT = "stdio"
glaip_sdk/models.py CHANGED
@@ -5,6 +5,7 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from collections.abc import AsyncGenerator
8
9
  from datetime import datetime
9
10
  from typing import Any
10
11
 
@@ -60,6 +61,31 @@ class Agent(BaseModel):
60
61
  # Pass verbose flag through to enable event JSON output
61
62
  return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
62
63
 
64
+ async def arun(self, message: str, **kwargs) -> AsyncGenerator[dict, None]:
65
+ """Async run the agent with a message, yielding streaming JSON chunks.
66
+
67
+ Args:
68
+ message: The message to send to the agent
69
+ **kwargs: Additional arguments passed to arun_agent
70
+
71
+ Yields:
72
+ Dictionary containing parsed JSON chunks from the streaming response
73
+
74
+ Raises:
75
+ RuntimeError: When no client is available
76
+ AgentTimeoutError: When agent execution times out
77
+ Exception: For other unexpected errors
78
+ """
79
+ if not self._client:
80
+ raise RuntimeError(
81
+ "No client available. Use client.get_agent_by_id() to get a client-connected agent."
82
+ )
83
+ # Automatically pass the agent name for better context
84
+ kwargs.setdefault("agent_name", self.name)
85
+
86
+ async for chunk in self._client.arun_agent(self.id, message, **kwargs):
87
+ yield chunk
88
+
63
89
  def update(self, **kwargs) -> "Agent":
64
90
  """Update agent attributes."""
65
91
  if not self._client:
@@ -110,12 +136,26 @@ class Tool(BaseModel):
110
136
  return "No script content available"
111
137
 
112
138
  def update(self, **kwargs) -> "Tool":
113
- """Update tool attributes."""
139
+ """Update tool attributes.
140
+
141
+ Supports both metadata updates and file uploads.
142
+ Pass 'file' parameter to update tool code via file upload.
143
+ """
114
144
  if not self._client:
115
145
  raise RuntimeError(
116
146
  "No client available. Use client.get_tool_by_id() to get a client-connected tool."
117
147
  )
118
- updated_tool = self._client.tools.update_tool(self.id, **kwargs)
148
+
149
+ # Check if file upload is requested
150
+ if "file" in kwargs:
151
+ file_path = kwargs.pop("file") # Remove file from kwargs for metadata
152
+ updated_tool = self._client.tools.update_tool_via_file(
153
+ self.id, file_path, **kwargs
154
+ )
155
+ else:
156
+ # Regular metadata update
157
+ updated_tool = self._client.tools.update_tool(self.id, **kwargs)
158
+
119
159
  # Update current instance with new data
120
160
  for key, value in updated_tool.model_dump().items():
121
161
  if hasattr(self, key):
@@ -128,7 +168,7 @@ class Tool(BaseModel):
128
168
  raise RuntimeError(
129
169
  "No client available. Use client.get_tool_by_id() to get a client-connected tool."
130
170
  )
131
- self._client.tools.delete_tool(self.id)
171
+ self._client.delete_tool(self.id)
132
172
 
133
173
 
134
174
  class MCP(BaseModel):
@@ -0,0 +1,29 @@
1
+ """Custom Rich components with copy-friendly defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich import box
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+
10
+ class AIPPanel(Panel):
11
+ """Rich Panel configured without vertical borders by default."""
12
+
13
+ def __init__(self, *args, **kwargs):
14
+ kwargs.setdefault("box", box.HORIZONTALS)
15
+ kwargs.setdefault("padding", (0, 1))
16
+ super().__init__(*args, **kwargs)
17
+
18
+
19
+ class AIPTable(Table):
20
+ """Rich Table configured without vertical borders by default."""
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ kwargs.setdefault("box", box.HORIZONTALS)
24
+ kwargs.setdefault("show_edge", False)
25
+ kwargs.setdefault("pad_edge", False)
26
+ super().__init__(*args, **kwargs)
27
+
28
+
29
+ __all__ = ["AIPPanel", "AIPTable"]