glaip-sdk 0.0.2__py3-none-any.whl → 0.0.4__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 (40) hide show
  1. glaip_sdk/__init__.py +2 -2
  2. glaip_sdk/_version.py +51 -0
  3. glaip_sdk/branding.py +145 -0
  4. glaip_sdk/cli/commands/agents.py +876 -166
  5. glaip_sdk/cli/commands/configure.py +46 -104
  6. glaip_sdk/cli/commands/init.py +43 -118
  7. glaip_sdk/cli/commands/mcps.py +86 -161
  8. glaip_sdk/cli/commands/tools.py +196 -57
  9. glaip_sdk/cli/main.py +43 -29
  10. glaip_sdk/cli/utils.py +258 -27
  11. glaip_sdk/client/__init__.py +54 -2
  12. glaip_sdk/client/agents.py +196 -237
  13. glaip_sdk/client/base.py +62 -2
  14. glaip_sdk/client/mcps.py +63 -20
  15. glaip_sdk/client/tools.py +236 -81
  16. glaip_sdk/config/constants.py +10 -3
  17. glaip_sdk/exceptions.py +13 -0
  18. glaip_sdk/models.py +21 -5
  19. glaip_sdk/utils/__init__.py +116 -18
  20. glaip_sdk/utils/client_utils.py +284 -0
  21. glaip_sdk/utils/rendering/__init__.py +1 -0
  22. glaip_sdk/utils/rendering/formatting.py +211 -0
  23. glaip_sdk/utils/rendering/models.py +53 -0
  24. glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
  25. glaip_sdk/utils/rendering/renderer/base.py +827 -0
  26. glaip_sdk/utils/rendering/renderer/config.py +33 -0
  27. glaip_sdk/utils/rendering/renderer/console.py +54 -0
  28. glaip_sdk/utils/rendering/renderer/debug.py +82 -0
  29. glaip_sdk/utils/rendering/renderer/panels.py +123 -0
  30. glaip_sdk/utils/rendering/renderer/progress.py +118 -0
  31. glaip_sdk/utils/rendering/renderer/stream.py +198 -0
  32. glaip_sdk/utils/rendering/steps.py +168 -0
  33. glaip_sdk/utils/run_renderer.py +22 -1086
  34. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/METADATA +8 -36
  35. glaip_sdk-0.0.4.dist-info/RECORD +41 -0
  36. glaip_sdk/cli/config.py +0 -592
  37. glaip_sdk/utils.py +0 -167
  38. glaip_sdk-0.0.2.dist-info/RECORD +0 -28
  39. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/WHEEL +0 -0
  40. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/mcps.py CHANGED
@@ -10,6 +10,7 @@ from typing import Any
10
10
 
11
11
  from glaip_sdk.client.base import BaseClient
12
12
  from glaip_sdk.models import MCP
13
+ from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
13
14
 
14
15
  # Set up module-level logger
15
16
  logger = logging.getLogger("glaip_sdk.mcps")
@@ -30,7 +31,7 @@ class MCPClient(BaseClient):
30
31
  def list_mcps(self) -> list[MCP]:
31
32
  """List all MCPs."""
32
33
  data = self._request("GET", "/mcps/")
33
- return [MCP(**mcp_data)._set_client(self) for mcp_data in (data or [])]
34
+ return create_model_instances(data, MCP, self)
34
35
 
35
36
  def get_mcp_by_id(self, mcp_id: str) -> MCP:
36
37
  """Get MCP by ID."""
@@ -41,13 +42,8 @@ class MCPClient(BaseClient):
41
42
  """Find MCPs by name."""
42
43
  # Backend doesn't support name query parameter, so we fetch all and filter client-side
43
44
  data = self._request("GET", "/mcps/")
44
- mcps = [MCP(**mcp_data)._set_client(self) for mcp_data in (data or [])]
45
-
46
- if name:
47
- # Client-side filtering by name (case-insensitive)
48
- mcps = [mcp for mcp in mcps if name.lower() in mcp.name.lower()]
49
-
50
- return mcps
45
+ mcps = create_model_instances(data, MCP, self)
46
+ return find_by_name(mcps, name, case_sensitive=False)
51
47
 
