rubricloop 0.1.2__tar.gz → 0.1.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 (49) hide show
  1. {rubricloop-0.1.2 → rubricloop-0.1.4}/PKG-INFO +6 -5
  2. {rubricloop-0.1.2 → rubricloop-0.1.4}/README.md +5 -4
  3. rubricloop-0.1.4/examples/verify_reply_registry/README.md +65 -0
  4. rubricloop-0.1.4/examples/verify_reply_registry/build_package.py +122 -0
  5. rubricloop-0.1.4/examples/verify_reply_registry/run-input.json +6 -0
  6. {rubricloop-0.1.2 → rubricloop-0.1.4}/pyproject.toml +1 -1
  7. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/__init__.py +1 -1
  8. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/cli.py +19 -1
  9. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/__init__.py +2 -1
  10. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/catalog.py +19 -0
  11. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/support.py +35 -0
  12. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/registry.py +1 -1
  13. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_registry_cli.py +55 -1
  14. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_wave_one_packs.py +27 -0
  15. {rubricloop-0.1.2 → rubricloop-0.1.4}/.gitignore +0 -0
  16. {rubricloop-0.1.2 → rubricloop-0.1.4}/LICENSE +0 -0
  17. {rubricloop-0.1.2 → rubricloop-0.1.4}/RLPACK.md +0 -0
  18. {rubricloop-0.1.2 → rubricloop-0.1.4}/VETTING.md +0 -0
  19. {rubricloop-0.1.2 → rubricloop-0.1.4}/examples/sql_agent.py +0 -0
  20. {rubricloop-0.1.2 → rubricloop-0.1.4}/examples/verify_reply.py +0 -0
  21. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/__init__.py +0 -0
  22. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/common.py +0 -0
  23. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/sql.py +0 -0
  24. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/structured.py +0 -0
  25. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/examples/__init__.py +0 -0
  26. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/examples/verify_reply.py +0 -0
  27. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/loop.py +0 -0
  28. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/base.py +0 -0
  29. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/commerce.py +0 -0
  30. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/finance.py +0 -0
  31. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/logistics.py +0 -0
  32. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/official.py +0 -0
  33. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/refund.py +0 -0
  34. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/security.py +0 -0
  35. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/sql.py +0 -0
  36. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/vetting.py +0 -0
  37. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/py.typed +0 -0
  38. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/rlpack.py +0 -0
  39. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/rubric.py +0 -0
  40. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/sandboxes/__init__.py +0 -0
  41. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/sandboxes/sqlite.py +0 -0
  42. {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/types.py +0 -0
  43. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_common_checks.py +0 -0
  44. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_invariant.py +0 -0
  45. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_loop.py +0 -0
  46. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_refund_pack.py +0 -0
  47. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_rlpack.py +0 -0
  48. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_sql_pack.py +0 -0
  49. {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_vetting_scenarios.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rubricloop
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Bounded, auditable verification loops for AI agent output.
5
5
  Project-URL: Homepage, https://rubricloop.com
6
6
  Project-URL: Documentation, https://rubricloop.com/docs
@@ -179,10 +179,11 @@ When the package is ready to share, create an API key in the
179
179
  hosted registry:
180
180
 
181
181
  ```bash
182
- rubricloop --server https://api.rubricloop.dev login --api-key "$RUBRICLOOP_API_KEY"
183
- rubricloop --server https://api.rubricloop.dev push dist/acme-check.rlpack --tag latest
184
- rubricloop --server https://api.rubricloop.dev vet acme/check@latest
185
- rubricloop --server https://api.rubricloop.dev pull acme/check@latest
182
+ rubricloop login --api-key "$RUBRICLOOP_API_KEY"
183
+ rubricloop create acme/check --visibility private
184
+ rubricloop push dist/acme-check.rlpack --tag latest
185
+ rubricloop vet acme/check@latest
186
+ rubricloop pull acme/check@latest
186
187
  ```
187
188
 
188
189
  Public community packages are free. Private repositories are available only to
@@ -150,10 +150,11 @@ When the package is ready to share, create an API key in the
150
150
  hosted registry:
151
151
 
152
152
  ```bash
153
- rubricloop --server https://api.rubricloop.dev login --api-key "$RUBRICLOOP_API_KEY"
154
- rubricloop --server https://api.rubricloop.dev push dist/acme-check.rlpack --tag latest
155
- rubricloop --server https://api.rubricloop.dev vet acme/check@latest
156
- rubricloop --server https://api.rubricloop.dev pull acme/check@latest
153
+ rubricloop login --api-key "$RUBRICLOOP_API_KEY"
154
+ rubricloop create acme/check --visibility private
155
+ rubricloop push dist/acme-check.rlpack --tag latest
156
+ rubricloop vet acme/check@latest
157
+ rubricloop pull acme/check@latest
157
158
  ```
158
159
 
159
160
  Public community packages are free. Private repositories are available only to
@@ -0,0 +1,65 @@
1
+ # Reply Quick Start registry lifecycle
2
+
3
+ This package uses the same three deterministic rules as
4
+ `sdk/examples/verify_reply.py`: 40 to 70 words, the word `timeline`, and no
5
+ `guaranteed refund` promise.
6
+
7
+ You can run the same checks through Hosted Verification before publishing and
8
+ vetting your own package:
9
+
10
+ ```bash
11
+ curl https://api.rubricloop.dev/v1/verify \
12
+ -H "Content-Type: application/json" \
13
+ -H "X-API-Key: $RUBRICLOOP_API_KEY" \
14
+ -d '{
15
+ "pack_id": "support/reply-v1",
16
+ "output": {
17
+ "reply": "We apologize for the delay with your order. Our team is currently investigating the issue and working to resolve it as quickly as possible. You can expect an update on the timeline for your order shortly. Thank you for your patience."
18
+ },
19
+ "context": {}
20
+ }'
21
+ ```
22
+
23
+ To publish the package itself:
24
+
25
+ ```bash
26
+ pip install rubricloop openai
27
+ export OPENAI_API_KEY="sk-..."
28
+ python sdk/examples/verify_reply.py
29
+
30
+ python sdk/examples/verify_reply_registry/build_package.py \
31
+ --namespace YOUR_NAMESPACE
32
+
33
+ rubricloop login
34
+ rubricloop create \
35
+ YOUR_NAMESPACE/support-reply \
36
+ --visibility private \
37
+ --description "Checks support replies before they are sent."
38
+ rubricloop validate dist/support-reply.rlpack
39
+ rubricloop test dist/support-reply.rlpack
40
+ rubricloop push \
41
+ dist/support-reply.rlpack \
42
+ --tag latest
43
+ rubricloop vet \
44
+ YOUR_NAMESPACE/support-reply@latest
45
+ ```
46
+
47
+ Pull the immutable bundle digest returned by `push`, not the mutable tag:
48
+
49
+ ```bash
50
+ rubricloop pull \
51
+ YOUR_NAMESPACE/support-reply@sha256:BUNDLE_DIGEST \
52
+ --output dist/pulled-support-reply.rlpack
53
+ cmp dist/support-reply.rlpack dist/pulled-support-reply.rlpack
54
+ ```
55
+
56
+ Once vetting passes and hosted execution is approved for that exact digest:
57
+
58
+ ```bash
59
+ rubricloop run \
60
+ YOUR_NAMESPACE/support-reply@sha256:BUNDLE_DIGEST \
61
+ --input sdk/examples/verify_reply_registry/run-input.json
62
+ ```
63
+
64
+ No credential belongs in this folder. `login` reads the key without echoing it
65
+ and stores it in the operating system keychain.
@@ -0,0 +1,122 @@
1
+ """Build the registry package used by the OpenAI reply Quick Start."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from rubricloop import build_rlpack, create_manifest, read_rlpack
10
+
11
+ VALID_REPLY = (
12
+ "We apologize for the delay with your order. Our team is currently investigating "
13
+ "the issue and working to resolve it as quickly as possible. You can expect an "
14
+ "update on the timeline for your order shortly. Thank you for your patience."
15
+ )
16
+
17
+
18
+ def manifest(namespace: str, version: str) -> dict:
19
+ golden = [
20
+ {
21
+ "id": f"valid-reply-{index:02d}",
22
+ "input": {"reply": VALID_REPLY},
23
+ "context": {},
24
+ "expected": {"decision": "allow"},
25
+ }
26
+ for index in range(1, 21)
27
+ ]
28
+ return create_manifest(
29
+ namespace=namespace,
30
+ name="support-reply",
31
+ version=version,
32
+ title="Support reply",
33
+ description="Checks reply length, timeline wording, and refund promises.",
34
+ license="MIT",
35
+ categories=("Support", "Safety"),
36
+ rules=(
37
+ {
38
+ "id": "reply.length",
39
+ "kind": "text.word_count",
40
+ "severity": "error",
41
+ "description": "Reply must contain 40 to 70 words.",
42
+ "config": {"field": "reply", "minimum": 40, "maximum": 70},
43
+ },
44
+ {
45
+ "id": "reply.timeline",
46
+ "kind": "text.required_terms",
47
+ "severity": "error",
48
+ "description": "Reply must explain the timeline.",
49
+ "config": {"field": "reply", "terms": ["timeline"]},
50
+ },
51
+ {
52
+ "id": "reply.promise",
53
+ "kind": "text.forbidden_terms",
54
+ "severity": "error",
55
+ "description": "Reply must not promise a guaranteed refund.",
56
+ "config": {"field": "reply", "terms": ["guaranteed refund"]},
57
+ },
58
+ ),
59
+ tests={
60
+ "golden": golden,
61
+ "mutation": [
62
+ {
63
+ "id": "forbidden-refund-promise",
64
+ "input": {"reply": "We offer a guaranteed refund."},
65
+ "context": {},
66
+ "expected": {"decision": "deny"},
67
+ }
68
+ ],
69
+ "fail_closed": [
70
+ {
71
+ "id": "missing-reply",
72
+ "input": {},
73
+ "context": {},
74
+ "expected": {"decision": "deny"},
75
+ }
76
+ ],
77
+ },
78
+ examples=(
79
+ {
80
+ "name": "delayed-order",
81
+ "input": {"reply": VALID_REPLY},
82
+ "context": {},
83
+ },
84
+ ),
85
+ provenance={
86
+ "builder": "rubricloop-sdk",
87
+ "source_repository": "https://rubricloop.com/docs",
88
+ "source_revision": "verify-reply-quick-start",
89
+ "built_at": "",
90
+ },
91
+ )
92
+
93
+
94
+ def main() -> None:
95
+ parser = argparse.ArgumentParser()
96
+ parser.add_argument("--namespace", required=True)
97
+ parser.add_argument("--version", default="1.0.0")
98
+ parser.add_argument("--output", type=Path, default=Path("dist/support-reply.rlpack"))
99
+ args = parser.parse_args()
100
+
101
+ bundle = build_rlpack(
102
+ manifest(args.namespace, args.version),
103
+ {
104
+ "payload/quick-start.json": json.dumps(
105
+ {
106
+ "example": "sdk/examples/verify_reply.py",
107
+ "model": "gpt-4o-mini",
108
+ },
109
+ sort_keys=True,
110
+ ).encode()
111
+ },
112
+ )
113
+ package = read_rlpack(bundle)
114
+ args.output.parent.mkdir(parents=True, exist_ok=True)
115
+ args.output.write_bytes(bundle)
116
+ print(args.output)
117
+ print(package.manifest_digest)
118
+ print(package.archive_digest)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
@@ -0,0 +1,6 @@
1
+ {
2
+ "output": {
3
+ "reply": "We apologize for the delay with your order. Our team is currently investigating the issue and working to resolve it as quickly as possible. You can expect an update on the timeline for your order shortly. Thank you for your patience."
4
+ },
5
+ "context": {}
6
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rubricloop"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Bounded, auditable verification loops for AI agent output."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -56,4 +56,4 @@ __all__ = [
56
56
  "verify",
57
57
  ]
58
58
 
59
- __version__ = "0.1.2"
59
+ __version__ = "0.1.4"
@@ -23,12 +23,16 @@ from .rlpack import RlpackError, read_rlpack
23
23
 
24
24
  def parser() -> argparse.ArgumentParser:
25
25
  root = argparse.ArgumentParser(prog="rubricloop")
26
- root.add_argument("--server", default=DEFAULT_SERVER)
26
+ root.add_argument("--server", default=DEFAULT_SERVER, help=argparse.SUPPRESS)
27
27
  commands = root.add_subparsers(dest="command", required=True)
28
28
  login = commands.add_parser("login")
29
29
  login.add_argument("--api-key")
30
30
  init = commands.add_parser("init")
31
31
  init.add_argument("directory", nargs="?", default=".")
32
+ create = commands.add_parser("create")
33
+ create.add_argument("repository")
34
+ create.add_argument("--description", default="")
35
+ create.add_argument("--visibility", choices=("public", "private"), default="private")
32
36
  for command in ("validate", "test"):
33
37
  item = commands.add_parser(command)
34
38
  item.add_argument("package")
@@ -89,6 +93,20 @@ def execute(args: argparse.Namespace) -> dict | str:
89
93
  + "\n"
90
94
  )
91
95
  return str(target)
96
+ if args.command == "create":
97
+ namespace, slash, name = args.repository.partition("/")
98
+ if not slash or not namespace or not name or "/" in name:
99
+ raise RegistryClientError("repository must be namespace/name")
100
+ return client.json(
101
+ "POST",
102
+ "/v1/registry/repositories",
103
+ payload={
104
+ "namespace": namespace,
105
+ "name": name,
106
+ "description": args.description,
107
+ "visibility": args.visibility,
108
+ },
109
+ )
92
110
  if args.command in {"validate", "test"}:
93
111
  package = _package(args.package)
94
112
  if args.command == "validate":
@@ -16,7 +16,7 @@ from .refund import (
16
16
  )
17
17
  from .security import CVE_SCHEMA_VERSION, check_cve_record
18
18
  from .sql import sql_safe
19
- from .support import check_support_agent
19
+ from .support import check_support_agent, check_support_reply
20
20
 
21
21
  __all__ = [
22
22
  "CVE_SCHEMA_VERSION",
@@ -37,6 +37,7 @@ __all__ = [
37
37
  "check_refund",
38
38
  "check_shipment_docs",
39
39
  "check_support_agent",
40
+ "check_support_reply",
40
41
  "official_manifest",
41
42
  "official_packages",
42
43
  "refund_policy_pack",
@@ -32,6 +32,13 @@ SUPPORT_CONTEXT = {
32
32
  },
33
33
  "now": "2026-09-23T09:00:00Z",
34
34
  }
35
+ SUPPORT_REPLY_OUTPUT = {
36
+ "reply": (
37
+ "We apologize for the delay with your order. Our team is currently investigating "
38
+ "the issue and working to resolve it as quickly as possible. You can expect an "
39
+ "update on the timeline for your order shortly. Thank you for your patience."
40
+ )
41
+ }
35
42
 
36
43
  COMMERCE_OUTPUT = {
37
44
  "quote_id": "Q-100",
@@ -118,6 +125,18 @@ SQL_OUTPUT = {"query": "SELECT name FROM users WHERE country = 'DE'"}
118
125
  SQL_CONTEXT = {"scenario": "users-by-country"}
119
126
 
120
127
  PACKS = (
128
+ PackMetadata(
129
+ id="support/reply-v1",
130
+ version="1",
131
+ name="Support reply",
132
+ description="Checks reply length, timeline wording, and unsupported refund promises.",
133
+ category="Support",
134
+ tier="oss",
135
+ status="preview",
136
+ execution="gate_only",
137
+ input_example=SUPPORT_REPLY_OUTPUT,
138
+ context_example={},
139
+ ),
121
140
  PackMetadata(
122
141
  id="support/agent-v1",
123
142
  version="1",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  from datetime import datetime
6
7
  from typing import Any
7
8
 
@@ -9,6 +10,40 @@ from ..checks.structured import text_contains_all
9
10
  from .base import PackCheck, PackResult
10
11
  from .refund import RefundOrder, RefundPolicy, check_refund
11
12
 
13
+ _WORD = re.compile(r"\b[\w'-]+\b", re.UNICODE)
14
+
15
+
16
+ def check_support_reply(output: dict[str, Any], context: dict[str, Any]) -> PackResult:
17
+ """Check the same reply rules used by the public Quick Start."""
18
+ del context
19
+ checks = PackCheck()
20
+ reply = output.get("reply")
21
+ text = reply if isinstance(reply, str) else ""
22
+ word_count = len(_WORD.findall(text))
23
+
24
+ checks.require(
25
+ 40 <= word_count <= 70,
26
+ rule_id="reply.length",
27
+ name="reply length",
28
+ evidence=f"reply contains {word_count} words; expected 40 to 70",
29
+ hint="write a reply containing 40 to 70 words",
30
+ )
31
+ checks.require(
32
+ "timeline" in text.casefold(),
33
+ rule_id="reply.timeline",
34
+ name="timeline wording",
35
+ evidence="reply does not contain the word timeline",
36
+ hint="explain the timeline in the reply",
37
+ )
38
+ checks.require(
39
+ "guaranteed refund" not in text.casefold(),
40
+ rule_id="reply.promise",
41
+ name="refund promise",
42
+ evidence="reply contains the forbidden phrase guaranteed refund",
43
+ hint="remove the unsupported refund promise",
44
+ )
45
+ return checks.result(output, metrics={"word_count": word_count})
46
+
12
47
 
13
48
  def check_support_agent(output: dict[str, Any], context: dict[str, Any]) -> PackResult:
14
49
  checks = PackCheck()
@@ -14,7 +14,7 @@ from urllib.parse import urlsplit
14
14
  from .rlpack import read_rlpack
15
15
 
16
16
  KEYCHAIN_SERVICE = "com.rubricloop.registry"
17
- DEFAULT_SERVER = "http://localhost:8080"
17
+ DEFAULT_SERVER = "https://api.rubricloop.dev"
18
18
  LOCKFILE = "rubricloop.lock"
19
19
 
20
20
 
@@ -6,6 +6,7 @@ import pytest
6
6
 
7
7
  from rubricloop.cli import execute, parser
8
8
  from rubricloop.registry import (
9
+ DEFAULT_SERVER,
9
10
  RegistryClient,
10
11
  RegistryClientError,
11
12
  load_credential,
@@ -15,8 +16,25 @@ from rubricloop.registry import (
15
16
  )
16
17
 
17
18
 
19
+ def test_cli_defaults_to_hosted_registry():
20
+ assert DEFAULT_SERVER == "https://api.rubricloop.dev"
21
+ assert parser().parse_args(["login"]).server == DEFAULT_SERVER
22
+ assert "--server" not in parser().format_help()
23
+
24
+
18
25
  def test_cli_exposes_complete_lifecycle():
19
- for command in ("login", "init", "validate", "test", "push", "vet", "publish", "pull", "run"):
26
+ for command in (
27
+ "login",
28
+ "init",
29
+ "create",
30
+ "validate",
31
+ "test",
32
+ "push",
33
+ "vet",
34
+ "publish",
35
+ "pull",
36
+ "run",
37
+ ):
20
38
  assert command in parser()._subparsers._group_actions[0].choices
21
39
 
22
40
 
@@ -26,6 +44,42 @@ def test_init_creates_project_without_credentials(tmp_path):
26
44
  assert json.loads((tmp_path / "rubricloop.json").read_text())["format_version"] == 1
27
45
 
28
46
 
47
+ def test_create_calls_repository_endpoint(monkeypatch):
48
+ calls = []
49
+ monkeypatch.setattr(
50
+ RegistryClient,
51
+ "json",
52
+ lambda self, method, path, **kwargs: calls.append((method, path, kwargs)) or {"id": "repo"},
53
+ )
54
+ result = execute(
55
+ parser().parse_args(
56
+ [
57
+ "create",
58
+ "acme/reply-check",
59
+ "--visibility",
60
+ "public",
61
+ "--description",
62
+ "Checks support replies.",
63
+ ]
64
+ )
65
+ )
66
+ assert result == {"id": "repo"}
67
+ assert calls == [
68
+ (
69
+ "POST",
70
+ "/v1/registry/repositories",
71
+ {
72
+ "payload": {
73
+ "namespace": "acme",
74
+ "name": "reply-check",
75
+ "description": "Checks support replies.",
76
+ "visibility": "public",
77
+ }
78
+ },
79
+ )
80
+ ]
81
+
82
+
29
83
  def test_credentials_use_keychain_or_explicit_environment(monkeypatch):
30
84
  values = {}
31
85
  backend_type = type("Backend", (), {})
@@ -9,6 +9,7 @@ from rubricloop.packs import (
9
9
  check_quote_invoice,
10
10
  check_shipment_docs,
11
11
  check_support_agent,
12
+ check_support_reply,
12
13
  )
13
14
  from rubricloop.packs.catalog import (
14
15
  COMMERCE_CONTEXT,
@@ -31,6 +32,32 @@ def test_support_golden_set(case):
31
32
  assert check_support_agent(output, deepcopy(SUPPORT_CONTEXT)).decision == "allow"
32
33
 
33
34
 
35
+ def test_support_reply_matches_quick_start_rules():
36
+ valid = check_support_reply(
37
+ {
38
+ "reply": (
39
+ "We apologize for the delay with your order. Our team is currently "
40
+ "investigating the issue and working to resolve it as quickly as possible. "
41
+ "You can expect an update on the timeline for your order shortly. Thank "
42
+ "you for your patience."
43
+ )
44
+ },
45
+ {},
46
+ )
47
+ invalid = check_support_reply(
48
+ {"reply": "We promise a guaranteed refund and will update you soon."},
49
+ {},
50
+ )
51
+
52
+ assert valid.decision == "allow"
53
+ assert invalid.decision == "deny"
54
+ assert {failure.rule_id for failure in invalid.failures} == {
55
+ "reply.length",
56
+ "reply.timeline",
57
+ "reply.promise",
58
+ }
59
+
60
+
34
61
  @pytest.mark.parametrize("case", range(20))
35
62
  def test_commerce_golden_set(case):
36
63
  output = deepcopy(COMMERCE_OUTPUT)
File without changes
File without changes
File without changes
File without changes