glaip-sdk 0.0.1b5__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.
@@ -0,0 +1,309 @@
1
+ """Tool management commands.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import json
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+
13
+ from glaip_sdk.utils import is_uuid
14
+
15
+ from ..utils import (
16
+ get_client,
17
+ handle_ambiguous_resource,
18
+ output_flags,
19
+ output_list,
20
+ output_result,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ @click.group(name="tools", no_args_is_help=True)
27
+ def tools_group():
28
+ """Tool management operations."""
29
+ pass
30
+
31
+
32
+ def _resolve_tool(ctx, client, ref, select=None):
33
+ """Resolve tool reference (ID or name) with ambiguity handling."""
34
+ if is_uuid(ref):
35
+ return client.get_tool(ref)
36
+
37
+ # Use find_tools for name-based resolution
38
+ matches = client.find_tools(name=ref)
39
+ if not matches:
40
+ raise click.ClickException(f"Tool not found: {ref}")
41
+
42
+ if len(matches) == 1:
43
+ return matches[0]
44
+ elif len(matches) > 1:
45
+ if select:
46
+ idx = int(select) - 1
47
+ if not (0 <= idx < len(matches)):
48
+ raise click.ClickException(f"--select must be 1..{len(matches)}")
49
+ return matches[idx]
50
+ return handle_ambiguous_resource(ctx, "tool", ref, matches)
51
+ else:
52
+ raise click.ClickException(f"Tool not found: {ref}")
53
+
54
+
55
+ @tools_group.command(name="list")
56
+ @output_flags()
57
+ @click.pass_context
58
+ def list_tools(ctx):
59
+ """List all tools."""
60
+ try:
61
+ client = get_client(ctx)
62
+ tools = client.list_tools()
63
+
64
+ # Define table columns: (data_key, header, style, width)
65
+ columns = [
66
+ ("id", "ID", "dim", 36),
67
+ ("name", "Name", "cyan", None),
68
+ ("framework", "Framework", "blue", None),
69
+ ]
70
+
71
+ # Transform function for safe dictionary access
72
+ def transform_tool(tool):
73
+ # Handle both dict and object formats
74
+ if isinstance(tool, dict):
75
+ return {
76
+ "id": str(tool.get("id", "N/A")),
77
+ "name": tool.get("name", "N/A"),
78
+ "framework": tool.get("framework", "N/A"),
79
+ }
80
+ else:
81
+ # Fallback to attribute access
82
+ return {
83
+ "id": str(getattr(tool, "id", "N/A")),
84
+ "name": getattr(tool, "name", "N/A"),
85
+ "framework": getattr(tool, "framework", "N/A"),
86
+ }
87
+
88
+ output_list(ctx, tools, "🔧 Available Tools", columns, transform_tool)
89
+
90
+ except Exception as e:
91
+ raise click.ClickException(str(e))
92
+
93
+
94
+ @tools_group.command()
95
+ @click.option(
96
+ "--file",
97
+ type=click.Path(exists=True),
98
+ help="Tool file to upload (optional for metadata-only tools)",
99
+ )
100
+ @click.option(
101
+ "--name",
102
+ help="Tool name (required for metadata-only tools, extracted from script if file provided)",
103
+ )
104
+ @click.option(
105
+ "--description",
106
+ help="Tool description (optional - extracted from script if file provided)",
107
+ )
108
+ @click.option(
109
+ "--tags",
110
+ help="Comma-separated tags for the tool",
111
+ )
112
+ @output_flags()
113
+ @click.pass_context
114
+ def create(ctx, file, name, description, tags):
115
+ """Create a new tool."""
116
+ try:
117
+ client = get_client(ctx)
118
+
119
+ # Validate required parameters based on creation method
120
+ if not file:
121
+ # Metadata-only tool creation
122
+ if not name:
123
+ raise click.ClickException(
124
+ "--name is required when creating metadata-only tools"
125
+ )
126
+
127
+ # Create tool based on whether file is provided
128
+ if file:
129
+ # File-based tool creation - use create_tool_from_code for proper plugin processing
130
+ with open(file, encoding="utf-8") as f:
131
+ code_content = f.read()
132
+
133
+ # Extract name from file if not provided
134
+ if not name:
135
+ import os
136
+
137
+ name = os.path.splitext(os.path.basename(file))[0]
138
+
139
+ # Create tool plugin using the upload endpoint
140
+ tool = client.create_tool_from_code(name, code_content)
141
+ else:
142
+ # Metadata-only tool creation
143
+ tool_kwargs = {}
144
+ if name:
145
+ tool_kwargs["name"] = name
146
+ if description:
147
+ tool_kwargs["description"] = description
148
+ if tags:
149
+ tool_kwargs["tags"] = [tag.strip() for tag in tags.split(",")]
150
+
151
+ tool = client.create_tool(**tool_kwargs)
152
+
153
+ if ctx.obj.get("view") == "json":
154
+ click.echo(json.dumps(tool.model_dump(), indent=2))
155
+ else:
156
+ # Rich output
157
+ creation_method = (
158
+ "file upload (custom)" if file else "metadata only (native)"
159
+ )
160
+ panel = Panel(
161
+ f"[green]✅ Tool '{tool.name}' created successfully via {creation_method}![/green]\n\n"
162
+ f"ID: {tool.id}\n"
163
+ f"Framework: langchain (default)\n"
164
+ f"Type: {'custom' if file else 'native'} (auto-detected)\n"
165
+ f"Description: {getattr(tool, 'description', 'No description')}",
166
+ title="🔧 Tool Created",
167
+ border_style="green",
168
+ )
169
+ console.print(panel)
170
+
171
+ except Exception as e:
172
+ if ctx.obj.get("view") == "json":
173
+ click.echo(json.dumps({"error": str(e)}, indent=2))
174
+ else:
175
+ console.print(f"[red]Error creating tool: {e}[/red]")
176
+ raise click.ClickException(str(e))
177
+
178
+
179
+ @tools_group.command()
180
+ @click.argument("tool_ref")
181
+ @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
182
+ @output_flags()
183
+ @click.pass_context
184
+ def get(ctx, tool_ref, select):
185
+ """Get tool details."""
186
+ try:
187
+ client = get_client(ctx)
188
+
189
+ # Resolve tool with ambiguity handling
190
+ tool = _resolve_tool(ctx, client, tool_ref, select)
191
+
192
+ # Create result data with all available fields from backend
193
+ result_data = {
194
+ "id": str(getattr(tool, "id", "N/A")),
195
+ "name": getattr(tool, "name", "N/A"),
196
+ "tool_type": getattr(tool, "tool_type", "N/A"),
197
+ "framework": getattr(tool, "framework", "N/A"),
198
+ "version": getattr(tool, "version", "N/A"),
199
+ "description": getattr(tool, "description", "N/A"),
200
+ }
201
+
202
+ output_result(
203
+ ctx, result_data, title="Tool Details", panel_title=f"🔧 {tool.name}"
204
+ )
205
+
206
+ except Exception as e:
207
+ raise click.ClickException(str(e))
208
+
209
+
210
+ @tools_group.command()
211
+ @click.argument("tool_id")
212
+ @click.option(
213
+ "--file", type=click.Path(exists=True), help="New tool file for code update"
214
+ )
215
+ @click.option("--description", help="New description")
216
+ @click.option("--tags", help="Comma-separated tags")
217
+ @output_flags()
218
+ @click.pass_context
219
+ def update(ctx, tool_id, file, description, tags):
220
+ """Update a tool (code or metadata)."""
221
+ try:
222
+ client = get_client(ctx)
223
+
224
+ # Get tool by ID (no ambiguity handling needed)
225
+ try:
226
+ tool = client.get_tool_by_id(tool_id)
227
+ except Exception as e:
228
+ raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
229
+
230
+ update_data = {}
231
+
232
+ if description:
233
+ update_data["description"] = description
234
+
235
+ if tags:
236
+ update_data["tags"] = [tag.strip() for tag in tags.split(",")]
237
+
238
+ if file:
239
+ # Update code
240
+ updated_tool = tool.update(file_path=file)
241
+ if ctx.obj.get("view") != "json":
242
+ console.print(f"[green]✓[/green] Tool code updated from {file}")
243
+ elif update_data:
244
+ # Update metadata
245
+ updated_tool = tool.update(**update_data)
246
+ if ctx.obj.get("view") != "json":
247
+ console.print("[green]✓[/green] Tool metadata updated")
248
+ else:
249
+ if ctx.obj.get("view") != "json":
250
+ console.print("[yellow]No updates specified[/yellow]")
251
+ return
252
+
253
+ if ctx.obj.get("view") == "json":
254
+ click.echo(json.dumps(updated_tool.model_dump(), indent=2))
255
+ else:
256
+ console.print(
257
+ f"[green]✅ Tool '{updated_tool.name}' updated successfully[/green]"
258
+ )
259
+
260
+ except Exception as e:
261
+ if ctx.obj.get("view") == "json":
262
+ click.echo(json.dumps({"error": str(e)}, indent=2))
263
+ else:
264
+ console.print(f"[red]Error updating tool: {e}[/red]")
265
+ raise click.ClickException(str(e))
266
+
267
+
268
+ @tools_group.command()
269
+ @click.argument("tool_id")
270
+ @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
271
+ @output_flags()
272
+ @click.pass_context
273
+ def delete(ctx, tool_id, yes):
274
+ """Delete a tool."""
275
+ try:
276
+ client = get_client(ctx)
277
+
278
+ # Get tool by ID (no ambiguity handling needed)
279
+ try:
280
+ tool = client.get_tool_by_id(tool_id)
281
+ except Exception as e:
282
+ raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
283
+
284
+ # Confirm deletion
285
+ if not yes and not click.confirm(
286
+ f"Are you sure you want to delete tool '{tool.name}'?"
287
+ ):
288
+ if ctx.obj.get("view") != "json":
289
+ console.print("Deletion cancelled.")
290
+ return
291
+
292
+ tool.delete()
293
+
294
+ if ctx.obj.get("view") == "json":
295
+ click.echo(
296
+ json.dumps(
297
+ {"success": True, "message": f"Tool '{tool.name}' deleted"},
298
+ indent=2,
299
+ )
300
+ )
301
+ else:
302
+ console.print(f"[green]✅ Tool '{tool.name}' deleted successfully[/green]")
303
+
304
+ except Exception as e:
305
+ if ctx.obj.get("view") == "json":
306
+ click.echo(json.dumps({"error": str(e)}, indent=2))
307
+ else:
308
+ console.print(f"[red]Error deleting tool: {e}[/red]")
309
+ raise click.ClickException(str(e))