52
48
  def create_mcp(
53
49
  self,
@@ -66,18 +62,14 @@ class MCPClient(BaseClient):
66
62
  if config:
67
63
  payload["config"] = config
68
64
 
69
- # Create the MCP (backend returns only the ID)
70
- response_data = self._request("POST", "/mcps/", json=payload)
71
-
72
- # Extract the ID from the response
73
- if isinstance(response_data, dict) and "id" in response_data:
74
- mcp_id = response_data["id"]
75
- else:
76
- # Fallback: assume response_data is the ID directly
77
- mcp_id = str(response_data)
78
-
79
- # Fetch the full MCP details
80
- return self.get_mcp_by_id(mcp_id)
65
+ # Create the MCP and fetch full details
66
+ full_mcp_data = self._post_then_fetch(
67
+ id_key="id",
68
+ post_endpoint="/mcps/",
69
+ get_endpoint_fmt="/mcps/{id}",
70
+ json=payload,
71
+ )
72
+ return MCP(**full_mcp_data)._set_client(self)
81
73
 
82
74
  def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
83
75
  """Update an existing MCP."""
@@ -92,3 +84,54 @@ class MCPClient(BaseClient):
92
84
  """Get tools available from an MCP."""
93
85
  data = self._request("GET", f"/mcps/{mcp_id}/tools")
94
86
  return data or []
87
+
88
+ def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
89
+ """Test MCP connection using configuration.
90
+
91
+ Args:
92
+ config: MCP configuration dictionary
93
+
94
+ Returns:
95
+ dict: Connection test result
96
+
97
+ Raises:
98
+ Exception: If connection test fails
99
+ """
100
+ try:
101
+ response = self._request("POST", "/mcps/connect", json=config)
102
+ return response
103
+ except Exception as e:
104
+ logger.error(f"Failed to test MCP connection: {e}")
105
+ raise
106
+
107
+ def test_mcp_connection_from_config(self, config: dict[str, Any]) -> dict[str, Any]:
108
+ """Test MCP connection using configuration (alias for test_mcp_connection).
109
+
110
+ Args:
111
+ config: MCP configuration dictionary
112
+
113
+ Returns:
114
+ dict: Connection test result
115
+ """
116
+ return self.test_mcp_connection(config)
117
+
118
+ def get_mcp_tools_from_config(self, config: dict[str, Any]) -> list[dict[str, Any]]:
119
+ """Fetch tools from MCP configuration without saving.
120
+
121
+ Args:
122
+ config: MCP configuration dictionary
123
+
124
+ Returns:
125
+ list: List of available tools from the MCP
126
+
127
+ Raises:
128
+ Exception: If tool fetching fails
129
+ """
130
+ try:
131
+ response = self._request("POST", "/mcps/connect/tools", json=config)
132
+ if response is None:
133
+ return []
134
+ return response.get("tools", []) or []
135
+ except Exception as e:
136
+ logger.error(f"Failed to get MCP tools from config: {e}")
137
+ raise
glaip_sdk/client/tools.py CHANGED
@@ -6,9 +6,15 @@ Authors:
6
6
  """
7
7
 
8
8
  import logging
9
+ import os
10
+ import tempfile
9
11
 
10
12
  from glaip_sdk.client.base import BaseClient
11
13
  from glaip_sdk.models import Tool
14
+ from glaip_sdk.utils.client_utils import (
15
+ create_model_instances,
16
+ find_by_name,
17
+ )
12
18
 
13
19
  # Set up module-level logger
14
20
  logger = logging.getLogger("glaip_sdk.tools")
@@ -29,7 +35,7 @@ class ToolClient(BaseClient):
29
35
  def list_tools(self) -> list[Tool]:
30
36
  """List all tools."""
31
37
  data = self._request("GET", "/tools/")
32
- return [Tool(**tool_data)._set_client(self) for tool_data in (data or [])]
38
+ return create_model_instances(data, Tool, self)
33
39
 
34
40
  def get_tool_by_id(self, tool_id: str) -> Tool:
35
41
  """Get tool by ID."""
@@ -40,81 +46,190 @@ class ToolClient(BaseClient):
40
46
  """Find tools by name."""
41
47
  # Backend doesn't support name query parameter, so we fetch all and filter client-side
42
48
  data = self._request("GET", "/tools/")
43
- tools = [Tool(**tool_data)._set_client(self) for tool_data in (data or [])]
49
+ tools = create_model_instances(data, Tool, self)
50
+ return find_by_name(tools, name, case_sensitive=False)
44
51
 
45
- if name:
46
- # Client-side filtering by name (case-insensitive)
47
- tools = [tool for tool in tools if name.lower() in tool.name.lower()]
52
+ def _validate_and_read_file(self, file_path: str) -> str:
53
+ """Validate file exists and read its content.
48
54
 
49
- return tools
55
+ Args:
56
+ file_path: Path to the file to read
50
57
 
51
- def create_tool(
58
+ Returns:
59
+ str: File content
60
+
61
+ Raises:
62
+ FileNotFoundError: If file doesn't exist
63
+ """
64
+ if not os.path.exists(file_path):
65
+ raise FileNotFoundError(f"Tool file not found: {file_path}")
66
+
67
+ with open(file_path, encoding="utf-8") as f:
68
+ return f.read()
69
+
70
+ def _extract_name_from_file(self, file_path: str) -> str:
71
+ """Extract tool name from file path.
72
+
73
+ Args:
74
+ file_path: Path to the file
75
+
76
+ Returns:
77
+ str: Extracted name (filename without extension)
78
+ """
79
+ return os.path.splitext(os.path.basename(file_path))[0]
80
+
81
+ def _prepare_upload_data(
82
+ self, name: str, framework: str, description: str | None = None, **kwargs
83
+ ) -> dict:
84
+ """Prepare upload data dictionary.
85
+
86
+ Args:
87
+ name: Tool name
88
+ framework: Tool framework
89
+ description: Optional description
90
+ **kwargs: Additional parameters
91
+
92
+ Returns:
93
+ dict: Upload data dictionary
94
+ """
95
+ data = {
96
+ "name": name,
97
+ "framework": framework,
98
+ }
99
+
100
+ if description:
101
+ data["description"] = description
102
+
103
+ # Handle tags if provided in kwargs
104
+ if "tags" in kwargs and kwargs["tags"]:
105
+ if isinstance(kwargs["tags"], list):
106
+ data["tags"] = ",".join(kwargs["tags"])
107
+ else:
108
+ data["tags"] = kwargs["tags"]
109
+
110
+ # Include any other kwargs in the upload data
111
+ for key, value in kwargs.items():
112
+ if key not in ["tags"]: # tags already handled above
113
+ data[key] = value
114
+
115
+ return data
116
+
117
+ def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
118
+ """Upload tool file to server.
119
+
120
+ Args:
121
+ file_path: Path to temporary file to upload
122
+ upload_data: Dictionary with upload metadata
123
+
124
+ Returns:
125
+ Tool: Created tool object
126
+ """
127
+ with open(file_path, "rb") as fb:
128
+ files = {
129
+ "file": (os.path.basename(file_path), fb, "application/octet-stream"),
130
+ }
131
+
132
+ response = self._request(
133
+ "POST",
134
+ "/tools/upload",
135
+ files=files,
136
+ data=upload_data,
137
+ )
138
+
139
+ return Tool(**response)._set_client(self)
140
+
141
+ def _create_tool_from_file(
52
142
  self,
143
+ file_path: str,
53
144
  name: str | None = None,
54
- tool_type: str = "custom",
55
145
  description: str | None = None,
56
- tool_script: str | None = None,
57
- tool_file: str | None = None,
58
- file_path: str | None = None,
59
- code: str | None = None,
60
146
  framework: str = "langchain",
61
147
  **kwargs,
62
148
  ) -> Tool:
63
- """Create a new tool.
149
+ """Create tool from file content using upload endpoint.
64
150
 
65
151
  Args:
66
- name: Tool name (required if not provided via file)
67
- tool_type: Tool type (defaults to "custom")
68
- description: Tool description (optional)
69
- tool_script: Tool script content (optional)
70
- tool_file: Tool file path (optional)
71
- file_path: Alternative to tool_file (for compatibility)
72
- code: Alternative to tool_script (for compatibility)
73
- framework: Tool framework (defaults to "langchain")
74
- **kwargs: Additional tool parameters
152
+ file_path: Path to tool file
153
+ name: Optional tool name (auto-detected if not provided)
154
+ description: Optional tool description
155
+ framework: Tool framework
156
+ **kwargs: Additional parameters
157
+
158
+ Returns:
159
+ Tool: Created tool object
75
160
  """
76
- # Handle compatibility parameters
77
- if file_path and not tool_file:
78
- tool_file = file_path
79
- if code and not tool_script:
80
- tool_script = code
161
+ # Read and validate file
162
+ file_content = self._validate_and_read_file(file_path)
81
163
 
82
- # Auto-detect name from file if not provided
83
- if not name and tool_file:
84
- import os
164
+ # Auto-detect name if not provided
165
+ if not name:
166
+ name = self._extract_name_from_file(file_path)
85
167
 
86
- name = os.path.splitext(os.path.basename(tool_file))[0]
168
+ # Handle description - generate default if not provided or empty
169
+ if description is None or description == "":
170
+ # Generate default description based on tool_type if available
171
+ tool_type = kwargs.get("tool_type", "custom")
172
+ description = f"A {tool_type} tool"
87
173
 
88
- if not name:
89
- raise ValueError(
90
- "Tool name is required (either explicitly or via file path)"
174
+ # Create temporary file for upload
175
+ with tempfile.NamedTemporaryFile(
176
+ mode="w",
177
+ suffix=".py",
178
+ prefix=f"{name}_",
179
+ delete=False,
180
+ encoding="utf-8",
181
+ ) as temp_file:
182
+ temp_file.write(file_content)
183
+ temp_file_path = temp_file.name
184
+
185
+ try:
186
+ # Prepare upload data
187
+ upload_data = self._prepare_upload_data(
188
+ name=name, framework=framework, description=description, **kwargs
91
189
  )
92
190
 
93
- # Auto-detect description if not provided
94
- if not description:
95
- description = f"A {tool_type} tool"
191
+ # Upload file
192
+ return self._upload_tool_file(temp_file_path, upload_data)
96
193
 
97
- payload = {
98
- "name": name,
99
- "tool_type": tool_type,
100
- "description": description,
101
- "framework": framework,
102
- **kwargs,
103
- }
194
+ finally:
195
+ # Clean up temporary file
196
+ try:
197
+ os.unlink(temp_file_path)
198
+ except OSError:
199
+ pass # Ignore cleanup errors
104
200
 
105
- if tool_script:
106
- payload["tool_script"] = tool_script
107
- if tool_file:
108
- payload["tool_file"] = tool_file
201
+ def create_tool(
202
+ self,
203
+ file_path: str,
204
+ name: str | None = None,
205
+ description: str | None = None,
206
+ framework: str = "langchain",
207
+ **kwargs,
208
+ ) -> Tool:
209
+ """Create a new tool from a file.
109
210
 
110
- data = self._request("POST", "/tools/", json=payload)
111
- return Tool(**data)._set_client(self)
211
+ Args:
212
+ file_path: File path to tool script (required) - file content will be read and processed as plugin
213
+ name: Tool name (auto-detected from file if not provided)
214
+ description: Tool description (auto-generated if not provided)
215
+ framework: Tool framework (defaults to "langchain")
216
+ **kwargs: Additional tool parameters
217
+ """
218
+ return self._create_tool_from_file(
219
+ file_path=file_path,
220
+ name=name,
221
+ description=description,
222
+ framework=framework,
223
+ **kwargs,
224
+ )
112
225
 
113
226
  def create_tool_from_code(
114
227
  self,
115
228
  name: str,
116
229
  code: str,
117
230
  framework: str = "langchain",
231
+ description: str | None = None,
232
+ tags: list[str] | None = None,
118
233
  ) -> Tool:
119
234
  """Create a new tool plugin from code string.
120
235
 
@@ -126,38 +241,35 @@ class ToolClient(BaseClient):
126
241
  name: Name for the tool (used for temporary file naming)
127
242
  code: Python code containing the tool plugin
128
243
  framework: Tool framework (defaults to "langchain")
244
+ description: Optional tool description
245
+ tags: Optional list of tags
129
246
 
130
247
  Returns:
131
248
  Tool: The created tool object
132
249
  """
133
- import os
134
- import tempfile
135
-
136
250
  # Create a temporary file with the tool code
137
251
  with tempfile.NamedTemporaryFile(
138
- mode="w", suffix=".py", prefix=f"{name}_", delete=False, encoding="utf-8"
252
+ mode="w",
253
+ suffix=".py",
254
+ prefix=f"{name}_",
255
+ delete=False,
256
+ encoding="utf-8",
139
257
  ) as temp_file:
140
258
  temp_file.write(code)
141
259
  temp_file_path = temp_file.name
142
260
 
143
261
  try:
144
- # Read the file content and upload it
145
- with open(temp_file_path, encoding="utf-8") as f:
146
- script_content = f.read()
147
-
148
- # Create a file-like object for upload
149
- import io
150
-
151
- file_obj = io.BytesIO(script_content.encode("utf-8"))
152
- file_obj.name = os.path.basename(temp_file_path)
262
+ # Prepare upload data using shared helper
263
+ upload_data = self._prepare_upload_data(
264
+ name=name,
265
+ framework=framework,
266
+ description=description,
267
+ tags=tags if tags else None,
268
+ )
153
269
 
154
- # Use multipart form data for file upload
155
- files = {"file": (file_obj.name, file_obj, "text/plain")}
156
- data = {"framework": framework}
270
+ # Upload file using shared helper
271
+ return self._upload_tool_file(temp_file_path, upload_data)
157
272
 
158
- # Make the upload request
159
- response = self._request("POST", "/tools/upload", files=files, data=data)
160
- return Tool(**response)._set_client(self)
161
273
  finally:
162
274
  # Clean up the temporary file
163
275
  try:
@@ -174,20 +286,63 @@ class ToolClient(BaseClient):
174
286
  """Delete a tool."""
175
287
  self._request("DELETE", f"/tools/{tool_id}")
176
288
 
177
- def install_tool(self, tool_id: str) -> bool:
178
- """Install a tool."""
289
+ def get_tool_script(self, tool_id: str) -> str:
290
+ """Get the tool script content.
291
+
292
+ Args:
293
+ tool_id: The ID of the tool
294
+
295
+ Returns:
296
+ str: The tool script content
297
+
298
+ Raises:
299
+ Exception: If the tool script cannot be retrieved
300
+ """
179
301
  try:
180
- self._request("POST", f"/tools/{tool_id}/install")
181
- return True
302
+ response = self._request("GET", f"/tools/{tool_id}/script")
303
+ return response.get("script", "") or response.get("content", "")
182
304
  except Exception as e:
183
- logger.error(f"Failed to install tool {tool_id}: {e}")
184
- return False
305
+ logger.error(f"Failed to get tool script for {tool_id}: {e}")
306
+ raise
307
+
308
+ def update_tool_via_file(self, tool_id: str, file_path: str, **kwargs) -> Tool:
309
+ """Update a tool plugin via file upload.
310
+
311
+ Args:
312
+ tool_id: The ID of the tool to update
313
+ file_path: Path to the new tool file
314
+ **kwargs: Additional metadata to update (name, description, tags, etc.)
315
+
316
+ Returns:
317
+ Tool: The updated tool object
318
+
319
+ Raises:
320
+ FileNotFoundError: If the file doesn't exist
321
+ Exception: If the update fails
322
+ """
323
+ # Validate file exists
324
+ self._validate_and_read_file(file_path)
185
325
 
186
- def uninstall_tool(self, tool_id: str) -> bool:
187
- """Uninstall a tool."""
188
326
  try:
189
- self._request("POST", f"/tools/{tool_id}/uninstall")
190
- return True
327
+ # Prepare multipart upload
328
+ with open(file_path, "rb") as fb:
329
+ files = {
330
+ "file": (
331
+ os.path.basename(file_path),
332
+ fb,
333
+ "application/octet-stream",
334
+ ),
335
+ }
336
+
337
+ response = self._request(
338
+ "PUT",
339
+ f"/tools/{tool_id}/upload",
340
+ files=files,
341
+ data=kwargs, # Pass kwargs directly as data
342
+ )
343
+
344
+ return Tool(**response)._set_client(self)
345
+
191
346
  except Exception as e:
192
- logger.error(f"Failed to uninstall tool {tool_id}: {e}")
193
- return False
347
+ logger.error(f"Failed to update tool {tool_id} via file: {e}")
348
+ raise
@@ -10,11 +10,12 @@ DEFAULT_MODEL_PROVIDER = "openai"
10
10
 
11
11
  # Default timeout values
12
12
  DEFAULT_TIMEOUT = 30.0
13
- DEFAULT_AGENT_TIMEOUT = 300.0
13
+ DEFAULT_AGENT_RUN_TIMEOUT = 300.0
14
+
15
+ # User agent and version
14
16
 
15
- # User agent
16
17
  SDK_NAME = "glaip-sdk"
17
- SDK_VERSION = "0.1.1"
18
+ SDK_VERSION = "0.1.0"
18
19
 
19
20
  # Reserved names that cannot be used for agents/tools
20
21
  RESERVED_NAMES = {
@@ -26,3 +27,9 @@ RESERVED_NAMES = {
26
27
  "demo",
27
28
  "sample",
28
29
  }
30
+
31
+ # Agent creation/update constants
32
+ DEFAULT_AGENT_TYPE = "config"
33
+ DEFAULT_AGENT_FRAMEWORK = "langchain"
34
+ DEFAULT_AGENT_VERSION = "1.0"
35
+ DEFAULT_AGENT_PROVIDER = "openai"
glaip_sdk/exceptions.py CHANGED
@@ -87,6 +87,19 @@ class TimeoutError(APIError):
87
87
  pass
88
88
 
89
89
 
90
+ class AgentTimeoutError(TimeoutError):
91
+ """Agent execution timeout with specific duration information."""
92
+
93
+ def __init__(self, timeout_seconds: float, agent_name: str = None):
94
+ agent_info = f" for agent '{agent_name}'" if agent_name else ""
95
+ message = (
96
+ f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
97
+ )
98
+ super().__init__(message)
99
+ self.timeout_seconds = timeout_seconds
100
+ self.agent_name = agent_name
101
+
102
+
90
103
  class ClientError(APIError):
91
104
  """Client-side error (e.g., invalid request format, missing parameters)."""
92
105
 
glaip_sdk/models.py CHANGED
@@ -5,10 +5,13 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from datetime import datetime
8
9
  from typing import Any
9
10
 
10
11
  from pydantic import BaseModel
11
12
 
13
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
14
+
12
15
 
13
16
  class Agent(BaseModel):
14
17
  """Agent model for API responses."""
@@ -22,11 +25,17 @@ class Agent(BaseModel):
22
25
  version: str | None = None
23
26
  tools: list[dict[str, Any]] | None = None # Backend returns ToolReference objects
24
27
  agents: list[dict[str, Any]] | None = None # Backend returns AgentReference objects
28
+ mcps: list[dict[str, Any]] | None = None # Backend returns MCPReference objects
29
+ tool_configs: dict[str, Any] | None = (
30
+ None # Backend returns tool configurations keyed by tool UUID
31
+ )
25
32
  agent_config: dict[str, Any] | None = None
26
- timeout: int = 300
33
+ timeout: int = DEFAULT_AGENT_RUN_TIMEOUT
27
34
  metadata: dict[str, Any] | None = None
28
35
  language_model_id: str | None = None
29
36
  a2a_profile: dict[str, Any] | None = None
37
+ created_at: datetime | None = None # Backend returns creation timestamp
38
+ updated_at: datetime | None = None # Backend returns last update timestamp
30
39
  _client: Any = None
31
40
 
32
41
  def _set_client(self, client):
@@ -34,15 +43,22 @@ class Agent(BaseModel):
34
43
  self._client = client
35
44
  return self
36
45
 
37
- def run(self, message: str, **kwargs) -> str:
38
- """Run the agent with a message."""
46
+ def run(self, message: str, verbose: bool = False, **kwargs) -> str:
47
+ """Run the agent with a message.
48
+
49
+ Args:
50
+ message: The message to send to the agent
51
+ verbose: Enable verbose output and event JSON logging
52
+ **kwargs: Additional arguments passed to run_agent
53
+ """
39
54
  if not self._client:
40
55
  raise RuntimeError(
41
56
  "No client available. Use client.get_agent_by_id() to get a client-connected agent."
42
57
  )
43
58
  # Automatically pass the agent name for better renderer display
44
59
  kwargs.setdefault("agent_name", self.name)
45
- return self._client.run_agent(self.id, message, **kwargs)
60
+ # Pass verbose flag through to enable event JSON output
61
+ return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
46
62
 
47
63
  def update(self, **kwargs) -> "Agent":
48
64
  """Update agent attributes."""
@@ -112,7 +128,7 @@ class Tool(BaseModel):
112
128
  raise RuntimeError(
113
129
  "No client available. Use client.get_tool_by_id() to get a client-connected tool."
114
130
  )
115
- self._client.tools.delete_tool(self.id)
131
+ self._client.delete_tool(self.id)
116
132
 
117
133
 
118
134
  class MCP(BaseModel):