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.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. 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