theoremql 0.3.1__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.
- theorem/__init__.py +54 -0
- theorem/__main__.py +3 -0
- theorem/ast_nodes.py +175 -0
- theorem/canonical.py +200 -0
- theorem/cli.py +367 -0
- theorem/engine/__init__.py +0 -0
- theorem/engine/dedup.py +107 -0
- theorem/engine/executor.py +1154 -0
- theorem/engine/health.py +24 -0
- theorem/engine/storage.py +693 -0
- theorem/engine/text.py +53 -0
- theorem/engine/writes.py +500 -0
- theorem/ingest/__init__.py +4 -0
- theorem/ingest/bulk.py +213 -0
- theorem/ingest/chunk.py +88 -0
- theorem/ingest/envelope.py +31 -0
- theorem/ingest/extract.py +125 -0
- theorem/ingest/normalize.py +393 -0
- theorem/ingest/playbook.py +199 -0
- theorem/ingest/runners.py +141 -0
- theorem/ingest/sniff.py +118 -0
- theorem/ingest/stage.py +196 -0
- theorem/parser.py +772 -0
- theorem/prompt.py +224 -0
- theorem/py.typed +0 -0
- theorem/schema.py +151 -0
- theorem/session.py +278 -0
- theorem/verifier.py +518 -0
- theoremql-0.3.1.dist-info/METADATA +203 -0
- theoremql-0.3.1.dist-info/RECORD +32 -0
- theoremql-0.3.1.dist-info/WHEEL +4 -0
- theoremql-0.3.1.dist-info/entry_points.txt +3 -0
theorem/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""theorem: a graph query and construction language agents can't get wrong.
|
|
2
|
+
|
|
3
|
+
The whole surface an embedding application needs:
|
|
4
|
+
|
|
5
|
+
from theorem import Schema, Session
|
|
6
|
+
|
|
7
|
+
with Session("mydb", Schema()) as db:
|
|
8
|
+
print(db.run('derive class supplier from entity with {country: str}'))
|
|
9
|
+
print(db.run('assert supplier {name: "VoltaChem", country: "DE"} as v'))
|
|
10
|
+
print(db.run('find supplier where country = "DE" as s\\nreturn s.name'))
|
|
11
|
+
|
|
12
|
+
`Schema()` is the base schema: `entity` to derive domain classes from,
|
|
13
|
+
plus the document classes the ingest pipeline uses. `Schema.supply_chain()`
|
|
14
|
+
adds the demo classes the tutorial is written against.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .canonical import CanonicalError, canonical
|
|
18
|
+
from .engine.executor import ExecError, Limits, limits
|
|
19
|
+
from .engine.storage import Store, StoreError, StoreLocked
|
|
20
|
+
from .ingest.bulk import LoadError, load_edges, load_nodes
|
|
21
|
+
from .parser import ParseError, parse
|
|
22
|
+
from .prompt import Answer, agent_prompt, answer, repair_prompt
|
|
23
|
+
from .schema import ClassDef, EdgeDef, Schema
|
|
24
|
+
from .session import Session
|
|
25
|
+
from .verifier import VerifyError, verify
|
|
26
|
+
|
|
27
|
+
__version__ = "0.3.1"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Answer",
|
|
31
|
+
"CanonicalError",
|
|
32
|
+
"ClassDef",
|
|
33
|
+
"EdgeDef",
|
|
34
|
+
"ExecError",
|
|
35
|
+
"Limits",
|
|
36
|
+
"LoadError",
|
|
37
|
+
"ParseError",
|
|
38
|
+
"Schema",
|
|
39
|
+
"Session",
|
|
40
|
+
"Store",
|
|
41
|
+
"StoreError",
|
|
42
|
+
"StoreLocked",
|
|
43
|
+
"VerifyError",
|
|
44
|
+
"__version__",
|
|
45
|
+
"agent_prompt",
|
|
46
|
+
"answer",
|
|
47
|
+
"canonical",
|
|
48
|
+
"limits",
|
|
49
|
+
"load_edges",
|
|
50
|
+
"load_nodes",
|
|
51
|
+
"parse",
|
|
52
|
+
"repair_prompt",
|
|
53
|
+
"verify",
|
|
54
|
+
]
|
theorem/__main__.py
ADDED
theorem/ast_nodes.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""AST node definitions for theorem statements.
|
|
2
|
+
|
|
3
|
+
Col is a tuple of dotted path segments: ("sups", "name") for sups.name.
|
|
4
|
+
Cond is a list of (joiner, Clause) pairs; the first joiner is always "and".
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
Col = tuple[str, ...]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Clause:
|
|
16
|
+
col: Col
|
|
17
|
+
op: str # = != > >= < <= contains
|
|
18
|
+
value: object
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Cond = list[tuple[str, Clause]] # joiner is "and" | "or"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Stmt:
|
|
26
|
+
line: int = field(default=0, kw_only=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Find(Stmt):
|
|
31
|
+
target: str # class name, or "nodes" | "dup_candidates" | "class"
|
|
32
|
+
cond: Cond
|
|
33
|
+
name: str
|
|
34
|
+
order_by: Col | None = None
|
|
35
|
+
desc: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Follow(Stmt):
|
|
40
|
+
src: str
|
|
41
|
+
edge: str
|
|
42
|
+
role: str
|
|
43
|
+
name: str
|
|
44
|
+
cond: Cond = field(default_factory=list) # filters the arrival node
|
|
45
|
+
optional: bool = False # "or none": keep rows that matched nothing
|
|
46
|
+
upto: int | None = None # "upto N": walk 1..N times; None means once,
|
|
47
|
+
# 0 means "upto any", to exhaustion
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Or(Stmt):
|
|
52
|
+
"""Separates alternative branches; their results are unioned."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Keep(Stmt):
|
|
57
|
+
"""Filter the rows that exist at this point, groups included."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
cond: Cond
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class GroupBy(Stmt):
|
|
65
|
+
col: Col
|
|
66
|
+
name: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Aggregate(Stmt):
|
|
71
|
+
op: str # count sum avg min max
|
|
72
|
+
distinct: bool
|
|
73
|
+
col: Col
|
|
74
|
+
name: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Compute(Stmt):
|
|
79
|
+
left: Col
|
|
80
|
+
op: str # plus minus times over same
|
|
81
|
+
right: Col
|
|
82
|
+
name: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Return(Stmt):
|
|
87
|
+
cols: list[Col]
|
|
88
|
+
order_by: Col | None
|
|
89
|
+
desc: bool
|
|
90
|
+
limit: int | None
|
|
91
|
+
budget: int # defaulted to 2000 when unstated
|
|
92
|
+
after: str | None # position token @t-N
|
|
93
|
+
distinct: bool = False # dedup on the projected values, not identity
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Continue(Stmt):
|
|
98
|
+
handle: str # continuation token @cXXXX
|
|
99
|
+
budget: int
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class AssertNode(Stmt):
|
|
104
|
+
cls: str
|
|
105
|
+
props: dict[str, object]
|
|
106
|
+
source: str | None
|
|
107
|
+
name: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class AssertEdge(Stmt):
|
|
112
|
+
edge: str
|
|
113
|
+
role_refs: dict[str, str] # role name -> binding name or node id
|
|
114
|
+
source: str | None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class Merge(Stmt):
|
|
119
|
+
a: str
|
|
120
|
+
b: str
|
|
121
|
+
policy: str # "newest" or "source <provenance>"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class Distinct(Stmt):
|
|
126
|
+
a: str
|
|
127
|
+
b: str
|
|
128
|
+
reason: str
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class Refine(Stmt):
|
|
133
|
+
ref: str
|
|
134
|
+
into_cls: str
|
|
135
|
+
mapping: dict[str, str] # target prop -> source column name
|
|
136
|
+
name: str
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class Compact(Stmt):
|
|
141
|
+
src: str
|
|
142
|
+
name: str
|
|
143
|
+
props: dict[str, object]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class Retire(Stmt):
|
|
148
|
+
ref: str
|
|
149
|
+
reason: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class Flag(Stmt):
|
|
154
|
+
ref: str
|
|
155
|
+
reason: str
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class DeriveClass(Stmt):
|
|
160
|
+
name: str
|
|
161
|
+
base: str
|
|
162
|
+
props: dict[str, str] # prop name -> type name (str|int|float|bool)
|
|
163
|
+
quota: int | None = None
|
|
164
|
+
dedup: float | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class DeriveEdge(Stmt):
|
|
169
|
+
name: str
|
|
170
|
+
roles: dict[str, str] # role name -> class name, exactly two
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class SchemaStmt(Stmt):
|
|
175
|
+
pass
|
theorem/canonical.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Render a parsed program back to its one canonical spelling.
|
|
2
|
+
|
|
3
|
+
The language has exactly one way to write each operation, so two correct
|
|
4
|
+
answers to the same question are the same program. That is what makes a
|
|
5
|
+
plan cache and an audit log possible. It held of the text until the
|
|
6
|
+
parser started accepting one redundant spelling, a condition qualified by
|
|
7
|
+
the binding its own statement creates, and normalizing it away.
|
|
8
|
+
|
|
9
|
+
Printing the parse restores the property at the level a cache can use:
|
|
10
|
+
|
|
11
|
+
canonical(query) == canonical(other) iff they are the same program
|
|
12
|
+
|
|
13
|
+
Round-tripping is the test that keeps this honest. `parse(canonical(p))`
|
|
14
|
+
must equal `parse(p)` for every program, which is checked against every
|
|
15
|
+
query the benchmark has ever generated.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .ast_nodes import (
|
|
21
|
+
Aggregate,
|
|
22
|
+
AssertEdge,
|
|
23
|
+
AssertNode,
|
|
24
|
+
Clause,
|
|
25
|
+
Col,
|
|
26
|
+
Compact,
|
|
27
|
+
Compute,
|
|
28
|
+
Cond,
|
|
29
|
+
Continue,
|
|
30
|
+
DeriveClass,
|
|
31
|
+
DeriveEdge,
|
|
32
|
+
Distinct,
|
|
33
|
+
Find,
|
|
34
|
+
Flag,
|
|
35
|
+
Follow,
|
|
36
|
+
GroupBy,
|
|
37
|
+
Keep,
|
|
38
|
+
Merge,
|
|
39
|
+
Or,
|
|
40
|
+
Refine,
|
|
41
|
+
Retire,
|
|
42
|
+
Return,
|
|
43
|
+
SchemaStmt,
|
|
44
|
+
Stmt,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CanonicalError(Exception):
|
|
49
|
+
"""A statement this printer does not know how to render."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def canonical(program: str) -> str:
|
|
53
|
+
"""The canonical text of a program, given its text."""
|
|
54
|
+
from .parser import parse
|
|
55
|
+
|
|
56
|
+
return render(parse(program))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def render(stmts: list[Stmt]) -> str:
|
|
60
|
+
return "\n".join(render_stmt(s) for s in stmts)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _col(col: Col) -> str:
|
|
64
|
+
return ".".join(col)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _quoted(v: str) -> str:
|
|
68
|
+
"""A string literal, with the escapes the parser undoes put back.
|
|
69
|
+
|
|
70
|
+
Backslash first, so escaping a quote does not double the backslash
|
|
71
|
+
that escapes it.
|
|
72
|
+
"""
|
|
73
|
+
return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _literal(v: object) -> str:
|
|
77
|
+
if type(v).__name__ == "_Missing":
|
|
78
|
+
return "none"
|
|
79
|
+
if isinstance(v, bool):
|
|
80
|
+
return "true" if v else "false"
|
|
81
|
+
if isinstance(v, str):
|
|
82
|
+
return _quoted(v)
|
|
83
|
+
if isinstance(v, float) and v.is_integer():
|
|
84
|
+
# 3.0 and 3 compare equal and mean the same filter; one spelling.
|
|
85
|
+
return str(int(v))
|
|
86
|
+
return str(v)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _clause(c: Clause) -> str:
|
|
90
|
+
return f"{_col(c.col)} {c.op} {_literal(c.value)}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cond(cond: Cond) -> str:
|
|
94
|
+
out = []
|
|
95
|
+
for i, (joiner, clause) in enumerate(cond):
|
|
96
|
+
out.append(_clause(clause) if i == 0 else f"{joiner} {_clause(clause)}")
|
|
97
|
+
return " ".join(out)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _where(cond: Cond) -> str:
|
|
101
|
+
return f" where {_cond(cond)}" if cond else ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _order(col: Col | None, desc: bool) -> str:
|
|
105
|
+
if col is None:
|
|
106
|
+
return ""
|
|
107
|
+
return f" order by {_col(col)}" + (" desc" if desc else "")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _props(props: dict) -> str:
|
|
111
|
+
return "{" + ", ".join(f"{k}: {_literal(v)}" for k, v in props.items()) + "}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_stmt(stmt: Stmt) -> str:
|
|
115
|
+
match stmt:
|
|
116
|
+
case Find(target=t, cond=cond, name=n, order_by=ob, desc=d):
|
|
117
|
+
# The condition comes before `as`, which is the spelling the
|
|
118
|
+
# grammar leads with; the trailing form parses to the same tree.
|
|
119
|
+
return f"find {t}{_where(cond)}{_order(ob, d)} as {n}"
|
|
120
|
+
case Follow(
|
|
121
|
+
src=src, edge=e, role=r, name=n, cond=cond, optional=opt, upto=upto
|
|
122
|
+
):
|
|
123
|
+
reach = ""
|
|
124
|
+
if upto == 0:
|
|
125
|
+
reach = " upto any"
|
|
126
|
+
elif upto is not None and upto != 1:
|
|
127
|
+
reach = f" upto {upto}"
|
|
128
|
+
tail = " or none" if opt else ""
|
|
129
|
+
return f"follow {src} {e} {r}{_where(cond)}{reach} as {n}{tail}"
|
|
130
|
+
case Or():
|
|
131
|
+
return "or"
|
|
132
|
+
case GroupBy(col=col, name=n):
|
|
133
|
+
return f"group by {_col(col)} as {n}"
|
|
134
|
+
case Aggregate(op=op, distinct=dist, col=col, name=n):
|
|
135
|
+
return f"{op}{' distinct' if dist else ''} {_col(col)} as {n}"
|
|
136
|
+
case Keep(name=n, cond=cond):
|
|
137
|
+
return f"keep {n} where {_cond(cond)}"
|
|
138
|
+
case Compute(left=lhs, op=op, right=rhs, name=n):
|
|
139
|
+
return f"compute {_col(lhs)} {op} {_col(rhs)} as {n}"
|
|
140
|
+
case Return(
|
|
141
|
+
cols=cols,
|
|
142
|
+
order_by=ob,
|
|
143
|
+
desc=d,
|
|
144
|
+
limit=lim,
|
|
145
|
+
budget=budget,
|
|
146
|
+
after=after,
|
|
147
|
+
distinct=dist,
|
|
148
|
+
):
|
|
149
|
+
out = "return" + (" distinct" if dist else "")
|
|
150
|
+
out += " " + ", ".join(_col(c) for c in cols)
|
|
151
|
+
out += _order(ob, d)
|
|
152
|
+
if lim is not None:
|
|
153
|
+
out += f" limit {lim}"
|
|
154
|
+
if budget != 2000: # the default is not written
|
|
155
|
+
out += f" budget {budget} tokens"
|
|
156
|
+
if after is not None:
|
|
157
|
+
out += f" after {after}"
|
|
158
|
+
return out
|
|
159
|
+
case Continue(handle=h, budget=budget):
|
|
160
|
+
out = f"continue {h}"
|
|
161
|
+
if budget != 2000:
|
|
162
|
+
out += f" budget {budget} tokens"
|
|
163
|
+
return out
|
|
164
|
+
case SchemaStmt():
|
|
165
|
+
return "schema"
|
|
166
|
+
case AssertNode(cls=cls, props=props, source=src, name=n):
|
|
167
|
+
out = f"assert {cls} {_props(props)}"
|
|
168
|
+
if src:
|
|
169
|
+
out += f" source {src}"
|
|
170
|
+
return out + f" as {n}"
|
|
171
|
+
case AssertEdge(edge=e, role_refs=roles, source=src):
|
|
172
|
+
args = ", ".join(f"{r}: {v}" for r, v in roles.items())
|
|
173
|
+
out = f"assert edge {e}({args})"
|
|
174
|
+
return out + (f" source {src}" if src else "")
|
|
175
|
+
case Merge(a=a, b=b, policy=policy):
|
|
176
|
+
out = f"merge {a}, {b}"
|
|
177
|
+
return out + (f" prefer {policy}" if policy else "")
|
|
178
|
+
case Distinct(a=a, b=b, reason=reason):
|
|
179
|
+
return f"distinct {a}, {b} reason {_quoted(reason)}"
|
|
180
|
+
case Refine(ref=ref, into_cls=cls, mapping=mapping, name=n):
|
|
181
|
+
cols = ", ".join(f"{k}: col {_quoted(v)}" for k, v in mapping.items())
|
|
182
|
+
return f"refine {ref} into {cls} with {{{cols}}} as {n}"
|
|
183
|
+
case Compact(src=src, name=n, props=props):
|
|
184
|
+
return f"compact {src} as {n} {_props(props)}"
|
|
185
|
+
case Retire(ref=ref, reason=reason):
|
|
186
|
+
return f"retire {ref} reason {_quoted(reason)}"
|
|
187
|
+
case Flag(ref=ref, reason=reason):
|
|
188
|
+
return f"flag {ref} reason {_quoted(reason)}"
|
|
189
|
+
case DeriveClass(name=n, base=base, props=props, quota=quota, dedup=dedup):
|
|
190
|
+
decls = ", ".join(f"{k}: {v}" for k, v in props.items())
|
|
191
|
+
out = f"derive class {n} from {base} with {{{decls}}}"
|
|
192
|
+
if quota is not None:
|
|
193
|
+
out += f" quota {quota}"
|
|
194
|
+
if dedup is not None:
|
|
195
|
+
out += f" dedup {dedup}"
|
|
196
|
+
return out
|
|
197
|
+
case DeriveEdge(name=n, roles=roles):
|
|
198
|
+
args = ", ".join(f"{r}: {c}" for r, c in roles.items())
|
|
199
|
+
return f"derive edge {n}({args})"
|
|
200
|
+
raise CanonicalError(f"cannot render {type(stmt).__name__}")
|