applied-cli 0.6.3__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {applied_cli-0.6.3 → applied_cli-0.6.4}/PKG-INFO +1 -1
  2. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/cli.py +15 -0
  3. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/tools.py +54 -2
  4. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/scenarios.py +16 -2
  5. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/PKG-INFO +1 -1
  6. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/SOURCES.txt +1 -0
  7. {applied_cli-0.6.3 → applied_cli-0.6.4}/pyproject.toml +1 -1
  8. applied_cli-0.6.4/tests/test_scenario_bulk_run_contact.py +116 -0
  9. {applied_cli-0.6.3 → applied_cli-0.6.4}/README.md +0 -0
  10. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/__init__.py +0 -0
  11. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/agent_scoped_flows.py +0 -0
  12. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/auth.py +0 -0
  13. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/client.py +0 -0
  14. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/conversation_lookup.py +0 -0
  15. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/conversations.py +0 -0
  16. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/credentials.py +0 -0
  17. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/flow_helpers.py +0 -0
  18. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/formatters.py +0 -0
  19. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/mcp.py +0 -0
  20. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/recovery.py +0 -0
  21. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/toolkit.py +0 -0
  22. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/__init__.py +0 -0
  23. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/agents.py +0 -0
  24. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/articles.py +0 -0
  25. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/catalog.py +0 -0
  26. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/connectors.py +0 -0
  27. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/content.py +0 -0
  28. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/conversations.py +0 -0
  29. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/domains.py +0 -0
  30. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/flows.py +0 -0
  31. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/knowledge.py +0 -0
  32. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/manifest.py +0 -0
  33. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/products.py +0 -0
  34. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/taxonomy.py +0 -0
  35. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli/v2/tickets.py +0 -0
  36. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/dependency_links.txt +0 -0
  37. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/entry_points.txt +0 -0
  38. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/requires.txt +0 -0
  39. {applied_cli-0.6.3 → applied_cli-0.6.4}/applied_cli.egg-info/top_level.txt +0 -0
  40. {applied_cli-0.6.3 → applied_cli-0.6.4}/setup.cfg +0 -0
  41. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_agent_scoped_flows.py +0 -0
  42. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_audit_tools.py +0 -0
  43. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_auth_context.py +0 -0
  44. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_benchmark_clone.py +0 -0
  45. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_benchmark_delete_guardrail.py +0 -0
  46. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_benchmark_scenario_tools.py +0 -0
  47. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_cli.py +0 -0
  48. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_cli_v2.py +0 -0
  49. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_client.py +0 -0
  50. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_client_v2.py +0 -0
  51. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_conversation_tools.py +0 -0
  52. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_flow_tools.py +0 -0
  53. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_knowledge_content_tools.py +0 -0
  54. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_recovery.py +0 -0
  55. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_scenario_bulk_cancel.py +0 -0
  56. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_toolkit_contract.py +0 -0
  57. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_agents.py +0 -0
  58. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_articles.py +0 -0
  59. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_catalog_and_mcp.py +0 -0
  60. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_connectors.py +0 -0
  61. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_content.py +0 -0
  62. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_conversations.py +0 -0
  63. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_flows.py +0 -0
  64. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_knowledge.py +0 -0
  65. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_products.py +0 -0
  66. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_scenarios.py +0 -0
  67. {applied_cli-0.6.3 → applied_cli-0.6.4}/tests/test_v2_taxonomy.py +0 -0
  68. {applied_cli-0.6.3 → applied_cli-0.6.4}/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
