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
@@ -5,19 +5,20 @@ Authors:
5
5
  """
6
6
 
7
7
  import json
8
+ import re
8
9
 
9
10
  import click
10
11
  from rich.console import Console
11
12
  from rich.panel import Panel
12
-
13
- from glaip_sdk.utils import is_uuid
13
+ from rich.text import Text
14
14
 
15
15
  from ..utils import (
16
+ coerce_to_row,
16
17
  get_client,
17
- handle_ambiguous_resource,
18
18
  output_flags,
19
19
  output_list,
20
20
  output_result,
21
+ resolve_resource,
21
22
  )
22
23
 
23
24
  console = Console()
@@ -31,25 +32,61 @@ def tools_group():
31
32
 
32
33
  def _resolve_tool(ctx, client, ref, select=None):
33
34
  """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}")
35
+ return resolve_resource(
36
+ ctx,
37
+ ref,
38
+ get_by_id=client.get_tool,
39
+ find_by_name=client.find_tools,
40
+ label="Tool",
41
+ select=select,
42
+ )
43
+
44
+
45
+ # ----------------------------- Helpers --------------------------------- #
46
+
47
+
48
+ def _extract_internal_name(code: str) -> str:
49
+ """Extract plugin class name attribute from tool code."""
50
+ m = re.search(r'^\s*name\s*:\s*str\s*=\s*"([^"]+)"', code, re.M)
51
+ if not m:
52
+ m = re.search(r'^\s*name\s*=\s*"([^"]+)"', code, re.M)
53
+ if not m:
54
+ raise click.ClickException(
55
+ "Could not find plugin 'name' attribute in the tool file. "
56
+ 'Ensure your plugin class defines e.g. name: str = "my_tool".'
57
+ )
58
+ return m.group(1)
59
+
60
+
61
+ def _validate_name_match(provided: str | None, internal: str) -> str:
62
+ """Validate provided --name against internal name; return effective name."""
63
+ if provided and provided != internal:
64
+ raise click.ClickException(
65
+ f"--name '{provided}' does not match plugin internal name '{internal}'. "
66
+ "Either update the code or pass a matching --name."
67
+ )
68
+ return provided or internal
69
+
70
+
71
+ def _check_duplicate_name(client, tool_name: str) -> None:
72
+ """Raise if a tool with the same name already exists."""
73
+ try:
74
+ existing = client.find_tools(name=tool_name)
75
+ if existing:
76
+ raise click.ClickException(
77
+ f"A tool named '{tool_name}' already exists. "
78
+ "Please change your plugin's 'name' to a unique value, then re-run."
79
+ )
80
+ except click.ClickException:
81
+ # Re-raise ClickException (intended error)
82
+ raise
83
+ except Exception:
84
+ # Non-fatal: best-effort duplicate check for other errors
85
+ pass
86
+
87
+
88
+ def _parse_tags(tags: str | None) -> list[str]:
89
+ return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
53
90
 
54
91
 
55
92
  @tools_group.command(name="list")
@@ -70,20 +107,10 @@ def list_tools(ctx):
70
107
 
71
108
  # Transform function for safe dictionary access
72
109
  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
- }
110
+ row = coerce_to_row(tool, ["id", "name", "framework"])
111
+ # Ensure id is always a string
112
+ row["id"] = str(row["id"])
113
+ return row
87
114
 
88
115
  output_list(ctx, tools, "🔧 Available Tools", columns, transform_tool)
89
116
 
@@ -92,6 +119,7 @@ def list_tools(ctx):
92
119
 
93
120
 
94
121
  @tools_group.command()
122
+ @click.argument("file_arg", required=False, type=click.Path(exists=True))
95
123
  @click.option(
96
124
  "--file",
97
125
  type=click.Path(exists=True),
@@ -111,11 +139,15 @@ def list_tools(ctx):
111
139
  )
112
140
  @output_flags()
113
141
  @click.pass_context
114
- def create(ctx, file, name, description, tags):
142
+ def create(ctx, file_arg, file, name, description, tags):
115
143
  """Create a new tool."""
116
144
  try:
117
145
  client = get_client(ctx)
118
146
 
147
+ # Allow positional file argument for better DX (matches examples)
148
+ if not file and file_arg:
149
+ file = file_arg
150
+
119
151
  # Validate required parameters based on creation method
120
152
  if not file:
121
153
  # Metadata-only tool creation
@@ -126,27 +158,33 @@ def create(ctx, file, name, description, tags):
126
158
 
127
159
  # Create tool based on whether file is provided
128
160
  if file:
129
- # File-based tool creation - use create_tool_from_code for proper plugin processing
161
+ # File-based tool creation validate internal plugin name, no rewriting
130
162
  with open(file, encoding="utf-8") as f:
131
163
  code_content = f.read()
132
164
 
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)
165
+ internal_name = _extract_internal_name(code_content)
166
+ tool_name = _validate_name_match(name, internal_name)
167
+ _check_duplicate_name(client, tool_name)
168
+
169
+ # Upload the plugin code as-is (no rewrite)
170
+ tool = client.create_tool_from_code(
171
+ tool_name,
172
+ code_content,
173
+ framework="langchain", # Always langchain
174
+ description=description,
175
+ tags=_parse_tags(tags),
176
+ )
141
177
  else:
142
178
  # Metadata-only tool creation
143
179
  tool_kwargs = {}
144
180
  if name:
145
181
  tool_kwargs["name"] = name
182
+ tool_kwargs["tool_type"] = "custom" # Always custom
183
+ tool_kwargs["framework"] = "langchain" # Always langchain
146
184
  if description:
147
185
  tool_kwargs["description"] = description
148
186
  if tags:
149
- tool_kwargs["tags"] = [tag.strip() for tag in tags.split(",")]
187
+ tool_kwargs["tags"] = _parse_tags(tags)
150
188
 
151
189
  tool = client.create_tool(**tool_kwargs)
152
190
 
@@ -160,8 +198,8 @@ def create(ctx, file, name, description, tags):
160
198
  panel = Panel(
161
199
  f"[green]✅ Tool '{tool.name}' created successfully via {creation_method}![/green]\n\n"
162
200
  f"ID: {tool.id}\n"
163
- f"Framework: langchain (default)\n"
164
- f"Type: {'custom' if file else 'native'} (auto-detected)\n"
201
+ f"Framework: {getattr(tool, 'framework', 'N/A')} (default)\n"
202
+ f"Type: {getattr(tool, 'tool_type', 'N/A')} (auto-detected)\n"
165
203
  f"Description: {getattr(tool, 'description', 'No description')}",
166
204
  title="🔧 Tool Created",
167
205
  border_style="green",
@@ -172,7 +210,7 @@ def create(ctx, file, name, description, tags):
172
210
  if ctx.obj.get("view") == "json":
173
211
  click.echo(json.dumps({"error": str(e)}, indent=2))
174
212
  else:
175
- console.print(f"[red]Error creating tool: {e}[/red]")
213
+ console.print(Text(f"[red]Error creating tool: {e}[/red]"))
176
214
  raise click.ClickException(str(e))
177
215
 
178
216
 
@@ -239,15 +277,15 @@ def update(ctx, tool_id, file, description, tags):
239
277
  # Update code
240
278
  updated_tool = tool.update(file_path=file)
241
279
  if ctx.obj.get("view") != "json":
242
- console.print(f"[green]✓[/green] Tool code updated from {file}")
280
+ console.print(Text(f"[green]✓[/green] Tool code updated from {file}"))
243
281
  elif update_data:
244
282
  # Update metadata
245
283
  updated_tool = tool.update(**update_data)
246
284
  if ctx.obj.get("view") != "json":
247
- console.print("[green]✓[/green] Tool metadata updated")
285
+ console.print(Text("[green]✓[/green] Tool metadata updated"))
248
286
  else:
249
287
  if ctx.obj.get("view") != "json":
250
- console.print("[yellow]No updates specified[/yellow]")
288
+ console.print(Text("[yellow]No updates specified[/yellow]"))
251
289
  return
252
290
 
253
291
  if ctx.obj.get("view") == "json":
@@ -261,7 +299,7 @@ def update(ctx, tool_id, file, description, tags):
261
299
  if ctx.obj.get("view") == "json":
262
300
  click.echo(json.dumps({"error": str(e)}, indent=2))
263
301
  else:
264
- console.print(f"[red]Error updating tool: {e}[/red]")
302
+ console.print(Text(f"[red]Error updating tool: {e}[/red]"))
265
303
  raise click.ClickException(str(e))
266
304
 
267
305
 
@@ -286,7 +324,7 @@ def delete(ctx, tool_id, yes):
286
324
  f"Are you sure you want to delete tool '{tool.name}'?"
287
325
  ):
288
326
  if ctx.obj.get("view") != "json":
289
- console.print("Deletion cancelled.")
327
+ console.print(Text("Deletion cancelled."))
290
328
  return
291
329
 
292
330
  tool.delete()
@@ -299,11 +337,112 @@ def delete(ctx, tool_id, yes):
299
337
  )
300
338
  )
301
339
  else:
