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 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
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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__}")