oro-env-runtime 0.1.0__py3-none-any.whl

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 (38) hide show
  1. oro_env_runtime/__init__.py +23 -0
  2. oro_env_runtime/acceptance.py +171 -0
  3. oro_env_runtime/agent_tools.py +127 -0
  4. oro_env_runtime/attributes.py +216 -0
  5. oro_env_runtime/catalog.py +260 -0
  6. oro_env_runtime/contracts.py +15 -0
  7. oro_env_runtime/environment.py +435 -0
  8. oro_env_runtime/families/__init__.py +33 -0
  9. oro_env_runtime/families/base.py +234 -0
  10. oro_env_runtime/families/constraint_satisfaction.py +157 -0
  11. oro_env_runtime/families/intent_decomposition.py +113 -0
  12. oro_env_runtime/families/justification.py +498 -0
  13. oro_env_runtime/families/preference_reasoning.py +783 -0
  14. oro_env_runtime/families/ranking.py +573 -0
  15. oro_env_runtime/families/recovery.py +389 -0
  16. oro_env_runtime/families/retrieval_recall.py +384 -0
  17. oro_env_runtime/ledger.py +41 -0
  18. oro_env_runtime/loop.py +239 -0
  19. oro_env_runtime/observations.py +185 -0
  20. oro_env_runtime/openrouter.py +99 -0
  21. oro_env_runtime/pack.py +132 -0
  22. oro_env_runtime/product_facts.py +179 -0
  23. oro_env_runtime/reward.py +315 -0
  24. oro_env_runtime/runtime.py +336 -0
  25. oro_env_runtime/schema.py +366 -0
  26. oro_env_runtime/search.py +64 -0
  27. oro_env_runtime/tf4_evidence.py +708 -0
  28. oro_env_runtime/tf4_hybrid_release_gate.json +77 -0
  29. oro_env_runtime/tf4_judge.py +777 -0
  30. oro_env_runtime/tf4_judge_contract.py +168 -0
  31. oro_env_runtime/tf4_proof.py +386 -0
  32. oro_env_runtime/user_sim.py +259 -0
  33. oro_env_runtime/validation.py +394 -0
  34. oro_env_runtime/verify.py +226 -0
  35. oro_env_runtime-0.1.0.dist-info/METADATA +34 -0
  36. oro_env_runtime-0.1.0.dist-info/RECORD +38 -0
  37. oro_env_runtime-0.1.0.dist-info/WHEEL +4 -0
  38. oro_env_runtime-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,23 @@
