arango-query-core 0.2.8__tar.gz → 0.2.9__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.8 → arango_query_core-0.2.9}/CHANGELOG.md +18 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/PKG-INFO +1 -1
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/grounding.py +33 -3
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/pyproject.toml +1 -1
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_grounding.py +75 -0
- arango_query_core-0.2.8/.claude/settings.json +0 -8
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/.claude/skills/pr-review/SKILL.md +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/.cursor/rules/fix-upstream-dont-fork.mdc +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/.github/workflows/ci.yml +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/.github/workflows/publish.yml +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/.gitignore +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/LICENSE +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/README.md +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/__init__.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/aql.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/errors.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/exec.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/extensions.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/mapping.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/__init__.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/engine.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/fewshot.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/host.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/pathindex.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/postconditions.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/providers.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/seams.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/nl/synthbank.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/owl_rdflib.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/owl_turtle.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/arango_query_core/py.typed +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/docs/PRD.md +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/fixtures/mappings/pg.owl.ttl +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/fixtures/nl_system_prompts_0_2_6.json +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_helpers.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_property_encoding.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_resolver_normalization.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_supertypes.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_vci_detection.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_engine.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_engine_extraction_and_resilience.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_engine_host_extensions.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_engine_request_context.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_fewshot.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_host.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_pathindex.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_postconditions.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_providers.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_nl_synthbank.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_owl_rdflib.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_owl_turtle_roundtrip.py +0 -0
- {arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_upstream_markers.py +0 -0
|
@@ -6,6 +6,24 @@ 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.9] - 2026-11-12
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Grounding finds a name told apart by a short token.** The scorer shared by
|
|
14
|
+
`LabelIndex` and `PredicateIndex` dropped every token under three
|
|
15
|
+
characters, so "HP Inc.", "GE Co.", "3M Co.", "AT&T Inc." or the "31" of
|
|
16
|
+
"Account Name 31" among "Account Name 1" to "200" was never retrieved, even
|
|
17
|
+
when the question gave the exact stored name. A token under three characters
|
|
18
|
+
now counts when the question holds it as a whole word, never as a substring
|
|
19
|
+
("GE" is not found in "manager"). Before short words are read, possessive and
|
|
20
|
+
contraction endings are removed ("Media's", "don't"), accented words stay
|
|
21
|
+
whole ("Société" yields no "t"), a repeated short token counts once ("J&J"),
|
|
22
|
+
and one- and two-letter English words ("of", "at", "ok") are not counted.
|
|
23
|
+
Tokens of three or more characters match exactly as before. Retrieval can
|
|
24
|
+
only gain results; an item that matched before keeps its long-token hits, so
|
|
25
|
+
its rank changes only against items that now also match a short token.
|
|
26
|
+
|
|
9
27
|
## [0.2.7] - 2026-11-06
|
|
10
28
|
|
|
11
29
|
### 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.9
|
|
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
|
|
@@ -54,6 +54,29 @@ _STOP = frozenset(
|
|
|
54
54
|
"please list show find get".split()
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
#: A token shorter than this matches only as a whole word of the other side,
|
|
58
|
+
#: never as a substring: "ge" is inside "manager", but "GE" names a company.
|
|
59
|
+
_SHORT = 3
|
|
60
|
+
|
|
61
|
+
#: One- and two-letter English words a label can carry ("Bank of America")
|
|
62
|
+
#: that would otherwise tie every question that uses them. Applied only to
|
|
63
|
+
#: short tokens; longer stop words are in :data:`_STOP`.
|
|
64
|
+
_SHORT_STOP = _STOP | frozenset("am as at be go he if it me ok pm so up us vs we".split())
|
|
65
|
+
|
|
66
|
+
#: An English possessive or contraction ending: "Media's", "don't", "we'll".
|
|
67
|
+
_CLITIC = re.compile(r"['’](?:s|t|d|m|ll|re|ve)\b")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _short_words(text: str) -> set[str]:
|
|
71
|
+
"""The whole words of ``text`` under :data:`_SHORT` characters, as a short
|
|
72
|
+
token may match them: possessive and contraction endings removed first
|
|
73
|
+
(no "s" from "Media's", no "t" from "don't"), and words split on any
|
|
74
|
+
non-letter, accented letters included, so "Société" stays one word and
|
|
75
|
+
yields no "t" or "g". A set, so a label repeating one ("J&J") counts it once."""
|
|
76
|
+
words = re.findall(r"[^\W_]+", _CLITIC.sub("", text.lower()))
|
|
77
|
+
return {w for w in words if len(w) < _SHORT and w not in _SHORT_STOP}
|
|
78
|
+
|
|
79
|
+
|
|
57
80
|
# C0 control chars (0x00-0x1F) + DEL (0x7F), including \n and \r — a
|
|
58
81
|
# maliciously-labeled instance (e.g. rdfs:label containing "\nignore
|
|
59
82
|
# previous instructions") must render on a single bullet line and
|
|
@@ -103,7 +126,10 @@ def _token_substring_retrieve(
|
|
|
103
126
|
dump: bool = False,
|
|
104
127
|
) -> list[_T]:
|
|
105
128
|
"""Top-k items whose ``get_labels(item)`` tokens appear (as
|
|
106
|
-
substrings) in the question, or vice versa.
|
|
129
|
+
substrings) in the question, or vice versa. A token of three or more
|
|
130
|
+
characters matches as a substring; a shorter one ("HP", "3M", "31")
|
|
131
|
+
only as a whole word of the question (:func:`_short_words`), since as a
|
|
132
|
+
substring "ge" would be found in "manager".
|
|
107
133
|
|
|
108
134
|
Both-direction substring matching survives plural/inflection
|
|
109
135
|
mismatches (e.g. "Transistors" in the question vs. a "Transistor"
|
|
@@ -128,15 +154,19 @@ def _token_substring_retrieve(
|
|
|
128
154
|
old ``if hits:`` guard dropped it before ``k`` was ever applied).
|
|
129
155
|
"""
|
|
130
156
|
ql = question.lower()
|
|
131
|
-
q_tokens = {t for t in re.findall(r"[a-z0-9]+", ql) if len(t) >=
|
|
157
|
+
q_tokens = {t for t in re.findall(r"[a-z0-9]+", ql) if len(t) >= _SHORT and t not in _STOP}
|
|
158
|
+
q_short = _short_words(question)
|
|
132
159
|
scored: list[tuple[int, int, _T]] = []
|
|
133
160
|
for item in items:
|
|
134
161
|
hits = 0
|
|
135
162
|
best_len = 999
|
|
136
163
|
for lab in get_labels(item):
|
|
137
164
|
labl = lab.lower()
|
|
138
|
-
lab_tokens = [t for t in re.findall(r"[a-z0-9]+", labl) if len(t) >=
|
|
165
|
+
lab_tokens = [t for t in re.findall(r"[a-z0-9]+", labl) if len(t) >= _SHORT]
|
|
139
166
|
h = sum(1 for t in lab_tokens if t in ql)
|
|
167
|
+
# A short token ("HP", "3M", the "31" of "Account Name 31") counts
|
|
168
|
+
# only when the question has it as a whole word (:func:`_short_words`).
|
|
169
|
+
h += len(_short_words(lab) & q_short)
|
|
140
170
|
h += sum(1 for t in q_tokens if t in labl and not any(t == lt for lt in lab_tokens))
|
|
141
171
|
if h > hits:
|
|
142
172
|
hits, best_len = h, len(labl)
|
|
@@ -38,6 +38,81 @@ def test_topk_and_ranking() -> None:
|
|
|
38
38
|
assert top2[1] == e2
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _companies() -> list[GroundedEntity]:
|
|
42
|
+
"""Short-named companies among longer names, as a CRM's account list holds them."""
|
|
43
|
+
names = ["HP", "GE", "3M", "EY", "AT&T", "Acme Holdings", "Meridian Logistics", "Bank of America"]
|
|
44
|
+
names += [f"{a} {n}" for a in ("Blue", "Atlas", "Golden") for n in ("Systems", "Energy", "Media")]
|
|
45
|
+
return [GroundedEntity(id=n, labels=(n,), type="Account") for n in names]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_short_name_is_found_as_a_whole_word() -> None:
|
|
49
|
+
# A name whose only distinctive word is under three characters ("HP",
|
|
50
|
+
# "GE", "3M") was never retrieved: the scorer dropped every such token.
|
|
51
|
+
index = LabelIndex(_companies())
|
|
52
|
+
for question, name in [
|
|
53
|
+
("Who owns HP?", "HP"),
|
|
54
|
+
("What is the ARR of GE?", "GE"),
|
|
55
|
+
("When does 3M renew?", "3M"),
|
|
56
|
+
("Show EY's open opportunities", "EY"),
|
|
57
|
+
("Who is the account owner for AT&T?", "AT&T"),
|
|
58
|
+
]:
|
|
59
|
+
assert [e.id for e in index.retrieve(question)][:1] == [name], question
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_short_token_does_not_match_inside_a_longer_word() -> None:
|
|
63
|
+
# "ge" is inside "manager" and "changed"; "hp" and "ey" inside nothing here.
|
|
64
|
+
# A short token counts only as a whole word, so none of these are offered.
|
|
65
|
+
index = LabelIndex(_companies())
|
|
66
|
+
for question in ("Who is the account manager?", "What changed in usage?", "Which keys expire?"):
|
|
67
|
+
ids = {e.id for e in index.retrieve(question)}
|
|
68
|
+
assert not ids & {"HP", "GE", "3M", "EY", "AT&T"}, (question, ids)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_short_stopword_in_a_label_matches_nothing() -> None:
|
|
72
|
+
# "of" in "Bank of America" must not tie every question that says "of".
|
|
73
|
+
index = LabelIndex(_companies())
|
|
74
|
+
assert "Bank of America" not in {e.id for e in index.retrieve("list of all renewals")}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_possessive_and_contraction_leave_no_short_fragment() -> None:
|
|
78
|
+
# "Media's" and "don't" must not yield a token "s" or "t" that matches
|
|
79
|
+
# "S.p.A." or "AT&T"; a possessive still finds its exact name first.
|
|
80
|
+
names = ["Atlas Media", "Atlas Media S.p.A.", "AT&T", "Macy's", "Acme"]
|
|
81
|
+
index = LabelIndex([GroundedEntity(id=n, labels=(n,), type="") for n in names])
|
|
82
|
+
assert index.retrieve("What is Atlas Media's ARR?")[0].id == "Atlas Media"
|
|
83
|
+
assert index.retrieve("Which accounts don't renew?") == []
|
|
84
|
+
assert index.retrieve("Who's the owner?") == []
|
|
85
|
+
assert index.retrieve("Who owns Macy's?")[0].id == "Macy's"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_accented_name_yields_no_short_fragments() -> None:
|
|
89
|
+
# "Société Générale" splits, ASCII-wise, into "soci t g n rale"; its short
|
|
90
|
+
# pieces must not match a question's stray letters.
|
|
91
|
+
entity = GroundedEntity(id="SG", labels=("Société Générale",), type="")
|
|
92
|
+
index = LabelIndex([entity])
|
|
93
|
+
assert index.retrieve("plan t for g n accounts") == []
|
|
94
|
+
assert index.retrieve("What is Société Générale's ARR?") == [entity]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_repeated_short_token_counts_once() -> None:
|
|
98
|
+
# "J&J" holds "j" twice; counted twice it would outrank the label "J".
|
|
99
|
+
# Counted once, the two tie and the shorter label wins.
|
|
100
|
+
names = ["J&J", "J"]
|
|
101
|
+
index = LabelIndex([GroundedEntity(id=n, labels=(n,), type="") for n in names])
|
|
102
|
+
assert [e.id for e in index.retrieve("Who is J?")] == ["J", "J&J"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_number_in_a_name_ranks_its_own_name_first() -> None:
|
|
106
|
+
# "Account Name 31" differs from its neighbours only by a two-digit number;
|
|
107
|
+
# "31" must pick it out, and must not match inside "131" or "2031".
|
|
108
|
+
names = [f"Account Name {n}" for n in range(1, 201)]
|
|
109
|
+
index = LabelIndex([GroundedEntity(id=n, labels=(n,), type="") for n in names])
|
|
110
|
+
assert index.retrieve("Who is the owner of Account Name 31?")[0].id == "Account Name 31"
|
|
111
|
+
assert index.retrieve("Who is the owner of Account Name 7?")[0].id == "Account Name 7"
|
|
112
|
+
top = [e.id for e in index.retrieve("renewals of Account Name due in 2031")]
|
|
113
|
+
assert "Account Name 31" not in top[:1]
|
|
114
|
+
|
|
115
|
+
|
|
41
116
|
def test_empty_on_no_match() -> None:
|
|
42
117
|
entity = GroundedEntity(id="http://ex.org/x", labels=("Completely Unrelated",), type="")
|
|
43
118
|
index = LabelIndex([entity])
|
|
File without changes
|
{arango_query_core-0.2.8 → arango_query_core-0.2.9}/.cursor/rules/fix-upstream-dont-fork.mdc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/fixtures/nl_system_prompts_0_2_6.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arango_query_core-0.2.8 → arango_query_core-0.2.9}/tests/test_mapping_resolver_normalization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|