302
- console.print(f"[green]✅ Tool '{tool.name}' deleted successfully[/green]")
340
+ console.print(
341
+ Text(f"[green]✅ Tool '{tool.name}' deleted successfully[/green]")
342
+ )
343
+
344
+ except Exception as e:
345
+ if ctx.obj.get("view") == "json":
346
+ click.echo(json.dumps({"error": str(e)}, indent=2))
347
+ else:
348
+ console.print(Text(f"[red]Error deleting tool: {e}[/red]"))
349
+ raise click.ClickException(str(e))
350
+
351
+
352
+ @tools_group.command()
353
+ @click.argument("tool_id")
354
+ @output_flags()
355
+ @click.pass_context
356
+ def script(ctx, tool_id):
357
+ """Get tool script content."""
358
+ try:
359
+ client = get_client(ctx)
360
+
361
+ # Get tool by ID (no ambiguity handling needed)
362
+ try:
363
+ tool = client.get_tool_by_id(tool_id)
364
+ except Exception as e:
365
+ raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
366
+
367
+ # Get tool script content
368
+ script_content = client.tools.get_tool_script(tool_id)
369
+
370
+ if ctx.obj.get("view") == "json":
371
+ click.echo(
372
+ json.dumps(
373
+ {
374
+ "tool_id": tool_id,
375
+ "tool_name": tool.name,
376
+ "script": script_content,
377
+ },
378
+ indent=2,
379
+ )
380
+ )
381
+ elif ctx.obj.get("output"):
382
+ # Save to file
383
+ output_file = ctx.obj.get("output")
384
+ with open(output_file, "w", encoding="utf-8") as f:
385
+ f.write(script_content)
386
+ console.print(f"[green]✅ Tool script saved to {output_file}[/green]")
387
+ else:
388
+ # Display in terminal
389
+ console.print(
390
+ Panel(
391
+ script_content,
392
+ title=f"🔧 Tool Script: {tool.name}",
393
+ border_style="cyan",
394
+ )
395
+ )
396
+
397
+ except Exception as e:
398
+ if ctx.obj.get("view") == "json":
399
+ click.echo(json.dumps({"error": str(e)}, indent=2))
400
+ else:
401
+ console.print(f"[red]Error getting tool script: {e}[/red]")
402
+ raise click.ClickException(str(e))
403
+
404
+
405
+ @tools_group.command()
406
+ @click.argument("tool_id")
407
+ @click.option(
408
+ "--file",
409
+ type=click.Path(exists=True),
410
+ required=True,
411
+ help="New tool file for code update",
412
+ )
413
+ @click.option("--name", help="New tool name")
414
+ @click.option("--description", help="New description")
415
+ @click.option("--tags", help="Comma-separated tags")
416
+ @output_flags()
417
+ @click.pass_context
418
+ def upload_update(ctx, tool_id, file, name, description, tags):
419
+ """Update a tool plugin via file upload."""
420
+ try:
421
+ client = get_client(ctx)
422
+
423
+ # Prepare update data
424
+ update_data = {}
425
+ if name:
426
+ update_data["name"] = name
427
+ if description:
428
+ update_data["description"] = description
429
+ if tags:
430
+ update_data["tags"] = [tag.strip() for tag in tags.split(",")]
431
+
432
+ # Update tool via file upload
433
+ updated_tool = client.tools.update_tool_via_file(tool_id, file, **update_data)
434
+
435
+ if ctx.obj.get("view") == "json":
436
+ click.echo(json.dumps(updated_tool.model_dump(), indent=2))
437
+ else:
438
+ console.print(
439
+ f"[green]✅ Tool '{updated_tool.name}' updated successfully via file upload[/green]"
440
+ )
441
+ console.print(f"[blue]📁 File: {file}[/blue]")
303
442
 
304
443
  except Exception as e:
305
444
  if ctx.obj.get("view") == "json":
306
445
  click.echo(json.dumps({"error": str(e)}, indent=2))
307
446
  else:
308
- console.print(f"[red]Error deleting tool: {e}[/red]")
447
+ console.print(Text(f"[red]Error updating tool: {e}[/red]"))
309
448
  raise click.ClickException(str(e))
glaip_sdk/cli/main.py CHANGED
@@ -4,20 +4,33 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ import os
8
+ import subprocess
7
9
  import sys
8
10
 
9
11
  import click
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.table import Table
10
15
 
16
+ from glaip_sdk import Client
17
+ from glaip_sdk._version import __version__ as _SDK_VERSION
18
+ from glaip_sdk.branding import AIPBranding
11
19
  from glaip_sdk.cli.commands.agents import agents_group
12
- from glaip_sdk.cli.commands.configure import config_group, configure_command
20
+ from glaip_sdk.cli.commands.configure import (
21
+ config_group,
22
+ configure_command,
23
+ load_config,
24
+ )
13
25
  from glaip_sdk.cli.commands.init import init_command
