applied-cli 0.5.74__tar.gz → 0.6.1__tar.gz
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.
- {applied_cli-0.5.74 → applied_cli-0.6.1}/PKG-INFO +2 -1
- applied_cli-0.6.1/applied_cli/__init__.py +20 -0
- applied_cli-0.6.1/applied_cli/auth.py +111 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/cli.py +355 -6
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/client.py +136 -0
- applied_cli-0.6.1/applied_cli/mcp.py +222 -0
- applied_cli-0.6.1/applied_cli/recovery.py +400 -0
- applied_cli-0.6.1/applied_cli/toolkit.py +255 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/tools.py +201 -7
- applied_cli-0.6.1/applied_cli/v2/__init__.py +11 -0
- applied_cli-0.6.1/applied_cli/v2/agents.py +448 -0
- applied_cli-0.6.1/applied_cli/v2/articles.py +364 -0
- applied_cli-0.6.1/applied_cli/v2/catalog.py +278 -0
- applied_cli-0.6.1/applied_cli/v2/connectors.py +122 -0
- applied_cli-0.6.1/applied_cli/v2/content.py +337 -0
- applied_cli-0.6.1/applied_cli/v2/conversations.py +709 -0
- applied_cli-0.6.1/applied_cli/v2/domains.py +142 -0
- applied_cli-0.6.1/applied_cli/v2/flows.py +1889 -0
- applied_cli-0.6.1/applied_cli/v2/knowledge.py +609 -0
- applied_cli-0.6.1/applied_cli/v2/manifest.py +129 -0
- applied_cli-0.6.1/applied_cli/v2/products.py +350 -0
- applied_cli-0.6.1/applied_cli/v2/scenarios.py +1012 -0
- applied_cli-0.6.1/applied_cli/v2/taxonomy.py +261 -0
- applied_cli-0.6.1/applied_cli/v2/tickets.py +292 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/PKG-INFO +2 -1
- applied_cli-0.6.1/applied_cli.egg-info/SOURCES.txt +63 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/requires.txt +1 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/pyproject.toml +2 -1
- applied_cli-0.6.1/tests/test_auth_context.py +51 -0
- applied_cli-0.6.1/tests/test_benchmark_clone.py +242 -0
- applied_cli-0.6.1/tests/test_cli_v2.py +164 -0
- applied_cli-0.6.1/tests/test_client_v2.py +95 -0
- applied_cli-0.6.1/tests/test_recovery.py +351 -0
- applied_cli-0.6.1/tests/test_toolkit_contract.py +230 -0
- applied_cli-0.6.1/tests/test_v2_agents.py +536 -0
- applied_cli-0.6.1/tests/test_v2_articles.py +387 -0
- applied_cli-0.6.1/tests/test_v2_catalog_and_mcp.py +327 -0
- applied_cli-0.6.1/tests/test_v2_connectors.py +100 -0
- applied_cli-0.6.1/tests/test_v2_content.py +253 -0
- applied_cli-0.6.1/tests/test_v2_conversations.py +426 -0
- applied_cli-0.6.1/tests/test_v2_flows.py +1740 -0
- applied_cli-0.6.1/tests/test_v2_knowledge.py +551 -0
- applied_cli-0.6.1/tests/test_v2_products.py +382 -0
- applied_cli-0.6.1/tests/test_v2_scenarios.py +876 -0
- applied_cli-0.6.1/tests/test_v2_taxonomy.py +284 -0
- applied_cli-0.6.1/tests/test_v2_tickets.py +328 -0
- applied_cli-0.5.74/applied_cli/__init__.py +0 -9
- applied_cli-0.5.74/applied_cli.egg-info/SOURCES.txt +0 -26
- {applied_cli-0.5.74 → applied_cli-0.6.1}/README.md +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/setup.cfg +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_cli.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_client.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_knowledge_content_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.11
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
19
|
Requires-Dist: typer>=0.9.0
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Applied Labs CLI - shared client library for CLI and MCP server."""
|
|
2
|
+
|
|
3
|
+
from applied_cli import tools
|
|
4
|
+
from applied_cli.auth import AuthContext
|
|
5
|
+
from applied_cli.client import AppliedClient
|
|
6
|
+
from applied_cli.formatters import to_csv, to_json
|
|
7
|
+
from applied_cli.toolkit import ToolResult, ToolSpec
|
|
8
|
+
|
|
9
|
+
__version__ = "0.6.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AppliedClient",
|
|
13
|
+
"AuthContext",
|
|
14
|
+
"ToolResult",
|
|
15
|
+
"ToolSpec",
|
|
16
|
+
"tools",
|
|
17
|
+
"to_csv",
|
|
18
|
+
"to_json",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Shared authentication context for local CLI and MCP-injected sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from applied_cli.client import AppliedAPIError, AppliedClient
|
|
11
|
+
from applied_cli.credentials import load_credentials
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthStatus(BaseModel):
|
|
17
|
+
ok: bool
|
|
18
|
+
shop_id: str | None = None
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL
|
|
20
|
+
message: str
|
|
21
|
+
status_code: int | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthContext(BaseModel):
|
|
25
|
+
token: str
|
|
26
|
+
shop_id: str | None = None
|
|
27
|
+
base_url: str = DEFAULT_BASE_URL
|
|
28
|
+
access_mode: str | None = None
|
|
29
|
+
|
|
30
|
+
def to_client(self) -> AppliedClient:
|
|
31
|
+
return AppliedClient(
|
|
32
|
+
token=self.token,
|
|
33
|
+
shop_id=self.shop_id,
|
|
34
|
+
base_url=self.base_url,
|
|
35
|
+
access_mode=self.access_mode,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def validate(self) -> AuthStatus:
|
|
39
|
+
client = self.to_client()
|
|
40
|
+
try:
|
|
41
|
+
await client.request(
|
|
42
|
+
"GET",
|
|
43
|
+
"/v1/agents/",
|
|
44
|
+
params={"limit": 1},
|
|
45
|
+
shop_id=self.shop_id,
|
|
46
|
+
)
|
|
47
|
+
except AppliedAPIError as exc:
|
|
48
|
+
return AuthStatus(
|
|
49
|
+
ok=False,
|
|
50
|
+
shop_id=self.shop_id,
|
|
51
|
+
base_url=self.base_url,
|
|
52
|
+
message=str(exc),
|
|
53
|
+
status_code=exc.status_code,
|
|
54
|
+
)
|
|
55
|
+
return AuthStatus(
|
|
56
|
+
ok=True,
|
|
57
|
+
shop_id=self.shop_id,
|
|
58
|
+
base_url=self.base_url,
|
|
59
|
+
message="Authenticated.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CredentialProvider(Protocol):
|
|
64
|
+
def load(self) -> AuthContext | None:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EnvCredentialProvider:
|
|
69
|
+
"""Load credentials from environment variables for automation/MCP smoke tests."""
|
|
70
|
+
|
|
71
|
+
def load(self) -> AuthContext | None:
|
|
72
|
+
token = os.environ.get("APPLIED_TOKEN") or os.environ.get("APPLIED_API_TOKEN")
|
|
73
|
+
if not token:
|
|
74
|
+
return None
|
|
75
|
+
return AuthContext(
|
|
76
|
+
token=token,
|
|
77
|
+
shop_id=os.environ.get("APPLIED_SHOP_ID"),
|
|
78
|
+
base_url=os.environ.get("APPLIED_BASE_URL", DEFAULT_BASE_URL),
|
|
79
|
+
access_mode=os.environ.get("APPLIED_ASSISTANT_ACCESS_MODE"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StoredCredentialProvider:
|
|
84
|
+
"""Load credentials written by `applied login`."""
|
|
85
|
+
|
|
86
|
+
def load(self) -> AuthContext | None:
|
|
87
|
+
credentials = load_credentials()
|
|
88
|
+
if not credentials or not credentials.get("api_token"):
|
|
89
|
+
return None
|
|
90
|
+
return AuthContext(
|
|
91
|
+
token=credentials["api_token"],
|
|
92
|
+
shop_id=credentials.get("shop_id"),
|
|
93
|
+
base_url=credentials.get("base_url", DEFAULT_BASE_URL),
|
|
94
|
+
access_mode=os.environ.get("APPLIED_ASSISTANT_ACCESS_MODE"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ChainedCredentialProvider:
|
|
99
|
+
def __init__(self, *providers: CredentialProvider):
|
|
100
|
+
self.providers = providers
|
|
101
|
+
|
|
102
|
+
def load(self) -> AuthContext | None:
|
|
103
|
+
for provider in self.providers:
|
|
104
|
+
context = provider.load()
|
|
105
|
+
if context:
|
|
106
|
+
return context
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def default_credential_provider() -> CredentialProvider:
|
|
111
|
+
return ChainedCredentialProvider(EnvCredentialProvider(), StoredCredentialProvider())
|
|
@@ -10,12 +10,21 @@ import typer
|
|
|
10
10
|
|
|
11
11
|
from applied_cli import __version__, tools
|
|
12
12
|
from applied_cli.agent_scoped_flows import run_agent_scoped_flow_command
|
|
13
|
+
from applied_cli.auth import AuthContext, default_credential_provider
|
|
13
14
|
from applied_cli.client import AppliedClient
|
|
14
15
|
from applied_cli.credentials import (
|
|
15
16
|
clear_credentials,
|
|
16
17
|
load_credentials,
|
|
17
18
|
save_credentials,
|
|
18
19
|
)
|
|
20
|
+
from applied_cli.toolkit import render_tool_result
|
|
21
|
+
from applied_cli.v2.catalog import (
|
|
22
|
+
execute_tool as execute_v2_tool,
|
|
23
|
+
)
|
|
24
|
+
from applied_cli.v2.catalog import (
|
|
25
|
+
get_tool_catalog,
|
|
26
|
+
)
|
|
27
|
+
from applied_cli.v2.manifest import build_doctor_report, build_tool_manifest
|
|
19
28
|
|
|
20
29
|
app = typer.Typer(
|
|
21
30
|
name="applied",
|
|
@@ -24,9 +33,24 @@ app = typer.Typer(
|
|
|
24
33
|
)
|
|
25
34
|
content_app = typer.Typer(help="Inspect synced content items.")
|
|
26
35
|
app.add_typer(content_app, name="content")
|
|
36
|
+
v2_app = typer.Typer(help="Run v2 typed Applied tools.")
|
|
37
|
+
v2_tools_app = typer.Typer(help="Inspect v2 tool schemas.")
|
|
38
|
+
v2_conversations_app = typer.Typer(help="Conversation tools.")
|
|
39
|
+
v2_app.add_typer(v2_tools_app, name="tools")
|
|
40
|
+
v2_app.add_typer(v2_conversations_app, name="conversations")
|
|
41
|
+
app.add_typer(v2_app, name="v2")
|
|
27
42
|
|
|
28
43
|
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
29
44
|
DEFAULT_CLIENT_URL = "https://appliedlabs.ai"
|
|
45
|
+
DEFAULT_RECOVERY_DIR_OPTION = typer.Option(
|
|
46
|
+
"/tmp/applied-labs-recovery-jun4-1915",
|
|
47
|
+
"--recovery-dir",
|
|
48
|
+
help="Directory containing exports/ and deleted_only/ recovery files",
|
|
49
|
+
)
|
|
50
|
+
AGENT_DEPLOY_AGENT_IDS_ARGUMENT = typer.Argument(
|
|
51
|
+
...,
|
|
52
|
+
help="One or more Agent IDs to deploy",
|
|
53
|
+
)
|
|
30
54
|
|
|
31
55
|
|
|
32
56
|
def _version_callback(value: bool) -> None:
|
|
@@ -144,6 +168,47 @@ def _build_conversation_filters(
|
|
|
144
168
|
return filters or None
|
|
145
169
|
|
|
146
170
|
|
|
171
|
+
def _v2_auth_context() -> AuthContext | None:
|
|
172
|
+
return default_credential_provider().load()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_v2_input(value: str | None) -> dict:
|
|
176
|
+
if not value:
|
|
177
|
+
return {}
|
|
178
|
+
parsed = _parse_json_option(value, option_name="--input")
|
|
179
|
+
return parsed or {}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _v2_tool_metadata(include_debug: bool = False) -> list[dict]:
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
"name": spec.name,
|
|
186
|
+
"namespace": spec.namespace,
|
|
187
|
+
"description": spec.description,
|
|
188
|
+
"read_write_mode": spec.read_write_mode,
|
|
189
|
+
"tags": spec.tags,
|
|
190
|
+
}
|
|
191
|
+
for spec in get_tool_catalog(include_debug=include_debug)
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _v2_tool_detail(tool_name: str, include_debug: bool = False) -> dict:
|
|
196
|
+
catalog = get_tool_catalog(include_debug=include_debug)
|
|
197
|
+
spec = catalog.get(tool_name)
|
|
198
|
+
if spec is None:
|
|
199
|
+
typer.echo(f"Unknown v2 tool: {tool_name}", err=True)
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
return {
|
|
202
|
+
"name": spec.name,
|
|
203
|
+
"namespace": spec.namespace,
|
|
204
|
+
"description": spec.description,
|
|
205
|
+
"read_write_mode": spec.read_write_mode,
|
|
206
|
+
"tags": spec.tags,
|
|
207
|
+
"examples": spec.examples,
|
|
208
|
+
"input_schema": spec.input_schema(),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
147
212
|
# -----------------------------------------------------------------------------
|
|
148
213
|
# Auth commands
|
|
149
214
|
# -----------------------------------------------------------------------------
|
|
@@ -230,7 +295,13 @@ def logout() -> None:
|
|
|
230
295
|
|
|
231
296
|
|
|
232
297
|
@app.command()
|
|
233
|
-
def whoami(
|
|
298
|
+
def whoami(
|
|
299
|
+
verify: bool = typer.Option(
|
|
300
|
+
True,
|
|
301
|
+
"--verify/--no-verify",
|
|
302
|
+
help="Verify the token against the Applied API.",
|
|
303
|
+
),
|
|
304
|
+
) -> None:
|
|
234
305
|
"""Show current authentication status."""
|
|
235
306
|
creds = load_credentials()
|
|
236
307
|
if not creds or not creds.get("api_token"):
|
|
@@ -243,6 +314,189 @@ def whoami() -> None:
|
|
|
243
314
|
typer.echo(f"Base URL: {creds.get('base_url', DEFAULT_BASE_URL)}")
|
|
244
315
|
if creds.get("shop_id"):
|
|
245
316
|
typer.echo(f"Shop ID: {creds['shop_id']}")
|
|
317
|
+
if verify:
|
|
318
|
+
context = AuthContext(
|
|
319
|
+
token=creds["api_token"],
|
|
320
|
+
shop_id=creds.get("shop_id"),
|
|
321
|
+
base_url=creds.get("base_url", DEFAULT_BASE_URL),
|
|
322
|
+
)
|
|
323
|
+
status = asyncio.run(context.validate())
|
|
324
|
+
typer.echo(f"Verified: {'yes' if status.ok else 'no'}")
|
|
325
|
+
if not status.ok:
|
|
326
|
+
typer.echo(status.message, err=True)
|
|
327
|
+
raise typer.Exit(1)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@v2_conversations_app.command("search")
|
|
331
|
+
def v2_conversations_search(
|
|
332
|
+
search: str | None = typer.Option(None, "--search", help="Full-text search query"),
|
|
333
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Max conversations"),
|
|
334
|
+
json_output: bool = typer.Option(
|
|
335
|
+
False,
|
|
336
|
+
"--json",
|
|
337
|
+
help="Render the structured tool result as JSON.",
|
|
338
|
+
),
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Search conversations through the v2 typed tool catalog."""
|
|
341
|
+
|
|
342
|
+
async def _run():
|
|
343
|
+
return await execute_v2_tool(
|
|
344
|
+
"conversations_search",
|
|
345
|
+
{"search": search, "limit": limit},
|
|
346
|
+
auth_context=_v2_auth_context(),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
result = asyncio.run(_run())
|
|
350
|
+
typer.echo(render_tool_result(result, "json" if json_output else "text"))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@v2_tools_app.command("list")
|
|
354
|
+
def v2_tools_list(
|
|
355
|
+
include_debug: bool = typer.Option(
|
|
356
|
+
False,
|
|
357
|
+
"--include-debug",
|
|
358
|
+
help="Include gated debug tools.",
|
|
359
|
+
),
|
|
360
|
+
json_output: bool = typer.Option(
|
|
361
|
+
False,
|
|
362
|
+
"--json",
|
|
363
|
+
help="Render tool metadata as JSON.",
|
|
364
|
+
),
|
|
365
|
+
) -> None:
|
|
366
|
+
"""List registered v2 Applied tools."""
|
|
367
|
+
payload = {"tools": _v2_tool_metadata(include_debug=include_debug)}
|
|
368
|
+
if json_output:
|
|
369
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
for row in payload["tools"]:
|
|
373
|
+
first_line = row["description"].splitlines()[0]
|
|
374
|
+
typer.echo(
|
|
375
|
+
f"{row['name']}\t{row['read_write_mode']}\t{row['namespace']}\t{first_line}"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@v2_tools_app.command("describe")
|
|
380
|
+
def v2_tools_describe(
|
|
381
|
+
tool_name: str = typer.Argument(..., help="v2 tool name"),
|
|
382
|
+
include_debug: bool = typer.Option(
|
|
383
|
+
False,
|
|
384
|
+
"--include-debug",
|
|
385
|
+
help="Include gated debug tools.",
|
|
386
|
+
),
|
|
387
|
+
json_output: bool = typer.Option(
|
|
388
|
+
False,
|
|
389
|
+
"--json",
|
|
390
|
+
help="Render schema as JSON.",
|
|
391
|
+
),
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Describe one v2 Applied tool and its input schema."""
|
|
394
|
+
payload = _v2_tool_detail(tool_name, include_debug=include_debug)
|
|
395
|
+
if json_output:
|
|
396
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
typer.echo(f"{payload['name']} ({payload['read_write_mode']})")
|
|
400
|
+
typer.echo(payload["description"])
|
|
401
|
+
typer.echo(json.dumps(payload["input_schema"], indent=2, default=str))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@v2_tools_app.command("manifest")
|
|
405
|
+
def v2_tools_manifest(
|
|
406
|
+
include_debug: bool = typer.Option(
|
|
407
|
+
False,
|
|
408
|
+
"--include-debug",
|
|
409
|
+
help="Include gated debug tools.",
|
|
410
|
+
),
|
|
411
|
+
json_output: bool = typer.Option(
|
|
412
|
+
False,
|
|
413
|
+
"--json",
|
|
414
|
+
help="Render the complete manifest as JSON.",
|
|
415
|
+
),
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Emit the complete v2 tool manifest for agents and MCP packaging."""
|
|
418
|
+
payload = build_tool_manifest(include_debug=include_debug)
|
|
419
|
+
if json_output:
|
|
420
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
typer.echo(
|
|
424
|
+
f"Applied CLI v2 manifest: {payload['tool_count']} tools "
|
|
425
|
+
f"across {len(payload['namespace_counts'])} namespaces"
|
|
426
|
+
)
|
|
427
|
+
for name, count in payload["namespace_counts"].items():
|
|
428
|
+
typer.echo(f"{name}\t{count}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@v2_app.command("doctor")
|
|
432
|
+
def v2_doctor(
|
|
433
|
+
include_debug: bool = typer.Option(
|
|
434
|
+
False,
|
|
435
|
+
"--include-debug",
|
|
436
|
+
help="Include gated debug tools in the catalog check.",
|
|
437
|
+
),
|
|
438
|
+
validate_auth: bool = typer.Option(
|
|
439
|
+
False,
|
|
440
|
+
"--validate-auth",
|
|
441
|
+
help="Verify configured credentials against the Applied API.",
|
|
442
|
+
),
|
|
443
|
+
json_output: bool = typer.Option(
|
|
444
|
+
False,
|
|
445
|
+
"--json",
|
|
446
|
+
help="Render diagnostics as JSON.",
|
|
447
|
+
),
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Check v2 catalog, MCP registration, and optional auth readiness."""
|
|
450
|
+
|
|
451
|
+
async def _run():
|
|
452
|
+
return await build_doctor_report(
|
|
453
|
+
include_debug=include_debug,
|
|
454
|
+
validate_auth=validate_auth,
|
|
455
|
+
credential_provider=default_credential_provider(),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
payload = asyncio.run(_run())
|
|
459
|
+
if json_output:
|
|
460
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
auth = payload["auth"]
|
|
464
|
+
typer.echo(f"Applied CLI {payload['version']}")
|
|
465
|
+
typer.echo(f"Catalog tools: {payload['catalog']['tool_count']}")
|
|
466
|
+
typer.echo(f"MCP registrar: {payload['mcp']['registrar']}")
|
|
467
|
+
typer.echo(f"Auth configured: {'yes' if auth['configured'] else 'no'}")
|
|
468
|
+
if validate_auth:
|
|
469
|
+
typer.echo(f"Auth valid: {'yes' if auth['ok'] else 'no'}")
|
|
470
|
+
if auth["message"]:
|
|
471
|
+
typer.echo(auth["message"])
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@v2_app.command("run")
|
|
475
|
+
def v2_run(
|
|
476
|
+
tool_name: str = typer.Argument(..., help="v2 tool name"),
|
|
477
|
+
input_json: str | None = typer.Option(
|
|
478
|
+
None,
|
|
479
|
+
"--input",
|
|
480
|
+
"-i",
|
|
481
|
+
help="JSON object input for the tool.",
|
|
482
|
+
),
|
|
483
|
+
json_output: bool = typer.Option(
|
|
484
|
+
False,
|
|
485
|
+
"--json",
|
|
486
|
+
help="Render the structured tool result as JSON.",
|
|
487
|
+
),
|
|
488
|
+
) -> None:
|
|
489
|
+
"""Run any v2 Applied tool by name with JSON input."""
|
|
490
|
+
|
|
491
|
+
async def _run():
|
|
492
|
+
return await execute_v2_tool(
|
|
493
|
+
tool_name,
|
|
494
|
+
_parse_v2_input(input_json),
|
|
495
|
+
auth_context=_v2_auth_context(),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
result = asyncio.run(_run())
|
|
499
|
+
typer.echo(render_tool_result(result, "json" if json_output else "text"))
|
|
246
500
|
|
|
247
501
|
|
|
248
502
|
# -----------------------------------------------------------------------------
|
|
@@ -1343,6 +1597,49 @@ def benchmark_create(
|
|
|
1343
1597
|
typer.echo(result)
|
|
1344
1598
|
|
|
1345
1599
|
|
|
1600
|
+
@app.command("benchmark-clone")
|
|
1601
|
+
def benchmark_clone(
|
|
1602
|
+
source_benchmark_id: str = typer.Argument(
|
|
1603
|
+
..., help="Benchmark to copy scenarios from"
|
|
1604
|
+
),
|
|
1605
|
+
dest_benchmark_id: str = typer.Option(
|
|
1606
|
+
None, "--dest-benchmark-id", help="Existing destination benchmark UUID"
|
|
1607
|
+
),
|
|
1608
|
+
dest_benchmark_name: str = typer.Option(
|
|
1609
|
+
None,
|
|
1610
|
+
"--dest-benchmark-name",
|
|
1611
|
+
help="Destination benchmark name (found or created under --target-agent-id)",
|
|
1612
|
+
),
|
|
1613
|
+
target_agent_id: str = typer.Option(
|
|
1614
|
+
None,
|
|
1615
|
+
"--target-agent-id",
|
|
1616
|
+
help="Agent for the destination benchmark when created by name "
|
|
1617
|
+
"(e.g. port an email benchmark onto the chat agent)",
|
|
1618
|
+
),
|
|
1619
|
+
apply: bool = typer.Option(
|
|
1620
|
+
False, "--apply", help="Write changes (default is a dry-run plan)"
|
|
1621
|
+
),
|
|
1622
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1623
|
+
format: str = typer.Option(
|
|
1624
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1625
|
+
),
|
|
1626
|
+
) -> None:
|
|
1627
|
+
"""Copy all scenarios from one benchmark into another (e.g. email -> chat)."""
|
|
1628
|
+
client = get_client(shop_id=shop_id)
|
|
1629
|
+
result = asyncio.run(
|
|
1630
|
+
tools.benchmark_clone(
|
|
1631
|
+
client,
|
|
1632
|
+
source_benchmark_id=source_benchmark_id,
|
|
1633
|
+
dest_benchmark_id=dest_benchmark_id,
|
|
1634
|
+
dest_benchmark_name=dest_benchmark_name,
|
|
1635
|
+
target_agent_id=target_agent_id,
|
|
1636
|
+
dry_run=not apply,
|
|
1637
|
+
output_format=format,
|
|
1638
|
+
)
|
|
1639
|
+
)
|
|
1640
|
+
typer.echo(result)
|
|
1641
|
+
|
|
1642
|
+
|
|
1346
1643
|
@app.command()
|
|
1347
1644
|
def scenarios(
|
|
1348
1645
|
benchmark_id: str = typer.Option(
|
|
@@ -1523,6 +1820,59 @@ def scenario_run_update_cmd(
|
|
|
1523
1820
|
typer.echo(result)
|
|
1524
1821
|
|
|
1525
1822
|
|
|
1823
|
+
@app.command("scenario-recover-catalog")
|
|
1824
|
+
def scenario_recover_catalog(
|
|
1825
|
+
recovery_dir: Path = DEFAULT_RECOVERY_DIR_OPTION,
|
|
1826
|
+
apply: bool = typer.Option(
|
|
1827
|
+
False,
|
|
1828
|
+
"--apply",
|
|
1829
|
+
help="Create missing benchmarks/scenarios and restore rated runs",
|
|
1830
|
+
),
|
|
1831
|
+
restore_ratings: bool = typer.Option(
|
|
1832
|
+
True,
|
|
1833
|
+
"--restore-ratings/--no-restore-ratings",
|
|
1834
|
+
help="Restore rated scenario runs linked to recovered scenarios",
|
|
1835
|
+
),
|
|
1836
|
+
max_rated_runs: int | None = typer.Option(
|
|
1837
|
+
None,
|
|
1838
|
+
"--max-rated-runs",
|
|
1839
|
+
help="Cap rated run restore count for staged recovery",
|
|
1840
|
+
),
|
|
1841
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1842
|
+
format: str = typer.Option(
|
|
1843
|
+
"json", "--format", "-f", help="Output format: json or text"
|
|
1844
|
+
),
|
|
1845
|
+
) -> None:
|
|
1846
|
+
"""Recover deleted benchmark/scenario catalog rows from local PITR exports."""
|
|
1847
|
+
from applied_cli.recovery import recover_scenario_catalog
|
|
1848
|
+
|
|
1849
|
+
client = get_client(shop_id=shop_id)
|
|
1850
|
+
result = asyncio.run(
|
|
1851
|
+
recover_scenario_catalog(
|
|
1852
|
+
client,
|
|
1853
|
+
recovery_dir=recovery_dir,
|
|
1854
|
+
dry_run=not apply,
|
|
1855
|
+
restore_ratings=restore_ratings,
|
|
1856
|
+
max_rated_runs=max_rated_runs,
|
|
1857
|
+
)
|
|
1858
|
+
)
|
|
1859
|
+
if format == "text":
|
|
1860
|
+
typer.echo(
|
|
1861
|
+
"\n".join(
|
|
1862
|
+
[
|
|
1863
|
+
f"dry_run: {result['dry_run']}",
|
|
1864
|
+
f"shop_id: {result['shop_id']}",
|
|
1865
|
+
f"restore_point_utc: {result['restore_point_utc']}",
|
|
1866
|
+
f"benchmarks: {result['benchmarks']}",
|
|
1867
|
+
f"scenarios: {result['scenarios']}",
|
|
1868
|
+
f"scenario_runs: {result['scenario_runs']}",
|
|
1869
|
+
]
|
|
1870
|
+
)
|
|
1871
|
+
)
|
|
1872
|
+
return
|
|
1873
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
1874
|
+
|
|
1875
|
+
|
|
1526
1876
|
@app.command("scenario-bulk-run")
|
|
1527
1877
|
def scenario_bulk_run(
|
|
1528
1878
|
scenario_ids: str = typer.Option(
|
|
@@ -2475,7 +2825,7 @@ def product_update(
|
|
|
2475
2825
|
|
|
2476
2826
|
@app.command("agent-deploy")
|
|
2477
2827
|
def agent_deploy(
|
|
2478
|
-
agent_ids: list[str] =
|
|
2828
|
+
agent_ids: list[str] = AGENT_DEPLOY_AGENT_IDS_ARGUMENT,
|
|
2479
2829
|
description: str = typer.Option("", "--description", "-d", help="Optional revision description"),
|
|
2480
2830
|
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2481
2831
|
) -> None:
|
|
@@ -2580,7 +2930,6 @@ def agent_revision_diff(
|
|
|
2580
2930
|
Example:
|
|
2581
2931
|
applied agent-revision-diff <agent_id> 54 55
|
|
2582
2932
|
"""
|
|
2583
|
-
import difflib
|
|
2584
2933
|
|
|
2585
2934
|
client = get_client(shop_id=shop_id)
|
|
2586
2935
|
|
|
@@ -2661,7 +3010,7 @@ def agent_revision_diff(
|
|
|
2661
3010
|
for field in ("model", "escalation_mode", "use_guardrails", "auto_reply", "response_delay_in_seconds"):
|
|
2662
3011
|
typer.echo(f" {field}: {agent.get(field)}")
|
|
2663
3012
|
|
|
2664
|
-
typer.echo(
|
|
3013
|
+
typer.echo("\n--- PROMPT (guardrail, current) ---")
|
|
2665
3014
|
guardrail = agent.get("guardrail") or ""
|
|
2666
3015
|
typer.echo(f" Length: {len(guardrail)} chars")
|
|
2667
3016
|
typer.echo(f" First 300 chars:\n{guardrail[:300]}")
|
|
@@ -2673,8 +3022,8 @@ def agent_revision_diff(
|
|
|
2673
3022
|
for f in active[:20]:
|
|
2674
3023
|
typer.echo(f" [{f.get('trigger','?')}] {f.get('name')} ({f.get('status')})")
|
|
2675
3024
|
|
|
2676
|
-
typer.echo(
|
|
2677
|
-
typer.echo(
|
|
3025
|
+
typer.echo("\nNote: Full prompt diff requires snapshot storage per revision (not yet available in API).")
|
|
3026
|
+
typer.echo("To investigate: compare guardrail text above against the known-good version,")
|
|
2678
3027
|
typer.echo(f"and check if any flows were added/removed between {rev_a['created_at'][:10]} and {rev_b['created_at'][:10]}.")
|
|
2679
3028
|
|
|
2680
3029
|
|