1
+ """Portable sealed-pack execution and verification for Oro environments."""
2
+
3
+ from oro_env_runtime.contracts import (
4
+ ENV_CONTRACT_VERSION,
5
+ RESULT_SCHEMA_VERSION,
6
+ RUNTIME_VERSION,
7
+ TOOL_CONTRACT_VERSION,
8
+ VERIFIER_VERSION,
9
+ )
10
+ from oro_env_runtime.pack import COMPILED_EPOCH_VERSION
11
+ from oro_env_runtime.runtime import TaskSession
12
+ from oro_env_runtime.validation import validate_epoch
13
+
14
+ __all__ = [
15
+ "COMPILED_EPOCH_VERSION",
16
+ "ENV_CONTRACT_VERSION",
17
+ "RESULT_SCHEMA_VERSION",
18
+ "RUNTIME_VERSION",
19
+ "TOOL_CONTRACT_VERSION",
20
+ "TaskSession",
21
+ "VERIFIER_VERSION",
22
+ "validate_epoch",
23
+ ]
@@ -0,0 +1,171 @@
1
+ """Terminal acceptance contracts for compiled commerce tasks.
2
+
3
+ The search/filter tools expose raw catalog strings. Reward should not. This module converts
4
+ catalog labels into a small canonical layer so verifier correctness tracks the shopper-visible
5
+ contract rather than exact retailer category or brand spellings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from collections.abc import Iterable
12
+
13
+ from .attributes import attribute_matches
14
+ from .catalog import Catalog
15
+ from .schema import (
16
+ CandidateRef,
17
+ HardConstraints,
18
+ )
19
+
20
+ _CATEGORY_ALIASES: dict[str, str] = {
21
+ "video cards": "graphics_card",
22
+ "graphics cards": "graphics_card",
23
+ "video graphics cards": "graphics_card",
24
+ "gpu": "graphics_card",
25
+ "gpus": "graphics_card",
26
+ "mobile phones": "smartphone",
27
+ "cell phones": "smartphone",
28
+ "phones": "smartphone",
29
+ "smartphones": "smartphone",
30
+ "unlocked phones": "smartphone",
31
+ "smart phones": "smartphone",
32
+ "wi fi": "networking",
33
+ "wifi": "networking",
34
+ "smartwatches": "smartwatch",
35
+ "smart watches": "smartwatch",
36
+ }
37
+
38
+
39
+ def _norm(text: str | None) -> str:
40
+ return " ".join(re.findall(r"[a-z0-9]+", (text or "").casefold()))
41
+
42
+
43
+ def _tokens(text: str | None) -> set[str]:
44
+ return set(re.findall(r"[a-z0-9]+", (text or "").casefold()))
45
+
46
+
47
+ def _stemmed_tokens(text: str | None) -> set[str]:
48
+ out: set[str] = set()
49
+ for token in _tokens(text):
50
+ out.add(token)
51
+ if len(token) > 3 and token.endswith("s"):
52
+ out.add(token[:-1])
53
+ return out
54
+
55
+
56
+ def canonical_category_labels(labels: Iterable[str | None]) -> set[str]:
57
+ out: set[str] = set()
58
+ for label in labels:
59
+ low = _norm(label)
60
+ if not low:
61
+ continue
62
+ out.add(low)
63
+ out.add(_CATEGORY_ALIASES.get(low, low))
64
+ return out
65
+
66
+
67
+ def _path_has(candidate_path: Iterable[str | None], labels: Iterable[str]) -> bool:
68
+ path_labels = canonical_category_labels(candidate_path)
69
+ wanted = canonical_category_labels(labels)
70
+ return bool(path_labels & wanted)
71
+
72
+
73
+ def _title_category_fallback(
74
+ expected: str, candidate_path: Iterable[str | None], title: str | None
75
+ ) -> bool:
76
+ tokens = _stemmed_tokens(title)
77
+ exp = _norm(expected)
78
+ if exp in {"gaming monitors", "gaming monitor"}:
79
+ return (
80
+ _path_has(candidate_path, ["Monitors", "Computer Monitors"])
81
+ and {"gaming", "monitor"} <= tokens
82
+ )
83
+ if exp in {"over ear", "over-ear", "over ear headphones", "over-ear headphones"}:
84
+ return _path_has(candidate_path, ["Headphones"]) and (
85
+ {"over", "ear"} <= tokens or "overear" in tokens or "circumaural" in tokens
86
+ )
87
+ return False
88
+
89
+
90
+ def category_matches(
91
+ expected: str | None,
92
+ candidate_path: Iterable[str | None],
93
+ *,
94
+ title: str | None = None,
95
+ ) -> bool:
96
+ if not expected:
97
+ return True
98
+ expected_labels = canonical_category_labels([expected])
99
+ candidate_labels = canonical_category_labels(candidate_path)
100
+ if expected_labels & candidate_labels:
101
+ return True
102
+ return _title_category_fallback(expected, candidate_path, title)
103
+
104
+
105
+ def brand_matches(
106
+ expected: str | None, actual: str | None, *, title: str | None = None
107
+ ) -> bool:
108
+ if not expected:
109
+ return True
110
+ exp = _norm(expected)
111
+ act = _norm(actual)
112
+ if not exp:
113
+ return True
114
+ if exp == act:
115
+ return True
116
+ exp_tokens = _tokens(expected)
117
+ actual_tokens = _tokens(actual)
118
+ if exp_tokens and exp_tokens <= actual_tokens:
119
+ return True
120
+ return False
121
+
122
+
123
+ def candidate_satisfies_hard(
124
+ catalog: Catalog, ref: CandidateRef, hard: HardConstraints
125
+ ) -> bool:
126
+ if not catalog.exists(ref):
127
+ return False
128
+ meta = catalog.meta(ref)
129
+ rec = catalog.by_id(ref.product_id)
130
+ if hard.require_in_stock and not meta.in_stock:
131
+ return False
132
+ if meta.price > hard.budget_usd + 0.01:
133
+ return False
134
+ if not category_matches(
135
+ hard.category, meta.category_path, title=str(rec.get("title") or "")
136
+ ):
137
+ return False
138
+ if not brand_matches(hard.brand, meta.brand, title=str(rec.get("title") or "")):
139
+ return False
140
+ # Attribute gates further restrict the eligible pool; keys are pre-validated at HardConstraints construction.
141
+ for key, wanted in hard.attributes.items():
142
+ if not attribute_matches(rec, key, wanted):
143
+ return False
144
+ return True
145
+
146
+
147
+ def hard_constraint_refs(catalog: Catalog, hard: HardConstraints) -> list[CandidateRef]:
148
+ refs = [
149
+ meta.ref
150
+ for meta in catalog.all_candidates()
151
+ if candidate_satisfies_hard(catalog, meta.ref, hard)
152
+ ]
153
+ return _dedupe_refs(refs)
154
+
155
+
156
+ def _dedupe_refs(refs: Iterable[CandidateRef]) -> list[CandidateRef]:
157
+ seen: set[str] = set()
158
+ out: list[CandidateRef] = []
159
+ for ref in refs:
160
+ if ref.key() not in seen:
161
+ seen.add(ref.key())
162
+ out.append(ref)
163
+ return out
164
+
165
+
166
+ __all__ = [
167
+ "brand_matches",
168
+ "candidate_satisfies_hard",
169
+ "category_matches",
170
+ "hard_constraint_refs",
171
+ ]
@@ -0,0 +1,127 @@
1
+ """Agent tool surface: function-calling specs + dispatch into the environment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _TOOLS = [
6
+ (
7
+ "search",
8
+ "Full-text search the catalog. Returns up to 10 products.",
9
+ {
10
+ "query": {"type": "string"},
11
+ "k": {"type": "integer"},
12
+ "in_stock": {"type": "boolean"},
13
+ },
14
+ ["query"],
15
+ ),
16
+ (
17
+ "filter",
18
+ "Filter the catalog by structured constraints. Returns up to 10 candidates.",
19
+ {
20
+ "category": {"type": "string"},
21
+ "brand": {"type": "string"},
22
+ "max_price": {"type": "number"},
23
+ "in_stock": {"type": "boolean"},
24
+ "k": {"type": "integer"},
25
+ },
26
+ [],
27
+ ),
28
+ (
29
+ "view",
30
+ "View one product, raw product facts, and its variants (sku, current price, current stock).",
31
+ {"product_id": {"type": "string"}},
32
+ ["product_id"],
33
+ ),
34
+ (
35
+ "compare",
36
+ "Compare up to 5 products side by side, including raw product facts.",
37
+ {"product_ids": {"type": "array", "items": {"type": "string"}}},
38
+ ["product_ids"],
39
+ ),
40
+ (
41
+ "inspect_stock",
42
+ "Check the CURRENT stock and price of a specific variant. Use this to verify before ordering.",
43
+ {"product_id": {"type": "string"}, "sku": {"type": "string"}},
44
+ ["product_id", "sku"],
45
+ ),
46
+ ("inspect_cart", "Inspect the current cart.", {}, []),
47
+ (
48
+ "add_to_cart",
49
+ "Add a specific variant to the cart.",
50
+ {"product_id": {"type": "string"}, "sku": {"type": "string"}},
51
+ ["product_id", "sku"],
52
+ ),
53
+ (
54
+ "remove_from_cart",
55
+ "Remove a variant from the cart.",
56
+ {"product_id": {"type": "string"}, "sku": {"type": "string"}},
57
+ ["product_id", "sku"],
58
+ ),
59
+ (
60
+ "message",
61
+ "Send a short message to the shopper.",
62
+ {"content": {"type": "string"}},
63
+ ["content"],
64
+ ),
65
+ (
66
+ "place_test_order",
67
+ "Place the order for a variant in the cart. Ends the session. "
68
+ "When the shopper asked for a reason, include structured justification claims grounded in viewed facts.",
69
+ {
70
+ "product_id": {"type": "string"},
71
+ "sku": {"type": "string"},
72
+ "justification": {
73
+ "type": "object",
74
+ "properties": {
75
+ "summary": {"type": "string"},
76
+ "claims": {
77
+ "type": "array",
78
+ "items": {
79
+ "type": "object",
80
+ "properties": {
81
+ "field": {
82
+ "type": "string",
83
+ "description": "One of category, resolution, refresh_rate, screen_size, price, in_stock, brand, streaming, microphone, privacy_cover, autofocus, hdr.",
84
+ },
85
+ "value": {
86
+ "type": "string",
87
+ "description": "The claimed value, e.g. Webcams, 4k, 49.98, true, Adesso.",
88
+ },
89
+ "evidence": {
90
+ "type": "string",
91
+ "description": "Short source phrase from view/compare/inspect_stock.",
92
+ },
93
+ },
94
+ "required": ["field", "value"],
95
+ },
96
+ },
97
+ },
98
+ },
99
+ },
100
+ [],
101
+ ),
102
+ ]
103
+
104
+
105
+ def tool_specs(*, exclude_names: set[str] | frozenset[str] = frozenset()) -> list[dict]:
106
+ specs = []
107
+ for name, desc, props, required in _TOOLS:
108
+ if name in exclude_names:
109
+ continue
110
+ specs.append(
111
+ {
112
+ "type": "function",
113
+ "function": {
114
+ "name": name,
115
+ "description": desc,
116
+ "parameters": {
117
+ "type": "object",
118
+ "properties": props,
119
+ "required": required,
120
+ },
121
+ },
122
+ }
123
+ )
124
+ return specs
125
+
126
+
127
+ __all__ = ["tool_specs"]
@@ -0,0 +1,216 @@
1
+ """Attribute-reliability gate for hard constraints.
2
+
3
+ The `attributes` layer is LLM-synthesized and noisy, so only a canonical concept whose
4
+ resolved value is unambiguous and cross-checkable may carry a HARD gate
5
+ (``HardConstraints.attributes``); every other concept must be a shadow ``latent_pref``.
6
+
7
+ The raw `attributes` dict is too sparse and fragmented to trust directly (the same concept
8
+ splits across `ram`/`memory`/`system memory (ram)`, `resolution`/`maximum resolution`, etc.),
9
+ so the resolver reads the richer `specification` field and the product title, canonicalizing
10
+ fragmented spellings. The gate allowlist is derived offline from a reliability audit: a concept
11
+ is gate-worthy only at coverage >= 8% AND title-agreement >= 90% over N >= 40 samples. High
12
+ coverage alone does not qualify a concept -- `color` has the widest coverage yet stays
13
+ shadow-only.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+
20
+ # canonical concept -> the raw key spellings it may appear under in specification/attributes
21
+ _GATE_RAW_KEYS: dict[str, tuple[str, ...]] = {
22
+ "ram": (
23
+ "ram",
24
+ "memory",
25
+ "system memory (ram)",
26
+ "memory (ram)",
27
+ "installed memory",
28
+ "system memory",
29
+ ),
30
+ "resolution": (
31
+ "resolution",
32
+ "screen resolution",
33
+ "maximum resolution",
34
+ "native resolution",
35
+ "display resolution",
36
+ ),
37
+ "screen_size": ("screen size", "display size", "screen_size", "screen size (in)"),
38
+ "refresh_rate": ("refresh rate", "maximum refresh rate", "refresh rate (max)"),
39
+ }
40
+
41
+ # a key may carry a HARD gate only if it is here; anything else stays a shadow latent_pref
42
+ RELIABLE_GATE_KEYS: frozenset[str] = frozenset(_GATE_RAW_KEYS)
43
+ # concepts measured but rejected by the gate bar; kept for families choosing a shadow scoring field
44
+ SHADOW_ONLY_KEYS: frozenset[str] = frozenset(
45
+ {"storage", "color", "processor", "operating_system", "connectivity"}
46
+ )
47
+
48
+ _GB_RE = re.compile(r"(\d+(?:\.\d+)?)\s*(tb|gb)\b", re.I)
49
+ _INCH_RE = re.compile(r"(\d+(?:\.\d+)?)\s*(?:inch|in|\")", re.I)
50
+ _HZ_RE = re.compile(r"(\d+(?:\.\d+)?)\s*hz", re.I)
51
+ _WXH_RE = re.compile(r"(\d{3,5})\s*[x×]\s*(\d{3,5})")
52
+ # the concept word must immediately follow the number, so "16GB Memory - 512GB SSD" and a GPU
53
+ # "RTX 4070Ti 16GB" do not resolve ram from the storage or VRAM figure.
54
+ _RAM_TITLE = re.compile(r"(\d+(?:\.\d+)?)\s*gb\s*(?:ram|memory|unified memory)\b", re.I)
55
+
56
+
57
+ def _first(v: object) -> str | None:
58
+ if isinstance(v, list):
59
+ return str(v[0]) if v else None
60
+ return str(v) if v is not None else None
61
+
62
+
63
+ def _norm_gb(text: str | None) -> str | None:
64
+ if not text:
65
+ return None
66
+ m = _GB_RE.search(text)
67
+ if not m:
68
+ return None
69
+ val = float(m.group(1)) * (1024 if m.group(2).lower() == "tb" else 1)
70
+ return f"{val:g}gb"
71
+
72
+
73
+ def _norm_inch(text: str | None) -> str | None:
74
+ if not text:
75
+ return None
76
+ m = _INCH_RE.search(text)
77
+ return f"{float(m.group(1)):g}in" if m else None
78
+
79
+
80
+ def _norm_hz(text: str | None) -> str | None:
81
+ if not text:
82
+ return None
83
+ m = _HZ_RE.search(text)
84
+ return f"{float(m.group(1)):g}hz" if m else None
85
+
86
+
87
+ def _norm_res(text: str | None) -> str | None:
88
+ if not text:
89
+ return None
90
+ low = text.lower()
91
+ m = _WXH_RE.search(low)
92
+ if m:
93
+ w = int(m.group(1))
94
+ return (
95
+ "4k" if w >= 3840 else "qhd" if w >= 2560 else "fhd" if w >= 1920 else "hd"
96
+ )
97
+ if "4k" in low or "uhd" in low or "2160" in low:
98
+ return "4k"
99
+ if "qhd" in low or "1440" in low:
100
+ return "qhd"
101
+ if "fhd" in low or "1080" in low or "full hd" in low:
102
+ return "fhd"
103
+ if re.search(r"\b(hd|720)\b", low):
104
+ return "hd"
105
+ return None
106
+
107
+
108
+ _NORMALIZERS = {
109
+ "ram": _norm_gb,
110
+ "resolution": _norm_res,
111
+ "screen_size": _norm_inch,
112
+ "refresh_rate": _norm_hz,
113
+ }
114
+
115
+
116
+ def _normalize(key: str, text: str | None) -> str | None:
117
+ norm = _NORMALIZERS.get(key)
118
+ return norm(text) if norm else None
119
+
120
+
121
+ def _title_value(key: str, title: str) -> str | None:
122
+ if key == "ram":
123
+ m = _RAM_TITLE.search(title)
124
+ return f"{float(m.group(1)):g}gb" if m else None
125
+ if key in ("resolution", "screen_size", "refresh_rate"):
126
+ return _normalize(key, title)
127
+ return None
128
+
129
+
130
+ def resolve_attribute(rec: dict, key: str) -> str | None:
131
+ """Canonical value for a candidate's product record, or None when unreliable/absent.
132
+
133
+ Reads the best reliable source in order: specification, attributes, then the product
134
+ title. Returns None for any key outside :data:`RELIABLE_GATE_KEYS`.
135
+ """
136
+ if key not in RELIABLE_GATE_KEYS:
137
+ return None
138
+ raw_keys = _GATE_RAW_KEYS[key]
139
+ for src_name in ("specification", "attributes"):
140
+ src = rec.get(src_name)
141
+ if not isinstance(src, dict):
142
+ continue
143
+ low = {k.strip().lower(): v for k, v in src.items()}
144
+ for rk in raw_keys:
145
+ if rk in low:
146
+ val = _normalize(key, _first(low[rk]))
147
+ if val:
148
+ return val
149
+ return _title_value(key, str(rec.get("title") or ""))
150
+
151
+
152
+ def attribute_matches(rec: dict, key: str, wanted: str) -> bool:
153
+ """True iff the candidate's reliable value for ``key`` equals the normalized ``wanted``."""
154
+ got = resolve_attribute(rec, key)
155
+ if got is None:
156
+ return False
157
+ want = _normalize(key, str(wanted))
158
+ return want is not None and got == want
159
+
160
+
161
+ def _variant_options_values(key: str, text: str) -> set[str]:
162
+ """Every value the concept's adjacency oracle finds in a variant options string: the number
163
+ must sit next to its concept word/unit, so a storage or VRAM GB figure is never read as RAM."""
164
+ if key == "ram":
165
+ return {f"{float(n):g}gb" for n in _RAM_TITLE.findall(text)}
166
+ if key == "screen_size":
167
+ return {f"{float(m.group(1)):g}in" for m in _INCH_RE.finditer(text)}
168
+ if key == "refresh_rate":
169
+ return {f"{float(m.group(1)):g}hz" for m in _HZ_RE.finditer(text)}
170
+ v = _norm_res(text) # resolution is a keyword/WxH label, single-valued per string
171
+ return {v} if v else set()
172
+
173
+
174
+ def resolve_variant_attribute(rec: dict, options: str | None, key: str) -> str | None:
175
+ """Per-variant value TF3 gates on, read from the SKU ``options`` string via the adjacency oracle.
176
+
177
+ ``resolve_attribute`` is product-level and cannot separate a 16GB SKU from an 8GB SKU of one
178
+ product. This reads ``options`` first (the number must be adjacent to its concept word/unit),
179
+ so "16GB RAM / 512GB SSD" -> ram=16gb while the 512GB storage and a bare GPU VRAM "16GB" are
180
+ never read as RAM. An options string encoding two or more distinct values is ambiguous and
181
+ returns None. Only when options is silent on the concept does it fall back to the product-level
182
+ resolver. Returns None for any key outside :data:`RELIABLE_GATE_KEYS`.
183
+
184
+ NOTE: the fallback is product-level, so a caller gating on ``ram`` MUST category-scope to
185
+ system-RAM categories (laptops/desktops/phones/tablets) -- a GPU's VRAM ``memory`` key resolves
186
+ here otherwise. TF3 enforces that scope in ``env/families/constraint_satisfaction.py``.
187
+ """
188
+ if key not in RELIABLE_GATE_KEYS:
189
+ return None
190
+ vals = _variant_options_values(key, options or "")
191
+ if len(vals) > 1: # ambiguous options string -> refuse to gate
192
+ return None
193
+ if vals:
194
+ return next(iter(vals))
195
+ return resolve_attribute(rec, key)
196
+
197
+
198
+ def assert_gate_keys(attributes: dict[str, str]) -> None:
199
+ """Compile-time gate: every hard-gated attribute key must be reliable, else it must be a shadow
200
+ latent_pref. Raised at ``HardConstraints`` construction (task compile time)."""
201
+ bad = sorted(k for k in attributes if k not in RELIABLE_GATE_KEYS)
202
+ if bad:
203
+ raise ValueError(
204
+ f"HardConstraints.attributes may gate only on reliable keys "
205
+ f"{sorted(RELIABLE_GATE_KEYS)}; {bad} is not gate-reliable and must be a latent_pref."
206
+ )
207
+
208
+
209
+ __all__ = [
210
+ "RELIABLE_GATE_KEYS",
211
+ "SHADOW_ONLY_KEYS",
212
+ "resolve_attribute",
213
+ "resolve_variant_attribute",
214
+ "attribute_matches",
215
+ "assert_gate_keys",
216
+ ]