applied-cli 0.6.0__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.6.0 → applied_cli-0.6.1}/PKG-INFO +1 -1
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/cli.py +101 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/client.py +54 -0
- applied_cli-0.6.1/applied_cli/recovery.py +400 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/tools.py +197 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/domains.py +1 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/scenarios.py +64 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/SOURCES.txt +3 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/pyproject.toml +1 -1
- applied_cli-0.6.1/tests/test_benchmark_clone.py +242 -0
- applied_cli-0.6.1/tests/test_recovery.py +351 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/README.md +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/__init__.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/catalog.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/setup.cfg +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_cli.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_client.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.0 → applied_cli-0.6.1}/tests/test_v2_tickets.py +0 -0
|
@@ -42,6 +42,11 @@ app.add_typer(v2_app, name="v2")
|
|
|
42
42
|
|
|
43
43
|
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
44
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
|
+
)
|
|
45
50
|
AGENT_DEPLOY_AGENT_IDS_ARGUMENT = typer.Argument(
|
|
46
51
|
...,
|
|
47
52
|
help="One or more Agent IDs to deploy",
|
|
@@ -1592,6 +1597,49 @@ def benchmark_create(
|
|
|
1592
1597
|
typer.echo(result)
|
|
1593
1598
|
|
|
1594
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
|
+
|
|
1595
1643
|
@app.command()
|
|
1596
1644
|
def scenarios(
|
|
1597
1645
|
benchmark_id: str = typer.Option(
|
|
@@ -1772,6 +1820,59 @@ def scenario_run_update_cmd(
|
|
|
1772
1820
|
typer.echo(result)
|
|
1773
1821
|
|
|
1774
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
|
+
|
|
1775
1876
|
@app.command("scenario-bulk-run")
|
|
1776
1877
|
def scenario_bulk_run(
|
|
1777
1878
|
scenario_ids: str = typer.Option(
|
|
@@ -2426,6 +2426,40 @@ class AppliedClient:
|
|
|
2426
2426
|
break
|
|
2427
2427
|
return all_results
|
|
2428
2428
|
|
|
2429
|
+
async def list_recovery_scenarios(
|
|
2430
|
+
self,
|
|
2431
|
+
*,
|
|
2432
|
+
agent_id: str,
|
|
2433
|
+
names: list[str],
|
|
2434
|
+
limit: int = 50,
|
|
2435
|
+
) -> list[dict]:
|
|
2436
|
+
"""List scenarios for recovery by exact name and agent."""
|
|
2437
|
+
by_id: dict[str, dict] = {}
|
|
2438
|
+
for name in sorted({item for item in names if item}):
|
|
2439
|
+
params: dict[str, Any] = {
|
|
2440
|
+
"agent": agent_id,
|
|
2441
|
+
"limit": limit,
|
|
2442
|
+
"name": name,
|
|
2443
|
+
"summary": "true",
|
|
2444
|
+
}
|
|
2445
|
+
page = 1
|
|
2446
|
+
while True:
|
|
2447
|
+
params["page"] = page
|
|
2448
|
+
data = await self._request(
|
|
2449
|
+
"GET",
|
|
2450
|
+
"/v1/conversation-scenarios/",
|
|
2451
|
+
params=params,
|
|
2452
|
+
)
|
|
2453
|
+
for scenario in self._normalize_response(data):
|
|
2454
|
+
scenario_id = scenario.get("id")
|
|
2455
|
+
if scenario_id:
|
|
2456
|
+
by_id[str(scenario_id)] = scenario
|
|
2457
|
+
if isinstance(data, dict) and data.get("next"):
|
|
2458
|
+
page += 1
|
|
2459
|
+
else:
|
|
2460
|
+
break
|
|
2461
|
+
return list(by_id.values())
|
|
2462
|
+
|
|
2429
2463
|
async def get_scenario(self, scenario_id: str) -> dict:
|
|
2430
2464
|
"""Get a single scenario by ID."""
|
|
2431
2465
|
return await self._request("GET", f"/v1/conversation-scenarios/{scenario_id}/")
|
|
@@ -2469,6 +2503,22 @@ class AppliedClient:
|
|
|
2469
2503
|
"PATCH", f"/v1/conversation-scenarios/{scenario_id}/", body=kwargs
|
|
2470
2504
|
)
|
|
2471
2505
|
|
|
2506
|
+
async def create_recovery_scenario(self, body: dict[str, Any]) -> dict:
|
|
2507
|
+
"""Create a scenario with raw recovery fields."""
|
|
2508
|
+
return await self._request("POST", "/v1/conversation-scenarios/", body=body)
|
|
2509
|
+
|
|
2510
|
+
async def update_recovery_scenario(
|
|
2511
|
+
self,
|
|
2512
|
+
scenario_id: str,
|
|
2513
|
+
body: dict[str, Any],
|
|
2514
|
+
) -> dict:
|
|
2515
|
+
"""Patch scenario benchmark links with raw recovery fields."""
|
|
2516
|
+
return await self._request(
|
|
2517
|
+
"PATCH",
|
|
2518
|
+
f"/v1/conversation-scenarios/{scenario_id}/",
|
|
2519
|
+
body=body,
|
|
2520
|
+
)
|
|
2521
|
+
|
|
2472
2522
|
# -------------------------------------------------------------------------
|
|
2473
2523
|
# Scenario Runs
|
|
2474
2524
|
# -------------------------------------------------------------------------
|
|
@@ -2506,6 +2556,10 @@ class AppliedClient:
|
|
|
2506
2556
|
"""Update a scenario run's properties (ratings, feedback, etc.)."""
|
|
2507
2557
|
return await self._request("PATCH", f"/v1/scenario-runs/{run_id}/", body=kwargs)
|
|
2508
2558
|
|
|
2559
|
+
async def create_recovery_scenario_run(self, body: dict[str, Any]) -> dict:
|
|
2560
|
+
"""Create a scenario run linked to an existing output conversation."""
|
|
2561
|
+
return await self._request("POST", "/v1/scenario-runs/", body=body)
|
|
2562
|
+
|
|
2509
2563
|
async def delete_scenario(self, scenario_id: str) -> None:
|
|
2510
2564
|
"""Delete a scenario."""
|
|
2511
2565
|
await self._request("DELETE", f"/v1/conversation-scenarios/{scenario_id}/")
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Helpers for recovering scenario catalog rows from local PITR exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RecoveryDataset:
|
|
14
|
+
shop_id: str
|
|
15
|
+
restore_point_utc: str | None
|
|
16
|
+
benchmarks: list[dict[str, Any]]
|
|
17
|
+
scenarios: list[dict[str, Any]]
|
|
18
|
+
scenario_benchmark_links: list[dict[str, Any]]
|
|
19
|
+
scenario_runs: list[dict[str, Any]]
|
|
20
|
+
|
|
21
|
+
def links_by_scenario(self) -> dict[str, list[str]]:
|
|
22
|
+
links: dict[str, list[str]] = defaultdict(list)
|
|
23
|
+
for link in self.scenario_benchmark_links:
|
|
24
|
+
scenario_id = link.get("scenario_id")
|
|
25
|
+
benchmark_id = link.get("benchmark_id")
|
|
26
|
+
if scenario_id and benchmark_id:
|
|
27
|
+
links[str(scenario_id)].append(str(benchmark_id))
|
|
28
|
+
return links
|
|
29
|
+
|
|
30
|
+
def benchmark_ids_for_scenario(self, scenario: dict[str, Any]) -> list[str]:
|
|
31
|
+
scenario_id = str(scenario.get("id") or "")
|
|
32
|
+
ids: list[str] = []
|
|
33
|
+
primary = scenario.get("benchmark_id")
|
|
34
|
+
if primary:
|
|
35
|
+
ids.append(str(primary))
|
|
36
|
+
for benchmark_id in self.links_by_scenario().get(scenario_id, []):
|
|
37
|
+
if benchmark_id not in ids:
|
|
38
|
+
ids.append(benchmark_id)
|
|
39
|
+
return ids
|
|
40
|
+
|
|
41
|
+
def rated_runs(self) -> list[dict[str, Any]]:
|
|
42
|
+
return [run for run in self.scenario_runs if _is_rated_run(run)]
|
|
43
|
+
|
|
44
|
+
def rated_runs_by_scenario(self) -> dict[str, list[dict[str, Any]]]:
|
|
45
|
+
runs: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
46
|
+
for run in self.rated_runs():
|
|
47
|
+
scenario_id = run.get("scenario_id")
|
|
48
|
+
if scenario_id:
|
|
49
|
+
runs[str(scenario_id)].append(run)
|
|
50
|
+
return runs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
54
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_ndjson(path: Path) -> list[dict[str, Any]]:
|
|
58
|
+
rows: list[dict[str, Any]] = []
|
|
59
|
+
with path.open(encoding="utf-8") as handle:
|
|
60
|
+
for line in handle:
|
|
61
|
+
stripped = line.strip()
|
|
62
|
+
if stripped:
|
|
63
|
+
rows.append(json.loads(stripped))
|
|
64
|
+
return rows
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_recovery_dataset(recovery_dir: str | Path) -> RecoveryDataset:
|
|
68
|
+
root = Path(recovery_dir)
|
|
69
|
+
exports = root / "exports"
|
|
70
|
+
deleted_only = root / "deleted_only"
|
|
71
|
+
export_manifest = _read_json(exports / "manifest.json")
|
|
72
|
+
deleted_manifest = _read_json(deleted_only / "manifest.json")
|
|
73
|
+
|
|
74
|
+
return RecoveryDataset(
|
|
75
|
+
shop_id=str(deleted_manifest.get("shop_id") or export_manifest.get("shop_id")),
|
|
76
|
+
restore_point_utc=deleted_manifest.get("restore_point_utc")
|
|
77
|
+
or export_manifest.get("restore_point_utc"),
|
|
78
|
+
benchmarks=_read_ndjson(exports / "core_conversationbenchmark.ndjson"),
|
|
79
|
+
scenarios=_read_ndjson(deleted_only / "core_conversationscenario.ndjson"),
|
|
80
|
+
scenario_benchmark_links=_read_ndjson(
|
|
81
|
+
deleted_only / "core_conversationscenariobenchmark.ndjson"
|
|
82
|
+
),
|
|
83
|
+
scenario_runs=_read_ndjson(deleted_only / "core_scenariorun.ndjson"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _agent_id(row: dict[str, Any]) -> str | None:
|
|
88
|
+
if row.get("agent_id"):
|
|
89
|
+
return str(row["agent_id"])
|
|
90
|
+
agent = row.get("agent")
|
|
91
|
+
if isinstance(agent, dict) and agent.get("id"):
|
|
92
|
+
return str(agent["id"])
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _input_conversation_id(row: dict[str, Any]) -> str | None:
|
|
97
|
+
if row.get("input_conversation_id"):
|
|
98
|
+
return str(row["input_conversation_id"])
|
|
99
|
+
input_conversation = row.get("input_conversation")
|
|
100
|
+
if isinstance(input_conversation, dict) and input_conversation.get("id"):
|
|
101
|
+
return str(input_conversation["id"])
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _current_benchmark_ids(row: dict[str, Any]) -> list[str]:
|
|
106
|
+
ids: list[str] = []
|
|
107
|
+
primary = row.get("benchmark")
|
|
108
|
+
if isinstance(primary, dict) and primary.get("id"):
|
|
109
|
+
ids.append(str(primary["id"]))
|
|
110
|
+
for benchmark in row.get("benchmarks") or []:
|
|
111
|
+
if isinstance(benchmark, dict) and benchmark.get("id"):
|
|
112
|
+
benchmark_id = str(benchmark["id"])
|
|
113
|
+
if benchmark_id not in ids:
|
|
114
|
+
ids.append(benchmark_id)
|
|
115
|
+
return ids
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_rated_run(run: dict[str, Any]) -> bool:
|
|
119
|
+
return bool(
|
|
120
|
+
run.get("pass_status") is not None
|
|
121
|
+
or run.get("csat_score") is not None
|
|
122
|
+
or run.get("reference_score") is not None
|
|
123
|
+
or (run.get("reference_notes") or "")
|
|
124
|
+
or (run.get("feedback") or "")
|
|
125
|
+
or run.get("is_escalated") is True
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _scenario_run_body(run: dict[str, Any], scenario_id: str) -> dict[str, Any]:
|
|
130
|
+
body: dict[str, Any] = {"scenario": scenario_id}
|
|
131
|
+
if run.get("output_conversation_id"):
|
|
132
|
+
body["output_conversation_id"] = str(run["output_conversation_id"])
|
|
133
|
+
if run.get("pass_status") is not None:
|
|
134
|
+
body["pass_status"] = run.get("pass_status")
|
|
135
|
+
if run.get("csat_score") is not None:
|
|
136
|
+
body["csat_score"] = run.get("csat_score")
|
|
137
|
+
if run.get("reference_score") is not None:
|
|
138
|
+
body["reference_score"] = run.get("reference_score")
|
|
139
|
+
if run.get("reference_notes"):
|
|
140
|
+
body["reference_notes"] = run.get("reference_notes")
|
|
141
|
+
if run.get("feedback") is not None:
|
|
142
|
+
body["feedback"] = run.get("feedback")
|
|
143
|
+
if run.get("is_escalated") is True:
|
|
144
|
+
body["is_escalated"] = True
|
|
145
|
+
return body
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _run_signature(row: dict[str, Any]) -> tuple[Any, ...]:
|
|
149
|
+
output_conversation = row.get("output_conversation")
|
|
150
|
+
output_conversation_id = row.get("output_conversation_id")
|
|
151
|
+
if isinstance(output_conversation, dict) and output_conversation.get("id"):
|
|
152
|
+
output_conversation_id = output_conversation["id"]
|
|
153
|
+
return (
|
|
154
|
+
str(output_conversation_id) if output_conversation_id else None,
|
|
155
|
+
row.get("pass_status"),
|
|
156
|
+
row.get("csat_score"),
|
|
157
|
+
row.get("reference_score"),
|
|
158
|
+
row.get("reference_notes") or "",
|
|
159
|
+
row.get("feedback") or "",
|
|
160
|
+
bool(row.get("is_escalated")),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def _list_current_benchmarks(
|
|
165
|
+
client: Any, agent_ids: list[str]
|
|
166
|
+
) -> list[dict[str, Any]]:
|
|
167
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
168
|
+
for agent_id in agent_ids:
|
|
169
|
+
for benchmark in await client.list_benchmarks(agent_id=agent_id, limit=500):
|
|
170
|
+
benchmark_id = benchmark.get("id")
|
|
171
|
+
if benchmark_id:
|
|
172
|
+
by_id[str(benchmark_id)] = benchmark
|
|
173
|
+
return list(by_id.values())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def _list_current_scenarios(
|
|
177
|
+
client: Any, scenarios: list[dict[str, Any]]
|
|
178
|
+
) -> list[dict[str, Any]]:
|
|
179
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
180
|
+
names_by_agent: dict[str, list[str]] = defaultdict(list)
|
|
181
|
+
for scenario in scenarios:
|
|
182
|
+
agent_id = _agent_id(scenario)
|
|
183
|
+
name = scenario.get("name")
|
|
184
|
+
if agent_id and name:
|
|
185
|
+
names_by_agent[agent_id].append(str(name))
|
|
186
|
+
|
|
187
|
+
for agent_id, names in sorted(names_by_agent.items()):
|
|
188
|
+
if hasattr(client, "list_recovery_scenarios"):
|
|
189
|
+
rows = await client.list_recovery_scenarios(
|
|
190
|
+
agent_id=agent_id,
|
|
191
|
+
names=names,
|
|
192
|
+
limit=50,
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
rows = await client.list_scenarios(
|
|
196
|
+
agent_id=agent_id,
|
|
197
|
+
limit=500,
|
|
198
|
+
fetch_all=True,
|
|
199
|
+
)
|
|
200
|
+
for scenario in rows:
|
|
201
|
+
scenario_id = scenario.get("id")
|
|
202
|
+
if scenario_id:
|
|
203
|
+
by_id[str(scenario_id)] = scenario
|
|
204
|
+
return list(by_id.values())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def recover_scenario_catalog(
|
|
208
|
+
client: Any,
|
|
209
|
+
*,
|
|
210
|
+
recovery_dir: str | Path,
|
|
211
|
+
dry_run: bool = True,
|
|
212
|
+
restore_ratings: bool = True,
|
|
213
|
+
max_rated_runs: int | None = None,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
dataset = load_recovery_dataset(recovery_dir)
|
|
216
|
+
benchmark_agent_ids = sorted(
|
|
217
|
+
{agent_id for row in dataset.benchmarks if (agent_id := _agent_id(row))}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
current_benchmarks = await _list_current_benchmarks(client, benchmark_agent_ids)
|
|
221
|
+
current_benchmark_by_key = {
|
|
222
|
+
(_agent_id(row), row.get("name")): row for row in current_benchmarks
|
|
223
|
+
}
|
|
224
|
+
benchmark_id_map: dict[str, str] = {}
|
|
225
|
+
benchmark_summary = {
|
|
226
|
+
"available": len(dataset.benchmarks),
|
|
227
|
+
"existing": 0,
|
|
228
|
+
"to_create": 0,
|
|
229
|
+
"created": 0,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for benchmark in dataset.benchmarks:
|
|
233
|
+
old_id = str(benchmark["id"])
|
|
234
|
+
key = (_agent_id(benchmark), benchmark.get("name"))
|
|
235
|
+
current = current_benchmark_by_key.get(key)
|
|
236
|
+
if current and current.get("id"):
|
|
237
|
+
benchmark_summary["existing"] += 1
|
|
238
|
+
benchmark_id_map[old_id] = str(current["id"])
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
benchmark_summary["to_create"] += 1
|
|
242
|
+
if dry_run:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
created = await client.create_benchmark(
|
|
246
|
+
agent_id=str(benchmark["agent_id"]),
|
|
247
|
+
name=benchmark["name"],
|
|
248
|
+
description=benchmark.get("description") or "",
|
|
249
|
+
)
|
|
250
|
+
benchmark_summary["created"] += 1
|
|
251
|
+
benchmark_id_map[old_id] = str(created["id"])
|
|
252
|
+
|
|
253
|
+
current_scenarios = await _list_current_scenarios(client, dataset.scenarios)
|
|
254
|
+
current_scenario_by_exact = {
|
|
255
|
+
(_agent_id(row), row.get("name"), _input_conversation_id(row)): row
|
|
256
|
+
for row in current_scenarios
|
|
257
|
+
}
|
|
258
|
+
current_scenario_by_name = {
|
|
259
|
+
(_agent_id(row), row.get("name")): row for row in current_scenarios
|
|
260
|
+
}
|
|
261
|
+
scenario_id_map: dict[str, str] = {}
|
|
262
|
+
scenario_summary = {
|
|
263
|
+
"available": len(dataset.scenarios),
|
|
264
|
+
"existing": 0,
|
|
265
|
+
"to_create": 0,
|
|
266
|
+
"created": 0,
|
|
267
|
+
"to_update_links": 0,
|
|
268
|
+
"updated_links": 0,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for scenario in dataset.scenarios:
|
|
272
|
+
old_id = str(scenario["id"])
|
|
273
|
+
current = current_scenario_by_exact.get(
|
|
274
|
+
(
|
|
275
|
+
_agent_id(scenario),
|
|
276
|
+
scenario.get("name"),
|
|
277
|
+
_input_conversation_id(scenario),
|
|
278
|
+
)
|
|
279
|
+
) or current_scenario_by_name.get((_agent_id(scenario), scenario.get("name")))
|
|
280
|
+
|
|
281
|
+
old_benchmark_ids = dataset.benchmark_ids_for_scenario(scenario)
|
|
282
|
+
current_benchmark_ids = [
|
|
283
|
+
benchmark_id_map[old_benchmark_id]
|
|
284
|
+
for old_benchmark_id in old_benchmark_ids
|
|
285
|
+
if old_benchmark_id in benchmark_id_map
|
|
286
|
+
]
|
|
287
|
+
primary_benchmark_id = (
|
|
288
|
+
benchmark_id_map.get(str(scenario.get("benchmark_id")))
|
|
289
|
+
if scenario.get("benchmark_id")
|
|
290
|
+
else (current_benchmark_ids[0] if current_benchmark_ids else None)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if current and current.get("id"):
|
|
294
|
+
scenario_summary["existing"] += 1
|
|
295
|
+
current_id = str(current["id"])
|
|
296
|
+
scenario_id_map[old_id] = current_id
|
|
297
|
+
missing_links = [
|
|
298
|
+
benchmark_id
|
|
299
|
+
for benchmark_id in current_benchmark_ids
|
|
300
|
+
if benchmark_id not in _current_benchmark_ids(current)
|
|
301
|
+
]
|
|
302
|
+
if missing_links:
|
|
303
|
+
scenario_summary["to_update_links"] += 1
|
|
304
|
+
if not dry_run:
|
|
305
|
+
body = {"benchmark_ids": _current_benchmark_ids(current)}
|
|
306
|
+
for benchmark_id in missing_links:
|
|
307
|
+
if benchmark_id not in body["benchmark_ids"]:
|
|
308
|
+
body["benchmark_ids"].append(benchmark_id)
|
|
309
|
+
if primary_benchmark_id:
|
|
310
|
+
body["benchmark_id"] = primary_benchmark_id
|
|
311
|
+
await client.update_recovery_scenario(current_id, body)
|
|
312
|
+
scenario_summary["updated_links"] += 1
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
scenario_summary["to_create"] += 1
|
|
316
|
+
if dry_run:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
body: dict[str, Any] = {
|
|
320
|
+
"agent_id": str(scenario["agent_id"]),
|
|
321
|
+
"allow_remote_ticket_creation": bool(
|
|
322
|
+
scenario.get("allow_remote_ticket_creation")
|
|
323
|
+
),
|
|
324
|
+
"benchmark_ids": current_benchmark_ids,
|
|
325
|
+
"context": scenario.get("context") or {},
|
|
326
|
+
"input_conversation_id": str(scenario["input_conversation_id"]),
|
|
327
|
+
"name": scenario["name"],
|
|
328
|
+
}
|
|
329
|
+
if primary_benchmark_id:
|
|
330
|
+
body["benchmark_id"] = primary_benchmark_id
|
|
331
|
+
|
|
332
|
+
created = await client.create_recovery_scenario(body)
|
|
333
|
+
scenario_summary["created"] += 1
|
|
334
|
+
scenario_id_map[old_id] = str(created["id"])
|
|
335
|
+
|
|
336
|
+
rated_runs_by_scenario = dataset.rated_runs_by_scenario()
|
|
337
|
+
rated_runs = dataset.rated_runs()
|
|
338
|
+
if max_rated_runs is not None:
|
|
339
|
+
rated_runs = rated_runs[:max_rated_runs]
|
|
340
|
+
allowed_old_run_ids = {str(run["id"]) for run in rated_runs if run.get("id")}
|
|
341
|
+
run_summary = {
|
|
342
|
+
"rated_available": len(dataset.rated_runs()),
|
|
343
|
+
"to_create": len(rated_runs) if restore_ratings else 0,
|
|
344
|
+
"created": 0,
|
|
345
|
+
"skipped_existing": 0,
|
|
346
|
+
"skipped_missing_scenario": 0,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if restore_ratings:
|
|
350
|
+
existing_run_signatures: dict[str, set[tuple[Any, ...]]] = {}
|
|
351
|
+
for scenario in dataset.scenarios:
|
|
352
|
+
old_scenario_id = str(scenario["id"])
|
|
353
|
+
current_scenario_id = scenario_id_map.get(old_scenario_id)
|
|
354
|
+
scenario_runs = [
|
|
355
|
+
run
|
|
356
|
+
for run in rated_runs_by_scenario.get(old_scenario_id, [])
|
|
357
|
+
if str(run.get("id")) in allowed_old_run_ids
|
|
358
|
+
]
|
|
359
|
+
for run in scenario_runs:
|
|
360
|
+
if not current_scenario_id:
|
|
361
|
+
run_summary["skipped_missing_scenario"] += 1
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
body = _scenario_run_body(run, current_scenario_id)
|
|
365
|
+
if dry_run:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
if current_scenario_id not in existing_run_signatures:
|
|
369
|
+
existing_runs = await client.list_scenario_runs(
|
|
370
|
+
scenario_id=current_scenario_id,
|
|
371
|
+
latest=False,
|
|
372
|
+
limit=500,
|
|
373
|
+
)
|
|
374
|
+
existing_run_signatures[current_scenario_id] = {
|
|
375
|
+
_run_signature(existing_run) for existing_run in existing_runs
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
signature = _run_signature(body)
|
|
379
|
+
if signature in existing_run_signatures[current_scenario_id]:
|
|
380
|
+
run_summary["skipped_existing"] += 1
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
await client.create_recovery_scenario_run(body)
|
|
384
|
+
existing_run_signatures[current_scenario_id].add(signature)
|
|
385
|
+
run_summary["created"] += 1
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
"dry_run": dry_run,
|
|
389
|
+
"shop_id": dataset.shop_id,
|
|
390
|
+
"restore_point_utc": dataset.restore_point_utc,
|
|
391
|
+
"source": {
|
|
392
|
+
"benchmarks": len(dataset.benchmarks),
|
|
393
|
+
"scenarios": len(dataset.scenarios),
|
|
394
|
+
"scenario_benchmark_links": len(dataset.scenario_benchmark_links),
|
|
395
|
+
"scenario_runs": len(dataset.scenario_runs),
|
|
396
|
+
},
|
|
397
|
+
"benchmarks": benchmark_summary,
|
|
398
|
+
"scenarios": scenario_summary,
|
|
399
|
+
"scenario_runs": run_summary,
|
|
400
|
+
}
|