tensorcode 0.1.0a1__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.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
tensorcode/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""TensorCode: typed cognitive operations with swappable implementations.
|
|
2
|
+
|
|
3
|
+
Pre-alpha. The facades exported here (``parse``, ``classify``, ``choose``, ``rank``,
|
|
4
|
+
``check``, ``verify``, ``propose``, ``invoke``), the outcome values, records and the
|
|
5
|
+
runtime are the public API; submodules not re-exported here may change without notice.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0a1"
|
|
9
|
+
|
|
10
|
+
from .actions import Plan, RunnablePlan, Step, action, invoke, plan_order, run_plan
|
|
11
|
+
from .context import Packed, approx_tokens, dedupe, pack, shingle_similarity
|
|
12
|
+
from .ops import Constraint, Objective, check, choose, classify, parse, propose, rank, verify
|
|
13
|
+
from .outcomes import Receipt, Score, Unknown, Verdict
|
|
14
|
+
from .records import (
|
|
15
|
+
Claim,
|
|
16
|
+
Conflict,
|
|
17
|
+
Evidence,
|
|
18
|
+
Interval,
|
|
19
|
+
Opaque,
|
|
20
|
+
Patch,
|
|
21
|
+
Ref,
|
|
22
|
+
Retract,
|
|
23
|
+
SetField,
|
|
24
|
+
Store,
|
|
25
|
+
Tell,
|
|
26
|
+
TypeRegistry,
|
|
27
|
+
Var,
|
|
28
|
+
)
|
|
29
|
+
from .runtime import Budget, Output, Policy, Profile, Request, Runtime, Trace, Traits, implementation, use
|
|
30
|
+
|
|
31
|
+
from . import actions, records
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Plan",
|
|
35
|
+
"Step",
|
|
36
|
+
"action",
|
|
37
|
+
"plan_order",
|
|
38
|
+
"RunnablePlan",
|
|
39
|
+
"invoke",
|
|
40
|
+
"run_plan",
|
|
41
|
+
"Packed",
|
|
42
|
+
"approx_tokens",
|
|
43
|
+
"dedupe",
|
|
44
|
+
"pack",
|
|
45
|
+
"shingle_similarity",
|
|
46
|
+
"Constraint",
|
|
47
|
+
"Objective",
|
|
48
|
+
"check",
|
|
49
|
+
"choose",
|
|
50
|
+
"classify",
|
|
51
|
+
"parse",
|
|
52
|
+
"propose",
|
|
53
|
+
"rank",
|
|
54
|
+
"verify",
|
|
55
|
+
"Receipt",
|
|
56
|
+
"Score",
|
|
57
|
+
"Unknown",
|
|
58
|
+
"Verdict",
|
|
59
|
+
"Claim",
|
|
60
|
+
"Conflict",
|
|
61
|
+
"Evidence",
|
|
62
|
+
"Interval",
|
|
63
|
+
"Opaque",
|
|
64
|
+
"Patch",
|
|
65
|
+
"Ref",
|
|
66
|
+
"Retract",
|
|
67
|
+
"SetField",
|
|
68
|
+
"Store",
|
|
69
|
+
"Tell",
|
|
70
|
+
"TypeRegistry",
|
|
71
|
+
"Var",
|
|
72
|
+
"Budget",
|
|
73
|
+
"Output",
|
|
74
|
+
"Policy",
|
|
75
|
+
"Profile",
|
|
76
|
+
"Request",
|
|
77
|
+
"Runtime",
|
|
78
|
+
"Trace",
|
|
79
|
+
"Traits",
|
|
80
|
+
"implementation",
|
|
81
|
+
"use",
|
|
82
|
+
"actions",
|
|
83
|
+
"records",
|
|
84
|
+
]
|
tensorcode/actions.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Effects: registered actions, invocation receipts, and runnable plans.
|
|
2
|
+
|
|
3
|
+
* An action is a typed value whose class declares its effect semantics.
|
|
4
|
+
* ``invoke`` runs one action through an executor and returns a ``Receipt``.
|
|
5
|
+
It does not retry: whether a retry is safe depends on facts the caller must
|
|
6
|
+
weigh (idempotency, observed state), which is the recovery agent's job.
|
|
7
|
+
* A ``Plan`` is data. ``plan_order`` checks its structure (ids, registered actions, dependencies) and gives an execution order.
|
|
8
|
+
|
|
9
|
+
Nothing here makes a physical effect exactly-once. Idempotency keys make
|
|
10
|
+
retries safe only when the executor honors them; otherwise, reconcile by
|
|
11
|
+
observation before retrying.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Callable, Literal, Protocol, Sequence
|
|
18
|
+
|
|
19
|
+
from .outcomes import Receipt, Unknown, Verdict
|
|
20
|
+
from .runtime import current
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ActionSpec:
|
|
25
|
+
name: str
|
|
26
|
+
effect: Literal["read", "write", "external"]
|
|
27
|
+
idempotent: bool # the *target system* treats repeats with the same key as one effect
|
|
28
|
+
reversible: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def action(*, effect: Literal["read", "write", "external"], idempotent: bool, reversible: bool = False) -> Callable[[type], type]:
|
|
32
|
+
def mark(cls: type) -> type:
|
|
33
|
+
cls.__action__ = ActionSpec(cls.__name__, effect, idempotent, reversible) # type: ignore[attr-defined]
|
|
34
|
+
return cls
|
|
35
|
+
|
|
36
|
+
return mark
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def spec_of(act: Any) -> ActionSpec | None:
|
|
40
|
+
return getattr(type(act), "__action__", None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Executor(Protocol):
|
|
44
|
+
def execute(self, act: Any, *, key: str | None) -> Receipt: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def invoke(act: Any, *, executor: Executor, key: str | None) -> Receipt:
|
|
48
|
+
rt = current()
|
|
49
|
+
spec = spec_of(act)
|
|
50
|
+
span = rt.trace.open("invoke", target=type(act).__name__, input=act)
|
|
51
|
+
span.labels["key"] = key
|
|
52
|
+
if spec is None:
|
|
53
|
+
receipt = Receipt(act, "rejected", error="not a registered action")
|
|
54
|
+
elif spec.effect != "read" and key is None:
|
|
55
|
+
receipt = Receipt(act, "rejected", error="write actions require an idempotency key")
|
|
56
|
+
else:
|
|
57
|
+
try:
|
|
58
|
+
receipt = executor.execute(act, key=key)
|
|
59
|
+
except Exception as exc: # noqa: BLE001 - an exception after dispatch may still have had an effect
|
|
60
|
+
receipt = Receipt(act, "indeterminate", retryable=False, idempotency_key=key, error=f"{type(exc).__name__}: {exc}")
|
|
61
|
+
return span.close(receipt, "answer")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------ plans
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class Step:
|
|
69
|
+
id: str
|
|
70
|
+
action: Any
|
|
71
|
+
needs: tuple[str, ...] = () # execution dependencies, not world relationships
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class Plan:
|
|
76
|
+
steps: tuple[Step, ...]
|
|
77
|
+
rationale: str = ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class RunnablePlan:
|
|
82
|
+
"""A plan whose structure checks out, with an execution order. No permissions involved."""
|
|
83
|
+
|
|
84
|
+
plan: Plan
|
|
85
|
+
order: tuple[str, ...] = field(default=())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def plan_order(plan: Plan) -> RunnablePlan | Verdict:
|
|
89
|
+
"""Check only what execution needs: unique ids, registered actions, resolvable dependencies, no cycle."""
|
|
90
|
+
reasons = []
|
|
91
|
+
ids = [s.id for s in plan.steps]
|
|
92
|
+
if len(set(ids)) != len(ids):
|
|
93
|
+
reasons.append("duplicate step ids")
|
|
94
|
+
for s in plan.steps:
|
|
95
|
+
if spec_of(s.action) is None:
|
|
96
|
+
reasons.append(f"{s.id}: not a registered action")
|
|
97
|
+
for need in s.needs:
|
|
98
|
+
if need not in ids:
|
|
99
|
+
reasons.append(f"{s.id}: depends on unknown step {need}")
|
|
100
|
+
order = _topological(plan.steps) if not reasons else None
|
|
101
|
+
if order is None and not reasons:
|
|
102
|
+
reasons.append("dependency cycle")
|
|
103
|
+
if reasons:
|
|
104
|
+
return Verdict("fails", tuple(reasons))
|
|
105
|
+
return RunnablePlan(plan, tuple(order or ()))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _topological(steps: Sequence[Step]) -> list[str] | None:
|
|
109
|
+
remaining = {s.id: set(s.needs) for s in steps}
|
|
110
|
+
order: list[str] = []
|
|
111
|
+
while remaining:
|
|
112
|
+
ready = sorted(k for k, deps in remaining.items() if not deps)
|
|
113
|
+
if not ready:
|
|
114
|
+
return None
|
|
115
|
+
for k in ready:
|
|
116
|
+
order.append(k)
|
|
117
|
+
del remaining[k]
|
|
118
|
+
for deps in remaining.values():
|
|
119
|
+
deps.difference_update(ready)
|
|
120
|
+
return order
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_plan(runnable: RunnablePlan | Plan, *, executor: Executor, key_prefix: str) -> dict[str, Receipt | Unknown]:
|
|
124
|
+
"""Execute in dependency order. A step runs only if everything it needs was ``applied``."""
|
|
125
|
+
authorized = runnable if isinstance(runnable, RunnablePlan) else plan_order(runnable)
|
|
126
|
+
if isinstance(authorized, Verdict):
|
|
127
|
+
return {s.id: Unknown("not_run", "; ".join(authorized.reasons)) for s in runnable.steps} # type: ignore[union-attr]
|
|
128
|
+
by_id = {s.id: s for s in authorized.plan.steps}
|
|
129
|
+
results: dict[str, Receipt | Unknown] = {}
|
|
130
|
+
for step_id in authorized.order:
|
|
131
|
+
step = by_id[step_id]
|
|
132
|
+
blocked = [n for n in step.needs if not (isinstance(results[n], Receipt) and results[n].status == "applied")] # type: ignore[union-attr]
|
|
133
|
+
if blocked:
|
|
134
|
+
results[step_id] = Unknown("not_run", f"dependencies not applied: {blocked}")
|
|
135
|
+
continue
|
|
136
|
+
results[step_id] = invoke(step.action, executor=executor, key=f"{key_prefix}:{step_id}")
|
|
137
|
+
return results
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""What kind of thing a question asks for, and whether a candidate answer could be it.
|
|
2
|
+
|
|
3
|
+
A question carries a requirement its answer must satisfy — a place, a date, a count, a yes or
|
|
4
|
+
no — and nothing in this library represented that. Measured consequence: an extractive answerer
|
|
5
|
+
returns the entity a question travels *through* rather than the one it asks for ("Who replaced
|
|
6
|
+
the manager of Aston Villa that began at Leeds United?" → the manager, not the replacement) and
|
|
7
|
+
does so at high confidence, so no abstention threshold can catch it. An expected answer type is
|
|
8
|
+
the only signal that can, because the error is not uncertainty — it is a category error.
|
|
9
|
+
|
|
10
|
+
Two structures, both deterministic and both conservative:
|
|
11
|
+
|
|
12
|
+
``asked_for`` reads the requirement off the question's own surface (wh-word, head noun, copula
|
|
13
|
+
shape). ``mismatch`` rejects a candidate only when it is *confidently* of the wrong kind; a
|
|
14
|
+
candidate whose kind cannot be established is admitted, because refusing on ignorance costs
|
|
15
|
+
correct answers. ``shape`` distinguishes a question that names its own candidate answers
|
|
16
|
+
("Which ran longer, A or B?") from one that must travel through an intermediate — a distinction
|
|
17
|
+
that matters because the first may answer itself from its own words and the second may not.
|
|
18
|
+
|
|
19
|
+
Nothing here is trained and nothing needs a corpus; it is a representation, not a model.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AnswerType(str, Enum):
|
|
30
|
+
"""The kind of thing a question asks for. ``entity`` is the honest 'a thing, unspecified'."""
|
|
31
|
+
|
|
32
|
+
person = "person"
|
|
33
|
+
place = "place"
|
|
34
|
+
date = "date"
|
|
35
|
+
number = "number"
|
|
36
|
+
yes_no = "yes_no"
|
|
37
|
+
entity = "entity"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Shape(str, Enum):
|
|
41
|
+
"""Whether a question names its own candidate answers."""
|
|
42
|
+
|
|
43
|
+
comparison = "comparison"
|
|
44
|
+
bridge = "bridge"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
MONTHS = ("january", "february", "march", "april", "may", "june", "july", "august",
|
|
48
|
+
"september", "october", "november", "december")
|
|
49
|
+
#: nouns that name a person by role, so "which director" asks for a person
|
|
50
|
+
PERSON_NOUNS = ("person", "man", "woman", "author", "director", "actor", "actress", "singer", "writer",
|
|
51
|
+
"player", "artist", "founder", "president", "ceo", "musician", "composer", "producer",
|
|
52
|
+
"politician", "scientist", "poet", "coach", "manager", "owner", "leader", "king", "queen",
|
|
53
|
+
"senator", "governor", "mayor", "judge", "athlete", "driver", "guitarist", "drummer",
|
|
54
|
+
"journalist", "actor's", "father", "mother", "son", "daughter", "brother", "sister", "wife",
|
|
55
|
+
"husband", "star", "host", "narrator", "designer", "architect", "publisher", "editor")
|
|
56
|
+
PLACE_NOUNS = ("place", "city", "country", "state", "county", "town", "village", "province", "region",
|
|
57
|
+
"island", "river", "mountain", "street", "location", "venue", "stadium", "district",
|
|
58
|
+
"continent", "capital", "borough", "territory", "nation")
|
|
59
|
+
DATE_NOUNS = ("year", "date", "month", "decade", "birthday", "anniversary")
|
|
60
|
+
NUMBER_NOUNS = ("number", "count", "population", "total", "amount", "height", "length",
|
|
61
|
+
"distance", "duration", "percentage", "price", "cost")
|
|
62
|
+
YES_NO_OPENERS = ("are", "is", "was", "were", "do", "does", "did", "has", "have", "had", "can", "could",
|
|
63
|
+
"will", "would", "should", "am")
|
|
64
|
+
COMPARISON_CUES = (" or ", "both", "which came first", "same", "more than", "less than", "older",
|
|
65
|
+
"younger", "larger", "smaller", "bigger", "longer", "shorter", "taller", "earlier",
|
|
66
|
+
"later", "first,", "between", "greater", "which one", "most recent", "who is younger",
|
|
67
|
+
"which has a", "which was released first")
|
|
68
|
+
|
|
69
|
+
#: digits included: a tokenizer blind to numbers cannot tell that "28,776" answers a question
|
|
70
|
+
#: about a population, nor that a bridge answer of "1994" was lifted from the question
|
|
71
|
+
_WORD = re.compile(r"[a-z0-9']+")
|
|
72
|
+
_YEAR = re.compile(r"\b\d{3,4}\b(?:\s*(?:bc|bce|ad|ce))?", re.I)
|
|
73
|
+
_DIGIT = re.compile(r"\d")
|
|
74
|
+
_ALTERNATIVE = re.compile(r"\bor\b", re.I)
|
|
75
|
+
#: "43-year-old", "19-year veteran": a digit immediately before "year" makes it a modifier
|
|
76
|
+
_AGE_MODIFIER = re.compile(r"\d\s*-?\s*$")
|
|
77
|
+
_NUMBER_WORDS = ("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
|
78
|
+
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
|
|
79
|
+
"eighteen", "nineteen", "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
|
|
80
|
+
"eighty", "ninety", "hundred", "thousand", "million", "billion", "dozen", "zero",
|
|
81
|
+
"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth",
|
|
82
|
+
"tenth", "eleventh", "twelfth", "thirteenth", "twentieth")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _words(text: str) -> list[str]:
|
|
86
|
+
return _WORD.findall(text.lower())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def shape(question: str) -> Shape:
|
|
90
|
+
"""Does the question name its own candidates? Read off the surface, never from a dataset field."""
|
|
91
|
+
low = f" {question.lower().strip()} "
|
|
92
|
+
return Shape.comparison if any(c in low for c in COMPARISON_CUES) else Shape.bridge
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def asked_for(question: str) -> AnswerType:
|
|
96
|
+
"""The kind of answer the question requires, from its wh-word and head noun.
|
|
97
|
+
|
|
98
|
+
Read from the LAST wh-phrase, not the first: a multi-hop question states the hop it travels
|
|
99
|
+
through before the thing it asks for ("...the brother of the Secretary who was born in what
|
|
100
|
+
year?" asks for a year, not a person), and reading the first cue gets those backwards.
|
|
101
|
+
"""
|
|
102
|
+
found = [(m.group(0), m.start()) for m in _WORD.finditer(question.lower())]
|
|
103
|
+
words = [w for w, _ in found]
|
|
104
|
+
if not words:
|
|
105
|
+
return AnswerType.entity
|
|
106
|
+
wh = [(i, w) for i, w in enumerate(words) if w in ("who", "whom", "whose", "where", "when", "what", "which", "how")]
|
|
107
|
+
if not wh:
|
|
108
|
+
return AnswerType.yes_no if words[0] in YES_NO_OPENERS else AnswerType.entity
|
|
109
|
+
# English fronts the interrogative: if the question opens with one (after an optional
|
|
110
|
+
# preposition), that is the ask. Otherwise the ask is the last one, and any earlier wh is a
|
|
111
|
+
# relative clause describing the hop ("the school WHERE he is chancellor") rather than asking.
|
|
112
|
+
i, head = wh[0] if wh[0][0] <= 2 else wh[-1]
|
|
113
|
+
if head in ("who", "whom", "whose"):
|
|
114
|
+
return AnswerType.person
|
|
115
|
+
if head == "where":
|
|
116
|
+
return AnswerType.place
|
|
117
|
+
if head == "when":
|
|
118
|
+
return AnswerType.date
|
|
119
|
+
if head == "how":
|
|
120
|
+
nxt = words[i + 1] if i + 1 < len(words) else ""
|
|
121
|
+
if nxt in ("many", "much", "old", "tall", "long", "far", "big", "large", "high", "deep", "wide"):
|
|
122
|
+
return AnswerType.number
|
|
123
|
+
return AnswerType.entity
|
|
124
|
+
for j, w in enumerate(words[i + 1 : i + 5], start=i + 1): # "what/which <noun>": the noun carries it
|
|
125
|
+
if w == "year" and _AGE_MODIFIER.search(question[: found[j][1]]):
|
|
126
|
+
continue # "43-year-old", "19-year veteran": an age modifier, not the thing asked for
|
|
127
|
+
if w in DATE_NOUNS:
|
|
128
|
+
return AnswerType.date
|
|
129
|
+
if w in PERSON_NOUNS:
|
|
130
|
+
return AnswerType.person
|
|
131
|
+
if w in PLACE_NOUNS:
|
|
132
|
+
return AnswerType.place
|
|
133
|
+
if w in NUMBER_NOUNS:
|
|
134
|
+
return AnswerType.number
|
|
135
|
+
return AnswerType.entity
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def could_be(text: str, want: AnswerType) -> bool:
|
|
139
|
+
"""Could this string be a ``want``? Conservative: unknown kinds are admitted.
|
|
140
|
+
|
|
141
|
+
Only the kinds with a reliable surface signature are checked — a date looks like a date and a
|
|
142
|
+
number looks like a number. ``person``, ``place`` and ``entity`` share one surface (a
|
|
143
|
+
capitalised name), so a person is never rejected for looking like a place; doing that needs a
|
|
144
|
+
gazetteer this does not have, and a wrong rejection costs a correct answer.
|
|
145
|
+
"""
|
|
146
|
+
s = (text or "").strip()
|
|
147
|
+
if not s:
|
|
148
|
+
return True
|
|
149
|
+
low = s.lower()
|
|
150
|
+
if want is AnswerType.yes_no:
|
|
151
|
+
return low in ("yes", "no")
|
|
152
|
+
if want is AnswerType.date:
|
|
153
|
+
return bool(_YEAR.search(s)) or any(m in low for m in MONTHS) or bool(re.fullmatch(r"\d{1,2}[/-]\d{1,2}([/-]\d{2,4})?", s))
|
|
154
|
+
if want is AnswerType.number:
|
|
155
|
+
return bool(_DIGIT.search(s)) or any(w in _words(s) for w in _NUMBER_WORDS)
|
|
156
|
+
# person, place and entity are never rejected. Rejecting bare numbers here looked safe and was
|
|
157
|
+
# not: on train it threw away a Ferrari '458', an area code '284', a South Park episode '201'
|
|
158
|
+
# and the single '212' — numbers name things routinely, so the rule cost correct answers and
|
|
159
|
+
# caught nothing that the kinds above do not already catch.
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _normal(text: str) -> str:
|
|
164
|
+
return " ".join(_words(text))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def contains_words(haystack: str, needle: str) -> bool:
|
|
168
|
+
"""Does ``needle`` appear in ``haystack`` as a whole run of words?
|
|
169
|
+
|
|
170
|
+
Word runs, not characters: "no" is a character substring of "northeastern Ontario" and a
|
|
171
|
+
character test therefore reports that a yes/no answer was lifted from a question about a
|
|
172
|
+
place. Every containment question in this library is about words.
|
|
173
|
+
"""
|
|
174
|
+
h, n = _words(haystack), _words(needle)
|
|
175
|
+
if not n or len(n) > len(h):
|
|
176
|
+
return False
|
|
177
|
+
return any(h[i : i + len(n)] == n for i in range(len(h) - len(n) + 1))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def from_question(span: str, question: str) -> bool:
|
|
181
|
+
"""Is this candidate lifted from the question's own words?
|
|
182
|
+
|
|
183
|
+
On HotpotQA train this is true of the gold answer for 1.6% of bridge questions and 39% of
|
|
184
|
+
comparison questions — a comparison names its own candidates, so the signal only means
|
|
185
|
+
anything for the bridge shape.
|
|
186
|
+
"""
|
|
187
|
+
return contains_words(question, span)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class Rejection:
|
|
192
|
+
"""Why a candidate cannot be this question's answer."""
|
|
193
|
+
|
|
194
|
+
reason: str
|
|
195
|
+
detail: str
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def mismatch(question: str, span: str, *, check_self_reference: bool = True) -> Rejection | None:
|
|
199
|
+
"""``None`` if the candidate is admissible, a :class:`Rejection` if it is confidently wrong.
|
|
200
|
+
|
|
201
|
+
A comparison question is not checked at all: its answer is one of the options it names, so it
|
|
202
|
+
may be lifted from the question, and "which has a greater population, A or B?" is answered by
|
|
203
|
+
a place name rather than by a number. Measured on train, an either/or question that opens with
|
|
204
|
+
an auxiliary is answered by an option rather than by yes/no often enough that even the yes/no
|
|
205
|
+
check costs more than it buys there.
|
|
206
|
+
"""
|
|
207
|
+
if not (span or "").strip():
|
|
208
|
+
return None
|
|
209
|
+
want, form = asked_for(question), shape(question)
|
|
210
|
+
if form is Shape.comparison:
|
|
211
|
+
if want is AnswerType.yes_no and not _ALTERNATIVE.search(question) and not could_be(span, want):
|
|
212
|
+
# "Are both Jonathan Marray and Wayne Black British?" names two entities but offers no
|
|
213
|
+
# alternative to choose between, and 218 of 220 such questions on train take a yes or a
|
|
214
|
+
# no. An either/or comparison is the opposite case and stays unchecked.
|
|
215
|
+
return Rejection("wrong_answer_type", f"{span!r} cannot answer a yes/no comparison")
|
|
216
|
+
return None # otherwise its answer is one of the options it names, in whatever form
|
|
217
|
+
if not could_be(span, want):
|
|
218
|
+
return Rejection("wrong_answer_type", f"{span!r} cannot be a {want.value}")
|
|
219
|
+
if check_self_reference and from_question(span, question):
|
|
220
|
+
return Rejection("taken_from_the_question",
|
|
221
|
+
f"{span!r} is already in the question, so it is what the question travels through")
|
|
222
|
+
return None
|