14
26
  from glaip_sdk.cli.commands.mcps import mcps_group
15
27
  from glaip_sdk.cli.commands.models import models_group
16
28
  from glaip_sdk.cli.commands.tools import tools_group
29
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
17
30
 
18
31
 
19
32
  @click.group()
20
- @click.version_option(version="0.1.1", prog_name="aip")
33
+ @click.version_option(version=_SDK_VERSION, prog_name="aip")
21
34
  @click.option("--api-url", envvar="AIP_API_URL", help="AIP API URL")
22
35
  @click.option("--api-key", envvar="AIP_API_KEY", help="AIP API Key")
23
36
  @click.option("--timeout", default=30.0, help="Request timeout in seconds")
@@ -37,11 +50,12 @@ def main(ctx, api_url, api_key, timeout, view, no_tty):
37
50
  agents, tools, MCPs, and more.
38
51
 
39
52
  Examples:
53
+ aip version # Show detailed version info
40
54
  aip configure # Configure credentials
55
+ aip init # Initialize configuration
41
56
  aip agents list # List all agents
42
57
  aip tools create my_tool.py # Create a new tool
43
- aip agents run my-agent "Hello" # Run an agent
44
- aip init # Initialize configuration
58
+ aip agents run my-agent "Hello world" # Run an agent
45
59
  """
46
60
 
47
61
  # Store configuration in context
@@ -75,21 +89,19 @@ def status(ctx):
75
89
  """Show connection status and basic info."""
76
90
  config = {}
77
91
  try:
78
- from rich.console import Console
79
- from rich.panel import Panel
80
- from rich.table import Table
81
-
82
- from glaip_sdk import Client
83
- from glaip_sdk.cli.commands.configure import load_config
84
-
85
92
  console = Console()
86
93
 
94
+ # Display AIP status banner
95
+ branding = AIPBranding.create_from_sdk(
96
+ sdk_version=_SDK_VERSION, package_name="glaip-sdk"
97
+ )
98
+ branding.display_status_banner("ready")
99
+
87
100
  # Load config from file and merge with context
88
101
  file_config = load_config()
89
102
  context_config = ctx.obj or {}
90
103
 
91
104
  # Load environment variables (middle priority)
92
- import os
93
105
 
94
106
  env_config = {}
95
107
  if os.getenv("AIP_API_URL"):
@@ -146,7 +158,7 @@ def status(ctx):
146
158
  Panel(
147
159
  f"[bold green]✅ Connected to AIP Platform[/bold green]\n"
148
160
  f"🔗 API URL: {client.api_url}\n"
149
- f"⏱️ Timeout: {config.get('timeout', 30.0)}s",
161
+ f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
150
162
  title="🚀 Connection Status",
151
163
  border_style="green",
152
164
  )
@@ -191,9 +203,10 @@ def status(ctx):
191
203
  @main.command()
192
204
  def version():
193
205
  """Show version information."""
194
- from glaip_sdk.config.constants import SDK_VERSION
195
-
196
- click.echo(f"aip version {SDK_VERSION}")
206
+ branding = AIPBranding.create_from_sdk(
207
+ sdk_version=_SDK_VERSION, package_name="glaip-sdk"
208
+ )
209
+ branding.display_version_panel()
197
210
 
198
211
 
199
212
  @main.command()
@@ -201,17 +214,13 @@ def version():
201
214
  "--check-only", is_flag=True, help="Only check for updates without installing"
202
215
  )
203
216
  @click.option(
204
- "--force", is_flag=True, help="Force update even if no new version available"
217
+ "--force",
218
+ is_flag=True,
219
+ help="Force reinstall even if already up-to-date (adds --force-reinstall)",
205
220
  )
206
221
  def update(check_only: bool, force: bool):
207
222
  """Update AIP SDK to the latest version from PyPI."""
208
223
  try:
209
- import subprocess
210
- import sys
211
-
212
- from rich.console import Console
213
- from rich.panel import Panel
214
-
215
224
  console = Console()
216
225
 
217
226
  if check_only:
@@ -238,12 +247,17 @@ def update(check_only: bool, force: bool):
238
247
 
239
248
  # Update using pip
240
249
  try:
241
- subprocess.run(
242
- [sys.executable, "-m", "pip", "install", "--upgrade", "glaip-sdk"],
243
- capture_output=True,
244
- text=True,
245
- check=True,
246
- )
250
+ cmd = [
251
+ sys.executable,
252
+ "-m",
253
+ "pip",
254
+ "install",
255
+ "--upgrade",
256
+ "glaip-sdk",
257
+ ]
258
+ if force:
259
+ cmd.insert(5, "--force-reinstall")
260
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
247
261
 
248
262
  console.print(
249
263
  Panel(