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.
- oro_env_runtime/__init__.py +23 -0
- oro_env_runtime/acceptance.py +171 -0
- oro_env_runtime/agent_tools.py +127 -0
- oro_env_runtime/attributes.py +216 -0
- oro_env_runtime/catalog.py +260 -0
- oro_env_runtime/contracts.py +15 -0
- oro_env_runtime/environment.py +435 -0
- oro_env_runtime/families/__init__.py +33 -0
- oro_env_runtime/families/base.py +234 -0
- oro_env_runtime/families/constraint_satisfaction.py +157 -0
- oro_env_runtime/families/intent_decomposition.py +113 -0
- oro_env_runtime/families/justification.py +498 -0
- oro_env_runtime/families/preference_reasoning.py +783 -0
- oro_env_runtime/families/ranking.py +573 -0
- oro_env_runtime/families/recovery.py +389 -0
- oro_env_runtime/families/retrieval_recall.py +384 -0
- oro_env_runtime/ledger.py +41 -0
- oro_env_runtime/loop.py +239 -0
- oro_env_runtime/observations.py +185 -0
- oro_env_runtime/openrouter.py +99 -0
- oro_env_runtime/pack.py +132 -0
- oro_env_runtime/product_facts.py +179 -0
- oro_env_runtime/reward.py +315 -0
- oro_env_runtime/runtime.py +336 -0
- oro_env_runtime/schema.py +366 -0
- oro_env_runtime/search.py +64 -0
- oro_env_runtime/tf4_evidence.py +708 -0
- oro_env_runtime/tf4_hybrid_release_gate.json +77 -0
- oro_env_runtime/tf4_judge.py +777 -0
- oro_env_runtime/tf4_judge_contract.py +168 -0
- oro_env_runtime/tf4_proof.py +386 -0
- oro_env_runtime/user_sim.py +259 -0
- oro_env_runtime/validation.py +394 -0
- oro_env_runtime/verify.py +226 -0
- oro_env_runtime-0.1.0.dist-info/METADATA +34 -0
- oro_env_runtime-0.1.0.dist-info/RECORD +38 -0
- oro_env_runtime-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|