3
+ Version: 0.6.4
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1916,6 +1916,18 @@ def scenario_bulk_run(
1916
1916
  target_agent_id: str = typer.Option(
1917
1917
  None, "--target-agent-id", help="Optional target agent for reruns"
1918
1918
  ),
1919
+ contact_email: str = typer.Option(
1920
+ None,
1921
+ "--contact-email",
1922
+ help="Run as a contact with this email (resolves/creates it) so test "
1923
+ "conversations carry an email — fixes 'Email is not present' failures",
1924
+ ),
1925
+ contact_id: str = typer.Option(
1926
+ None, "--contact-id", help="Run scenarios as this existing contact UUID"
1927
+ ),
1928
+ anonymous: bool = typer.Option(
1929
+ False, "--anonymous", help="Run with an anonymous contact"
1930
+ ),
1919
1931
  shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1920
1932
  format: str = typer.Option(
1921
1933
  "text", "--format", "-f", help="Output format: text or json"
@@ -1929,6 +1941,9 @@ def scenario_bulk_run(
1929
1941
  scenario_ids=_parse_csv_option(scenario_ids),
1930
1942
  benchmark_id=benchmark_id,
1931
1943
  target_agent_id=target_agent_id,
1944
+ contact_email=contact_email,
1945
+ contact_id=contact_id,
1946
+ anonymous=anonymous,
1932
1947
  output_format=format,
1933
1948
  )
1934
1949
  )
@@ -6082,23 +6082,68 @@ async def scenario_run_delete(
6082
6082
  return f"Scenario run {run_id} deleted successfully."
6083
6083
 
6084
6084
 
6085
+ async def _resolve_contact_override(
6086
+ client: AppliedClient,
6087
+ *,
6088
+ contact_override: dict | None,
6089
+ contact_id: str | None,
6090
+ contact_email: str | None,
6091
+ anonymous: bool,
6092
+ ) -> dict | None:
6093
+ """Build the contact_override payload from the convenience arguments.
6094
+
6095
+ An explicit contact_override dict wins. Otherwise anonymous > contact_id >
6096
+ contact_email (which resolves/creates a contact so the test conversation
6097
+ carries a real email).
6098
+ """
6099
+ if contact_override:
6100
+ return contact_override
6101
+ if anonymous:
6102
+ return {"mode": "anonymous"}
6103
+ if contact_id:
6104
+ return {"mode": "contact", "contact_id": contact_id}
6105
+ if contact_email:
6106
+ contact = await client.get_or_create_contact(email=contact_email)
6107
+ resolved_id = contact.get("id")
6108
+ if not resolved_id:
6109
+ raise AppliedAPIError(
6110
+ f"Could not resolve a contact for email {contact_email}.",
6111
+ status_code=404,
6112
+ )
6113
+ return {"mode": "contact", "contact_id": str(resolved_id)}
6114
+ return None
6115
+
6116
+
6085
6117
  async def scenario_bulk_run(
6086
6118
  client: AppliedClient,
6087
6119
  scenario_ids: list[str] | None = None,
6088
6120
  benchmark_id: str | None = None,
6089
6121
  target_agent_id: str | None = None,
6090
6122
  contact_override: dict | None = None,
6123
+ contact_id: str | None = None,
6124
+ contact_email: str | None = None,
6125
+ anonymous: bool = False,
6091
6126
  output_format: str = "text",
6092
6127
  ) -> str:
6093
6128
  """
6094
6129
  Run multiple scenarios at once.
6095
6130
 
6131
+ By default a scenario run reuses the input conversation's contact, which on
6132
+ test/benchmark conversations often has no email — causing agents to respond
6133
+ "Email is not present in the conversation." Pass contact_email or contact_id
6134
+ to run the scenarios as a contact that has an email, so the test conversation
6135
+ carries it.
6136
+
6096
6137
  Args:
6097
6138
  client: Authenticated AppliedClient
6098
6139
  scenario_ids: List of scenario UUIDs to run
6099
6140
  benchmark_id: Run all scenarios in this benchmark
6100
6141
  target_agent_id: Optional agent to run against (for A/B testing)
6101
- contact_override: Optional contact override, e.g. {"mode": "contact", "contact_id": "<uuid>"}
6142
+ contact_override: Raw override, e.g. {"mode": "contact", "contact_id": "<uuid>"}
6143
+ (takes precedence over the convenience args below)
6144
+ contact_id: Run scenarios as this existing contact (gives test convos its email)
6145
+ contact_email: Resolve/create a contact with this email and run as them
6146
+ anonymous: Run with an anonymous contact (mode='anonymous')
6102
6147
 
6103
6148
  Returns:
6104
6149
  Summary of runs created
@@ -6123,10 +6168,17 @@ async def scenario_bulk_run(
6123
6168
  )
6124
6169
 
6125
6170
  try:
6171
+ effective_override = await _resolve_contact_override(
6172
+ client,
6173
+ contact_override=contact_override,
6174
+ contact_id=contact_id,
6175
+ contact_email=contact_email,
6176
+ anonymous=anonymous,
6177
+ )
6126
6178
  result = await client.bulk_run_scenarios(
6127
6179
  scenario_ids=resolved_scenario_ids,
6128
6180
  target_agent_id=target_agent_id,
6129
- contact_override=contact_override,
6181
+ contact_override=effective_override,
6130
6182
  )
6131
6183
  except AppliedAPIError as e:
6132
6184
  return _format_error(e)
@@ -27,6 +27,9 @@ class ScenariosBulkRunInput(StrictInput):
27
27
  benchmark_id: str | None = None
28
28
  target_agent_id: str | None = None
29
29
  contact_override: dict[str, Any] | None = None
30
+ contact_id: str | None = None
31
+ contact_email: str | None = None
32
+ anonymous: bool = False
30
33
 
31
34
 
32
35
  class ScenariosBulkCancelInput(StrictInput):
@@ -796,10 +799,19 @@ async def scenarios_bulk_run_handler(
796
799
  )
797
800
 
798
801
  try:
802
+ from applied_cli.tools import _resolve_contact_override
803
+
804
+ effective_override = await _resolve_contact_override(
805
+ client,
806
+ contact_override=params.contact_override,
807
+ contact_id=params.contact_id,
808
+ contact_email=params.contact_email,
809
+ anonymous=params.anonymous,
810
+ )
799
811
  result = await client.bulk_run_scenarios(
800
812
  scenario_ids=resolved_scenario_ids,
801
813
  target_agent_id=params.target_agent_id,
802
- contact_override=params.contact_override,
814
+ contact_override=effective_override,
803
815
  )
804
816
  except AppliedAPIError as exc:
805
817
  return _api_error_result(exc)
@@ -1050,7 +1062,9 @@ def scenario_specs() -> list[ToolSpec]:
1050
1062
  namespace="scenarios",
1051
1063
  description=(
1052
1064
  "Run selected scenarios or every scenario in a benchmark and "
1053
- "return the queued job metadata."
1065
+ "return the queued job metadata. Pass contact_email or contact_id "
1066
+ "to run as a contact with an email (fixes 'Email is not present' "
1067
+ "failures on test conversations)."
1054
1068
  ),
1055
1069
  input_model=ScenariosBulkRunInput,
1056
1070
  output_model=None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -50,6 +50,7 @@ tests/test_flow_tools.py
50
50
  tests/test_knowledge_content_tools.py
51
51
  tests/test_recovery.py
52
52
  tests/test_scenario_bulk_cancel.py
53
+ tests/test_scenario_bulk_run_contact.py
53
54
  tests/test_toolkit_contract.py
54
55
  tests/test_v2_agents.py
55
56
  tests/test_v2_articles.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.6.3"
3
+ version = "0.6.4"
4
4
  description = "CLI and shared client library for Applied Labs AI support agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,116 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from applied_cli import tools
6
+
7
+
8
+ class FakeRunClient:
9
+ def __init__(self):
10
+ self.bulk_kwargs = None
11
+ self.get_or_create_calls = []
12
+
13
+ async def list_scenarios(self, benchmark_id=None, limit=500, **kwargs):
14
+ return [{"id": "s1"}, {"id": "s2"}]
15
+
16
+ async def get_or_create_contact(self, email=None, name=None, phone=None):
17
+ self.get_or_create_calls.append(email)
18
+ return {"id": "contact-123", "email": email}
19
+
20
+ async def bulk_run_scenarios(
21
+ self, scenario_ids=None, target_agent_id=None, contact_override=None
22
+ ):
23
+ self.bulk_kwargs = {
24
+ "scenario_ids": scenario_ids,
25
+ "target_agent_id": target_agent_id,
26
+ "contact_override": contact_override,
27
+ }
28
+ return {
29
+ "job_id": "job-1",
30
+ "total": len(scenario_ids or []),
31
+ "queued": len(scenario_ids or []),
32
+ "scenario_run_ids": ["r1", "r2"],
33
+ "contact_override": contact_override,
34
+ }
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_no_contact_args_sends_no_override():
39
+ client = FakeRunClient()
40
+ await tools.scenario_bulk_run(client, benchmark_id="bench-1", output_format="json")
41
+ assert client.bulk_kwargs["contact_override"] is None
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_contact_id_builds_override():
46
+ client = FakeRunClient()
47
+ await tools.scenario_bulk_run(
48
+ client, benchmark_id="bench-1", contact_id="c-9", output_format="json"
49
+ )
50
+ assert client.bulk_kwargs["contact_override"] == {
51
+ "mode": "contact",
52
+ "contact_id": "c-9",
53
+ }
54
+
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_contact_email_resolves_then_overrides():
58
+ client = FakeRunClient()
59
+ result = await tools.scenario_bulk_run(
60
+ client,
61
+ benchmark_id="bench-1",
62
+ contact_email="casey@example.com",
63
+ output_format="json",
64
+ )
65
+ assert client.get_or_create_calls == ["casey@example.com"]
66
+ assert client.bulk_kwargs["contact_override"] == {
67
+ "mode": "contact",
68
+ "contact_id": "contact-123",
69
+ }
70
+ assert json.loads(result)["job_id"] == "job-1"
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_anonymous_mode():
75
+ client = FakeRunClient()
76
+ await tools.scenario_bulk_run(
77
+ client, benchmark_id="bench-1", anonymous=True, output_format="json"
78
+ )
79
+ assert client.bulk_kwargs["contact_override"] == {"mode": "anonymous"}
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_explicit_override_wins_over_convenience_args():
84
+ client = FakeRunClient()
85
+ await tools.scenario_bulk_run(
86
+ client,
87
+ benchmark_id="bench-1",
88
+ contact_override={"mode": "contact", "contact_id": "raw"},
89
+ contact_email="ignored@example.com",
90
+ output_format="json",
91
+ )
92
+ # The raw override is used; email resolution is skipped.
93
+ assert client.get_or_create_calls == []
94
+ assert client.bulk_kwargs["contact_override"] == {
95
+ "mode": "contact",
96
+ "contact_id": "raw",
97
+ }
98
+
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_v2_handler_threads_contact_email():
102
+ from applied_cli.v2.scenarios import (
103
+ ScenariosBulkRunInput,
104
+ scenarios_bulk_run_handler,
105
+ )
106
+
107
+ client = FakeRunClient()
108
+ result = await scenarios_bulk_run_handler(
109
+ client,
110
+ ScenariosBulkRunInput(benchmark_id="bench-1", contact_email="x@example.com"),
111
+ )
112
+ assert client.bulk_kwargs["contact_override"] == {
113
+ "mode": "contact",
114
+ "contact_id": "contact-123",
115
+ }
116
+ assert result.data["job_id"] == "job-1"
File without changes
File without changes