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/temporal.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Event time and ordering, so tense stops being a label and starts being a question.
|
|
2
|
+
|
|
3
|
+
The grammar reads tense and aspect and puts them in a feature; the projection then drops
|
|
4
|
+
them. So "the grain arrived" and "the grain will arrive" become the same claim, and
|
|
5
|
+
"what did you do before that" has nothing to stand on. Here a parsed tense becomes an
|
|
6
|
+
:class:`~tensorcode.records.Interval` on the claim, and events get times that can be
|
|
7
|
+
ordered, so the graph answers *when* and *in what order*.
|
|
8
|
+
|
|
9
|
+
Two clocks are kept apart, because conflating them is how a record of what was said
|
|
10
|
+
becomes a record of what happened:
|
|
11
|
+
|
|
12
|
+
* ``observed_at`` on evidence: when a source said it. Already in the store.
|
|
13
|
+
* the event's own time: when the thing happened. That is what this module adds.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
21
|
+
|
|
22
|
+
from .outcomes import Unknown
|
|
23
|
+
from .records import Claim, ClaimRecord, Evidence, Interval, Ref, Store
|
|
24
|
+
|
|
25
|
+
#: how a connective relates the clause it introduces to the main clause
|
|
26
|
+
CONNECTIVES: dict[str, str] = {
|
|
27
|
+
"before": "before", "after": "after", "since": "after", "until": "before",
|
|
28
|
+
"while": "during", "when": "during", "whenever": "during", "once": "after",
|
|
29
|
+
"then": "after", "earlier": "before", "later": "after", "meanwhile": "during",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AT = "happened_at"
|
|
33
|
+
ENDED = "ended_at"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def interval_for(features: Mapping[str, Any], now: datetime) -> Interval:
|
|
37
|
+
"""The interval a tense/aspect commits to, relative to ``now``.
|
|
38
|
+
|
|
39
|
+
Deliberately coarse: past means "ended before now", future means "starts after now",
|
|
40
|
+
present means "holds now". A coarse interval that is true beats a precise one invented.
|
|
41
|
+
"""
|
|
42
|
+
tense, aspect = features.get("tense"), features.get("aspect")
|
|
43
|
+
if tense == "past":
|
|
44
|
+
return Interval(None, now) if aspect != "perfect" else Interval(None, now)
|
|
45
|
+
if tense == "future":
|
|
46
|
+
return Interval(now, None)
|
|
47
|
+
if tense == "present":
|
|
48
|
+
return Interval(now, now) if aspect != "progressive" else Interval(now, None)
|
|
49
|
+
return Interval()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Event:
|
|
54
|
+
ref: Ref
|
|
55
|
+
at: datetime
|
|
56
|
+
kind: str | None = None
|
|
57
|
+
|
|
58
|
+
def describe(self) -> str:
|
|
59
|
+
return f"{self.ref.id}{f' ({self.kind})' if self.kind else ''} at {self.at.isoformat(timespec='seconds')}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tell_event(mind: Store, ref: Ref, *, at: datetime, kind: str | None = None, source: Ref,
|
|
63
|
+
ended: datetime | None = None, method: str = "temporal") -> Ref:
|
|
64
|
+
"""Record when an event happened (as distinct from when it was reported)."""
|
|
65
|
+
evidence = Evidence(source=source, observed_at=datetime.now(timezone.utc), method=method)
|
|
66
|
+
mind.tell(Claim(ref, AT, at, valid=Interval(at, ended or at)), evidence)
|
|
67
|
+
if kind:
|
|
68
|
+
mind.tell(Claim(ref, "is_a", kind, valid=Interval(at, ended or at)), evidence)
|
|
69
|
+
if ended:
|
|
70
|
+
mind.tell(Claim(ref, ENDED, ended, valid=Interval(at, ended)), evidence)
|
|
71
|
+
return ref
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def event_time(mind: Store, ref: Ref) -> datetime | Unknown:
|
|
75
|
+
"""When an event happened, by its own clock; falls back to nothing, never to a guess."""
|
|
76
|
+
for record in mind.claims(ref, AT):
|
|
77
|
+
value = record.claim.object
|
|
78
|
+
if isinstance(value, datetime):
|
|
79
|
+
return value
|
|
80
|
+
return Unknown("no_event_time", f"{ref.id} has no recorded time of happening")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def events(mind: Store, *, kind: str | None = None) -> list[Event]:
|
|
84
|
+
"""Every event with a recorded time, earliest first."""
|
|
85
|
+
out: list[Event] = []
|
|
86
|
+
for record in mind.claims(predicate=AT):
|
|
87
|
+
value = record.claim.object
|
|
88
|
+
if not isinstance(value, datetime):
|
|
89
|
+
continue
|
|
90
|
+
ref = record.claim.subject
|
|
91
|
+
of = next((r.claim.object for r in mind.claims(ref, "is_a")), None)
|
|
92
|
+
if kind is not None and of != kind:
|
|
93
|
+
continue
|
|
94
|
+
out.append(Event(ref, value, of if isinstance(of, str) else None))
|
|
95
|
+
return sorted(out, key=lambda e: (e.at, e.ref.id))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def relate(mind: Store, a: Ref, b: Ref, *, tolerance: timedelta = timedelta(0)) -> str | Unknown:
|
|
99
|
+
"""'before', 'after' or 'simultaneous' — or a refusal when either time is unrecorded."""
|
|
100
|
+
ta, tb = event_time(mind, a), event_time(mind, b)
|
|
101
|
+
if isinstance(ta, Unknown):
|
|
102
|
+
return ta
|
|
103
|
+
if isinstance(tb, Unknown):
|
|
104
|
+
return tb
|
|
105
|
+
if abs(ta - tb) <= tolerance:
|
|
106
|
+
return "simultaneous"
|
|
107
|
+
return "before" if ta < tb else "after"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def order(mind: Store, refs: Iterable[Ref]) -> tuple[list[Ref], list[Ref]]:
|
|
111
|
+
"""Refs sorted by their event time, and the ones that have no time to sort by."""
|
|
112
|
+
timed: list[tuple[datetime, Ref]] = []
|
|
113
|
+
untimed: list[Ref] = []
|
|
114
|
+
for ref in refs:
|
|
115
|
+
at = event_time(mind, ref)
|
|
116
|
+
(untimed if isinstance(at, Unknown) else timed).append(ref if isinstance(at, Unknown) else (at, ref)) # type: ignore[arg-type]
|
|
117
|
+
return [ref for _, ref in sorted(timed, key=lambda pair: (pair[0], pair[1].id))], untimed
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def before(mind: Store, ref: Ref, *, kind: str | None = None) -> list[Event]:
|
|
121
|
+
"""Events that happened before this one."""
|
|
122
|
+
at = event_time(mind, ref)
|
|
123
|
+
return [] if isinstance(at, Unknown) else [e for e in events(mind, kind=kind) if e.at < at and e.ref != ref]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def after(mind: Store, ref: Ref, *, kind: str | None = None) -> list[Event]:
|
|
127
|
+
at = event_time(mind, ref)
|
|
128
|
+
return [] if isinstance(at, Unknown) else [e for e in events(mind, kind=kind) if e.at > at and e.ref != ref]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def during(mind: Store, start: datetime, end: datetime, *, kind: str | None = None) -> list[Event]:
|
|
132
|
+
return [e for e in events(mind, kind=kind) if start <= e.at <= end]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def first_seen(record: ClaimRecord) -> datetime | None:
|
|
136
|
+
"""The earliest moment any source put this claim on record."""
|
|
137
|
+
times = [e.observed_at for e in record.evidence]
|
|
138
|
+
return min(times) if times else None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def changed_since(mind: Store, when: datetime, *, scope: Ref | None = None) -> list[Claim]:
|
|
142
|
+
"""Claims that came on record after ``when`` — what is new since a moment.
|
|
143
|
+
|
|
144
|
+
This reads the *reporting* clock, which is the right one for "what changed since my
|
|
145
|
+
last message": the question is about the record, not about when the world moved.
|
|
146
|
+
"""
|
|
147
|
+
out: list[tuple[datetime, Claim]] = []
|
|
148
|
+
for record in mind.claims(scope=scope) if scope is not None else mind.claims():
|
|
149
|
+
seen = first_seen(record)
|
|
150
|
+
if seen is not None and seen > when:
|
|
151
|
+
out.append((seen, record.claim))
|
|
152
|
+
return [claim for _, claim in sorted(out, key=lambda pair: pair[0])]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def since(mind: Store, ref: Ref, *, kind: str | None = None) -> list[Event] | Unknown:
|
|
156
|
+
"""Events after a named event — "what happened since the commit"."""
|
|
157
|
+
at = event_time(mind, ref)
|
|
158
|
+
return at if isinstance(at, Unknown) else after(mind, ref, kind=kind)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def connective_relation(word: str) -> str | Unknown:
|
|
162
|
+
"""What ordering a connective asserts between its clause and the main one."""
|
|
163
|
+
got = CONNECTIVES.get(word.strip().lower())
|
|
164
|
+
return got if got else Unknown("not_a_temporal_connective", word)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def tell_order(mind: Store, earlier: Ref, later: Ref, *, source: Ref, relation: str = "before",
|
|
168
|
+
method: str = "temporal:connective") -> Claim:
|
|
169
|
+
"""Record an ordering asserted by language, when neither event has a clock time.
|
|
170
|
+
|
|
171
|
+
"the grain arrived before the snow came" orders two events without dating either, and
|
|
172
|
+
that is worth keeping: it answers ordering questions that timestamps cannot.
|
|
173
|
+
"""
|
|
174
|
+
claim = Claim(earlier, relation, later)
|
|
175
|
+
mind.tell(claim, Evidence(source=source, observed_at=datetime.now(timezone.utc), method=method))
|
|
176
|
+
return claim
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def ordered_by_claims(mind: Store, a: Ref, b: Ref) -> str | Unknown:
|
|
180
|
+
"""Ordering from asserted ``before``/``after`` claims, for undated events."""
|
|
181
|
+
if mind.claims(a, "before", b):
|
|
182
|
+
return "before"
|
|
183
|
+
if mind.claims(b, "before", a):
|
|
184
|
+
return "after"
|
|
185
|
+
if mind.claims(a, "after", b):
|
|
186
|
+
return "after"
|
|
187
|
+
if mind.claims(b, "after", a):
|
|
188
|
+
return "before"
|
|
189
|
+
return Unknown("no_recorded_order", f"nothing orders {a.id} and {b.id}")
|
tensorcode/wants.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Wants: an open question as a first-class object, and the standing pull toward answering it.
|
|
2
|
+
|
|
3
|
+
``Unknown`` says an operation could not answer. That is where it stops, which is why a
|
|
4
|
+
mind built on it goes quiet instead of going looking. A ``Want`` is the same admission with
|
|
5
|
+
its consequences attached: what would satisfy it, what provenance the answer must have, and
|
|
6
|
+
which candidate satisfiers exist — a memory to search, a modality to look at, a command to
|
|
7
|
+
run, a person to ask, a derivation to attempt.
|
|
8
|
+
|
|
9
|
+
The runtime can then rank wants by what an answer is worth against what it costs, and hand
|
|
10
|
+
the chosen one to whoever can satisfy it. Abstention stays honest at both ends: looking up
|
|
11
|
+
an answer refuses to pick between disagreeing claims, refuses hearsay where the want asked
|
|
12
|
+
for an observation, and a want nothing can satisfy simply stays open.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Iterable, Literal
|
|
20
|
+
|
|
21
|
+
from .outcomes import Score, Unknown
|
|
22
|
+
from .records import ClaimRecord, Ref, Store
|
|
23
|
+
|
|
24
|
+
SatisfierKind = Literal["memory", "perceive", "command", "ask", "derive"]
|
|
25
|
+
|
|
26
|
+
#: which modalities count as first-hand, for a want that requires an observation
|
|
27
|
+
FIRSTHAND = ("pixels", "structure", "text")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Satisfier:
|
|
32
|
+
"""One way this want might be answered, with what it would cost to try."""
|
|
33
|
+
|
|
34
|
+
kind: SatisfierKind
|
|
35
|
+
detail: str # a place to look, a command to run, a question to put to someone
|
|
36
|
+
cost: float = 1.0 # caller's own units (seconds, tokens, keystrokes)
|
|
37
|
+
odds: Score = field(default_factory=lambda: Score(0.5, "uncalibrated"))
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def worth(self) -> float:
|
|
41
|
+
return self.odds.value / max(self.cost, 1e-6)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Want:
|
|
46
|
+
"""A question the mind is holding open."""
|
|
47
|
+
|
|
48
|
+
question: str
|
|
49
|
+
subject: Ref | None = None
|
|
50
|
+
predicate: str | None = None
|
|
51
|
+
expect: type | None = None # what kind of value would satisfy it
|
|
52
|
+
requires: str | None = None # provenance demand: "observed", "not-hearsay", or a modality name
|
|
53
|
+
satisfiers: tuple[Satisfier, ...] = ()
|
|
54
|
+
value: float = 1.0 # how much an answer is worth
|
|
55
|
+
asked_by: Ref | None = None # who wants to know (the user, a rule, a procedure)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def id(self) -> str:
|
|
59
|
+
key = f"{self.question}|{self.subject}|{self.predicate}|{self.requires}"
|
|
60
|
+
return "want:" + hashlib.sha256(key.encode()).hexdigest()[:12]
|
|
61
|
+
|
|
62
|
+
def best_satisfier(self) -> Satisfier | None:
|
|
63
|
+
return max(self.satisfiers, key=lambda s: (s.worth, s.kind), default=None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Answer:
|
|
68
|
+
"""A want satisfied, with the claims that answer it and how they were known."""
|
|
69
|
+
|
|
70
|
+
want_id: str
|
|
71
|
+
value: Any
|
|
72
|
+
claims: tuple[str, ...]
|
|
73
|
+
modality: tuple[str, ...] = ()
|
|
74
|
+
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
return f"{self.value!r} (from {len(self.claims)} claim{'s' * (len(self.claims) != 1)})"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def want_from(unknown: Unknown, question: str, *, subject: Ref | None = None, predicate: str | None = None,
|
|
80
|
+
satisfiers: Iterable[Satisfier] = (), value: float = 1.0) -> Want:
|
|
81
|
+
"""Turn a dead-end ``Unknown`` into a want, keeping its reason and any candidates it had."""
|
|
82
|
+
extra = tuple(Satisfier("derive", f"reconsider {cand!r}", cost=0.5, odds=score) for cand, score in unknown.candidates)
|
|
83
|
+
return Want(question=question, subject=subject, predicate=predicate,
|
|
84
|
+
satisfiers=tuple(satisfiers) + extra, value=value,
|
|
85
|
+
asked_by=Ref(f"unknown:{unknown.reason}"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Wants:
|
|
89
|
+
"""The standing set of open questions."""
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
self.open: dict[str, Want] = {}
|
|
93
|
+
self.answered: dict[str, Answer] = {}
|
|
94
|
+
self.abandoned: dict[str, str] = {}
|
|
95
|
+
|
|
96
|
+
def add(self, want: Want) -> str:
|
|
97
|
+
if want.id not in self.answered:
|
|
98
|
+
self.open[want.id] = want
|
|
99
|
+
return want.id
|
|
100
|
+
|
|
101
|
+
def drop(self, want_id: str, reason: str) -> None:
|
|
102
|
+
self.open.pop(want_id, None)
|
|
103
|
+
self.abandoned[want_id] = reason
|
|
104
|
+
|
|
105
|
+
def satisfied(self, want_id: str, answer: Answer) -> Answer:
|
|
106
|
+
self.open.pop(want_id, None)
|
|
107
|
+
self.answered[want_id] = answer
|
|
108
|
+
return answer
|
|
109
|
+
|
|
110
|
+
# -- ranking
|
|
111
|
+
|
|
112
|
+
def ranked(self) -> list[tuple[Want, Satisfier | None, float]]:
|
|
113
|
+
"""Open wants by what answering them is worth against what it would cost."""
|
|
114
|
+
rows = []
|
|
115
|
+
for want in self.open.values():
|
|
116
|
+
best = want.best_satisfier()
|
|
117
|
+
rows.append((want, best, want.value * (best.worth if best else 0.0)))
|
|
118
|
+
return sorted(rows, key=lambda row: (-row[2], row[0].id))
|
|
119
|
+
|
|
120
|
+
def next_to_pursue(self) -> tuple[Want, Satisfier] | None:
|
|
121
|
+
for want, satisfier, _ in self.ranked():
|
|
122
|
+
if satisfier is not None:
|
|
123
|
+
return want, satisfier
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
# -- answering from memory
|
|
127
|
+
|
|
128
|
+
def look_up(self, want: Want, mind: Store, *, frames: Any = None) -> Answer | Unknown:
|
|
129
|
+
"""Try to answer from what the mind already holds. Refuses to guess."""
|
|
130
|
+
if want.subject is None and want.predicate is None:
|
|
131
|
+
return Unknown("want_underspecified", f"{want.question!r} names neither a subject nor a predicate")
|
|
132
|
+
records = mind.claims(subject=want.subject, predicate=want.predicate)
|
|
133
|
+
if not records:
|
|
134
|
+
return Unknown("not_in_memory", f"nothing recorded for {want.question!r}")
|
|
135
|
+
allowed = [r for r in records if _provenance_ok(r, want.requires, frames)]
|
|
136
|
+
if not allowed:
|
|
137
|
+
kinds = sorted({m for r in records for m in _modalities(r, frames)})
|
|
138
|
+
return Unknown("provenance_unmet", f"{want.question!r} needs {want.requires}; have only {', '.join(kinds) or 'unknown'}",
|
|
139
|
+
candidates=tuple((r.claim.object, Score(0.5, "uncalibrated")) for r in records))
|
|
140
|
+
if want.expect is not None:
|
|
141
|
+
allowed = [r for r in allowed if isinstance(r.claim.object, want.expect)]
|
|
142
|
+
if not allowed:
|
|
143
|
+
return Unknown("wrong_type", f"{want.question!r} expects {want.expect.__name__}")
|
|
144
|
+
distinct = {repr(r.claim.object): r for r in allowed}
|
|
145
|
+
if len(distinct) > 1:
|
|
146
|
+
return Unknown("disagreement", f"memory holds {len(distinct)} different answers to {want.question!r}",
|
|
147
|
+
candidates=tuple((r.claim.object, Score(1 / len(distinct), "vote_share")) for r in distinct.values()))
|
|
148
|
+
record = next(iter(distinct.values()))
|
|
149
|
+
return Answer(want.id, record.claim.object, tuple(sorted(r.id for r in allowed)), _modalities(record, frames))
|
|
150
|
+
|
|
151
|
+
def pursue_from_memory(self, mind: Store, *, frames: Any = None) -> list[Answer]:
|
|
152
|
+
"""Answer every open want memory can already settle; leave the rest open."""
|
|
153
|
+
out = []
|
|
154
|
+
for want in list(self.open.values()):
|
|
155
|
+
found = self.look_up(want, mind, frames=frames)
|
|
156
|
+
if isinstance(found, Answer):
|
|
157
|
+
out.append(self.satisfied(want.id, found))
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
def as_unknown(self, want: Want) -> Unknown:
|
|
161
|
+
"""What to hand a caller that wanted an answer now: honest, with what we would try next."""
|
|
162
|
+
best = want.best_satisfier()
|
|
163
|
+
detail = f"{want.question}; next I would {best.kind} ({best.detail})" if best else f"{want.question}; nothing I can do would answer it"
|
|
164
|
+
return Unknown("open_want", detail)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _modalities(rec: ClaimRecord, frames: Any) -> tuple[str, ...]:
|
|
168
|
+
if frames is not None:
|
|
169
|
+
binding = frames.binding(rec.id)
|
|
170
|
+
if binding is not None:
|
|
171
|
+
return binding.modality
|
|
172
|
+
from .frames import modality_of
|
|
173
|
+
|
|
174
|
+
return modality_of(rec)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _provenance_ok(rec: ClaimRecord, requires: str | None, frames: Any) -> bool:
|
|
178
|
+
if requires is None:
|
|
179
|
+
return True
|
|
180
|
+
mods = _modalities(rec, frames)
|
|
181
|
+
if requires == "observed":
|
|
182
|
+
return any(m in FIRSTHAND for m in mods)
|
|
183
|
+
if requires == "not-hearsay":
|
|
184
|
+
return "hearsay" not in mods
|
|
185
|
+
return requires in mods
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tensorcode
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Typed cognitive operations with swappable implementations: rules, learned models, or general models, chosen by policy
|
|
5
|
+
Project-URL: Homepage, https://github.com/TensaCo/tensacode-py
|
|
6
|
+
Project-URL: Source, https://github.com/TensaCo/tensacode-py
|
|
7
|
+
Project-URL: Issues, https://github.com/TensaCo/tensacode-py/issues
|
|
8
|
+
Project-URL: Design, https://github.com/TensaCo/tensacode-py/tree/main/docs/revival
|
|
9
|
+
Author-email: Jacob Valdez <jacob.valdez@tensaco.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,cascade,cognitive-architecture,neurosymbolic,typed-operations
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: numpy>=1.26; extra == 'dev'
|
|
28
|
+
Requires-Dist: pydantic>=2.5; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Provides-Extra: learned
|
|
31
|
+
Requires-Dist: numpy>=1.26; extra == 'learned'
|
|
32
|
+
Requires-Dist: scikit-learn>=1.5; extra == 'learned'
|
|
33
|
+
Provides-Extra: learned-neural
|
|
34
|
+
Requires-Dist: torch>=2.1; extra == 'learned-neural'
|
|
35
|
+
Requires-Dist: transformers>=4.40; extra == 'learned-neural'
|
|
36
|
+
Provides-Extra: local-model
|
|
37
|
+
Requires-Dist: torch>=2.4; extra == 'local-model'
|
|
38
|
+
Requires-Dist: transformers>=4.45; extra == 'local-model'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# TensorCode
|
|
42
|
+
|
|
43
|
+
**Typed cognitive operations with swappable implementations.** Your program says *what*
|
|
44
|
+
it needs (parse this, classify that, choose an action under these constraints, check this
|
|
45
|
+
claim). A policy decides *which* implementation answers: rules, a learned model, or a
|
|
46
|
+
general model. Every answer is validated against the operation's type, every abstention
|
|
47
|
+
is an explicit value, and every attempt is traced.
|
|
48
|
+
|
|
49
|
+
> **Status: pre-alpha (0.1.0a1).** The API will change. The core has no third-party
|
|
50
|
+
> dependencies. Nothing here calls a language model unless you bind one.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install --pre tensorcode
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.11+.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import enum
|
|
62
|
+
import tensorcode as tc
|
|
63
|
+
from tensorcode.backends.builtin import KeywordClassifier
|
|
64
|
+
|
|
65
|
+
class Intent(enum.Enum):
|
|
66
|
+
LOST_CARD = "lost_card"
|
|
67
|
+
REFUND = "refund"
|
|
68
|
+
|
|
69
|
+
print(tc.classify("I lost my card", Intent))
|
|
70
|
+
# Unknown(reason='no_implementation', ...) <- nothing bound: an explicit Unknown, not a guess
|
|
71
|
+
|
|
72
|
+
rules = KeywordClassifier(Intent, {
|
|
73
|
+
Intent.LOST_CARD: [r"\blost\b", r"\bstolen\b"],
|
|
74
|
+
Intent.REFUND: [r"\brefund\b"],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
@tc.implementation("classify", name="fallback", version="1")
|
|
78
|
+
def ask_a_human(request):
|
|
79
|
+
return tc.Unknown("needs_human", request.subject)
|
|
80
|
+
|
|
81
|
+
with tc.use(tc.Runtime([rules, ask_a_human])) as rt:
|
|
82
|
+
print(tc.classify("I lost my card", Intent)) # Intent.LOST_CARD
|
|
83
|
+
print(tc.classify("where is my parcel?", Intent)) # Unknown(reason='needs_human', ...)
|
|
84
|
+
print(rt.trace.render())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
classify -> Intent.LOST_CARD [0.04 ms total, 0.01 ms backend]
|
|
89
|
+
- keyword-rules@1 answer
|
|
90
|
+
classify -> {'unknown': 'needs_human', ...} [0.03 ms total, 0.01 ms backend]
|
|
91
|
+
- keyword-rules@1 abstain: no_rule_matched
|
|
92
|
+
- fallback@1 abstain: needs_human, usd=? (unknown)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The program never names a backend. Swap the rules for a scikit-learn classifier or a local
|
|
96
|
+
model by changing the `Runtime`, not the program.
|
|
97
|
+
|
|
98
|
+
## Core ideas
|
|
99
|
+
|
|
100
|
+
**Operations fix the meaning.** Each facade validates its output and fails closed:
|
|
101
|
+
|
|
102
|
+
| Family | Operations |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| infer | `parse`, `classify`, `choose`, `rank` |
|
|
105
|
+
| check | `check`, `verify` |
|
|
106
|
+
| rewrite | `propose` (a `Patch`, inert until `Store.apply`) |
|
|
107
|
+
| act | `invoke` (returns a `Receipt`), `Plan`, `run_plan` |
|
|
108
|
+
| context | `pack`, `dedupe` |
|
|
109
|
+
|
|
110
|
+
`classify` estimates what *is true*. `choose` selects what *to do*, under an `Objective`
|
|
111
|
+
and hard `Constraint`s that TensorCode checks itself before any backend sees the options.
|
|
112
|
+
|
|
113
|
+
**Outcome values keep distinctions a caller must not collapse.**
|
|
114
|
+
- `Unknown` is not `False` and not a low-confidence guess. It raises if used as a boolean.
|
|
115
|
+
- `Verdict` has three states. `fails` and `unknown` are different.
|
|
116
|
+
- `Receipt` separates "did not happen" from "may have happened".
|
|
117
|
+
- `Score` says what kind of number it is. A similarity is not a probability, and a
|
|
118
|
+
probability must name the data it was calibrated on.
|
|
119
|
+
|
|
120
|
+
**The runtime binds implementations by policy.** An implementation declares `Traits`
|
|
121
|
+
(locality, egress, determinism, requirements) and a measured `Profile`. `Policy` filters
|
|
122
|
+
on hard constraints and orders a cascade; `Budget` caps cost and attempts across calls.
|
|
123
|
+
Unmeasured means unknown: a missing cost is never counted as zero, and a cost cap excludes
|
|
124
|
+
implementations whose cost is unknown.
|
|
125
|
+
|
|
126
|
+
**Records carry evidence.** `Store` holds entity and claim records with evidence, validity
|
|
127
|
+
intervals and scope, and detects conflicts between them.
|
|
128
|
+
|
|
129
|
+
## Optional extras
|
|
130
|
+
|
|
131
|
+
| Extra | Installs | For |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `learned` | scikit-learn, numpy | `tensorcode.backends.linear` |
|
|
134
|
+
| `local-model` | torch, transformers | `tensorcode.backends.hf_local` |
|
|
135
|
+
| `learned-neural` | torch, transformers | `tensorcode.backends.neural` |
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
The examples live in this repository, not in the package. Run them from a checkout:
|
|
140
|
+
|
|
141
|
+
| Example | Command |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| Recovery after failed or ambiguous actions | `python -m examples.recovery.demo` |
|
|
144
|
+
| Knowledge store: ingest, conflicts, queries | `python -m examples.knowledge.demo` |
|
|
145
|
+
| Packing context into a token budget | `python -m examples.context_select.demo` |
|
|
146
|
+
| Support router (needs the Banking77 train CSV) | `python -m examples.support_router.demo --train banking77_train.csv` |
|
|
147
|
+
| Decision service with HTTP API and operator UI | `python -m examples.decisions.service` |
|
|
148
|
+
| Live browser agents, no model calls (needs `playwright`, `numpy`, `pillow`) | `python -m examples.browser_agents.live` |
|
|
149
|
+
|
|
150
|
+
Expected output is checked in beside each demo (`OUTPUT*.txt`).
|
|
151
|
+
|
|
152
|
+
## What has been measured, honestly
|
|
153
|
+
|
|
154
|
+
The design notes in [`docs/revival/`](https://github.com/TensaCo/tensacode-py/blob/main/docs/revival/README.md) report measurements,
|
|
155
|
+
including the unflattering ones:
|
|
156
|
+
|
|
157
|
+
- A local zero-shot Qwen3-8B escalation tier **lowered** Banking77 selective accuracy from
|
|
158
|
+
94.1% to 91.0%. Tiers should be admitted only by measured quality.
|
|
159
|
+
- Recovery logic based on explicit facts made 0 duplicate money movements in 5,000
|
|
160
|
+
simulated episodes, compared with 4.8% for naive retry. The simulator and its fault model
|
|
161
|
+
are the project's own, and with wrong facts about the target system duplicates return (0.9%).
|
|
162
|
+
- The [evidence audit](https://github.com/TensaCo/tensacode-py/blob/main/docs/revival/11-evidence-audit.md) found that 13 of the project's 19
|
|
163
|
+
headline results ran in environments it wrote **and** were graded by code it wrote. On
|
|
164
|
+
public open-domain benchmarks, the cascade did worse than plainly prompting the same
|
|
165
|
+
model on 3 of 4.
|
|
166
|
+
|
|
167
|
+
Treat the agent results as demonstrations, not benchmarks.
|
|
168
|
+
|
|
169
|
+
## Repository layout
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
src/tensorcode/ the package
|
|
173
|
+
tests/ pytest suite (some tests use examples/ and eval/)
|
|
174
|
+
examples/ runnable programs and agents
|
|
175
|
+
eval/ evaluation scripts and result files (eval/results/*.json)
|
|
176
|
+
research/ experiments, including a civilization simulation
|
|
177
|
+
docs/revival/ design notes and measurements
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e ".[dev,learned]"
|
|
184
|
+
python -m pytest -q
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Evaluation scripts read downloaded datasets and trained artifacts from `$TENSORCODE_SCRATCH`
|
|
188
|
+
(default `~/.cache/tensorcode`). Tests that need those artifacts skip when they are absent.
|
|
189
|
+
|
|
190
|
+
The legacy 2023–2024 package (`tensacode`, with `Engine` and TCIR; never published) is
|
|
191
|
+
preserved at the git tag `legacy-2024-11`. It was never functional and is not compatible
|
|
192
|
+
with this one. The PyPI name `tensacode` is a placeholder that installs `tensorcode`.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
[MIT](https://github.com/TensaCo/tensacode-py/blob/main/LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
tensorcode/__init__.py,sha256=l_9uvB4iV6oudtolL6nAcm9aiuUNtc9OawNb4TeX_lo,1757
|
|
2
|
+
tensorcode/actions.py,sha256=aje5U5B2I5NPS4neaMX3Vonup7_P-r1rF3Esp3RQO9w,5270
|
|
3
|
+
tensorcode/answer_type.py,sha256=VJfyElor9FYWB9uEBASaOm8LDfEY_3R8y_GZw3ID_Wc,11506
|
|
4
|
+
tensorcode/awareness.py,sha256=l-MMW9FiNlzyJybovFiIXVWWmPTIkQ5v7ArLGNgATL4,15478
|
|
5
|
+
tensorcode/causal.py,sha256=hpQZo7nwo8jr7l-J_gctX8ihnoyjHLeclQKy6hmdELg,11339
|
|
6
|
+
tensorcode/change.py,sha256=UidBe9xMxR6wHABgt8jyBFpKYBw_ado21LTmLwqb9Ro,25745
|
|
7
|
+
tensorcode/chunking.py,sha256=PyFZSZd2IB3tU3Epa0R7KFRw2wFCvt-IVJcxrClGnN4,8866
|
|
8
|
+
tensorcode/cognition.py,sha256=8O3yAo1Y_hUS40gVfr8M6namOys8mndICKqr4RZZL20,15248
|
|
9
|
+
tensorcode/context.py,sha256=gaoydZjPlNOyMptOSJgkJcDPLlVO-jB3quxB_TSk3Ow,3289
|
|
10
|
+
tensorcode/control.py,sha256=YMoh4zdfvrvmOIWF337s3U01cV3Z_q-LKcv71QLdppY,14440
|
|
11
|
+
tensorcode/cues.py,sha256=SlzmpCRlfq0IXKdEJWhg1bWAQBMZVagB7EEEamTG7Vs,9084
|
|
12
|
+
tensorcode/expectation.py,sha256=C7t-peMTPFVZ53NYhmw_MB3R1p07sxw7X9GKT_8SbeI,12487
|
|
13
|
+
tensorcode/frames.py,sha256=111aKoy-BsGX4PwYb8Gc82oZEAiTDWZ2CvkZOBCHvA8,10520
|
|
14
|
+
tensorcode/memory.py,sha256=zTc2OPpQ4V6YljwIftcry-Zr6oP-0w2HVX6xEmqqHUU,15431
|
|
15
|
+
tensorcode/metacognition.py,sha256=dDtK0slx9iPUvXDig-elFBbs0OT7b9_2N6Apv385Z3k,15401
|
|
16
|
+
tensorcode/ops.py,sha256=PE_5h-d_s9ya390UChruJ63e1Z7QIRPGlbkDRFjGNfk,9357
|
|
17
|
+
tensorcode/outcomes.py,sha256=jHIz-k4-kMNRzIlYgBn-dZjbaXsYcYbwYV1LksW3T4I,3241
|
|
18
|
+
tensorcode/permanence.py,sha256=AJVx5hHWDiJyt7MD4DOz8xCYN4Lpurj-TDyknYh1GDg,16828
|
|
19
|
+
tensorcode/priming.py,sha256=loygI3tpvXqDwNRFQo2ziL3H1pni8AFNcZN6vQnKtp4,8712
|
|
20
|
+
tensorcode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
tensorcode/quantity.py,sha256=vE1uHxdt309-ceQJlabUjRI4xvYDcOluum0aSC68aQ4,12712
|
|
22
|
+
tensorcode/records.py,sha256=22PIx2UlR53Qno8wJJd4DzELgCSD0W60IRtFVnHwVQ0,28792
|
|
23
|
+
tensorcode/relation.py,sha256=l0FTze-ehHTnay24MKccHX81VBdTsKjJC68dAwfbJ3A,38445
|
|
24
|
+
tensorcode/runtime.py,sha256=VIrJ6CD3wSfzQw2_OXI3mjttvJTp8TjLpNPUrersHrY,18717
|
|
25
|
+
tensorcode/semantics_bridge.py,sha256=OB9PxMDQd-6uxuPpgooKaYVHBV3bAqRrjEBQvWEr1EA,14044
|
|
26
|
+
tensorcode/social.py,sha256=123-p5sV9EZfpXlSNvpUuUjvVuNs6oB-d2Y680M1BSI,15571
|
|
27
|
+
tensorcode/temporal.py,sha256=TzdXKR0mjT2y4VqWgEzdJHNLwfw3Ea1EFWapiDrEAbs,8131
|
|
28
|
+
tensorcode/wants.py,sha256=PikmA8J4lpmZvBfPVn1erKMwG3AXpUFyvjj4mhgU-A4,7998
|
|
29
|
+
tensorcode/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
tensorcode/backends/builtin.py,sha256=7PYSON5zIBN8ODewf-2kAPYY0hea7UZkx3nYCPFQxE8,6380
|
|
31
|
+
tensorcode/backends/hf_local.py,sha256=QcEbb1aRG2L32Jf5QwZZ25VnZ6_luMlUbICCBlLw-ag,3859
|
|
32
|
+
tensorcode/backends/linear.py,sha256=SzWO7xSXfeNIZfjAXACCJkSkBT-QxzaiHBExA9hTPCY,5192
|
|
33
|
+
tensorcode/backends/neural.py,sha256=kimyRKZ66u5Gy0n89uGnGt1ZjEJ10lpQqvVK85-K6xY,17664
|
|
34
|
+
tensorcode/language/__init__.py,sha256=gt8RZKBT0V3g06OO4sjJ-STOFV0unnal4HAfiZ6JdxY,1913
|
|
35
|
+
tensorcode/language/chart.py,sha256=yW_J4OVY-fjkcM7wMzgXnTiIWX18850XQeiKUDBszpc,24453
|
|
36
|
+
tensorcode/language/discourse.py,sha256=YMFDI_q-2hKW8THOwED3WpQMwfLSkEIai0lCQSlK-o4,5778
|
|
37
|
+
tensorcode/language/english.py,sha256=5_DubSoy2TY_17NCqL7yTk7qgPxuNuAHlQTgkKkCrUE,30323
|
|
38
|
+
tensorcode/language/features.py,sha256=gEkHNiEwQ2PGP66WIRhB4UPjeHsTjLTbdVXz5Dfc10o,3685
|
|
39
|
+
tensorcode/language/generate.py,sha256=YdeZG_q15bZtpOZ2IjXpMRwKcWPez1Oxo94tyakZFC8,25516
|
|
40
|
+
tensorcode/language/grammar.py,sha256=ohDAc9QjV3VW6SDGLP94DBERdth0Urw3Mmfuo3uDc2Y,38625
|
|
41
|
+
tensorcode/language/semantics.py,sha256=zZbcIo-_diigCV-sNB5sxsTGGM7TEUOA36mzkOZG0z0,14014
|
|
42
|
+
tensorcode/language/domains/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
tensorcode/language/domains/desktop.py,sha256=EE5OkwamI42N4K4LOc7G8njuQfWqAVDr0JLfedTpsZI,27227
|
|
44
|
+
tensorcode/learning/__init__.py,sha256=fug2qJJCzkndvGkIEP0Cj_sHnNUGwxPi0yEBntm82X8,1931
|
|
45
|
+
tensorcode/learning/certificate.py,sha256=GO3OIWgHNmtQeKa_ubbzDbcKOpL3FKG6htIBncftLI0,5841
|
|
46
|
+
tensorcode/learning/induce.py,sha256=1Je9ww3LEBYB7455UWYz53VIgBPcLPxsBB4ARkUku3k,12674
|
|
47
|
+
tensorcode/learning/library.py,sha256=1_P7K3hNDArEcqUZCv3OkkzmeMFxeLZwCc6ICbhlzv8,8973
|
|
48
|
+
tensorcode/learning/literals.py,sha256=M2ZNkWc9OsHQ46NsqhQ5S5Zf0zG8ordz9UFQuvSaz0c,5153
|
|
49
|
+
tensorcode/learning/verify.py,sha256=8cZ3th6Ykh1MKq-jjRQGAWTp4PqFlZacRC68pRgGgAY,11216
|
|
50
|
+
tensorcode-0.1.0a1.dist-info/METADATA,sha256=Q6-TmA7oOL24IIWiQgMecILsO7uoKsPKtDFoFZhti1M,8391
|
|
51
|
+
tensorcode-0.1.0a1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
52
|
+
tensorcode-0.1.0a1.dist-info/licenses/LICENSE,sha256=TiTkvbmxCUbINDzXo4oFwhjC6LR417aOQ8y-wh2pNVM,1074
|
|
53
|
+
tensorcode-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2026 Jacob Valdez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|