agentic-fabriq-sdk 0.1.5__py3-none-any.whl → 0.1.7__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.

Potentially problematic release.


This version of agentic-fabriq-sdk might be problematic. Click here for more details.

@@ -0,0 +1,474 @@
1
+ """
2
+ Tool management commands for the Agentic Fabric CLI.
3
+ """
4
+
5
+
6
+ import typer
7
+
8
+ from af_cli.core.client import get_client
9
+ from af_cli.core.output import debug, error, info, print_output, success, warning
10
+
11
+ app = typer.Typer(help="Tool management commands")
12
+
13
+
14
+ @app.command()
15
+ def list(
16
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
17
+ ):
18
+ """List your tool connections (configured and connected tools)."""
19
+ try:
20
+ with get_client() as client:
21
+ connections = client.get("/api/v1/user-connections")
22
+
23
+ if not connections:
24
+ warning("No tool connections found. Add connections in the dashboard UI.")
25
+ return
26
+
27
+ # Format for better display
28
+ display_data = []
29
+ for conn in connections:
30
+ # Format tool name nicely (e.g., "google_docs" -> "Google Docs")
31
+ tool_name = conn.get("tool", "N/A").replace("_", " ").title()
32
+
33
+ # Status indicator
34
+ status = "✓ Connected" if conn.get("connected") else "○ Configured"
35
+
36
+ display_data.append({
37
+ "Tool": tool_name,
38
+ "ID": conn.get("connection_id", "N/A"),
39
+ "Name": conn.get("display_name") or conn.get("connection_id", "N/A"),
40
+ "Status": status,
41
+ "Method": conn.get("method", "oauth"),
42
+ "Added": conn.get("created_at", "N/A")[:10] if conn.get("created_at") else "N/A",
43
+ })
44
+
45
+ print_output(
46
+ display_data,
47
+ format_type=format,
48
+ title="Your Tool Connections"
49
+ )
50
+
51
+ except Exception as e:
52
+ error(f"Failed to list tool connections: {e}")
53
+ raise typer.Exit(1)
54
+
55
+
56
+ @app.command()
57
+ def get(
58
+ connection_id: str = typer.Argument(..., help="Connection ID (e.g., 'google', 'slack')"),
59
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
60
+ ):
61
+ """Get tool connection details."""
62
+ try:
63
+ with get_client() as client:
64
+ # Get all user connections and find the matching one
65
+ connections = client.get("/api/v1/user-connections")
66
+
67
+ # Find the specific connection
68
+ connection = None
69
+ for conn in connections:
70
+ if conn.get("connection_id") == connection_id or conn.get("tool") == connection_id:
71
+ connection = conn
72
+ break
73
+
74
+ if not connection:
75
+ error(f"Connection '{connection_id}' not found")
76
+ info("Available connections:")
77
+ for conn in connections:
78
+ info(f" - {conn.get('tool')} (ID: {conn.get('connection_id')})")
79
+ raise typer.Exit(1)
80
+
81
+ # Format tool name nicely
82
+ tool_name = connection.get("tool", "N/A").replace("_", " ").title()
83
+
84
+ # Format the connection details for display
85
+ details = {
86
+ "Tool": tool_name,
87
+ "Connection ID": connection.get("connection_id", "N/A"),
88
+ "Display Name": connection.get("display_name") or connection.get("connection_id", "N/A"),
89
+ "Status": "✓ Connected" if connection.get("connected") else "○ Configured",
90
+ "Method": connection.get("method", "oauth"),
91
+ "Created": connection.get("created_at", "N/A"),
92
+ "Updated": connection.get("updated_at", "N/A"),
93
+ }
94
+
95
+ # Add tool-specific fields if present
96
+ if connection.get("team_name"):
97
+ details["Team Name"] = connection.get("team_name")
98
+ if connection.get("team_id"):
99
+ details["Team ID"] = connection.get("team_id")
100
+ if connection.get("bot_user_id"):
101
+ details["Bot User ID"] = connection.get("bot_user_id")
102
+ if connection.get("email"):
103
+ details["Email"] = connection.get("email")
104
+ if connection.get("login"):
105
+ details["GitHub Login"] = connection.get("login")
106
+ if connection.get("workspace_name"):
107
+ details["Workspace Name"] = connection.get("workspace_name")
108
+ if connection.get("scopes"):
109
+ details["Scopes"] = ", ".join(connection.get("scopes", []))
110
+
111
+ print_output(
112
+ details,
113
+ format_type=format,
114
+ title=f"{tool_name} Connection Details"
115
+ )
116
+
117
+ except Exception as e:
118
+ error(f"Failed to get tool connection: {e}")
119
+ raise typer.Exit(1)
120
+
121
+
122
+ @app.command()
123
+ def invoke(
124
+ tool_id: str = typer.Argument(..., help="Tool ID"),
125
+ method: str = typer.Option(..., "--method", "-m", help="Tool method to invoke"),
126
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
127
+ ):
128
+ """Invoke a tool."""
129
+ try:
130
+ with get_client() as client:
131
+ data = {
132
+ "method": method,
133
+ "parameters": {},
134
+ "context": {},
135
+ }
136
+
137
+ info(f"Invoking tool {tool_id} method {method}...")
138
+ response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
139
+
140
+ success("Tool invoked successfully")
141
+ print_output(response, format_type=format)
142
+
143
+ except Exception as e:
144
+ error(f"Failed to invoke tool: {e}")
145
+ raise typer.Exit(1)
146
+
147
+
148
+ @app.command()
149
+ def add(
150
+ tool: str = typer.Argument(..., help="Tool name (google, slack, notion, github, etc.)"),
151
+ connection_id: str = typer.Option(..., "--connection-id", help="Unique connection ID"),
152
+ display_name: str = typer.Option(None, "--display-name", help="Human-readable name"),
153
+ method: str = typer.Option(..., "--method", help="Connection method: 'api' or 'credentials'"),
154
+
155
+ # API method fields
156
+ token: str = typer.Option(None, "--token", help="API token (for api method)"),
157
+
158
+ # Credentials method fields
159
+ client_id: str = typer.Option(None, "--client-id", help="OAuth client ID (for credentials method)"),
160
+ client_secret: str = typer.Option(None, "--client-secret", help="OAuth client secret (for credentials method)"),
161
+ redirect_uri: str = typer.Option(None, "--redirect-uri", help="OAuth redirect URI (optional, auto-generated)"),
162
+ ):
163
+ """
164
+ Add a new tool connection with credentials.
165
+
166
+ Examples:
167
+ # Notion (api method - single token)
168
+ afctl tools add notion --connection-id notion-work --method api --token "secret_abc123"
169
+
170
+ # Google (credentials method - OAuth app)
171
+ afctl tools add google --connection-id google-work --method credentials \\
172
+ --client-id "123.apps.googleusercontent.com" \\
173
+ --client-secret "GOCSPX-abc123"
174
+
175
+ # Slack bot (api method)
176
+ afctl tools add slack --connection-id slack-bot --method api --token "xoxb-123..."
177
+ """
178
+ try:
179
+ from af_cli.core.config import get_config
180
+
181
+ with get_client() as client:
182
+ # Validate method
183
+ if method not in ["api", "credentials"]:
184
+ error("Method must be 'api' or 'credentials'")
185
+ raise typer.Exit(1)
186
+
187
+ # Validate API method requirements
188
+ if method == "api":
189
+ if not token:
190
+ error("API method requires --token")
191
+ info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method api --token YOUR_TOKEN")
192
+ raise typer.Exit(1)
193
+
194
+ # Validate credentials method requirements
195
+ elif method == "credentials":
196
+ if not client_id or not client_secret:
197
+ error("Credentials method requires --client-id and --client-secret")
198
+ info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method credentials \\")
199
+ info(f" --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET")
200
+ raise typer.Exit(1)
201
+
202
+ info(f"Creating connection: {connection_id}")
203
+ info(f"Tool: {tool}")
204
+ info(f"Method: {method}")
205
+
206
+ # Step 1: Create connection metadata
207
+ connection_data = {
208
+ "tool": tool,
209
+ "connection_id": connection_id,
210
+ "display_name": display_name or connection_id,
211
+ "method": method,
212
+ }
213
+
214
+ client.post("/api/v1/user-connections", data=connection_data)
215
+ success(f"✅ Connection entry created: {connection_id}")
216
+
217
+ # Step 2: Store credentials based on method
218
+ if method == "credentials":
219
+ # Auto-generate redirect_uri if not provided
220
+ if not redirect_uri:
221
+ config = get_config()
222
+ redirect_uri = f"{config.gateway_url}/api/v1/tools/{tool}/oauth/callback"
223
+ info(f"Using default redirect URI: {redirect_uri}")
224
+
225
+ # Store OAuth app config
226
+ info("Storing OAuth app credentials...")
227
+ config_payload = {
228
+ "client_id": client_id,
229
+ "client_secret": client_secret,
230
+ }
231
+ if redirect_uri:
232
+ config_payload["redirect_uri"] = redirect_uri
233
+
234
+ client.post(
235
+ f"/api/v1/tools/{tool}/config?connection_id={connection_id}",
236
+ data=config_payload
237
+ )
238
+ success("✅ OAuth app credentials stored")
239
+ info("")
240
+ info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
241
+
242
+ elif method == "api":
243
+ # Store API token directly
244
+ info("Storing API credentials...")
245
+
246
+ # Tool-specific endpoint and payload mappings
247
+ if tool == "notion":
248
+ # Notion uses /config endpoint with integration_token field
249
+ endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
250
+ cred_payload = {"integration_token": token}
251
+ else:
252
+ # Generic tools use /connection endpoint with api_token field
253
+ endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
254
+ cred_payload = {"api_token": token}
255
+
256
+ client.post(endpoint, data=cred_payload)
257
+ success("✅ API credentials stored")
258
+ success(f"✅ Connection '{connection_id}' is ready to use!")
259
+
260
+ # Show helpful info
261
+ info("")
262
+ info("View your connections:")
263
+ info(f" • List all: afctl tools list")
264
+ info(f" • View details: afctl tools get {connection_id}")
265
+
266
+ except Exception as e:
267
+ error(f"Failed to add connection: {e}")
268
+ raise typer.Exit(1)
269
+
270
+
271
+ @app.command()
272
+ def connect(
273
+ connection_id: str = typer.Argument(..., help="Connection ID to connect"),
274
+ ):
275
+ """Complete OAuth connection (open browser for authorization)."""
276
+ try:
277
+ import webbrowser
278
+ import time
279
+
280
+ with get_client() as client:
281
+ # Get connection info
282
+ connections = client.get("/api/v1/user-connections")
283
+
284
+ connection = None
285
+ for conn in connections:
286
+ if conn.get("connection_id") == connection_id:
287
+ connection = conn
288
+ break
289
+
290
+ if not connection:
291
+ error(f"Connection '{connection_id}' not found")
292
+ info("Run 'afctl tools list' to see available connections")
293
+ raise typer.Exit(1)
294
+
295
+ tool = connection["tool"]
296
+ method = connection["method"]
297
+
298
+ # Only credentials method needs OAuth completion
299
+ if method != "credentials":
300
+ error(f"Connection '{connection_id}' uses '{method}' method")
301
+ info("Only 'credentials' method connections need OAuth setup")
302
+ info("API connections are already connected after 'afctl tools add'")
303
+ raise typer.Exit(1)
304
+
305
+ # Check if already connected
306
+ if connection.get("connected"):
307
+ warning(f"Connection '{connection_id}' is already connected")
308
+ confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
309
+ if not confirm:
310
+ return
311
+
312
+ # Initiate OAuth flow
313
+ info(f"Initiating OAuth for {tool}...")
314
+
315
+ result = client.post(
316
+ f"/api/v1/tools/{tool}/connect/initiate?connection_id={connection_id}",
317
+ data={}
318
+ )
319
+
320
+ debug(f"Backend response: {result}")
321
+
322
+ # Different tools use different field names for the auth URL
323
+ auth_url = (
324
+ result.get("authorization_url") or
325
+ result.get("auth_url") or
326
+ result.get("oauth_url")
327
+ )
328
+
329
+ if not auth_url:
330
+ error("Failed to get authorization URL from backend")
331
+ error(f"Response keys: {list(result.keys())}")
332
+ debug(f"Full response: {result}")
333
+ raise typer.Exit(1)
334
+
335
+ info("Opening browser for authentication...")
336
+ info("")
337
+ info(f"If browser doesn't open, visit: {auth_url}")
338
+
339
+ # Open browser
340
+ webbrowser.open(auth_url)
341
+
342
+ info("")
343
+ info("Waiting for authorization...")
344
+ info("(Complete the login in your browser)")
345
+
346
+ # Poll for connection completion
347
+ max_attempts = 120 # 2 minutes
348
+ for attempt in range(max_attempts):
349
+ time.sleep(1)
350
+
351
+ # Check connection status
352
+ connections = client.get("/api/v1/user-connections")
353
+ for conn in connections:
354
+ if conn.get("connection_id") == connection_id:
355
+ if conn.get("connected"):
356
+ info("")
357
+ success(f"✅ Successfully connected to {tool}!")
358
+
359
+ # Show connection details
360
+ info(f"Connection ID: {connection_id}")
361
+ if conn.get("email"):
362
+ info(f"Email: {conn['email']}")
363
+ if conn.get("team_name"):
364
+ info(f"Team: {conn['team_name']}")
365
+ if conn.get("login"):
366
+ info(f"GitHub: {conn['login']}")
367
+
368
+ return
369
+ break
370
+
371
+ # Timeout
372
+ error("")
373
+ error("Timeout: Authorization not completed within 2 minutes")
374
+ info("Please try again or check your browser")
375
+ raise typer.Exit(1)
376
+
377
+ except Exception as e:
378
+ error(f"Failed to connect: {e}")
379
+ raise typer.Exit(1)
380
+
381
+
382
+ @app.command()
383
+ def disconnect(
384
+ connection_id: str = typer.Argument(..., help="Connection ID to disconnect"),
385
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
386
+ ):
387
+ """Disconnect a tool (remove credentials but keep connection entry)."""
388
+ try:
389
+ with get_client() as client:
390
+ # Get connection info
391
+ connections = client.get("/api/v1/user-connections")
392
+
393
+ connection = None
394
+ for conn in connections:
395
+ if conn.get("connection_id") == connection_id:
396
+ connection = conn
397
+ break
398
+
399
+ if not connection:
400
+ error(f"Connection '{connection_id}' not found")
401
+ raise typer.Exit(1)
402
+
403
+ tool = connection["tool"]
404
+ tool_display = connection.get("display_name") or connection_id
405
+
406
+ # Check if connected
407
+ if not connection.get("connected"):
408
+ error(f"Connection '{connection_id}' is already disconnected")
409
+ info(f"Use 'afctl tools get {connection_id}' to view status")
410
+ raise typer.Exit(1)
411
+
412
+ # Confirm
413
+ if not force:
414
+ warning(f"This will remove OAuth tokens/credentials for '{tool_display}'")
415
+ info("You can reconnect later with 'afctl tools connect'")
416
+ confirm = typer.confirm(f"Disconnect {tool} connection '{connection_id}'?")
417
+ if not confirm:
418
+ info("Cancelled")
419
+ return
420
+
421
+ # Delete connection credentials
422
+ client.delete(
423
+ f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
424
+ )
425
+
426
+ success(f"✅ Disconnected: {connection_id}")
427
+ info("Connection entry preserved.")
428
+ info(f"Run 'afctl tools connect {connection_id}' to reconnect.")
429
+
430
+ except Exception as e:
431
+ error(f"Failed to disconnect: {e}")
432
+ raise typer.Exit(1)
433
+
434
+
435
+ @app.command()
436
+ def remove(
437
+ connection_id: str = typer.Argument(..., help="Connection ID to remove"),
438
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
439
+ ):
440
+ """Remove a tool connection completely (delete entry and credentials)."""
441
+ try:
442
+ with get_client() as client:
443
+ # Get connection info
444
+ connections = client.get("/api/v1/user-connections")
445
+
446
+ connection = None
447
+ for conn in connections:
448
+ if conn.get("connection_id") == connection_id:
449
+ connection = conn
450
+ break
451
+
452
+ if not connection:
453
+ error(f"Connection '{connection_id}' not found")
454
+ raise typer.Exit(1)
455
+
456
+ tool = connection["tool"]
457
+ tool_display = connection.get("display_name") or connection_id
458
+
459
+ # Confirm
460
+ if not force:
461
+ warning("⚠️ This will permanently delete the connection and credentials")
462
+ confirm = typer.confirm(f"Remove {tool} connection '{tool_display}'?")
463
+ if not confirm:
464
+ info("Cancelled")
465
+ return
466
+
467
+ # Delete connection entry (backend will cascade delete credentials)
468
+ client.delete(f"/api/v1/user-connections/{connection_id}")
469
+
470
+ success(f"✅ Removed: {connection_id}")
471
+
472
+ except Exception as e:
473
+ error(f"Failed to remove: {e}")
474
+ raise typer.Exit(1)
@@ -0,0 +1,3 @@
1
+ """
2
+ Core utilities for the Agentic Fabric CLI.
3
+ """
af_cli/core/client.py ADDED
@@ -0,0 +1,127 @@
1
+ """
2
+ HTTP client for communicating with the Agentic Fabric Gateway.
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Dict, Optional
7
+ from urllib.parse import urljoin
8
+
9
+ import httpx
10
+ import typer
11
+
12
+ from af_cli.core.config import get_config
13
+ from af_cli.core.output import debug, error
14
+
15
+
16
+ class AFClient:
17
+ """HTTP client for Agentic Fabric Gateway API."""
18
+
19
+ def __init__(self, config: Optional[Dict] = None):
20
+ self.config = config or get_config()
21
+ self.client = httpx.Client(
22
+ base_url=self.config.gateway_url,
23
+ timeout=30.0,
24
+ follow_redirects=True,
25
+ )
26
+
27
+ def _get_headers(self) -> Dict[str, str]:
28
+ """Get HTTP headers for requests."""
29
+ return self.config.get_headers()
30
+
31
+ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
32
+ """Handle HTTP response."""
33
+ debug(f"Response: {response.status_code} {response.url}")
34
+
35
+ if response.status_code == 401:
36
+ error("Authentication failed. Please run 'afctl auth login'")
37
+ raise typer.Exit(1)
38
+
39
+ if response.status_code == 403:
40
+ error("Access denied. Check your permissions.")
41
+ raise typer.Exit(1)
42
+
43
+ if response.status_code >= 400:
44
+ try:
45
+ error_data = response.json()
46
+ # Try different error message fields (FastAPI uses "detail")
47
+ error_message = error_data.get("detail") or error_data.get("message") or "Unknown error"
48
+ error(f"API Error: {error_message}")
49
+ # Always show full response for debugging
50
+ debug(f"Response status: {response.status_code}")
51
+ debug(f"Request URL: {response.url}")
52
+ debug(f"Full response: {json.dumps(error_data, indent=2)}")
53
+ except:
54
+ error(f"HTTP Error: {response.status_code}")
55
+ debug(f"Response text: {response.text}")
56
+ raise typer.Exit(1)
57
+
58
+ try:
59
+ return response.json()
60
+ except:
61
+ return {"message": "Success"}
62
+
63
+ def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
64
+ """Make GET request."""
65
+ url = urljoin(self.config.gateway_url, path)
66
+ debug(f"GET {url}")
67
+
68
+ response = self.client.get(
69
+ path,
70
+ params=params,
71
+ headers=self._get_headers(),
72
+ )
73
+
74
+ return self._handle_response(response)
75
+
76
+ def post(self, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
77
+ """Make POST request."""
78
+ url = urljoin(self.config.gateway_url, path)
79
+ debug(f"POST {url}")
80
+
81
+ response = self.client.post(
82
+ path,
83
+ json=data,
84
+ headers=self._get_headers(),
85
+ )
86
+
87
+ return self._handle_response(response)
88
+
89
+ def put(self, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
90
+ """Make PUT request."""
91
+ url = urljoin(self.config.gateway_url, path)
92
+ debug(f"PUT {url}")
93
+
94
+ response = self.client.put(
95
+ path,
96
+ json=data,
97
+ headers=self._get_headers(),
98
+ )
99
+
100
+ return self._handle_response(response)
101
+
102
+ def delete(self, path: str) -> Dict[str, Any]:
103
+ """Make DELETE request."""
104
+ url = urljoin(self.config.gateway_url, path)
105
+ debug(f"DELETE {url}")
106
+
107
+ response = self.client.delete(
108
+ path,
109
+ headers=self._get_headers(),
110
+ )
111
+
112
+ return self._handle_response(response)
113
+
114
+ def close(self) -> None:
115
+ """Close the HTTP client."""
116
+ self.client.close()
117
+
118
+ def __enter__(self):
119
+ return self
120
+
121
+ def __exit__(self, exc_type, exc_val, exc_tb):
122
+ self.close()
123
+
124
+
125
+ def get_client() -> AFClient:
126
+ """Get HTTP client instance."""
127
+ return AFClient()