applied-cli 0.6.2__tar.gz → 0.6.4__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.6.2 → applied_cli-0.6.4}/PKG-INFO +1 -1
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/cli.py +36 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/tools.py +143 -2
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/domains.py +1 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/scenarios.py +67 -2
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/SOURCES.txt +2 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/pyproject.toml +1 -1
- applied_cli-0.6.4/tests/test_scenario_bulk_cancel.py +87 -0
- applied_cli-0.6.4/tests/test_scenario_bulk_run_contact.py +116 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/README.md +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/__init__.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/client.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/catalog.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/setup.cfg +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_cli.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_client.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.2 → applied_cli-0.6.4}/tests/test_v2_tickets.py +0 -0
|
@@ -1916,6 +1916,18 @@ def scenario_bulk_run(
|
|
|
1916
1916
|
target_agent_id: str = typer.Option(
|
|
1917
1917
|
None, "--target-agent-id", help="Optional target agent for reruns"
|
|
1918
1918
|
),
|
|
1919
|
+
contact_email: str = typer.Option(
|
|
1920
|
+
None,
|
|
1921
|
+
"--contact-email",
|
|
1922
|
+
help="Run as a contact with this email (resolves/creates it) so test "
|
|
1923
|
+
"conversations carry an email — fixes 'Email is not present' failures",
|
|
1924
|
+
),
|
|
1925
|
+
contact_id: str = typer.Option(
|
|
1926
|
+
None, "--contact-id", help="Run scenarios as this existing contact UUID"
|
|
1927
|
+
),
|
|
1928
|
+
anonymous: bool = typer.Option(
|
|
1929
|
+
False, "--anonymous", help="Run with an anonymous contact"
|
|
1930
|
+
),
|
|
1919
1931
|
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1920
1932
|
format: str = typer.Option(
|
|
1921
1933
|
"text", "--format", "-f", help="Output format: text or json"
|
|
@@ -1929,6 +1941,9 @@ def scenario_bulk_run(
|
|
|
1929
1941
|
scenario_ids=_parse_csv_option(scenario_ids),
|
|
1930
1942
|
benchmark_id=benchmark_id,
|
|
1931
1943
|
target_agent_id=target_agent_id,
|
|
1944
|
+
contact_email=contact_email,
|
|
1945
|
+
contact_id=contact_id,
|
|
1946
|
+
anonymous=anonymous,
|
|
1932
1947
|
output_format=format,
|
|
1933
1948
|
)
|
|
1934
1949
|
)
|
|
@@ -1956,6 +1971,27 @@ def scenario_bulk_status(
|
|
|
1956
1971
|
typer.echo(result)
|
|
1957
1972
|
|
|
1958
1973
|
|
|
1974
|
+
@app.command("scenario-bulk-cancel")
|
|
1975
|
+
def scenario_bulk_cancel(
|
|
1976
|
+
job_id: str = typer.Argument(..., help="Bulk run job ID"),
|
|
1977
|
+
apply: bool = typer.Option(
|
|
1978
|
+
False, "--apply", help="Cancel the pending runs (default is a dry-run plan)"
|
|
1979
|
+
),
|
|
1980
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1981
|
+
format: str = typer.Option(
|
|
1982
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1983
|
+
),
|
|
1984
|
+
) -> None:
|
|
1985
|
+
"""Cancel a stuck bulk run by deleting its queued/running scenario runs."""
|
|
1986
|
+
client = get_client(shop_id=shop_id)
|
|
1987
|
+
result = asyncio.run(
|
|
1988
|
+
tools.scenario_bulk_cancel(
|
|
1989
|
+
client, job_id=job_id, apply=apply, output_format=format
|
|
1990
|
+
)
|
|
1991
|
+
)
|
|
1992
|
+
typer.echo(result)
|
|
1993
|
+
|
|
1994
|
+
|
|
1959
1995
|
@app.command("audit-metric-fields")
|
|
1960
1996
|
def audit_metric_fields(
|
|
1961
1997
|
is_active: bool = typer.Option(
|
|
@@ -6082,23 +6082,68 @@ async def scenario_run_delete(
|
|
|
6082
6082
|
return f"Scenario run {run_id} deleted successfully."
|
|
6083
6083
|
|
|
6084
6084
|
|
|
6085
|
+
async def _resolve_contact_override(
|
|
6086
|
+
client: AppliedClient,
|
|
6087
|
+
*,
|
|
6088
|
+
contact_override: dict | None,
|
|
6089
|
+
contact_id: str | None,
|
|
6090
|
+
contact_email: str | None,
|
|
6091
|
+
anonymous: bool,
|
|
6092
|
+
) -> dict | None:
|
|
6093
|
+
"""Build the contact_override payload from the convenience arguments.
|
|
6094
|
+
|
|
6095
|
+
An explicit contact_override dict wins. Otherwise anonymous > contact_id >
|
|
6096
|
+
contact_email (which resolves/creates a contact so the test conversation
|
|
6097
|
+
carries a real email).
|
|
6098
|
+
"""
|
|
6099
|
+
if contact_override:
|
|
6100
|
+
return contact_override
|
|
6101
|
+
if anonymous:
|
|
6102
|
+
return {"mode": "anonymous"}
|
|
6103
|
+
if contact_id:
|
|
6104
|
+
return {"mode": "contact", "contact_id": contact_id}
|
|
6105
|
+
if contact_email:
|
|
6106
|
+
contact = await client.get_or_create_contact(email=contact_email)
|
|
6107
|
+
resolved_id = contact.get("id")
|
|
6108
|
+
if not resolved_id:
|
|
6109
|
+
raise AppliedAPIError(
|
|
6110
|
+
f"Could not resolve a contact for email {contact_email}.",
|
|
6111
|
+
status_code=404,
|
|
6112
|
+
)
|
|
6113
|
+
return {"mode": "contact", "contact_id": str(resolved_id)}
|
|
6114
|
+
return None
|
|
6115
|
+
|
|
6116
|
+
|
|
6085
6117
|
async def scenario_bulk_run(
|
|
6086
6118
|
client: AppliedClient,
|
|
6087
6119
|
scenario_ids: list[str] | None = None,
|
|
6088
6120
|
benchmark_id: str | None = None,
|
|
6089
6121
|
target_agent_id: str | None = None,
|
|
6090
6122
|
contact_override: dict | None = None,
|
|
6123
|
+
contact_id: str | None = None,
|
|
6124
|
+
contact_email: str | None = None,
|
|
6125
|
+
anonymous: bool = False,
|
|
6091
6126
|
output_format: str = "text",
|
|
6092
6127
|
) -> str:
|
|
6093
6128
|
"""
|
|
6094
6129
|
Run multiple scenarios at once.
|
|
6095
6130
|
|
|
6131
|
+
By default a scenario run reuses the input conversation's contact, which on
|
|
6132
|
+
test/benchmark conversations often has no email — causing agents to respond
|
|
6133
|
+
"Email is not present in the conversation." Pass contact_email or contact_id
|
|
6134
|
+
to run the scenarios as a contact that has an email, so the test conversation
|
|
6135
|
+
carries it.
|
|
6136
|
+
|
|
6096
6137
|
Args:
|
|
6097
6138
|
client: Authenticated AppliedClient
|
|
6098
6139
|
scenario_ids: List of scenario UUIDs to run
|
|
6099
6140
|
benchmark_id: Run all scenarios in this benchmark
|
|
6100
6141
|
target_agent_id: Optional agent to run against (for A/B testing)
|
|
6101
|
-
contact_override:
|
|
6142
|
+
contact_override: Raw override, e.g. {"mode": "contact", "contact_id": "<uuid>"}
|
|
6143
|
+
(takes precedence over the convenience args below)
|
|
6144
|
+
contact_id: Run scenarios as this existing contact (gives test convos its email)
|
|
6145
|
+
contact_email: Resolve/create a contact with this email and run as them
|
|
6146
|
+
anonymous: Run with an anonymous contact (mode='anonymous')
|
|
6102
6147
|
|
|
6103
6148
|
Returns:
|
|
6104
6149
|
Summary of runs created
|
|
@@ -6123,10 +6168,17 @@ async def scenario_bulk_run(
|
|
|
6123
6168
|
)
|
|
6124
6169
|
|
|
6125
6170
|
try:
|
|
6171
|
+
effective_override = await _resolve_contact_override(
|
|
6172
|
+
client,
|
|
6173
|
+
contact_override=contact_override,
|
|
6174
|
+
contact_id=contact_id,
|
|
6175
|
+
contact_email=contact_email,
|
|
6176
|
+
anonymous=anonymous,
|
|
6177
|
+
)
|
|
6126
6178
|
result = await client.bulk_run_scenarios(
|
|
6127
6179
|
scenario_ids=resolved_scenario_ids,
|
|
6128
6180
|
target_agent_id=target_agent_id,
|
|
6129
|
-
contact_override=
|
|
6181
|
+
contact_override=effective_override,
|
|
6130
6182
|
)
|
|
6131
6183
|
except AppliedAPIError as e:
|
|
6132
6184
|
return _format_error(e)
|
|
@@ -6230,6 +6282,95 @@ async def scenario_bulk_status(
|
|
|
6230
6282
|
return output
|
|
6231
6283
|
|
|
6232
6284
|
|
|
6285
|
+
_PENDING_RUN_STATUSES = ("queued", "running")
|
|
6286
|
+
|
|
6287
|
+
|
|
6288
|
+
async def scenario_bulk_cancel(
|
|
6289
|
+
client: AppliedClient,
|
|
6290
|
+
job_id: str,
|
|
6291
|
+
*,
|
|
6292
|
+
apply: bool = False,
|
|
6293
|
+
output_format: str = "text",
|
|
6294
|
+
) -> str:
|
|
6295
|
+
"""
|
|
6296
|
+
Cancel a stuck bulk scenario run job.
|
|
6297
|
+
|
|
6298
|
+
A bulk run can get stuck with runs left in 'queued' or 'running' forever, so
|
|
6299
|
+
the job never reports complete. This cancels the job by deleting those pending
|
|
6300
|
+
runs; completed and failed runs (and their result conversations) are preserved.
|
|
6301
|
+
Once no pending runs remain, the job reports as finished.
|
|
6302
|
+
|
|
6303
|
+
This replaces the manual workaround of deleting the 'agent-test-bulk-job:'
|
|
6304
|
+
key from browser localStorage.
|
|
6305
|
+
|
|
6306
|
+
Args:
|
|
6307
|
+
client: Authenticated AppliedClient
|
|
6308
|
+
job_id: The bulk run job UUID
|
|
6309
|
+
apply: If True, delete the pending runs; otherwise just report the plan
|
|
6310
|
+
output_format: 'text' (default) or 'json'
|
|
6311
|
+
|
|
6312
|
+
Returns:
|
|
6313
|
+
Summary of pending runs cancelled (or that would be cancelled).
|
|
6314
|
+
"""
|
|
6315
|
+
try:
|
|
6316
|
+
runs = await client.list_scenario_runs(
|
|
6317
|
+
bulk_job_id=job_id, latest=False, limit=1000
|
|
6318
|
+
)
|
|
6319
|
+
except AppliedAPIError as e:
|
|
6320
|
+
return _format_error(e)
|
|
6321
|
+
|
|
6322
|
+
pending = [r for r in runs if str(r.get("status") or "") in _PENDING_RUN_STATUSES]
|
|
6323
|
+
result: dict[str, Any] = {
|
|
6324
|
+
"job_id": job_id,
|
|
6325
|
+
"apply": apply,
|
|
6326
|
+
"total_runs": len(runs),
|
|
6327
|
+
"pending_runs": len(pending),
|
|
6328
|
+
"terminal_runs": len(runs) - len(pending),
|
|
6329
|
+
"cancelled": 0,
|
|
6330
|
+
"errors": [],
|
|
6331
|
+
}
|
|
6332
|
+
|
|
6333
|
+
if not runs:
|
|
6334
|
+
result["message"] = (
|
|
6335
|
+
f"No runs found for job {job_id} — it may already be cleared or the "
|
|
6336
|
+
f"job id is invalid. Nothing to cancel."
|
|
6337
|
+
)
|
|
6338
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6339
|
+
|
|
6340
|
+
if not pending:
|
|
6341
|
+
result["message"] = (
|
|
6342
|
+
f"Job {job_id} has no pending (queued/running) runs — nothing to "
|
|
6343
|
+
f"cancel. {result['terminal_runs']} run(s) already finished."
|
|
6344
|
+
)
|
|
6345
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6346
|
+
|
|
6347
|
+
if not apply:
|
|
6348
|
+
result["message"] = (
|
|
6349
|
+
f"Would cancel {len(pending)} pending run(s) (queued/running) for job "
|
|
6350
|
+
f"{job_id}; {result['terminal_runs']} finished run(s) preserved. "
|
|
6351
|
+
f"Re-run with --apply to cancel."
|
|
6352
|
+
)
|
|
6353
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6354
|
+
|
|
6355
|
+
cancelled = 0
|
|
6356
|
+
for run in pending:
|
|
6357
|
+
run_id = run.get("id")
|
|
6358
|
+
if not run_id:
|
|
6359
|
+
continue
|
|
6360
|
+
try:
|
|
6361
|
+
await client.delete_scenario_run(str(run_id))
|
|
6362
|
+
cancelled += 1
|
|
6363
|
+
except AppliedAPIError as e:
|
|
6364
|
+
result["errors"].append({"run_id": str(run_id), "error": str(e)})
|
|
6365
|
+
result["cancelled"] = cancelled
|
|
6366
|
+
result["message"] = (
|
|
6367
|
+
f"Cancelled {cancelled} pending run(s) for job {job_id}; "
|
|
6368
|
+
f"{result['terminal_runs']} finished run(s) preserved."
|
|
6369
|
+
+ (f" {len(result['errors'])} error(s)." if result["errors"] else "")
|
|
6370
|
+
)
|
|
6371
|
+
return to_json(result) if output_format == "json" else result["message"]
|
|
6372
|
+
|
|
6373
|
+
|
|
6233
6374
|
async def _load_audit_target_summaries(
|
|
6234
6375
|
client: AppliedClient,
|
|
6235
6376
|
ratings: list[dict[str, Any]],
|
|
@@ -119,6 +119,7 @@ DOMAIN_TOOL_RENAMES: dict[str, dict[str, str]] = {
|
|
|
119
119
|
"scenario_run_delete": "scenarios_runs_delete",
|
|
120
120
|
"scenario_bulk_run": "scenarios_bulk_run",
|
|
121
121
|
"scenario_bulk_status": "scenarios_bulk_status",
|
|
122
|
+
"scenario_bulk_cancel": "scenarios_bulk_cancel",
|
|
122
123
|
},
|
|
123
124
|
"taxonomy": {
|
|
124
125
|
"taxonomy_list": "taxonomy_items_list",
|
|
@@ -27,6 +27,14 @@ class ScenariosBulkRunInput(StrictInput):
|
|
|
27
27
|
benchmark_id: str | None = None
|
|
28
28
|
target_agent_id: str | None = None
|
|
29
29
|
contact_override: dict[str, Any] | None = None
|
|
30
|
+
contact_id: str | None = None
|
|
31
|
+
contact_email: str | None = None
|
|
32
|
+
anonymous: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ScenariosBulkCancelInput(StrictInput):
|
|
36
|
+
job_id: str
|
|
37
|
+
apply: bool = False
|
|
30
38
|
|
|
31
39
|
|
|
32
40
|
class BenchmarksListInput(StrictInput):
|
|
@@ -791,10 +799,19 @@ async def scenarios_bulk_run_handler(
|
|
|
791
799
|
)
|
|
792
800
|
|
|
793
801
|
try:
|
|
802
|
+
from applied_cli.tools import _resolve_contact_override
|
|
803
|
+
|
|
804
|
+
effective_override = await _resolve_contact_override(
|
|
805
|
+
client,
|
|
806
|
+
contact_override=params.contact_override,
|
|
807
|
+
contact_id=params.contact_id,
|
|
808
|
+
contact_email=params.contact_email,
|
|
809
|
+
anonymous=params.anonymous,
|
|
810
|
+
)
|
|
794
811
|
result = await client.bulk_run_scenarios(
|
|
795
812
|
scenario_ids=resolved_scenario_ids,
|
|
796
813
|
target_agent_id=params.target_agent_id,
|
|
797
|
-
contact_override=
|
|
814
|
+
contact_override=effective_override,
|
|
798
815
|
)
|
|
799
816
|
except AppliedAPIError as exc:
|
|
800
817
|
return _api_error_result(exc)
|
|
@@ -850,6 +867,38 @@ async def scenarios_bulk_status_handler(
|
|
|
850
867
|
)
|
|
851
868
|
|
|
852
869
|
|
|
870
|
+
async def scenarios_bulk_cancel_handler(
|
|
871
|
+
client: AppliedClient,
|
|
872
|
+
params: ScenariosBulkCancelInput,
|
|
873
|
+
) -> ToolResult[Any]:
|
|
874
|
+
from applied_cli import tools as legacy_tools
|
|
875
|
+
|
|
876
|
+
raw = await legacy_tools.scenario_bulk_cancel(
|
|
877
|
+
client,
|
|
878
|
+
job_id=params.job_id,
|
|
879
|
+
apply=params.apply,
|
|
880
|
+
output_format="json",
|
|
881
|
+
)
|
|
882
|
+
try:
|
|
883
|
+
data = json.loads(raw)
|
|
884
|
+
except (json.JSONDecodeError, TypeError):
|
|
885
|
+
return ToolResult(data={"message": raw}, summary=str(raw))
|
|
886
|
+
|
|
887
|
+
next_actions = []
|
|
888
|
+
if not params.apply and data.get("pending_runs"):
|
|
889
|
+
next_actions.append("Re-run with apply=true to cancel the pending runs.")
|
|
890
|
+
elif data.get("cancelled"):
|
|
891
|
+
next_actions.append(
|
|
892
|
+
f"Poll scenarios_bulk_status with job_id='{params.job_id}' to confirm "
|
|
893
|
+
f"the job now reports finished."
|
|
894
|
+
)
|
|
895
|
+
return ToolResult(
|
|
896
|
+
data=data,
|
|
897
|
+
summary=data.get("message", "Bulk cancel processed."),
|
|
898
|
+
next_actions=next_actions,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
853
902
|
def scenario_specs() -> list[ToolSpec]:
|
|
854
903
|
return [
|
|
855
904
|
ToolSpec(
|
|
@@ -1013,7 +1062,9 @@ def scenario_specs() -> list[ToolSpec]:
|
|
|
1013
1062
|
namespace="scenarios",
|
|
1014
1063
|
description=(
|
|
1015
1064
|
"Run selected scenarios or every scenario in a benchmark and "
|
|
1016
|
-
"return the queued job metadata."
|
|
1065
|
+
"return the queued job metadata. Pass contact_email or contact_id "
|
|
1066
|
+
"to run as a contact with an email (fixes 'Email is not present' "
|
|
1067
|
+
"failures on test conversations)."
|
|
1017
1068
|
),
|
|
1018
1069
|
input_model=ScenariosBulkRunInput,
|
|
1019
1070
|
output_model=None,
|
|
@@ -1034,4 +1085,18 @@ def scenario_specs() -> list[ToolSpec]:
|
|
|
1034
1085
|
read_write_mode="read",
|
|
1035
1086
|
tags=["scenario_bulk_status", "native"],
|
|
1036
1087
|
),
|
|
1088
|
+
ToolSpec(
|
|
1089
|
+
name="scenarios_bulk_cancel",
|
|
1090
|
+
namespace="scenarios",
|
|
1091
|
+
description=(
|
|
1092
|
+
"Cancel a stuck bulk scenario run job by deleting its queued/"
|
|
1093
|
+
"running runs (completed/failed runs are preserved). Dry-run by "
|
|
1094
|
+
"default; set apply=true to cancel."
|
|
1095
|
+
),
|
|
1096
|
+
input_model=ScenariosBulkCancelInput,
|
|
1097
|
+
output_model=None,
|
|
1098
|
+
handler=scenarios_bulk_cancel_handler,
|
|
1099
|
+
read_write_mode="write",
|
|
1100
|
+
tags=["scenario_bulk_cancel", "native"],
|
|
1101
|
+
),
|
|
1037
1102
|
]
|
|
@@ -49,6 +49,8 @@ tests/test_conversation_tools.py
|
|
|
49
49
|
tests/test_flow_tools.py
|
|
50
50
|
tests/test_knowledge_content_tools.py
|
|
51
51
|
tests/test_recovery.py
|
|
52
|
+
tests/test_scenario_bulk_cancel.py
|
|
53
|
+
tests/test_scenario_bulk_run_contact.py
|
|
52
54
|
tests/test_toolkit_contract.py
|
|
53
55
|
tests/test_v2_agents.py
|
|
54
56
|
tests/test_v2_articles.py
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from applied_cli import tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeBulkClient:
|
|
9
|
+
def __init__(self, runs):
|
|
10
|
+
self._runs = runs
|
|
11
|
+
self.deleted = []
|
|
12
|
+
|
|
13
|
+
async def list_scenario_runs(
|
|
14
|
+
self, scenario_id=None, benchmark_id=None, bulk_job_id=None, latest=False,
|
|
15
|
+
limit=50,
|
|
16
|
+
):
|
|
17
|
+
return list(self._runs)
|
|
18
|
+
|
|
19
|
+
async def delete_scenario_run(self, run_id):
|
|
20
|
+
self.deleted.append(run_id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
STUCK_RUNS = [
|
|
24
|
+
{"id": "r1", "status": "completed"},
|
|
25
|
+
{"id": "r2", "status": "queued"},
|
|
26
|
+
{"id": "r3", "status": "running"},
|
|
27
|
+
{"id": "r4", "status": "failed"},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_dry_run_reports_pending_without_deleting():
|
|
33
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
34
|
+
result = await tools.scenario_bulk_cancel(client, "job-1", output_format="json")
|
|
35
|
+
data = json.loads(result)
|
|
36
|
+
assert data["pending_runs"] == 2
|
|
37
|
+
assert data["terminal_runs"] == 2
|
|
38
|
+
assert data["cancelled"] == 0
|
|
39
|
+
assert client.deleted == [] # dry run deletes nothing
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_apply_deletes_only_pending_runs():
|
|
44
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
45
|
+
result = await tools.scenario_bulk_cancel(
|
|
46
|
+
client, "job-1", apply=True, output_format="json"
|
|
47
|
+
)
|
|
48
|
+
data = json.loads(result)
|
|
49
|
+
assert data["cancelled"] == 2
|
|
50
|
+
# Only the queued/running runs are deleted; completed/failed preserved.
|
|
51
|
+
assert set(client.deleted) == {"r2", "r3"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_no_pending_runs_is_a_noop():
|
|
56
|
+
client = FakeBulkClient(
|
|
57
|
+
[{"id": "r1", "status": "completed"}, {"id": "r2", "status": "failed"}]
|
|
58
|
+
)
|
|
59
|
+
result = await tools.scenario_bulk_cancel(
|
|
60
|
+
client, "job-1", apply=True, output_format="text"
|
|
61
|
+
)
|
|
62
|
+
assert "nothing to" in result.lower()
|
|
63
|
+
assert client.deleted == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_unknown_job_reports_no_runs():
|
|
68
|
+
client = FakeBulkClient([])
|
|
69
|
+
result = await tools.scenario_bulk_cancel(client, "missing", output_format="text")
|
|
70
|
+
assert "No runs found" in result
|
|
71
|
+
assert client.deleted == []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_v2_scenarios_bulk_cancel_handler_structured():
|
|
76
|
+
from applied_cli.v2.scenarios import (
|
|
77
|
+
ScenariosBulkCancelInput,
|
|
78
|
+
scenarios_bulk_cancel_handler,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
client = FakeBulkClient(STUCK_RUNS)
|
|
82
|
+
result = await scenarios_bulk_cancel_handler(
|
|
83
|
+
client, ScenariosBulkCancelInput(job_id="job-1", apply=False)
|
|
84
|
+
)
|
|
85
|
+
assert result.data["pending_runs"] == 2
|
|
86
|
+
assert "apply=true" in " ".join(result.next_actions)
|
|
87
|
+
assert client.deleted == []
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from applied_cli import tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeRunClient:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.bulk_kwargs = None
|
|
11
|
+
self.get_or_create_calls = []
|
|
12
|
+
|
|
13
|
+
async def list_scenarios(self, benchmark_id=None, limit=500, **kwargs):
|
|
14
|
+
return [{"id": "s1"}, {"id": "s2"}]
|
|
15
|
+
|
|
16
|
+
async def get_or_create_contact(self, email=None, name=None, phone=None):
|
|
17
|
+
self.get_or_create_calls.append(email)
|
|
18
|
+
return {"id": "contact-123", "email": email}
|
|
19
|
+
|
|
20
|
+
async def bulk_run_scenarios(
|
|
21
|
+
self, scenario_ids=None, target_agent_id=None, contact_override=None
|
|
22
|
+
):
|
|
23
|
+
self.bulk_kwargs = {
|
|
24
|
+
"scenario_ids": scenario_ids,
|
|
25
|
+
"target_agent_id": target_agent_id,
|
|
26
|
+
"contact_override": contact_override,
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
"job_id": "job-1",
|
|
30
|
+
"total": len(scenario_ids or []),
|
|
31
|
+
"queued": len(scenario_ids or []),
|
|
32
|
+
"scenario_run_ids": ["r1", "r2"],
|
|
33
|
+
"contact_override": contact_override,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_no_contact_args_sends_no_override():
|
|
39
|
+
client = FakeRunClient()
|
|
40
|
+
await tools.scenario_bulk_run(client, benchmark_id="bench-1", output_format="json")
|
|
41
|
+
assert client.bulk_kwargs["contact_override"] is None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_contact_id_builds_override():
|
|
46
|
+
client = FakeRunClient()
|
|
47
|
+
await tools.scenario_bulk_run(
|
|
48
|
+
client, benchmark_id="bench-1", contact_id="c-9", output_format="json"
|
|
49
|
+
)
|
|
50
|
+
assert client.bulk_kwargs["contact_override"] == {
|
|
51
|
+
"mode": "contact",
|
|
52
|
+
"contact_id": "c-9",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_contact_email_resolves_then_overrides():
|
|
58
|
+
client = FakeRunClient()
|
|
59
|
+
result = await tools.scenario_bulk_run(
|
|
60
|
+
client,
|
|
61
|
+
benchmark_id="bench-1",
|
|
62
|
+
contact_email="casey@example.com",
|
|
63
|
+
output_format="json",
|
|
64
|
+
)
|
|
65
|
+
assert client.get_or_create_calls == ["casey@example.com"]
|
|
66
|
+
assert client.bulk_kwargs["contact_override"] == {
|
|
67
|
+
"mode": "contact",
|
|
68
|
+
"contact_id": "contact-123",
|
|
69
|
+
}
|
|
70
|
+
assert json.loads(result)["job_id"] == "job-1"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_anonymous_mode():
|
|
75
|
+
client = FakeRunClient()
|
|
76
|
+
await tools.scenario_bulk_run(
|
|
77
|
+
client, benchmark_id="bench-1", anonymous=True, output_format="json"
|
|
78
|
+
)
|
|
79
|
+
assert client.bulk_kwargs["contact_override"] == {"mode": "anonymous"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_explicit_override_wins_over_convenience_args():
|
|
84
|
+
client = FakeRunClient()
|
|
85
|
+
await tools.scenario_bulk_run(
|
|
86
|
+
client,
|
|
87
|
+
benchmark_id="bench-1",
|
|
88
|
+
contact_override={"mode": "contact", "contact_id": "raw"},
|
|
89
|
+
contact_email="ignored@example.com",
|
|
90
|
+
output_format="json",
|
|
91
|
+
)
|
|
92
|
+
# The raw override is used; email resolution is skipped.
|
|
93
|
+
assert client.get_or_create_calls == []
|
|
94
|
+
assert client.bulk_kwargs["contact_override"] == {
|
|
95
|
+
"mode": "contact",
|
|
96
|
+
"contact_id": "raw",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_v2_handler_threads_contact_email():
|
|
102
|
+
from applied_cli.v2.scenarios import (
|
|
103
|
+
ScenariosBulkRunInput,
|
|
104
|
+
scenarios_bulk_run_handler,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
client = FakeRunClient()
|
|
108
|
+
result = await scenarios_bulk_run_handler(
|
|
109
|
+
client,
|
|
110
|
+
ScenariosBulkRunInput(benchmark_id="bench-1", contact_email="x@example.com"),
|
|
111
|
+
)
|
|
112
|
+
assert client.bulk_kwargs["contact_override"] == {
|
|
113
|
+
"mode": "contact",
|
|
114
|
+
"contact_id": "contact-123",
|
|
115
|
+
}
|
|
116
|
+
assert result.data["job_id"] == "job-1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|