applied-cli 0.6.11__tar.gz → 0.6.12__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.11 → applied_cli-0.6.12}/PKG-INFO +1 -1
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/__init__.py +1 -1
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/client.py +20 -1
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/tools.py +3 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/evals.py +53 -12
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.11 → applied_cli-0.6.12}/pyproject.toml +1 -1
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_client.py +29 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_evals.py +24 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/README.md +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/cli.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/catalog.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/domains.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/scenarios.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/setup.cfg +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_benchmark_list_with_results.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_benchmark_results.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_cli.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_scenario_bulk_cancel.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_scenario_bulk_run_contact.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_scenario_bulk_run_wait.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_scenario_create_bulk.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.11 → applied_cli-0.6.12}/tests/test_v2_tickets.py +0 -0
|
@@ -210,6 +210,21 @@ def _filter_has_explicit_lookup(field: str) -> bool:
|
|
|
210
210
|
return field.endswith(_EXPLICIT_FILTER_SUFFIXES)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
# Filters the server declares ONLY in scalar form (method filters, e.g. the
|
|
214
|
+
# evals review-state filters on ConversationFilterset). Rewriting these to
|
|
215
|
+
# `field__in` silently drops them — django-filter ignores unknown params —
|
|
216
|
+
# so they must pass through verbatim.
|
|
217
|
+
_SCALAR_ONLY_FILTER_FIELDS = frozenset(
|
|
218
|
+
{
|
|
219
|
+
"verdict",
|
|
220
|
+
"reviewed",
|
|
221
|
+
"needs_attention",
|
|
222
|
+
"exclude_audited",
|
|
223
|
+
"only_unaudited",
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
213
228
|
def _serialize_filter_value(value: Any) -> str:
|
|
214
229
|
if isinstance(value, (dict, list)):
|
|
215
230
|
return json.dumps(value, separators=(",", ":"), ensure_ascii=True)
|
|
@@ -260,7 +275,11 @@ def _apply_query_filter(params: dict[str, Any], field: str, cond: Any) -> None:
|
|
|
260
275
|
)
|
|
261
276
|
return
|
|
262
277
|
|
|
263
|
-
key =
|
|
278
|
+
key = (
|
|
279
|
+
field
|
|
280
|
+
if explicit_lookup or field in _SCALAR_ONLY_FILTER_FIELDS
|
|
281
|
+
else f"{field}__in"
|
|
282
|
+
)
|
|
264
283
|
params[key] = _serialize_filter_value(cond)
|
|
265
284
|
|
|
266
285
|
|
|
@@ -1186,6 +1186,9 @@ async def conversation_query(
|
|
|
1186
1186
|
{"sublabel_id": "<intent-uuid>"} — filter by intent
|
|
1187
1187
|
{"flags__in": "<flag-uuid>"} — has specific flag
|
|
1188
1188
|
{"resolution": {"nin": ["escalated"]}} — exclude escalated
|
|
1189
|
+
{"verdict": "ai_flagged"} — AI Reviewer flagged (also: pass, fail,
|
|
1190
|
+
disagree, agree, none)
|
|
1191
|
+
{"reviewed": False} — no human review verdict yet
|
|
1189
1192
|
search: Full-text search across conversation content
|
|
1190
1193
|
fields: Fields to return, e.g. ["id", "title", "contact.email", "score", "comment"]
|
|
1191
1194
|
When provided, fields override view-specific projections.
|
|
@@ -7,9 +7,10 @@ step. These tools expose that loop to an agent — grade conversations, record
|
|
|
7
7
|
verdicts, teach the Reviewer, and promote a bad conversation into the simulation
|
|
8
8
|
suite as a proposed test (with an auto-generated test customer).
|
|
9
9
|
|
|
10
|
-
All endpoints are shop-scoped and merchant-authenticated.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
All endpoints are shop-scoped and merchant-authenticated. Enumerate the review
|
|
11
|
+
queue directly with `review_status(segment="inbox")` (or "disagree"), or discover
|
|
12
|
+
conversations with `conversations_search` / `conversations_find` and feed their
|
|
13
|
+
ids here.
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from __future__ import annotations
|
|
@@ -43,8 +44,24 @@ class ReviewRunInput(StrictInput):
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class ReviewStatusInput(StrictInput):
|
|
46
|
-
conversation_ids: list[str] = Field(
|
|
47
|
-
|
|
47
|
+
conversation_ids: list[str] | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description=(
|
|
50
|
+
"Conversation ids to fetch verdict pairs + narration for. Omit and "
|
|
51
|
+
"pass segment to enumerate a queue segment instead."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
segment: Literal["inbox", "disagree"] | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description=(
|
|
57
|
+
"Enumerate a queue segment server-side, newest judgment first: "
|
|
58
|
+
"'inbox' (AI-flagged, no human verdict yet) or 'disagree' (human "
|
|
59
|
+
"and AI verdicts differ). Ignored when conversation_ids is set."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
limit: int | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="Max segment rows (server default 50, max 200).",
|
|
48
65
|
)
|
|
49
66
|
|
|
50
67
|
|
|
@@ -179,20 +196,42 @@ async def review_run_handler(
|
|
|
179
196
|
async def review_status_handler(
|
|
180
197
|
client: AppliedClient, params: ReviewStatusInput
|
|
181
198
|
) -> ToolResult[Any]:
|
|
182
|
-
ids = params.conversation_ids[:200]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
ids = (params.conversation_ids or [])[:200]
|
|
200
|
+
if not ids and not params.segment:
|
|
201
|
+
return ToolResult(
|
|
202
|
+
ok=False,
|
|
203
|
+
error=ToolError(
|
|
204
|
+
code="input.invalid",
|
|
205
|
+
message=(
|
|
206
|
+
"Provide conversation_ids, or segment='inbox'/'disagree' "
|
|
207
|
+
"to enumerate a queue segment."
|
|
208
|
+
),
|
|
209
|
+
retryable=False,
|
|
210
|
+
),
|
|
186
211
|
)
|
|
212
|
+
query: dict[str, Any] = {}
|
|
213
|
+
if ids:
|
|
214
|
+
query["ids"] = ",".join(ids)
|
|
215
|
+
else:
|
|
216
|
+
query["segment"] = params.segment
|
|
217
|
+
if params.limit is not None:
|
|
218
|
+
query["limit"] = params.limit
|
|
219
|
+
try:
|
|
220
|
+
data = await client.request("GET", "/v1/evals/status/", params=query)
|
|
187
221
|
except AppliedAPIError as exc:
|
|
188
222
|
return _api_error_result(exc)
|
|
189
223
|
rows = data.get("rows", []) if isinstance(data, dict) else []
|
|
224
|
+
summary = (
|
|
225
|
+
f"{len(rows)} row(s) in the '{params.segment}' segment."
|
|
226
|
+
if not ids
|
|
227
|
+
else f"Verdict pairs for {len(rows)} of {len(ids)} conversation(s)."
|
|
228
|
+
)
|
|
190
229
|
return ToolResult(
|
|
191
230
|
data=data,
|
|
192
|
-
summary=
|
|
231
|
+
summary=summary,
|
|
193
232
|
next_actions=[
|
|
194
233
|
"Use review_get for the full review pair + notes on one conversation.",
|
|
195
|
-
"Use
|
|
234
|
+
"Use review_set_verdict to confirm or overrule a verdict.",
|
|
196
235
|
],
|
|
197
236
|
)
|
|
198
237
|
|
|
@@ -424,7 +463,9 @@ def evals_specs() -> list[ToolSpec]:
|
|
|
424
463
|
namespace="review",
|
|
425
464
|
description=(
|
|
426
465
|
"Fetch the verdict pair (AI + human) and the Reviewer's one-line "
|
|
427
|
-
"reason for a set of conversations, plus shop-wide review counts."
|
|
466
|
+
"reason for a set of conversations, plus shop-wide review counts. "
|
|
467
|
+
"Pass segment='inbox' (AI-flagged, unreviewed) or 'disagree' "
|
|
468
|
+
"instead of ids to enumerate the review queue directly."
|
|
428
469
|
),
|
|
429
470
|
input_model=ReviewStatusInput,
|
|
430
471
|
output_model=None,
|
|
@@ -136,6 +136,35 @@ async def test_query_conversations_serializes_metric_event_filter_payload(monkey
|
|
|
136
136
|
)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_query_conversations_passes_scalar_only_filters_verbatim(monkeypatch):
|
|
141
|
+
"""Method filters like the evals review-state filters exist server-side only
|
|
142
|
+
in scalar form — rewriting them to `field__in` silently drops them."""
|
|
143
|
+
client = AppliedClient(token="test-token")
|
|
144
|
+
seen = {}
|
|
145
|
+
|
|
146
|
+
async def fake_request(method, path, params=None, body=None, shop_id=None):
|
|
147
|
+
seen["params"] = params
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
monkeypatch.setattr(client, "_request", fake_request)
|
|
151
|
+
|
|
152
|
+
await client.query_conversations(
|
|
153
|
+
filters={
|
|
154
|
+
"verdict": "ai_flagged",
|
|
155
|
+
"reviewed": False,
|
|
156
|
+
"resolution": "escalated",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert seen["params"]["verdict"] == "ai_flagged"
|
|
161
|
+
assert seen["params"]["reviewed"] == "false"
|
|
162
|
+
assert "verdict__in" not in seen["params"]
|
|
163
|
+
assert "reviewed__in" not in seen["params"]
|
|
164
|
+
# Ordinary scalars keep the historical `__in` rewrite.
|
|
165
|
+
assert seen["params"]["resolution__in"] == "escalated"
|
|
166
|
+
|
|
167
|
+
|
|
139
168
|
@pytest.mark.asyncio
|
|
140
169
|
async def test_analytics_report_posts_payload(monkeypatch):
|
|
141
170
|
client = AppliedClient(token="test-token")
|
|
@@ -76,6 +76,30 @@ async def test_review_status_joins_ids_into_query():
|
|
|
76
76
|
assert client.calls[0]["params"] == {"ids": "a,b,c"}
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_review_status_enumerates_segment_without_ids():
|
|
81
|
+
client = FakeEvalsClient({"rows": [{"conversation_id": "a"}], "counts": {}})
|
|
82
|
+
result = await _run("review_status", client, segment="inbox", limit=25)
|
|
83
|
+
assert result.ok
|
|
84
|
+
assert client.calls[0]["params"] == {"segment": "inbox", "limit": 25}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_review_status_ids_win_over_segment():
|
|
89
|
+
client = FakeEvalsClient({"rows": [], "counts": {}})
|
|
90
|
+
await _run("review_status", client, conversation_ids=["a"], segment="inbox")
|
|
91
|
+
assert client.calls[0]["params"] == {"ids": "a"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_review_status_requires_ids_or_segment():
|
|
96
|
+
client = FakeEvalsClient()
|
|
97
|
+
result = await _run("review_status", client)
|
|
98
|
+
assert not result.ok
|
|
99
|
+
assert result.error.code == "input.invalid"
|
|
100
|
+
assert client.calls == []
|
|
101
|
+
|
|
102
|
+
|
|
79
103
|
@pytest.mark.asyncio
|
|
80
104
|
async def test_review_set_verdict_omits_unset_optionals():
|
|
81
105
|
client = FakeEvalsClient({"verdict": "fail"})
|
|
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
|