applied-cli 0.6.9__tar.gz → 0.6.11__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.9 → applied_cli-0.6.11}/PKG-INFO +25 -1
- {applied_cli-0.6.9 → applied_cli-0.6.11}/README.md +24 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/__init__.py +1 -1
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/catalog.py +5 -1
- applied_cli-0.6.11/applied_cli/v2/evals.py +567 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/PKG-INFO +25 -1
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/SOURCES.txt +2 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/pyproject.toml +1 -1
- applied_cli-0.6.11/tests/test_v2_evals.py +163 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/cli.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/client.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/domains.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/scenarios.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/setup.cfg +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_benchmark_list_with_results.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_benchmark_results.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_cli.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_client.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_scenario_bulk_cancel.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_scenario_bulk_run_contact.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_scenario_bulk_run_wait.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_scenario_create_bulk.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.9 → applied_cli-0.6.11}/tests/test_v2_tickets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.11
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -82,6 +82,8 @@ Raw analytics SQL is not available through the public CLI surface.
|
|
|
82
82
|
|
|
83
83
|
## Benchmarks & Scenarios
|
|
84
84
|
|
|
85
|
+
> **LEGACY surface.** Benchmarks and scenarios are the older regression-suite system. The current evaluation system is the **/evals** surface (feedback + simulations + test data), driven by the `simulation_test_*`, `test_customer_*`, and `simulation_run_*` tools (MCP / `/v1/simulation-tests/`, `/v1/test-customers/`, `/v1/simulation-runs/`). The `feedback` field on `scenario_update` / `scenario_run_update` is a legacy free-text note, NOT the new Judgment+Comment feedback surface. Use the /evals tools for new evaluation work; the tools below are for maintaining existing benchmark suites.
|
|
86
|
+
|
|
85
87
|
A **benchmark** is a named regression suite; a **scenario** is one test conversation
|
|
86
88
|
(built from a real `input_conversation_id`) that can belong to one or more benchmarks.
|
|
87
89
|
The typical loop is: build a suite → run it → review the pass rate → fix → re-run.
|
|
@@ -97,6 +99,11 @@ applied benchmark-create --agent-id <agent_id> --name "Cancel Regression"
|
|
|
97
99
|
applied scenario-create --input-conversation-id <conversation_id> --name "<name>" \
|
|
98
100
|
--benchmark-id <benchmark_id>
|
|
99
101
|
|
|
102
|
+
# Build a suite fast from several real conversations at once. Each scenario is
|
|
103
|
+
# named from its conversation's title (or "<prefix> N" with --name-prefix).
|
|
104
|
+
applied scenario-create-bulk --conversation-ids <id1>,<id2>,<id3> \
|
|
105
|
+
--benchmark-id <benchmark_id>
|
|
106
|
+
|
|
100
107
|
# Port a suite to another agent (e.g. email -> chat). Cross-agent recreates the
|
|
101
108
|
# scenarios under the destination agent; same-agent just tags them in.
|
|
102
109
|
# Dry-run by default; add --apply to write.
|
|
@@ -116,6 +123,9 @@ applied scenario-bulk-cancel <job_id> --apply
|
|
|
116
123
|
# Review pass/fail health (pass_status reflects the latest run per scenario)
|
|
117
124
|
applied benchmark-results <benchmark_id> --format json
|
|
118
125
|
|
|
126
|
+
# Portfolio go/no-go: pass rates across all of an agent's benchmarks at a glance
|
|
127
|
+
applied benchmarks --agent-id <agent_id> --with-results --format json
|
|
128
|
+
|
|
119
129
|
# Rate scenarios as you evaluate
|
|
120
130
|
applied scenario-update <scenario_id> --pass-status pass --feedback "<note>"
|
|
121
131
|
|
|
@@ -167,8 +177,22 @@ conversations = await tools.conversation_query(
|
|
|
167
177
|
| `benchmark_clone` | Copy all scenarios from one benchmark into another |
|
|
168
178
|
| `benchmark_delete` | Delete a benchmark (guards against wiping scenarios) |
|
|
169
179
|
| `benchmark_results` | Pass/fail/unrated tally and pass rate for a benchmark |
|
|
180
|
+
| `benchmark_list` | List benchmarks (with per-benchmark pass rates via `with_results`) |
|
|
181
|
+
| `scenario_create_bulk` | Build scenarios from several conversations at once |
|
|
170
182
|
| `scenario_bulk_run` | Run scenarios (contact override + wait-to-completion) |
|
|
171
183
|
| `scenario_bulk_cancel` | Cancel a stuck bulk run's queued/running scenario runs |
|
|
184
|
+
| `review_run` | Grade real conversations with the AI Reviewer (pass/fail) |
|
|
185
|
+
| `review_status` | Verdict pairs + the Reviewer's reason for a set of conversations |
|
|
186
|
+
| `review_set_verdict` | Record the human verdict with a teaching reason |
|
|
187
|
+
| `review_teach` | Teach the Reviewer a rule directly |
|
|
188
|
+
| `review_make_test` | Turn a flagged conversation into a proposed regression test |
|
|
189
|
+
| `review_proposals_list` | The suite change-inbox (proposed adds/edits/retires/merges) |
|
|
190
|
+
| `review_proposals_decide` | Accept or dismiss a proposed test |
|
|
191
|
+
|
|
192
|
+
The full `review_*` namespace also includes `review_get`, `review_clear_verdict`,
|
|
193
|
+
`review_add_note`, `review_trust`, and `review_lessons` — the outer review loop:
|
|
194
|
+
grade real traffic, confirm/overrule with a reason that teaches the Reviewer, and
|
|
195
|
+
promote failures into the simulation suite as tests.
|
|
172
196
|
|
|
173
197
|
## Examples
|
|
174
198
|
|
|
@@ -56,6 +56,8 @@ Raw analytics SQL is not available through the public CLI surface.
|
|
|
56
56
|
|
|
57
57
|
## Benchmarks & Scenarios
|
|
58
58
|
|
|
59
|
+
> **LEGACY surface.** Benchmarks and scenarios are the older regression-suite system. The current evaluation system is the **/evals** surface (feedback + simulations + test data), driven by the `simulation_test_*`, `test_customer_*`, and `simulation_run_*` tools (MCP / `/v1/simulation-tests/`, `/v1/test-customers/`, `/v1/simulation-runs/`). The `feedback` field on `scenario_update` / `scenario_run_update` is a legacy free-text note, NOT the new Judgment+Comment feedback surface. Use the /evals tools for new evaluation work; the tools below are for maintaining existing benchmark suites.
|
|
60
|
+
|
|
59
61
|
A **benchmark** is a named regression suite; a **scenario** is one test conversation
|
|
60
62
|
(built from a real `input_conversation_id`) that can belong to one or more benchmarks.
|
|
61
63
|
The typical loop is: build a suite → run it → review the pass rate → fix → re-run.
|
|
@@ -71,6 +73,11 @@ applied benchmark-create --agent-id <agent_id> --name "Cancel Regression"
|
|
|
71
73
|
applied scenario-create --input-conversation-id <conversation_id> --name "<name>" \
|
|
72
74
|
--benchmark-id <benchmark_id>
|
|
73
75
|
|
|
76
|
+
# Build a suite fast from several real conversations at once. Each scenario is
|
|
77
|
+
# named from its conversation's title (or "<prefix> N" with --name-prefix).
|
|
78
|
+
applied scenario-create-bulk --conversation-ids <id1>,<id2>,<id3> \
|
|
79
|
+
--benchmark-id <benchmark_id>
|
|
80
|
+
|
|
74
81
|
# Port a suite to another agent (e.g. email -> chat). Cross-agent recreates the
|
|
75
82
|
# scenarios under the destination agent; same-agent just tags them in.
|
|
76
83
|
# Dry-run by default; add --apply to write.
|
|
@@ -90,6 +97,9 @@ applied scenario-bulk-cancel <job_id> --apply
|
|
|
90
97
|
# Review pass/fail health (pass_status reflects the latest run per scenario)
|
|
91
98
|
applied benchmark-results <benchmark_id> --format json
|
|
92
99
|
|
|
100
|
+
# Portfolio go/no-go: pass rates across all of an agent's benchmarks at a glance
|
|
101
|
+
applied benchmarks --agent-id <agent_id> --with-results --format json
|
|
102
|
+
|
|
93
103
|
# Rate scenarios as you evaluate
|
|
94
104
|
applied scenario-update <scenario_id> --pass-status pass --feedback "<note>"
|
|
95
105
|
|
|
@@ -141,8 +151,22 @@ conversations = await tools.conversation_query(
|
|
|
141
151
|
| `benchmark_clone` | Copy all scenarios from one benchmark into another |
|
|
142
152
|
| `benchmark_delete` | Delete a benchmark (guards against wiping scenarios) |
|
|
143
153
|
| `benchmark_results` | Pass/fail/unrated tally and pass rate for a benchmark |
|
|
154
|
+
| `benchmark_list` | List benchmarks (with per-benchmark pass rates via `with_results`) |
|
|
155
|
+
| `scenario_create_bulk` | Build scenarios from several conversations at once |
|
|
144
156
|
| `scenario_bulk_run` | Run scenarios (contact override + wait-to-completion) |
|
|
145
157
|
| `scenario_bulk_cancel` | Cancel a stuck bulk run's queued/running scenario runs |
|
|
158
|
+
| `review_run` | Grade real conversations with the AI Reviewer (pass/fail) |
|
|
159
|
+
| `review_status` | Verdict pairs + the Reviewer's reason for a set of conversations |
|
|
160
|
+
| `review_set_verdict` | Record the human verdict with a teaching reason |
|
|
161
|
+
| `review_teach` | Teach the Reviewer a rule directly |
|
|
162
|
+
| `review_make_test` | Turn a flagged conversation into a proposed regression test |
|
|
163
|
+
| `review_proposals_list` | The suite change-inbox (proposed adds/edits/retires/merges) |
|
|
164
|
+
| `review_proposals_decide` | Accept or dismiss a proposed test |
|
|
165
|
+
|
|
166
|
+
The full `review_*` namespace also includes `review_get`, `review_clear_verdict`,
|
|
167
|
+
`review_add_note`, `review_trust`, and `review_lessons` — the outer review loop:
|
|
168
|
+
grade real traffic, confirm/overrule with a reason that teaches the Reviewer, and
|
|
169
|
+
promote failures into the simulation suite as tests.
|
|
146
170
|
|
|
147
171
|
## Examples
|
|
148
172
|
|
|
@@ -18,6 +18,7 @@ from applied_cli.v2.connectors import connector_specs
|
|
|
18
18
|
from applied_cli.v2.content import content_specs
|
|
19
19
|
from applied_cli.v2.conversations import conversation_specs
|
|
20
20
|
from applied_cli.v2.domains import LEGACY_TOOL_RENAMES
|
|
21
|
+
from applied_cli.v2.evals import evals_specs
|
|
21
22
|
from applied_cli.v2.flows import flow_specs
|
|
22
23
|
from applied_cli.v2.knowledge import knowledge_specs
|
|
23
24
|
from applied_cli.v2.products import product_specs
|
|
@@ -99,7 +100,9 @@ def _input_model_for(func: Any, v2_name: str) -> type[BaseModel]:
|
|
|
99
100
|
}:
|
|
100
101
|
continue
|
|
101
102
|
annotation = _safe_annotation(parameter.annotation)
|
|
102
|
-
default =
|
|
103
|
+
default = (
|
|
104
|
+
... if parameter.default is inspect.Parameter.empty else parameter.default
|
|
105
|
+
)
|
|
103
106
|
fields[name] = (annotation, default)
|
|
104
107
|
|
|
105
108
|
if not fields:
|
|
@@ -225,6 +228,7 @@ def get_tool_catalog(include_debug: bool = False) -> ToolCatalog:
|
|
|
225
228
|
*connector_specs(),
|
|
226
229
|
*content_specs(),
|
|
227
230
|
*conversation_specs(),
|
|
231
|
+
*evals_specs(),
|
|
228
232
|
*flow_specs(),
|
|
229
233
|
*knowledge_specs(),
|
|
230
234
|
*product_specs(),
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""Native structured v2 tools for the evals / review loop.
|
|
2
|
+
|
|
3
|
+
The review loop over real traffic: the AI Reviewer grades a conversation, a human
|
|
4
|
+
confirms or overrules with a teaching reason, the reason folds back into the
|
|
5
|
+
Reviewer's criteria, and a flagged conversation becomes a regression test in one
|
|
6
|
+
step. These tools expose that loop to an agent — grade conversations, record
|
|
7
|
+
verdicts, teach the Reviewer, and promote a bad conversation into the simulation
|
|
8
|
+
suite as a proposed test (with an auto-generated test customer).
|
|
9
|
+
|
|
10
|
+
All endpoints are shop-scoped and merchant-authenticated. Discover conversations
|
|
11
|
+
to review with `conversations_search` / `conversations_find`, then feed their ids
|
|
12
|
+
here.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
from applied_cli.client import AppliedAPIError, AppliedClient
|
|
22
|
+
from applied_cli.toolkit import ToolError, ToolResult, ToolSpec
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StrictInput(BaseModel):
|
|
26
|
+
model_config = ConfigDict(extra="forbid")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── inputs ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReviewRunInput(StrictInput):
|
|
33
|
+
conversation_ids: list[str] = Field(
|
|
34
|
+
description="Conversation ids to grade (max 50 per call).",
|
|
35
|
+
)
|
|
36
|
+
force: bool = Field(
|
|
37
|
+
default=False,
|
|
38
|
+
description=(
|
|
39
|
+
"Re-grade even if an AI verdict already exists. Off by default so "
|
|
40
|
+
"already-reviewed conversations are skipped."
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ReviewStatusInput(StrictInput):
|
|
46
|
+
conversation_ids: list[str] = Field(
|
|
47
|
+
description="Conversation ids to fetch verdict pairs + narration for.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReviewGetInput(StrictInput):
|
|
52
|
+
conversation_id: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ReviewSetVerdictInput(StrictInput):
|
|
56
|
+
conversation_id: str
|
|
57
|
+
verdict: Literal["pass", "fail"]
|
|
58
|
+
reason: str | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description=(
|
|
61
|
+
"Why — the teaching signal. The Reviewer folds recent reasons into "
|
|
62
|
+
"its criteria automatically, so a clear reason improves future grading."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
pinned: bool | None = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="Pin this reason as a durable lesson that never rotates out.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ReviewClearVerdictInput(StrictInput):
|
|
72
|
+
conversation_id: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ReviewAddNoteInput(StrictInput):
|
|
76
|
+
conversation_id: str
|
|
77
|
+
kind: Literal["correction", "good"] = "correction"
|
|
78
|
+
observed: str | None = Field(
|
|
79
|
+
default=None, description="What the bot did wrong (corrections only)."
|
|
80
|
+
)
|
|
81
|
+
desired: str | None = Field(
|
|
82
|
+
default=None,
|
|
83
|
+
description="What it should have done, or why a good turn is worth reusing.",
|
|
84
|
+
)
|
|
85
|
+
message_id: str | None = Field(
|
|
86
|
+
default=None, description="The turn this note anchors to (optional)."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ReviewTeachInput(StrictInput):
|
|
91
|
+
text: str = Field(
|
|
92
|
+
description=(
|
|
93
|
+
"A rule to teach the Reviewer directly. Lands pinned and applies to "
|
|
94
|
+
"every review from the next one on."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EmptyInput(StrictInput):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ReviewMakeTestInput(StrictInput):
|
|
104
|
+
conversation_id: str = Field(
|
|
105
|
+
description="The conversation to turn into a proposed regression test.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProposalsListInput(StrictInput):
|
|
110
|
+
status: Literal["proposed", "accepted", "dismissed"] = "proposed"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ProposalsDecideInput(StrictInput):
|
|
114
|
+
proposal_id: str
|
|
115
|
+
decision: Literal["accepted", "dismissed"]
|
|
116
|
+
customer_state_ref: str | None = Field(
|
|
117
|
+
default=None,
|
|
118
|
+
description=(
|
|
119
|
+
"When accepting an add/merge that has no fixture yet, the test "
|
|
120
|
+
"customer (CustomerState) id to bind the test to."
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
reason: str | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="When dismissing, why — a negative training signal for miners.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _api_error_result(exc: AppliedAPIError) -> ToolResult[Any]:
|
|
133
|
+
return ToolResult(
|
|
134
|
+
ok=False,
|
|
135
|
+
error=ToolError(
|
|
136
|
+
code=f"api.{exc.status_code}",
|
|
137
|
+
message=str(exc),
|
|
138
|
+
status_code=exc.status_code,
|
|
139
|
+
retryable=exc.status_code in {408, 429, 500, 502, 503, 504},
|
|
140
|
+
recovery_hint=exc.suggestion,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── handlers ────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def review_run_handler(
|
|
149
|
+
client: AppliedClient, params: ReviewRunInput
|
|
150
|
+
) -> ToolResult[Any]:
|
|
151
|
+
ids = params.conversation_ids[:50]
|
|
152
|
+
if not ids:
|
|
153
|
+
return ToolResult(
|
|
154
|
+
ok=False,
|
|
155
|
+
error=ToolError(
|
|
156
|
+
code="input.invalid",
|
|
157
|
+
message="Provide at least one conversation id.",
|
|
158
|
+
retryable=False,
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
data = await client.request(
|
|
163
|
+
"POST",
|
|
164
|
+
"/v1/evals/review/",
|
|
165
|
+
body={"ids": ids, "force": params.force},
|
|
166
|
+
)
|
|
167
|
+
except AppliedAPIError as exc:
|
|
168
|
+
return _api_error_result(exc)
|
|
169
|
+
return ToolResult(
|
|
170
|
+
data=data,
|
|
171
|
+
summary=f"Ran the AI Reviewer over {len(ids)} conversation(s).",
|
|
172
|
+
next_actions=[
|
|
173
|
+
"Use review_status to read the verdicts and the Reviewer's reasons.",
|
|
174
|
+
"Use review_set_verdict to confirm or overrule a verdict.",
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def review_status_handler(
|
|
180
|
+
client: AppliedClient, params: ReviewStatusInput
|
|
181
|
+
) -> ToolResult[Any]:
|
|
182
|
+
ids = params.conversation_ids[:200]
|
|
183
|
+
try:
|
|
184
|
+
data = await client.request(
|
|
185
|
+
"GET", "/v1/evals/status/", params={"ids": ",".join(ids)}
|
|
186
|
+
)
|
|
187
|
+
except AppliedAPIError as exc:
|
|
188
|
+
return _api_error_result(exc)
|
|
189
|
+
rows = data.get("rows", []) if isinstance(data, dict) else []
|
|
190
|
+
return ToolResult(
|
|
191
|
+
data=data,
|
|
192
|
+
summary=f"Verdict pairs for {len(rows)} of {len(ids)} conversation(s).",
|
|
193
|
+
next_actions=[
|
|
194
|
+
"Use review_get for the full review pair + notes on one conversation.",
|
|
195
|
+
"Use review_run with the unreviewed ids to grade them.",
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def review_get_handler(
|
|
201
|
+
client: AppliedClient, params: ReviewGetInput
|
|
202
|
+
) -> ToolResult[Any]:
|
|
203
|
+
try:
|
|
204
|
+
data = await client.request("GET", f"/v1/evals/{params.conversation_id}/")
|
|
205
|
+
except AppliedAPIError as exc:
|
|
206
|
+
return _api_error_result(exc)
|
|
207
|
+
return ToolResult(
|
|
208
|
+
data=data,
|
|
209
|
+
summary=f"Review pair + notes for conversation {params.conversation_id}.",
|
|
210
|
+
next_actions=[
|
|
211
|
+
"Use review_set_verdict to record the human verdict.",
|
|
212
|
+
"Use review_make_test to turn a flagged conversation into a test.",
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def review_set_verdict_handler(
|
|
218
|
+
client: AppliedClient, params: ReviewSetVerdictInput
|
|
219
|
+
) -> ToolResult[Any]:
|
|
220
|
+
body: dict[str, Any] = {"verdict": params.verdict}
|
|
221
|
+
if params.reason is not None:
|
|
222
|
+
body["reason"] = params.reason
|
|
223
|
+
if params.pinned is not None:
|
|
224
|
+
body["pinned"] = params.pinned
|
|
225
|
+
try:
|
|
226
|
+
data = await client.request(
|
|
227
|
+
"POST", f"/v1/evals/{params.conversation_id}/review/", body=body
|
|
228
|
+
)
|
|
229
|
+
except AppliedAPIError as exc:
|
|
230
|
+
return _api_error_result(exc)
|
|
231
|
+
return ToolResult(
|
|
232
|
+
data=data,
|
|
233
|
+
summary=f"Recorded a '{params.verdict}' verdict.",
|
|
234
|
+
next_actions=(
|
|
235
|
+
[
|
|
236
|
+
"Use review_make_test to add this as a regression test.",
|
|
237
|
+
"Use review_add_note to flag the specific failing turn.",
|
|
238
|
+
]
|
|
239
|
+
if params.verdict == "fail"
|
|
240
|
+
else []
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def review_clear_verdict_handler(
|
|
246
|
+
client: AppliedClient, params: ReviewClearVerdictInput
|
|
247
|
+
) -> ToolResult[Any]:
|
|
248
|
+
try:
|
|
249
|
+
await client.request("DELETE", f"/v1/evals/{params.conversation_id}/review/")
|
|
250
|
+
except AppliedAPIError as exc:
|
|
251
|
+
return _api_error_result(exc)
|
|
252
|
+
return ToolResult(
|
|
253
|
+
data={"conversation_id": params.conversation_id, "cleared": True},
|
|
254
|
+
summary="Withdrew the human verdict.",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def review_add_note_handler(
|
|
259
|
+
client: AppliedClient, params: ReviewAddNoteInput
|
|
260
|
+
) -> ToolResult[Any]:
|
|
261
|
+
if not (params.observed or params.desired):
|
|
262
|
+
return ToolResult(
|
|
263
|
+
ok=False,
|
|
264
|
+
error=ToolError(
|
|
265
|
+
code="input.invalid",
|
|
266
|
+
message="A note needs observed and/or desired text.",
|
|
267
|
+
retryable=False,
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
body: dict[str, Any] = {"kind": params.kind}
|
|
271
|
+
if params.observed is not None:
|
|
272
|
+
body["observed"] = params.observed
|
|
273
|
+
if params.desired is not None:
|
|
274
|
+
body["desired"] = params.desired
|
|
275
|
+
if params.message_id is not None:
|
|
276
|
+
body["message_id"] = params.message_id
|
|
277
|
+
try:
|
|
278
|
+
data = await client.request(
|
|
279
|
+
"POST", f"/v1/evals/{params.conversation_id}/notes/", body=body
|
|
280
|
+
)
|
|
281
|
+
except AppliedAPIError as exc:
|
|
282
|
+
return _api_error_result(exc)
|
|
283
|
+
return ToolResult(data=data, summary=f"Added a '{params.kind}' note.")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def review_trust_handler(
|
|
287
|
+
client: AppliedClient, params: EmptyInput
|
|
288
|
+
) -> ToolResult[Any]:
|
|
289
|
+
try:
|
|
290
|
+
data = await client.request("GET", "/v1/evals/trust/")
|
|
291
|
+
except AppliedAPIError as exc:
|
|
292
|
+
return _api_error_result(exc)
|
|
293
|
+
pairs = data.get("total_pairs") if isinstance(data, dict) else None
|
|
294
|
+
return ToolResult(
|
|
295
|
+
data=data,
|
|
296
|
+
summary=(
|
|
297
|
+
f"Reviewer↔human calibration over {pairs} judged pair(s)."
|
|
298
|
+
if pairs is not None
|
|
299
|
+
else "Reviewer↔human calibration by topic."
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def review_lessons_handler(
|
|
305
|
+
client: AppliedClient, params: EmptyInput
|
|
306
|
+
) -> ToolResult[Any]:
|
|
307
|
+
try:
|
|
308
|
+
data = await client.request("GET", "/v1/evals/lessons/")
|
|
309
|
+
except AppliedAPIError as exc:
|
|
310
|
+
return _api_error_result(exc)
|
|
311
|
+
lessons = data.get("lessons", []) if isinstance(data, dict) else []
|
|
312
|
+
return ToolResult(
|
|
313
|
+
data=data,
|
|
314
|
+
summary=f"The Reviewer has learned {len(lessons)} lesson(s).",
|
|
315
|
+
next_actions=["Use review_teach to add a rule directly."],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
async def review_teach_handler(
|
|
320
|
+
client: AppliedClient, params: ReviewTeachInput
|
|
321
|
+
) -> ToolResult[Any]:
|
|
322
|
+
try:
|
|
323
|
+
data = await client.request(
|
|
324
|
+
"POST", "/v1/evals/lessons/", body={"text": params.text}
|
|
325
|
+
)
|
|
326
|
+
except AppliedAPIError as exc:
|
|
327
|
+
return _api_error_result(exc)
|
|
328
|
+
return ToolResult(
|
|
329
|
+
data=data,
|
|
330
|
+
summary="Taught the Reviewer a new rule (pinned).",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def review_make_test_handler(
|
|
335
|
+
client: AppliedClient, params: ReviewMakeTestInput
|
|
336
|
+
) -> ToolResult[Any]:
|
|
337
|
+
try:
|
|
338
|
+
data = await client.request(
|
|
339
|
+
"POST",
|
|
340
|
+
"/v1/simulation-test-proposals/from-conversation/",
|
|
341
|
+
body={"conversation_id": params.conversation_id},
|
|
342
|
+
)
|
|
343
|
+
except AppliedAPIError as exc:
|
|
344
|
+
return _api_error_result(exc)
|
|
345
|
+
return ToolResult(
|
|
346
|
+
data=data,
|
|
347
|
+
summary="Proposed a regression test (with an auto-built test customer).",
|
|
348
|
+
next_actions=[
|
|
349
|
+
"Use review_proposals_list to see the change inbox.",
|
|
350
|
+
"Use review_proposals_decide to accept it into the suite.",
|
|
351
|
+
],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def review_proposals_list_handler(
|
|
356
|
+
client: AppliedClient, params: ProposalsListInput
|
|
357
|
+
) -> ToolResult[Any]:
|
|
358
|
+
try:
|
|
359
|
+
data = await client.request(
|
|
360
|
+
"GET",
|
|
361
|
+
"/v1/simulation-test-proposals/",
|
|
362
|
+
params={"status": params.status},
|
|
363
|
+
)
|
|
364
|
+
except AppliedAPIError as exc:
|
|
365
|
+
return _api_error_result(exc)
|
|
366
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
367
|
+
return ToolResult(
|
|
368
|
+
data=data,
|
|
369
|
+
summary=f"{len(items)} {params.status} test proposal(s).",
|
|
370
|
+
next_actions=[
|
|
371
|
+
"Use review_proposals_decide to accept or dismiss a proposal.",
|
|
372
|
+
],
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def review_proposals_decide_handler(
|
|
377
|
+
client: AppliedClient, params: ProposalsDecideInput
|
|
378
|
+
) -> ToolResult[Any]:
|
|
379
|
+
body: dict[str, Any] = {"status": params.decision}
|
|
380
|
+
if params.customer_state_ref is not None:
|
|
381
|
+
body["customer_state_ref"] = params.customer_state_ref
|
|
382
|
+
if params.reason is not None:
|
|
383
|
+
body["reason"] = params.reason
|
|
384
|
+
try:
|
|
385
|
+
data = await client.request(
|
|
386
|
+
"PATCH",
|
|
387
|
+
f"/v1/simulation-test-proposals/{params.proposal_id}/",
|
|
388
|
+
body=body,
|
|
389
|
+
)
|
|
390
|
+
except AppliedAPIError as exc:
|
|
391
|
+
return _api_error_result(exc)
|
|
392
|
+
return ToolResult(
|
|
393
|
+
data=data,
|
|
394
|
+
summary=f"Marked proposal {params.proposal_id} {params.decision}.",
|
|
395
|
+
next_actions=(
|
|
396
|
+
["Use scenarios_bulk_run to run the newly added test."]
|
|
397
|
+
if params.decision == "accepted"
|
|
398
|
+
else []
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ── specs ───────────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def evals_specs() -> list[ToolSpec]:
|
|
407
|
+
return [
|
|
408
|
+
ToolSpec(
|
|
409
|
+
name="review_run",
|
|
410
|
+
namespace="review",
|
|
411
|
+
description=(
|
|
412
|
+
"Run the AI Reviewer over one or more real conversations. It grades "
|
|
413
|
+
"each pass/fail against learned criteria without ever reading the "
|
|
414
|
+
"human verdict. Skips already-reviewed conversations unless force=true."
|
|
415
|
+
),
|
|
416
|
+
input_model=ReviewRunInput,
|
|
417
|
+
output_model=None,
|
|
418
|
+
handler=review_run_handler,
|
|
419
|
+
read_write_mode="write",
|
|
420
|
+
tags=["evals", "native"],
|
|
421
|
+
),
|
|
422
|
+
ToolSpec(
|
|
423
|
+
name="review_status",
|
|
424
|
+
namespace="review",
|
|
425
|
+
description=(
|
|
426
|
+
"Fetch the verdict pair (AI + human) and the Reviewer's one-line "
|
|
427
|
+
"reason for a set of conversations, plus shop-wide review counts."
|
|
428
|
+
),
|
|
429
|
+
input_model=ReviewStatusInput,
|
|
430
|
+
output_model=None,
|
|
431
|
+
handler=review_status_handler,
|
|
432
|
+
read_write_mode="read",
|
|
433
|
+
tags=["evals", "native"],
|
|
434
|
+
),
|
|
435
|
+
ToolSpec(
|
|
436
|
+
name="review_get",
|
|
437
|
+
namespace="review",
|
|
438
|
+
description=(
|
|
439
|
+
"Get the full review of one conversation: the AI verdict + reason, "
|
|
440
|
+
"the human verdict, and every turn-level note."
|
|
441
|
+
),
|
|
442
|
+
input_model=ReviewGetInput,
|
|
443
|
+
output_model=None,
|
|
444
|
+
handler=review_get_handler,
|
|
445
|
+
read_write_mode="read",
|
|
446
|
+
tags=["evals", "native"],
|
|
447
|
+
),
|
|
448
|
+
ToolSpec(
|
|
449
|
+
name="review_set_verdict",
|
|
450
|
+
namespace="review",
|
|
451
|
+
description=(
|
|
452
|
+
"Record the human verdict on a conversation (pass/fail) with an "
|
|
453
|
+
"optional teaching reason. The reason folds into the Reviewer's "
|
|
454
|
+
"criteria automatically. Upserts — call again to change it."
|
|
455
|
+
),
|
|
456
|
+
input_model=ReviewSetVerdictInput,
|
|
457
|
+
output_model=None,
|
|
458
|
+
handler=review_set_verdict_handler,
|
|
459
|
+
read_write_mode="write",
|
|
460
|
+
tags=["evals", "native"],
|
|
461
|
+
),
|
|
462
|
+
ToolSpec(
|
|
463
|
+
name="review_clear_verdict",
|
|
464
|
+
namespace="review",
|
|
465
|
+
description="Withdraw the human verdict on a conversation (undo).",
|
|
466
|
+
input_model=ReviewClearVerdictInput,
|
|
467
|
+
output_model=None,
|
|
468
|
+
handler=review_clear_verdict_handler,
|
|
469
|
+
read_write_mode="write",
|
|
470
|
+
tags=["evals", "native"],
|
|
471
|
+
),
|
|
472
|
+
ToolSpec(
|
|
473
|
+
name="review_add_note",
|
|
474
|
+
namespace="review",
|
|
475
|
+
description=(
|
|
476
|
+
"Attach a turn-level note to a conversation: a correction (what the "
|
|
477
|
+
"bot did wrong / what it should have done) or a good example."
|
|
478
|
+
),
|
|
479
|
+
input_model=ReviewAddNoteInput,
|
|
480
|
+
output_model=None,
|
|
481
|
+
handler=review_add_note_handler,
|
|
482
|
+
read_write_mode="write",
|
|
483
|
+
tags=["evals", "native"],
|
|
484
|
+
),
|
|
485
|
+
ToolSpec(
|
|
486
|
+
name="review_trust",
|
|
487
|
+
namespace="review",
|
|
488
|
+
description=(
|
|
489
|
+
"Per-topic Reviewer↔human calibration: agreement, and false passes "
|
|
490
|
+
"(the Reviewer passed what a human flagged) — the direction to watch."
|
|
491
|
+
),
|
|
492
|
+
input_model=EmptyInput,
|
|
493
|
+
output_model=None,
|
|
494
|
+
handler=review_trust_handler,
|
|
495
|
+
read_write_mode="read",
|
|
496
|
+
tags=["evals", "native"],
|
|
497
|
+
),
|
|
498
|
+
ToolSpec(
|
|
499
|
+
name="review_lessons",
|
|
500
|
+
namespace="review",
|
|
501
|
+
description=(
|
|
502
|
+
"What the Reviewer has learned — the capped set of human signals "
|
|
503
|
+
"(verdict reasons, turn notes, taught rules, merged lessons) its "
|
|
504
|
+
"criteria compile from, with provenance."
|
|
505
|
+
),
|
|
506
|
+
input_model=EmptyInput,
|
|
507
|
+
output_model=None,
|
|
508
|
+
handler=review_lessons_handler,
|
|
509
|
+
read_write_mode="read",
|
|
510
|
+
tags=["evals", "native"],
|
|
511
|
+
),
|
|
512
|
+
ToolSpec(
|
|
513
|
+
name="review_teach",
|
|
514
|
+
namespace="review",
|
|
515
|
+
description=(
|
|
516
|
+
"Teach the Reviewer a rule directly. Lands pinned and applies to "
|
|
517
|
+
"every review from the next one on."
|
|
518
|
+
),
|
|
519
|
+
input_model=ReviewTeachInput,
|
|
520
|
+
output_model=None,
|
|
521
|
+
handler=review_teach_handler,
|
|
522
|
+
read_write_mode="write",
|
|
523
|
+
tags=["evals", "native"],
|
|
524
|
+
),
|
|
525
|
+
ToolSpec(
|
|
526
|
+
name="review_make_test",
|
|
527
|
+
namespace="review",
|
|
528
|
+
description=(
|
|
529
|
+
"Turn a reviewed conversation into a proposed regression test — "
|
|
530
|
+
"drafts both the test and a fictionalized test customer, and lands "
|
|
531
|
+
"it in the suite's change inbox. The one path from a bad "
|
|
532
|
+
"conversation to a suite candidate."
|
|
533
|
+
),
|
|
534
|
+
input_model=ReviewMakeTestInput,
|
|
535
|
+
output_model=None,
|
|
536
|
+
handler=review_make_test_handler,
|
|
537
|
+
read_write_mode="write",
|
|
538
|
+
tags=["evals", "native"],
|
|
539
|
+
),
|
|
540
|
+
ToolSpec(
|
|
541
|
+
name="review_proposals_list",
|
|
542
|
+
namespace="review",
|
|
543
|
+
description=(
|
|
544
|
+
"List test proposals in the change inbox (proposed / accepted / "
|
|
545
|
+
"dismissed) — adds, edits, retires, and merges awaiting a decision."
|
|
546
|
+
),
|
|
547
|
+
input_model=ProposalsListInput,
|
|
548
|
+
output_model=None,
|
|
549
|
+
handler=review_proposals_list_handler,
|
|
550
|
+
read_write_mode="read",
|
|
551
|
+
tags=["evals", "native"],
|
|
552
|
+
),
|
|
553
|
+
ToolSpec(
|
|
554
|
+
name="review_proposals_decide",
|
|
555
|
+
namespace="review",
|
|
556
|
+
description=(
|
|
557
|
+
"Accept or dismiss a test proposal. Accepting an add/merge without "
|
|
558
|
+
"a fixture needs a customer_state_ref; dismissing takes an optional "
|
|
559
|
+
"reason used as a negative training signal."
|
|
560
|
+
),
|
|
561
|
+
input_model=ProposalsDecideInput,
|
|
562
|
+
output_model=None,
|
|
563
|
+
handler=review_proposals_decide_handler,
|
|
564
|
+
read_write_mode="write",
|
|
565
|
+
tags=["evals", "native"],
|
|
566
|
+
),
|
|
567
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.11
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -82,6 +82,8 @@ Raw analytics SQL is not available through the public CLI surface.
|
|
|
82
82
|
|
|
83
83
|
## Benchmarks & Scenarios
|
|
84
84
|
|
|
85
|
+
> **LEGACY surface.** Benchmarks and scenarios are the older regression-suite system. The current evaluation system is the **/evals** surface (feedback + simulations + test data), driven by the `simulation_test_*`, `test_customer_*`, and `simulation_run_*` tools (MCP / `/v1/simulation-tests/`, `/v1/test-customers/`, `/v1/simulation-runs/`). The `feedback` field on `scenario_update` / `scenario_run_update` is a legacy free-text note, NOT the new Judgment+Comment feedback surface. Use the /evals tools for new evaluation work; the tools below are for maintaining existing benchmark suites.
|
|
86
|
+
|
|
85
87
|
A **benchmark** is a named regression suite; a **scenario** is one test conversation
|
|
86
88
|
(built from a real `input_conversation_id`) that can belong to one or more benchmarks.
|
|
87
89
|
The typical loop is: build a suite → run it → review the pass rate → fix → re-run.
|
|
@@ -97,6 +99,11 @@ applied benchmark-create --agent-id <agent_id> --name "Cancel Regression"
|
|
|
97
99
|
applied scenario-create --input-conversation-id <conversation_id> --name "<name>" \
|
|
98
100
|
--benchmark-id <benchmark_id>
|
|
99
101
|
|
|
102
|
+
# Build a suite fast from several real conversations at once. Each scenario is
|
|
103
|
+
# named from its conversation's title (or "<prefix> N" with --name-prefix).
|
|
104
|
+
applied scenario-create-bulk --conversation-ids <id1>,<id2>,<id3> \
|
|
105
|
+
--benchmark-id <benchmark_id>
|
|
106
|
+
|
|
100
107
|
# Port a suite to another agent (e.g. email -> chat). Cross-agent recreates the
|
|
101
108
|
# scenarios under the destination agent; same-agent just tags them in.
|
|
102
109
|
# Dry-run by default; add --apply to write.
|
|
@@ -116,6 +123,9 @@ applied scenario-bulk-cancel <job_id> --apply
|
|
|
116
123
|
# Review pass/fail health (pass_status reflects the latest run per scenario)
|
|
117
124
|
applied benchmark-results <benchmark_id> --format json
|
|
118
125
|
|
|
126
|
+
# Portfolio go/no-go: pass rates across all of an agent's benchmarks at a glance
|
|
127
|
+
applied benchmarks --agent-id <agent_id> --with-results --format json
|
|
128
|
+
|
|
119
129
|
# Rate scenarios as you evaluate
|
|
120
130
|
applied scenario-update <scenario_id> --pass-status pass --feedback "<note>"
|
|
121
131
|
|
|
@@ -167,8 +177,22 @@ conversations = await tools.conversation_query(
|
|
|
167
177
|
| `benchmark_clone` | Copy all scenarios from one benchmark into another |
|
|
168
178
|
| `benchmark_delete` | Delete a benchmark (guards against wiping scenarios) |
|
|
169
179
|
| `benchmark_results` | Pass/fail/unrated tally and pass rate for a benchmark |
|
|
180
|
+
| `benchmark_list` | List benchmarks (with per-benchmark pass rates via `with_results`) |
|
|
181
|
+
| `scenario_create_bulk` | Build scenarios from several conversations at once |
|
|
170
182
|
| `scenario_bulk_run` | Run scenarios (contact override + wait-to-completion) |
|
|
171
183
|
| `scenario_bulk_cancel` | Cancel a stuck bulk run's queued/running scenario runs |
|
|
184
|
+
| `review_run` | Grade real conversations with the AI Reviewer (pass/fail) |
|
|
185
|
+
| `review_status` | Verdict pairs + the Reviewer's reason for a set of conversations |
|
|
186
|
+
| `review_set_verdict` | Record the human verdict with a teaching reason |
|
|
187
|
+
| `review_teach` | Teach the Reviewer a rule directly |
|
|
188
|
+
| `review_make_test` | Turn a flagged conversation into a proposed regression test |
|
|
189
|
+
| `review_proposals_list` | The suite change-inbox (proposed adds/edits/retires/merges) |
|
|
190
|
+
| `review_proposals_decide` | Accept or dismiss a proposed test |
|
|
191
|
+
|
|
192
|
+
The full `review_*` namespace also includes `review_get`, `review_clear_verdict`,
|
|
193
|
+
`review_add_note`, `review_trust`, and `review_lessons` — the outer review loop:
|
|
194
|
+
grade real traffic, confirm/overrule with a reason that teaches the Reviewer, and
|
|
195
|
+
promote failures into the simulation suite as tests.
|
|
172
196
|
|
|
173
197
|
## Examples
|
|
174
198
|
|
|
@@ -28,6 +28,7 @@ applied_cli/v2/connectors.py
|
|
|
28
28
|
applied_cli/v2/content.py
|
|
29
29
|
applied_cli/v2/conversations.py
|
|
30
30
|
applied_cli/v2/domains.py
|
|
31
|
+
applied_cli/v2/evals.py
|
|
31
32
|
applied_cli/v2/flows.py
|
|
32
33
|
applied_cli/v2/knowledge.py
|
|
33
34
|
applied_cli/v2/manifest.py
|
|
@@ -62,6 +63,7 @@ tests/test_v2_catalog_and_mcp.py
|
|
|
62
63
|
tests/test_v2_connectors.py
|
|
63
64
|
tests/test_v2_content.py
|
|
64
65
|
tests/test_v2_conversations.py
|
|
66
|
+
tests/test_v2_evals.py
|
|
65
67
|
tests/test_v2_flows.py
|
|
66
68
|
tests/test_v2_knowledge.py
|
|
67
69
|
tests/test_v2_products.py
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from applied_cli.v2.catalog import get_tool_catalog
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeEvalsClient:
|
|
7
|
+
"""Records the raw request(method, path, params, body) each tool makes and
|
|
8
|
+
returns a canned response, so we assert the exact wire shape."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, response=None):
|
|
11
|
+
self.access_mode = "full_access"
|
|
12
|
+
self.shop_id = "shop-1"
|
|
13
|
+
self.calls: list[dict] = []
|
|
14
|
+
self._response = response if response is not None else {}
|
|
15
|
+
|
|
16
|
+
async def request(self, method, path, params=None, body=None, shop_id=None):
|
|
17
|
+
self.calls.append(
|
|
18
|
+
{"method": method, "path": path, "params": params, "body": body}
|
|
19
|
+
)
|
|
20
|
+
return self._response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _run(tool_name, client, **kwargs):
|
|
24
|
+
spec = get_tool_catalog()[tool_name]
|
|
25
|
+
return await spec.run(client, kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_all_review_tools_registered():
|
|
29
|
+
catalog = get_tool_catalog()
|
|
30
|
+
review = {s.name for s in catalog if s.namespace == "review"}
|
|
31
|
+
assert review == {
|
|
32
|
+
"review_run",
|
|
33
|
+
"review_status",
|
|
34
|
+
"review_get",
|
|
35
|
+
"review_set_verdict",
|
|
36
|
+
"review_clear_verdict",
|
|
37
|
+
"review_add_note",
|
|
38
|
+
"review_trust",
|
|
39
|
+
"review_lessons",
|
|
40
|
+
"review_teach",
|
|
41
|
+
"review_make_test",
|
|
42
|
+
"review_proposals_list",
|
|
43
|
+
"review_proposals_decide",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_review_run_posts_ids_and_force():
|
|
49
|
+
client = FakeEvalsClient({"graded": 2, "flagged": 1})
|
|
50
|
+
result = await _run("review_run", client, conversation_ids=["a", "b"], force=True)
|
|
51
|
+
assert result.ok
|
|
52
|
+
assert client.calls[0] == {
|
|
53
|
+
"method": "POST",
|
|
54
|
+
"path": "/v1/evals/review/",
|
|
55
|
+
"params": None,
|
|
56
|
+
"body": {"ids": ["a", "b"], "force": True},
|
|
57
|
+
}
|
|
58
|
+
assert result.data == {"graded": 2, "flagged": 1}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_review_run_rejects_empty_ids_without_calling_api():
|
|
63
|
+
client = FakeEvalsClient()
|
|
64
|
+
result = await _run("review_run", client, conversation_ids=[])
|
|
65
|
+
assert not result.ok
|
|
66
|
+
assert result.error.code == "input.invalid"
|
|
67
|
+
assert client.calls == []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_review_status_joins_ids_into_query():
|
|
72
|
+
client = FakeEvalsClient({"rows": [{"conversation_id": "a"}], "counts": {}})
|
|
73
|
+
await _run("review_status", client, conversation_ids=["a", "b", "c"])
|
|
74
|
+
assert client.calls[0]["method"] == "GET"
|
|
75
|
+
assert client.calls[0]["path"] == "/v1/evals/status/"
|
|
76
|
+
assert client.calls[0]["params"] == {"ids": "a,b,c"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_review_set_verdict_omits_unset_optionals():
|
|
81
|
+
client = FakeEvalsClient({"verdict": "fail"})
|
|
82
|
+
await _run(
|
|
83
|
+
"review_set_verdict",
|
|
84
|
+
client,
|
|
85
|
+
conversation_id="conv-1",
|
|
86
|
+
verdict="fail",
|
|
87
|
+
reason="Promised a refund it never issued.",
|
|
88
|
+
)
|
|
89
|
+
call = client.calls[0]
|
|
90
|
+
assert call["path"] == "/v1/evals/conv-1/review/"
|
|
91
|
+
assert call["body"] == {
|
|
92
|
+
"verdict": "fail",
|
|
93
|
+
"reason": "Promised a refund it never issued.",
|
|
94
|
+
}
|
|
95
|
+
assert "pinned" not in call["body"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_review_clear_verdict_deletes():
|
|
100
|
+
client = FakeEvalsClient(None)
|
|
101
|
+
result = await _run("review_clear_verdict", client, conversation_id="conv-1")
|
|
102
|
+
assert result.ok
|
|
103
|
+
assert client.calls[0]["method"] == "DELETE"
|
|
104
|
+
assert client.calls[0]["path"] == "/v1/evals/conv-1/review/"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
async def test_review_add_note_requires_text():
|
|
109
|
+
client = FakeEvalsClient()
|
|
110
|
+
result = await _run(
|
|
111
|
+
"review_add_note", client, conversation_id="conv-1", kind="correction"
|
|
112
|
+
)
|
|
113
|
+
assert not result.ok
|
|
114
|
+
assert result.error.code == "input.invalid"
|
|
115
|
+
assert client.calls == []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_review_teach_posts_rule_text():
|
|
120
|
+
client = FakeEvalsClient({"id": "note-1"})
|
|
121
|
+
await _run("review_teach", client, text="Always verify the subscription first.")
|
|
122
|
+
assert client.calls[0] == {
|
|
123
|
+
"method": "POST",
|
|
124
|
+
"path": "/v1/evals/lessons/",
|
|
125
|
+
"params": None,
|
|
126
|
+
"body": {"text": "Always verify the subscription first."},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.asyncio
|
|
131
|
+
async def test_review_make_test_hits_from_conversation():
|
|
132
|
+
client = FakeEvalsClient({"id": "prop-1", "kind": "add"})
|
|
133
|
+
await _run("review_make_test", client, conversation_id="conv-9")
|
|
134
|
+
assert client.calls[0]["path"] == (
|
|
135
|
+
"/v1/simulation-test-proposals/from-conversation/"
|
|
136
|
+
)
|
|
137
|
+
assert client.calls[0]["body"] == {"conversation_id": "conv-9"}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.asyncio
|
|
141
|
+
async def test_review_proposals_decide_accept_with_customer():
|
|
142
|
+
client = FakeEvalsClient({"status": "accepted"})
|
|
143
|
+
await _run(
|
|
144
|
+
"review_proposals_decide",
|
|
145
|
+
client,
|
|
146
|
+
proposal_id="prop-1",
|
|
147
|
+
decision="accepted",
|
|
148
|
+
customer_state_ref="cust-1",
|
|
149
|
+
)
|
|
150
|
+
call = client.calls[0]
|
|
151
|
+
assert call["method"] == "PATCH"
|
|
152
|
+
assert call["path"] == "/v1/simulation-test-proposals/prop-1/"
|
|
153
|
+
assert call["body"] == {"status": "accepted", "customer_state_ref": "cust-1"}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
async def test_unknown_field_rejected_before_handler():
|
|
158
|
+
client = FakeEvalsClient()
|
|
159
|
+
spec = get_tool_catalog()["review_trust"]
|
|
160
|
+
result = await spec.run(client, {"bogus": 1})
|
|
161
|
+
assert not result.ok
|
|
162
|
+
assert result.error.code == "input.invalid"
|
|
163
|
+
assert client.calls == []
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|