arango-query-core 0.2.5__tar.gz → 0.2.7__tar.gz
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.
- arango_query_core-0.2.7/.claude/skills/pr-review/SKILL.md +156 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/CHANGELOG.md +47 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/PKG-INFO +2 -1
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/README.md +1 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/__init__.py +2 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/mapping.py +30 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/__init__.py +5 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/engine.py +18 -3
- arango_query_core-0.2.7/arango_query_core/nl/host.py +223 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/postconditions.py +7 -5
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/docs/PRD.md +2 -1
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/pyproject.toml +1 -1
- arango_query_core-0.2.7/tests/fixtures/nl_system_prompts_0_2_6.json +12 -0
- arango_query_core-0.2.7/tests/test_mapping_property_encoding.py +74 -0
- arango_query_core-0.2.7/tests/test_nl_engine_host_extensions.py +247 -0
- arango_query_core-0.2.7/tests/test_nl_host.py +235 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_postconditions.py +25 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/.cursor/rules/fix-upstream-dont-fork.mdc +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/.github/workflows/ci.yml +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/.github/workflows/publish.yml +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/.gitignore +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/LICENSE +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/aql.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/errors.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/exec.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/extensions.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/fewshot.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/grounding.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/pathindex.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/providers.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/seams.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/nl/synthbank.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/owl_rdflib.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/owl_turtle.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/arango_query_core/py.typed +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/fixtures/mappings/pg.owl.ttl +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_mapping_helpers.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_mapping_resolver_normalization.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_mapping_supertypes.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_mapping_vci_detection.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_engine.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_engine_extraction_and_resilience.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_fewshot.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_grounding.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_pathindex.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_providers.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_nl_synthbank.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_owl_rdflib.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_owl_turtle_roundtrip.py +0 -0
- {arango_query_core-0.2.5 → arango_query_core-0.2.7}/tests/test_upstream_markers.py +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-review
|
|
3
|
+
description: Review an open arango-query-core pull request end to end — independently verify mergeability and the full CI gate (pytest, ruff, mypy) in an isolated worktree, trace the diff for correctness, and report a clear merge/no-merge recommendation. Use when asked to look at, review, or check a PR in this repo, or before merging one.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PR review (arango-query-core)
|
|
7
|
+
|
|
8
|
+
## Invocation
|
|
9
|
+
`/pr-review <PR number>` — or just ask to look at / review a PR.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
Don't trust GitHub's green checkmark or a skim of the diff alone. Independently
|
|
13
|
+
rebuild the PR's test/lint/type gate in a scratch worktree, trace the tricky
|
|
14
|
+
logic by hand against the new tests, and only then recommend merging. This repo
|
|
15
|
+
has hit real, silent failure modes (see **Gotchas** below) that a surface-level
|
|
16
|
+
review misses.
|
|
17
|
+
|
|
18
|
+
## Repo shape (read this first)
|
|
19
|
+
- **Primary repo:** `arango-solutions-org/arango-query-core` — PRs and issues
|
|
20
|
+
live here. **Secondary/mirror:** `ArthurKeen/arango-query-core` — code-only,
|
|
21
|
+
kept in sync by a dual-push `origin` remote; it does not carry its own PRs.
|
|
22
|
+
Always run `gh pr` / `gh issue` commands against the primary unless told
|
|
23
|
+
otherwise. (Confirm which is primary — it has changed before.)
|
|
24
|
+
- **Release gate:** PyPI publishing is pinned-artifact (CC-9): every merge to
|
|
25
|
+
`main` is a potential version bump. If the PR's `CHANGELOG.md` entry lands
|
|
26
|
+
under a version header that's already tagged and published, that's a bug in
|
|
27
|
+
the PR (or in a prior release) worth flagging, not something to wave through.
|
|
28
|
+
|
|
29
|
+
## Protocol
|
|
30
|
+
|
|
31
|
+
### 1. Pull the PR's shape
|
|
32
|
+
```bash
|
|
33
|
+
gh pr view <N> --repo arango-solutions-org/arango-query-core \
|
|
34
|
+
--json title,body,author,headRefName,baseRefName,commits,additions,deletions,changedFiles,mergeable,statusCheckRollup
|
|
35
|
+
gh pr diff <N> --repo arango-solutions-org/arango-query-core
|
|
36
|
+
```
|
|
37
|
+
Read the PR body as the author's own claim about scope and evidence (eval
|
|
38
|
+
numbers, measured deltas) — you're about to check whether the code backs it up.
|
|
39
|
+
|
|
40
|
+
### 2. Independently verify mergeability
|
|
41
|
+
Don't trust the `mergeable` field alone — it can read `UNKNOWN` right after a
|
|
42
|
+
push, and (per the gotchas below) a corrupted commit graph can make GitHub
|
|
43
|
+
report `CONFLICTING` even when the content is actually fine, or vice versa.
|
|
44
|
+
Cross-check with a real history-aware merge:
|
|
45
|
+
```bash
|
|
46
|
+
git fetch origin
|
|
47
|
+
git merge-tree --write-tree origin/main origin/<head-branch>
|
|
48
|
+
echo "exit: $?" # 0 and a single tree id = clean; conflict markers = real conflict
|
|
49
|
+
```
|
|
50
|
+
Use the **modern 3-ref-aware form** (`--write-tree`), never the bare legacy
|
|
51
|
+
`git merge-tree <a> <b>` — it ignores actual commit ancestry and reports false
|
|
52
|
+
conflicts for a branch that already contains `main`.
|
|
53
|
+
|
|
54
|
+
### 3. Rebuild the CI gate in an isolated worktree
|
|
55
|
+
Never run this against your own working tree (it may have unrelated local
|
|
56
|
+
changes, and installing a PR's deps into your main venv is how versions
|
|
57
|
+
drift). Mirror `.github/workflows/ci.yml` exactly:
|
|
58
|
+
```bash
|
|
59
|
+
git worktree add /tmp/pr<N>-check origin/<head-branch> --detach
|
|
60
|
+
cd /tmp/pr<N>-check
|
|
61
|
+
python3 -m venv .venv-check && source .venv-check/bin/activate
|
|
62
|
+
pip install -q -e ".[dev]"
|
|
63
|
+
|
|
64
|
+
pytest -q # mirrors: pytest -q --tb=short
|
|
65
|
+
ruff check .
|
|
66
|
+
ruff format --check .
|
|
67
|
+
mypy # mirrors: mypy (files = ["arango_query_core"])
|
|
68
|
+
```
|
|
69
|
+
All four must be clean. Note the test count and compare it to `main`'s —
|
|
70
|
+
a PR that doesn't grow the count for a new feature is under-tested.
|
|
71
|
+
|
|
72
|
+
Clean up when done:
|
|
73
|
+
```bash
|
|
74
|
+
cd /Users/arthurkeen/code/arango-query-core
|
|
75
|
+
git worktree remove /tmp/pr<N>-check --force
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. Trace the diff, don't just skim it
|
|
79
|
+
For anything touching control flow (retry loops, fallback logic, ordering
|
|
80
|
+
rules), hand-trace at least the two or three trickiest test cases against the
|
|
81
|
+
actual code — don't assume the tests passing proves the stated semantics.
|
|
82
|
+
Specifically check:
|
|
83
|
+
- **Scope honesty** — does the PR body list what's *not* included? A PR that
|
|
84
|
+
quietly expands scope beyond its own description is a flag, not a bonus.
|
|
85
|
+
- **Protocol/seam changes** — `QueryLanguageAdapter` seams are called
|
|
86
|
+
*unconditionally* by `NLQueryEngine` (no `hasattr` guard). A new seam means
|
|
87
|
+
every existing `FakeAdapter` in the test suite needs a stub for it, or the
|
|
88
|
+
merged tree will crash at prompt-build time. Grep for adapter classes in
|
|
89
|
+
`tests/` and confirm each implements every current seam.
|
|
90
|
+
- **CHANGELOG placement** — does the entry land under a version header that's
|
|
91
|
+
already tagged (`git tag -l`) and already on PyPI? If so, the version is
|
|
92
|
+
effectively lying about its own contents (see Repo shape above) — flag it
|
|
93
|
+
even though it's not this PR's fault to fix.
|
|
94
|
+
|
|
95
|
+
### 5. Report
|
|
96
|
+
State plainly: what the PR does, the hand-traced correctness notes, the
|
|
97
|
+
independent verification results (test count, ruff, mypy, merge-tree exit
|
|
98
|
+
code, CI check conclusions), any scope/changelog issues found, and an explicit
|
|
99
|
+
recommend-or-not. Don't hedge if everything checks out — say so and recommend
|
|
100
|
+
merging.
|
|
101
|
+
|
|
102
|
+
### 6. If asked to merge
|
|
103
|
+
```bash
|
|
104
|
+
gh pr merge <N> --repo arango-solutions-org/arango-query-core --merge --delete-branch
|
|
105
|
+
```
|
|
106
|
+
Re-check `mergeable`/CI immediately before merging — state can change between
|
|
107
|
+
review and merge. After merging, **sync manually** — merging on GitHub does
|
|
108
|
+
not update your local clone or the mirror:
|
|
109
|
+
```bash
|
|
110
|
+
git fetch origin && git checkout main && git merge --ff-only origin/main
|
|
111
|
+
git push origin main # dual-push: lands on both primary and mirror
|
|
112
|
+
```
|
|
113
|
+
`--delete-branch` only deletes the branch on the repo the PR lives in
|
|
114
|
+
(primary). If the branch was ever pushed to the mirror too, delete it there
|
|
115
|
+
explicitly: `git push https://github.com/ArthurKeen/arango-query-core.git --delete <branch>`.
|
|
116
|
+
|
|
117
|
+
## Gotchas (hard-won, don't re-learn these)
|
|
118
|
+
|
|
119
|
+
- **Never `git stash` mid-merge-conflict-resolution**, even for an unrelated
|
|
120
|
+
side-check (like "did this formatting issue pre-exist on main?"). Stashing
|
|
121
|
+
while `MERGE_HEAD` is set but conflicts are already resolved-and-staged
|
|
122
|
+
silently drops the merge state — the commit you make afterward looks fine
|
|
123
|
+
(correct file content) but has only **one parent**, so Git and GitHub don't
|
|
124
|
+
actually see it as containing `main`. Symptom: `mergeable` stays
|
|
125
|
+
`CONFLICTING` after a resolution that looks complete. Fix: check
|
|
126
|
+
`git log --format='%H %P' <commit>` — a merge commit must show two parents.
|
|
127
|
+
If it shows one, `git reset --hard` to before the merge and redo it cleanly
|
|
128
|
+
with no stash in between, then verify parent count *before* pushing.
|
|
129
|
+
- **A clean-looking commit can still get zero CI runs.** Twice now, a push
|
|
130
|
+
(once a corrupted single-parent "merge", once a plain version-bump commit
|
|
131
|
+
plus its tag) registered **no check-suite at all** — not even third-party
|
|
132
|
+
apps like GitGuardian. This isn't a code problem; it's GitHub event-delivery
|
|
133
|
+
flakiness. Verify with
|
|
134
|
+
`gh api repos/<repo>/commits/<sha>/check-suites --jq '.check_suites'`
|
|
135
|
+
before concluding anything is broken. A delete-and-recreate of a tag
|
|
136
|
+
sometimes forces redelivery; it isn't guaranteed to. Don't block a release
|
|
137
|
+
on it — fall back to the proven manual path (below) rather than debug
|
|
138
|
+
GitHub's webhook plumbing indefinitely.
|
|
139
|
+
- **PyPI publish fallback.** The `publish.yml` workflow uses trusted
|
|
140
|
+
publishing (OIDC, no token) gated to `github.repository ==
|
|
141
|
+
'ArthurKeen/arango-query-core'` and only fires on a `v*` tag push. If it
|
|
142
|
+
doesn't fire (see above) or you need to publish immediately: build and
|
|
143
|
+
check locally, then upload with the token in the gitignored `.env`:
|
|
144
|
+
```bash
|
|
145
|
+
python -m build --outdir <scratch>/dist
|
|
146
|
+
twine check <scratch>/dist/*
|
|
147
|
+
TWINE_USERNAME=__token__ TWINE_PASSWORD=$(grep '^PYPI_TOKEN=' .env | cut -d= -f2) \
|
|
148
|
+
twine upload --non-interactive <scratch>/dist/*
|
|
149
|
+
```
|
|
150
|
+
If the workflow does fire later for a version already manually published,
|
|
151
|
+
it will just hit a harmless `400 File already exists` on the duplicate
|
|
152
|
+
filename — not a problem, and actually confirms trusted publishing works.
|
|
153
|
+
- **PyPI's project JSON can lag a few seconds after upload** — `info.version`
|
|
154
|
+
may show the previous release momentarily. Re-check, or check
|
|
155
|
+
`'<version>' in releases` / hit the version's own release-page URL directly,
|
|
156
|
+
before concluding an upload failed.
|
|
@@ -6,6 +6,53 @@ exact versions (contextual-data-fabric CC-9) and re-run their golden /
|
|
|
6
6
|
eval suites on every bump — pre-1.0, **minor** bumps may break the API
|
|
7
7
|
and this file must say so explicitly.
|
|
8
8
|
|
|
9
|
+
## [0.2.7] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Host extensions** (`arango_query_core.nl.host`). A host whose engine
|
|
14
|
+
accepts more than the target language can now say so without rewriting
|
|
15
|
+
the adapter's grammar section. `HostExtensions` declares:
|
|
16
|
+
- `predicates`: `HostPredicate`, a predicate the host's engine accepts
|
|
17
|
+
(such as a registered text match), the `Class.property` names it may be
|
|
18
|
+
used on, what not to write instead, and where the engine refuses it;
|
|
19
|
+
- `rules`: the host's own limits that are not about one property, one
|
|
20
|
+
sentence each;
|
|
21
|
+
- `properties`: `PropertyFact`, per-property facts (`type`, `required`,
|
|
22
|
+
`orderable`, `description`; names follow `PropertyInfo`).
|
|
23
|
+
|
|
24
|
+
`host_prompt_section()` renders them as one section that adds to the
|
|
25
|
+
grammar and says that everything the grammar forbids stays forbidden.
|
|
26
|
+
Host prose is collapsed to one line, capped at 600 characters, and has
|
|
27
|
+
runs of `#` collapsed, so it cannot open a prompt section or carry the
|
|
28
|
+
`## Examples` cache marker. Names are validated when built. All four
|
|
29
|
+
names are exported from `arango_query_core.nl`.
|
|
30
|
+
- **`NLQueryEngine.host_extensions`**, `None` by default. The section goes
|
|
31
|
+
straight after the grammar section, inside the cacheable prefix, before
|
|
32
|
+
`## Examples`. With `None`, or a declaration that renders nothing, the
|
|
33
|
+
system prompt is byte-identical to 0.2.6's.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- The engine comment and the `nl.postconditions` docstrings claimed that
|
|
38
|
+
postcondition prompt sections stay inside the cacheable prefix. They are
|
|
39
|
+
appended last, so when a `## Examples` section is present they fall after
|
|
40
|
+
`split_system_for_anthropic_cache`'s split. Comments only; behaviour is
|
|
41
|
+
unchanged.
|
|
42
|
+
|
|
43
|
+
## [0.2.6] - 2026-10-29
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **Temporal property encodings.** A property may declare how its temporal
|
|
48
|
+
values are stored, `"encoding": "epoch_ms"` (integer milliseconds since the
|
|
49
|
+
epoch) or `"iso8601"` (an ISO-8601 string), read into
|
|
50
|
+
`PropertyInfo.encoding` (empty when not declared) and carried by
|
|
51
|
+
`schema_summary()`. Any other value is refused with `INVALID_MAPPING`.
|
|
52
|
+
The vocabulary is `TEMPORAL_ENCODINGS`. It is a physical detail, like
|
|
53
|
+
`field`, so OWL/Turtle does not carry it. arango-cypher-py lowers temporal
|
|
54
|
+
accessors, functions and arithmetic from it (LDBC WP-41).
|
|
55
|
+
|
|
9
56
|
## [0.2.5] - 2026-10-26
|
|
10
57
|
|
|
11
58
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: arango-query-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Shared conceptual-model and NL substrate for ArangoDB query transpilers (arango-cypher-py, arango-sparql-py)
|
|
5
5
|
Project-URL: Repository, https://github.com/ArthurKeen/arango-query-core
|
|
6
6
|
Project-URL: Changelog, https://github.com/ArthurKeen/arango-query-core/blob/main/CHANGELOG.md
|
|
@@ -81,6 +81,7 @@ fabric pin one artifact instead of forking the same code three ways.
|
|
|
81
81
|
| `arango_query_core.nl` | Language-agnostic NL→query engine: LLM providers (OpenAI / OpenRouter / Anthropic with prompt caching), BM25/dense few-shot retrieval, caller-supplied semantic postconditions, and the generate→validate→repair loop |
|
|
82
82
|
| `arango_query_core.nl.seams` | `QueryLanguageAdapter` — the eight language-specific seams an adapter implements (grammar prompt, corpus, validator, repair rules, guardrails, entity/predicate/path grounding) |
|
|
83
83
|
| `arango_query_core.nl.grounding` / `pathindex` | Entity, predicate, and relationship-path grounding retrieval (seams 6-8) — inject known IDs, schema conventions, and navigation hints into the prompt |
|
|
84
|
+
| `arango_query_core.nl.host` | `HostExtensions` — what the host's engine accepts beyond the target language (registered predicates, per-property facts, its own limits), rendered after the grammar section via `NLQueryEngine(host_extensions=...)` |
|
|
84
85
|
| `arango_query_core.nl.postconditions` | Caller-supplied semantic invariants enforced through the same generate→validate→retry budget, failing closed on exhaustion |
|
|
85
86
|
| `arango_query_core.nl.synthbank` | Ontology-agnostic query-shape template catalog for synthetic few-shot bank generation |
|
|
86
87
|
|
|
@@ -20,6 +20,7 @@ fabric pin one artifact instead of forking the same code three ways.
|
|
|
20
20
|
| `arango_query_core.nl` | Language-agnostic NL→query engine: LLM providers (OpenAI / OpenRouter / Anthropic with prompt caching), BM25/dense few-shot retrieval, caller-supplied semantic postconditions, and the generate→validate→repair loop |
|
|
21
21
|
| `arango_query_core.nl.seams` | `QueryLanguageAdapter` — the eight language-specific seams an adapter implements (grammar prompt, corpus, validator, repair rules, guardrails, entity/predicate/path grounding) |
|
|
22
22
|
| `arango_query_core.nl.grounding` / `pathindex` | Entity, predicate, and relationship-path grounding retrieval (seams 6-8) — inject known IDs, schema conventions, and navigation hints into the prompt |
|
|
23
|
+
| `arango_query_core.nl.host` | `HostExtensions` — what the host's engine accepts beyond the target language (registered predicates, per-property facts, its own limits), rendered after the grammar section via `NLQueryEngine(host_extensions=...)` |
|
|
23
24
|
| `arango_query_core.nl.postconditions` | Caller-supplied semantic invariants enforced through the same generate→validate→retry budget, failing closed on exhaustion |
|
|
24
25
|
| `arango_query_core.nl.synthbank` | Ontology-agnostic query-shape template catalog for synthetic few-shot bank generation |
|
|
25
26
|
|
|
@@ -5,6 +5,7 @@ from .extensions import ExtensionPolicy, ExtensionRegistry
|
|
|
5
5
|
from .mapping import (
|
|
6
6
|
COLLECTION_NAME_RE,
|
|
7
7
|
SUPERTYPE_STYLES,
|
|
8
|
+
TEMPORAL_ENCODINGS,
|
|
8
9
|
IndexInfo,
|
|
9
10
|
MappingBundle,
|
|
10
11
|
MappingResolver,
|
|
@@ -25,6 +26,7 @@ except ImportError:
|
|
|
25
26
|
__all__ = [
|
|
26
27
|
"COLLECTION_NAME_RE",
|
|
27
28
|
"SUPERTYPE_STYLES",
|
|
29
|
+
"TEMPORAL_ENCODINGS",
|
|
28
30
|
"AqlExecutor",
|
|
29
31
|
"AqlFragment",
|
|
30
32
|
"AqlQuery",
|
|
@@ -163,6 +163,14 @@ class IndexInfo:
|
|
|
163
163
|
traversal_safe: bool = True
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
#: Storage encodings of temporal property values.
|
|
167
|
+
#:
|
|
168
|
+
#: * ``epoch_ms`` — an integer, milliseconds since 1970-01-01T00:00:00Z.
|
|
169
|
+
#: * ``iso8601`` — an ISO-8601 string (``2010-01-03T15:10:31.499+00:00``,
|
|
170
|
+
#: or ``2010-01-03`` for a date).
|
|
171
|
+
TEMPORAL_ENCODINGS = frozenset({"epoch_ms", "iso8601"})
|
|
172
|
+
|
|
173
|
+
|
|
166
174
|
@dataclass(frozen=True)
|
|
167
175
|
class PropertyInfo:
|
|
168
176
|
"""Metadata for a single property/attribute on an entity or relationship.
|
|
@@ -187,6 +195,25 @@ class PropertyInfo:
|
|
|
187
195
|
# / numeric / free_text / boolean / other), inferred by the schema analyzer.
|
|
188
196
|
# Consumers choose a matching strategy from this rather than field names.
|
|
189
197
|
role: str = ""
|
|
198
|
+
# How a temporal value (``type`` date / datetime / …) is stored: one of
|
|
199
|
+
# ``TEMPORAL_ENCODINGS``, or empty when the mapping does not say.
|
|
200
|
+
# Consumers must not guess an encoding: comparisons, components
|
|
201
|
+
# (``.year``) and arithmetic differ between the two.
|
|
202
|
+
encoding: str = ""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _property_encoding(owner: str, name: str, raw: Any) -> str:
|
|
206
|
+
"""A property's declared ``encoding``, checked against ``TEMPORAL_ENCODINGS``."""
|
|
207
|
+
if raw in (None, ""):
|
|
208
|
+
return ""
|
|
209
|
+
encoding = str(raw)
|
|
210
|
+
if encoding not in TEMPORAL_ENCODINGS:
|
|
211
|
+
raise CoreError(
|
|
212
|
+
f"Property {owner}.{name} declares encoding {encoding!r}; expected one of "
|
|
213
|
+
f"{', '.join(sorted(TEMPORAL_ENCODINGS))}",
|
|
214
|
+
code="INVALID_MAPPING",
|
|
215
|
+
)
|
|
216
|
+
return encoding
|
|
190
217
|
|
|
191
218
|
|
|
192
219
|
@dataclass(frozen=True)
|
|
@@ -438,6 +465,7 @@ class MappingResolver:
|
|
|
438
465
|
numeric_like=bool(meta.get("numericLike") or meta.get("numeric_like")),
|
|
439
466
|
sample_values=tuple(str(s) for s in samples) if samples else (),
|
|
440
467
|
role=str(meta.get("role") or ""),
|
|
468
|
+
encoding=_property_encoding(label_or_type, name, meta.get("encoding")),
|
|
441
469
|
)
|
|
442
470
|
elif isinstance(meta, str):
|
|
443
471
|
result[name] = PropertyInfo(field=name, type=meta)
|
|
@@ -797,6 +825,7 @@ class MappingResolver:
|
|
|
797
825
|
**({"numericLike": True} if p.numeric_like else {}),
|
|
798
826
|
**({"sampleValues": list(p.sample_values)} if p.sample_values else {}),
|
|
799
827
|
**({"role": p.role} if p.role else {}),
|
|
828
|
+
**({"encoding": p.encoding} if p.encoding else {}),
|
|
800
829
|
}
|
|
801
830
|
for name, p in props.items()
|
|
802
831
|
},
|
|
@@ -831,6 +860,7 @@ class MappingResolver:
|
|
|
831
860
|
**({"numericLike": True} if p.numeric_like else {}),
|
|
832
861
|
**({"sampleValues": list(p.sample_values)} if p.sample_values else {}),
|
|
833
862
|
**({"role": p.role} if p.role else {}),
|
|
863
|
+
**({"encoding": p.encoding} if p.encoding else {}),
|
|
834
864
|
}
|
|
835
865
|
for name, p in props.items()
|
|
836
866
|
},
|
|
@@ -9,6 +9,7 @@ enters exclusively through :class:`~arango_query_core.nl.seams.QueryLanguageAdap
|
|
|
9
9
|
from .engine import NLQueryEngine, NLResult
|
|
10
10
|
from .fewshot import BM25Retriever, DenseRetriever, FewShotIndex, Retriever, cached_few_shot_index
|
|
11
11
|
from .grounding import GroundedEntity, GroundedPredicate, LabelIndex, PredicateIndex
|
|
12
|
+
from .host import HostExtensions, HostPredicate, PropertyFact, host_prompt_section
|
|
12
13
|
from .pathindex import ClassPath, ClassPathIndex
|
|
13
14
|
from .postconditions import (
|
|
14
15
|
Postcondition,
|
|
@@ -38,6 +39,8 @@ __all__ = [
|
|
|
38
39
|
"GroundedEntity",
|
|
39
40
|
"GroundedPredicate",
|
|
40
41
|
"GuardrailVerdict",
|
|
42
|
+
"HostExtensions",
|
|
43
|
+
"HostPredicate",
|
|
41
44
|
"LabelIndex",
|
|
42
45
|
"LLMProvider",
|
|
43
46
|
"NLQueryEngine",
|
|
@@ -48,6 +51,7 @@ __all__ = [
|
|
|
48
51
|
"PostconditionContext",
|
|
49
52
|
"PostconditionViolation",
|
|
50
53
|
"PredicateIndex",
|
|
54
|
+
"PropertyFact",
|
|
51
55
|
"QueryLanguageAdapter",
|
|
52
56
|
"Retriever",
|
|
53
57
|
"SHAPE_CATALOG",
|
|
@@ -55,6 +59,7 @@ __all__ = [
|
|
|
55
59
|
"ValidationResult",
|
|
56
60
|
"cached_few_shot_index",
|
|
57
61
|
"get_llm_provider",
|
|
62
|
+
"host_prompt_section",
|
|
58
63
|
"postcondition_prompt_sections",
|
|
59
64
|
"run_postconditions",
|
|
60
65
|
"split_system_for_anthropic_cache",
|
|
@@ -23,6 +23,7 @@ from collections.abc import Sequence
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
from .host import HostExtensions, host_prompt_section
|
|
26
27
|
from .postconditions import (
|
|
27
28
|
Postcondition,
|
|
28
29
|
PostconditionContext,
|
|
@@ -85,6 +86,11 @@ class NLQueryEngine:
|
|
|
85
86
|
adapter's path index (seam 8) and rendered AFTER the predicate
|
|
86
87
|
section — same post-cache-boundary discipline; the adapter resolves
|
|
87
88
|
the anchor/target identifiers from its own seam-6/7 state (D-02).
|
|
89
|
+
|
|
90
|
+
``host_extensions`` (:mod:`~arango_query_core.nl.host`) is rendered straight
|
|
91
|
+
after the grammar section, before ``## Examples``, so it is part of the
|
|
92
|
+
cacheable prefix. Caller postconditions' prompt sections come last; when a
|
|
93
|
+
``## Examples`` section is present they therefore fall after the cache split.
|
|
88
94
|
"""
|
|
89
95
|
|
|
90
96
|
provider: LLMProvider
|
|
@@ -103,6 +109,11 @@ class NLQueryEngine:
|
|
|
103
109
|
on :attr:`NLResult.provider_errors`. If no attempt ever returned a
|
|
104
110
|
response, the last exception is re-raised. False (the default) lets the
|
|
105
111
|
first exception propagate, as before."""
|
|
112
|
+
host_extensions: HostExtensions | None = None
|
|
113
|
+
"""What the host's engine accepts beyond the target language (:mod:`.host`),
|
|
114
|
+
rendered straight after the grammar section, inside the cacheable prefix.
|
|
115
|
+
``None`` or a declaration that renders nothing leaves the system prompt
|
|
116
|
+
byte-identical."""
|
|
106
117
|
|
|
107
118
|
def generate(self, question: str, *, schema_context: str = "", user_hint: str = "") -> NLResult:
|
|
108
119
|
"""Generate a query for *question*.
|
|
@@ -232,6 +243,9 @@ class NLQueryEngine:
|
|
|
232
243
|
|
|
233
244
|
def _system_prompt(self, question: str, schema_context: str) -> str:
|
|
234
245
|
sections = [self.adapter.grammar_prompt_section(schema_context)]
|
|
246
|
+
# Static per engine, so it sits with the grammar, before the ``## Examples``
|
|
247
|
+
# cache boundary. ``""`` when nothing is declared, and the join drops it.
|
|
248
|
+
sections.append(host_prompt_section(self.host_extensions))
|
|
235
249
|
index = self.adapter.few_shot_index()
|
|
236
250
|
if index is not None:
|
|
237
251
|
examples = index.format_prompt_section(
|
|
@@ -255,9 +269,10 @@ class NLQueryEngine:
|
|
|
255
269
|
if path_block:
|
|
256
270
|
sections.append(path_block)
|
|
257
271
|
# Caller postconditions announce their invariants up front so the model
|
|
258
|
-
# aims to satisfy them, not just get corrected after breaking one.
|
|
259
|
-
#
|
|
260
|
-
#
|
|
272
|
+
# aims to satisfy them, not just get corrected after breaking one. They are
|
|
273
|
+
# static per call but appended last: when an ``## Examples`` section is
|
|
274
|
+
# present, split_system_for_anthropic_cache splits there and these land in
|
|
275
|
+
# the uncached suffix; without one, the whole prompt is a single cached block.
|
|
261
276
|
sections.extend(postcondition_prompt_sections(self.postconditions))
|
|
262
277
|
return "\n\n".join(s for s in sections if s)
|
|
263
278
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""What a host's engine accepts beyond the target language, told to the NL engine.
|
|
2
|
+
|
|
3
|
+
A host may run the generated query on an engine that accepts more than the language
|
|
4
|
+
the adapter teaches: a registered predicate such as a text match, per-property facts
|
|
5
|
+
(a datatype, whether it may be sorted, whether it is always present), and limits of
|
|
6
|
+
its own. Seam 1 (``grammar_prompt_section``) is adapter-owned and whole, so without
|
|
7
|
+
this module the only way to say any of that is to rewrite the grammar.
|
|
8
|
+
|
|
9
|
+
* :class:`HostPredicate` — a predicate the host's engine accepts in addition to the
|
|
10
|
+
language, with the properties it may be used on and what not to write instead.
|
|
11
|
+
* :class:`PropertyFact` — facts about one property that change which queries succeed.
|
|
12
|
+
Field names follow :class:`arango_query_core.mapping.PropertyInfo`.
|
|
13
|
+
* :class:`HostExtensions` — the declaration: predicates, rules (the host's limits that
|
|
14
|
+
are not about one property, one sentence each) and property facts.
|
|
15
|
+
* :func:`host_prompt_section` — the system-prompt section; ``""`` when nothing is
|
|
16
|
+
declared. :class:`~arango_query_core.nl.engine.NLQueryEngine` renders it straight
|
|
17
|
+
after the grammar section, inside the cacheable prefix, when its
|
|
18
|
+
``host_extensions`` field is set.
|
|
19
|
+
|
|
20
|
+
Rules this module holds:
|
|
21
|
+
|
|
22
|
+
* **Nothing supplied, nothing changed.** ``None``, an empty declaration, a predicate
|
|
23
|
+
offered on no property, a fact that declares nothing and a blank rule all render
|
|
24
|
+
``""``, and the engine's system prompt is then byte-identical to one built without
|
|
25
|
+
the field.
|
|
26
|
+
* **It adds to the grammar and never lifts its rules.** One fixed sentence,
|
|
27
|
+
:data:`HOST_RULE`, says so in the section itself.
|
|
28
|
+
* **Host strings cannot open a prompt section.** Every piece of host prose is
|
|
29
|
+
collapsed to one line, capped at :data:`LINE_MAX` characters, and has its runs of
|
|
30
|
+
``#`` collapsed to one, so it can neither start a heading nor carry the
|
|
31
|
+
``## Examples`` marker that
|
|
32
|
+
:func:`~arango_query_core.nl.providers.split_system_for_anthropic_cache` splits at.
|
|
33
|
+
* **Names are validated when built.** Predicate names, namespaces and
|
|
34
|
+
``Class.property`` entries that are not plain names raise :class:`ValueError`, so
|
|
35
|
+
they never reach the prompt.
|
|
36
|
+
|
|
37
|
+
Checking that the model *used* a declared predicate is language-specific and stays
|
|
38
|
+
with the adapter or a host :class:`~arango_query_core.nl.postconditions.Postcondition`.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
|
|
46
|
+
from .grounding import _CONTROL_CHARS_RE, _WHITESPACE_RUN_RE
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"FACTS_HEADER",
|
|
50
|
+
"HOST_HEADER",
|
|
51
|
+
"HOST_RULE",
|
|
52
|
+
"LINE_MAX",
|
|
53
|
+
"RULES_HEADER",
|
|
54
|
+
"HostExtensions",
|
|
55
|
+
"HostPredicate",
|
|
56
|
+
"PropertyFact",
|
|
57
|
+
"host_prompt_section",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
HOST_HEADER = "## What this engine accepts beyond the rules above"
|
|
61
|
+
HOST_RULE = (
|
|
62
|
+
"These predicates are allowed in addition to the rules above, and only on the "
|
|
63
|
+
"properties listed for each. Everything the rules above forbid stays forbidden."
|
|
64
|
+
)
|
|
65
|
+
"""The one fixed sentence: the section adds to the grammar, never overrides it."""
|
|
66
|
+
RULES_HEADER = "## Limits this engine adds to the rules above"
|
|
67
|
+
FACTS_HEADER = "## Facts about properties"
|
|
68
|
+
|
|
69
|
+
LINE_MAX = 600
|
|
70
|
+
"""Cap on one piece of host prose. Host prose is a sentence or two, so the cap is
|
|
71
|
+
larger than a grounding label's (200), which cut a predicate's meaning short."""
|
|
72
|
+
|
|
73
|
+
_NAME_RE = re.compile(r"^[A-Za-z_][\w-]*(?::[A-Za-z_][\w-]*)?$")
|
|
74
|
+
"""A plain or prefixed name, e.g. ``ex:textMatch``."""
|
|
75
|
+
_PROPERTY_RE = re.compile(r"^[A-Za-z_][\w-]*\.[A-Za-z_][\w-]*$")
|
|
76
|
+
"""A ``Class.property`` name."""
|
|
77
|
+
_IRI_FORBIDDEN = frozenset('<>"{}|^`\\ \t\r\n')
|
|
78
|
+
_HASH_RUN_RE = re.compile(r"#{2,}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _clean(text: str) -> str:
|
|
82
|
+
"""One line of host prose that cannot open a prompt section.
|
|
83
|
+
|
|
84
|
+
Control characters and whitespace runs collapse to one space (the rule
|
|
85
|
+
``grounding._sanitize_label`` applies to labels), runs of ``#`` collapse to one
|
|
86
|
+
so no heading or ``## Examples`` marker survives, and the result is capped at
|
|
87
|
+
:data:`LINE_MAX`. Clean single-line prose without ``##`` passes through unchanged.
|
|
88
|
+
"""
|
|
89
|
+
cleaned = _WHITESPACE_RUN_RE.sub(" ", _CONTROL_CHARS_RE.sub(" ", text))
|
|
90
|
+
return _HASH_RUN_RE.sub("#", cleaned).strip()[:LINE_MAX].rstrip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class HostPredicate:
|
|
95
|
+
"""A predicate the host's engine accepts in addition to the language."""
|
|
96
|
+
|
|
97
|
+
name: str
|
|
98
|
+
"""As the model writes it, e.g. ``ex:textMatch``."""
|
|
99
|
+
iri: str
|
|
100
|
+
"""The namespace the name's prefix stands for, for the PREFIX line; ``""`` when the
|
|
101
|
+
name has no prefix."""
|
|
102
|
+
meaning: str
|
|
103
|
+
"""One sentence, in the host's words."""
|
|
104
|
+
usage: str
|
|
105
|
+
"""One example in the target language. It may span lines; each line is cleaned."""
|
|
106
|
+
applies_to: tuple[str, ...] = ()
|
|
107
|
+
"""``Class.property`` names it may be used on. Empty means it is offered nowhere,
|
|
108
|
+
and a predicate offered nowhere is left out of the prompt."""
|
|
109
|
+
instead_of: str = ""
|
|
110
|
+
"""What not to write instead, e.g. "an exact = comparison on these properties"."""
|
|
111
|
+
restrictions: tuple[str, ...] = ()
|
|
112
|
+
"""Where the host's engine refuses it, one short clause each, e.g. "inside OPTIONAL"."""
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
if not _NAME_RE.match(self.name):
|
|
116
|
+
raise ValueError(f"host predicate name {self.name!r} is not a plain or prefixed name")
|
|
117
|
+
if ":" in self.name and not self.iri:
|
|
118
|
+
raise ValueError(f"host predicate {self.name!r} has a prefix but no namespace iri")
|
|
119
|
+
if any(ch in _IRI_FORBIDDEN for ch in self.iri):
|
|
120
|
+
raise ValueError(f"host predicate {self.name!r}: namespace iri {self.iri!r} is malformed")
|
|
121
|
+
bad = [p for p in self.applies_to if not _PROPERTY_RE.match(p)]
|
|
122
|
+
if bad:
|
|
123
|
+
raise ValueError(f"host predicate {self.name!r}: not Class.property names: {bad}")
|
|
124
|
+
if not _clean(self.meaning) or not _clean(self.usage):
|
|
125
|
+
raise ValueError(f"host predicate {self.name!r} needs a meaning and a usage example")
|
|
126
|
+
|
|
127
|
+
def prompt_lines(self) -> list[str]:
|
|
128
|
+
"""The predicate's lines in the section."""
|
|
129
|
+
prefix = self.name.split(":", 1)[0] if ":" in self.name else ""
|
|
130
|
+
lines = [f"- {self.name} — {_clean(self.meaning)}"]
|
|
131
|
+
if prefix:
|
|
132
|
+
lines.append(f" Declare it with: PREFIX {prefix}: <{self.iri}>")
|
|
133
|
+
lines.append(f" Use it only on: {', '.join(self.applies_to)}")
|
|
134
|
+
instead_of = _clean(self.instead_of)
|
|
135
|
+
if instead_of:
|
|
136
|
+
lines.append(f" On those properties use it instead of {instead_of}.")
|
|
137
|
+
restrictions = [r for r in map(_clean, self.restrictions) if r]
|
|
138
|
+
if restrictions:
|
|
139
|
+
lines.append(" Refused: " + "; ".join(restrictions) + ".")
|
|
140
|
+
lines.append(" Example:")
|
|
141
|
+
lines += [f" {row}" for row in map(_clean, self.usage.splitlines()) if row]
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class PropertyFact:
|
|
147
|
+
"""Facts about one property. ``None`` or ``""`` means "not declared" and renders nothing."""
|
|
148
|
+
|
|
149
|
+
entity: str
|
|
150
|
+
property: str
|
|
151
|
+
type: str = ""
|
|
152
|
+
"""The value's datatype in the host's words, e.g. ``date``."""
|
|
153
|
+
required: bool | None = None
|
|
154
|
+
"""Whether every entity has the property."""
|
|
155
|
+
description: str = ""
|
|
156
|
+
"""What the property means."""
|
|
157
|
+
orderable: bool | None = None
|
|
158
|
+
"""Whether the host's engine may sort or deduplicate on it."""
|
|
159
|
+
|
|
160
|
+
def __post_init__(self) -> None:
|
|
161
|
+
if not _PROPERTY_RE.match(f"{self.entity}.{self.property}"):
|
|
162
|
+
raise ValueError(f"property fact {self.entity!r}.{self.property!r} is not a Class.property name")
|
|
163
|
+
|
|
164
|
+
def line(self) -> str:
|
|
165
|
+
"""The fact's line in the section; ``""`` when it declares nothing."""
|
|
166
|
+
parts = []
|
|
167
|
+
type_ = _clean(self.type)
|
|
168
|
+
if type_:
|
|
169
|
+
parts.append(f"type {type_}")
|
|
170
|
+
if self.required is not None:
|
|
171
|
+
parts.append("always present" if self.required else "may be missing")
|
|
172
|
+
if self.orderable is not None:
|
|
173
|
+
parts.append(
|
|
174
|
+
"may be sorted or deduplicated" if self.orderable else "may not be sorted or deduplicated"
|
|
175
|
+
)
|
|
176
|
+
description = _clean(self.description)
|
|
177
|
+
if description:
|
|
178
|
+
parts.append(description)
|
|
179
|
+
return f"- {self.entity}.{self.property}: " + "; ".join(parts) if parts else ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class HostExtensions:
|
|
184
|
+
"""What the host's engine accepts beyond the target language."""
|
|
185
|
+
|
|
186
|
+
predicates: tuple[HostPredicate, ...] = ()
|
|
187
|
+
properties: tuple[PropertyFact, ...] = ()
|
|
188
|
+
rules: tuple[str, ...] = ()
|
|
189
|
+
"""Limits the host's engine adds that are not about one property, one sentence each."""
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def empty(self) -> bool:
|
|
193
|
+
"""True when nothing would render."""
|
|
194
|
+
return (
|
|
195
|
+
not any(p.applies_to for p in self.predicates)
|
|
196
|
+
and not any(f.line() for f in self.properties)
|
|
197
|
+
and not any(_clean(r) for r in self.rules)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def host_prompt_section(ext: HostExtensions | None) -> str:
|
|
202
|
+
"""The system-prompt section for *ext*; ``""`` when it declares nothing that renders.
|
|
203
|
+
|
|
204
|
+
Blocks, each present only when it has content: the offered predicates under
|
|
205
|
+
:data:`HOST_HEADER` with :data:`HOST_RULE`, then the rules under
|
|
206
|
+
:data:`RULES_HEADER`, then the property facts under :data:`FACTS_HEADER`.
|
|
207
|
+
"""
|
|
208
|
+
if ext is None or ext.empty:
|
|
209
|
+
return ""
|
|
210
|
+
blocks: list[str] = []
|
|
211
|
+
offered = [p for p in ext.predicates if p.applies_to]
|
|
212
|
+
if offered:
|
|
213
|
+
lines = [HOST_HEADER, HOST_RULE]
|
|
214
|
+
for pred in offered:
|
|
215
|
+
lines += pred.prompt_lines()
|
|
216
|
+
blocks.append("\n".join(lines))
|
|
217
|
+
rules = [f"- {r}" for r in map(_clean, ext.rules) if r]
|
|
218
|
+
if rules:
|
|
219
|
+
blocks.append("\n".join([RULES_HEADER, *rules]))
|
|
220
|
+
facts = [line for line in (f.line() for f in ext.properties) if line]
|
|
221
|
+
if facts:
|
|
222
|
+
blocks.append("\n".join([FACTS_HEADER, *facts]))
|
|
223
|
+
return "\n\n".join(blocks)
|
|
@@ -12,8 +12,8 @@ a required filter. No syntactic check sees it and no human sees it in a demo.
|
|
|
12
12
|
|
|
13
13
|
A postcondition is a caller-supplied invariant. It rides the *same* retry
|
|
14
14
|
budget as parse and translate: the model is told the rule up front (via
|
|
15
|
-
:meth:`Postcondition.prompt_section`, rendered once
|
|
16
|
-
|
|
15
|
+
:meth:`Postcondition.prompt_section`, rendered once at the end of the system
|
|
16
|
+
prompt), and a violation is fed back into the next prompt exactly like a
|
|
17
17
|
validation failure — so the loop gets a chance to *correct* the query rather
|
|
18
18
|
than just reject it, and fails closed when the budget is spent — unless a
|
|
19
19
|
candidate drew only a *soft* violation (``PostconditionViolation.soft``), in
|
|
@@ -167,9 +167,11 @@ def postcondition_prompt_sections(
|
|
|
167
167
|
) -> list[str]:
|
|
168
168
|
"""Collect non-empty ``prompt_section()`` output, in caller order.
|
|
169
169
|
|
|
170
|
-
Rendered once
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
Rendered once, at the end of the system prompt, by the engine. When the
|
|
171
|
+
prompt has an ``## Examples`` section the sections fall after
|
|
172
|
+
:func:`~arango_query_core.nl.providers.split_system_for_anthropic_cache`'s
|
|
173
|
+
split, in the uncached suffix. A check that raises here (a broken
|
|
174
|
+
``prompt_section``) is skipped rather than aborting the whole generation.
|
|
173
175
|
"""
|
|
174
176
|
if not postconditions:
|
|
175
177
|
return []
|