ontosmith 0.1.0__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.
- api/__init__.py +14 -0
- api/server.py +440 -0
- harness/__init__.py +10 -0
- harness/canonicalization.py +298 -0
- harness/doc_check.py +113 -0
- harness/make_graph_image.py +103 -0
- harness/rag_eval.py +116 -0
- harness/rag_eval_embedder.py +164 -0
- harness/rag_eval_hybrid_search.py +130 -0
- harness/render_charts.py +449 -0
- harness/verification/__init__.py +33 -0
- harness/verification/audit.py +168 -0
- harness/verification/cell_runner.py +179 -0
- harness/verification/config.py +379 -0
- harness/verification/report.py +114 -0
- harness/verification/scoring.py +285 -0
- harness/verification/verify_cli.py +164 -0
- ontosmith-0.1.0.data/data/share/ontosmith/app/app.css +312 -0
- ontosmith-0.1.0.data/data/share/ontosmith/app/app.js +409 -0
- ontosmith-0.1.0.data/data/share/ontosmith/app/favicon.svg +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/app/index.html +72 -0
- ontosmith-0.1.0.data/data/share/ontosmith/app/logo-mark.svg +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/README.md +129 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-history.txt +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-people.txt +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-products.txt +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-workshops.txt +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/examples/triples.jsonl +7 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/conformance.json +100 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/fitted_qualifiers.json +33 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/pack.json +114 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/CENSUS.md +139 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/profile.json +316 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/profile_v2.json +816 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config.json +450 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config_v3.json +445 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config_v4.json +450 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/conformance.schema.json +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/core/conformance.json +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/core/pack.json +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/core/profile/setup_config.json +27 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/fetch_btt.py +107 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/fetch_gutenberg.py +93 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/conformance.json +60 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/pack.json +57 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/profile.json +1525 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/profile.md +141 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/setup_config.json +79 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/role_words.json +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/conformance.json +1 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/direction_declarations.json +442 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/pack.json +72 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/profile.json +9 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/setup_config.json +591 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/setup_config_v2.json +1140 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/tn_table.json +101 -0
- ontosmith-0.1.0.data/data/share/ontosmith/packs/pack.schema.json +1 -0
- ontosmith-0.1.0.dist-info/METADATA +507 -0
- ontosmith-0.1.0.dist-info/RECORD +115 -0
- ontosmith-0.1.0.dist-info/WHEEL +5 -0
- ontosmith-0.1.0.dist-info/entry_points.txt +2 -0
- ontosmith-0.1.0.dist-info/licenses/LICENSE +21 -0
- ontosmith-0.1.0.dist-info/top_level.txt +3 -0
- pipeline/__init__.py +4 -0
- pipeline/__main__.py +175 -0
- pipeline/analyze_hardened.py +138 -0
- pipeline/batch.py +274 -0
- pipeline/batch_cli.py +924 -0
- pipeline/batch_run.py +2581 -0
- pipeline/bindings-template.env +67 -0
- pipeline/biored_candidates.py +127 -0
- pipeline/calibration.py +58 -0
- pipeline/configure.py +540 -0
- pipeline/conflict_candidates.py +102 -0
- pipeline/connections.py +608 -0
- pipeline/decisions.py +409 -0
- pipeline/docred.py +113 -0
- pipeline/drift_check.py +281 -0
- pipeline/embedder.py +157 -0
- pipeline/ensemble.py +98 -0
- pipeline/er_candidates.py +80 -0
- pipeline/evaluate.py +163 -0
- pipeline/export_graph.py +68 -0
- pipeline/file_registry.py +716 -0
- pipeline/gate_cli.py +816 -0
- pipeline/graph_store.py +724 -0
- pipeline/graph_write.py +91 -0
- pipeline/hard_negatives.py +125 -0
- pipeline/heartbeat.py +39 -0
- pipeline/inspect.py +289 -0
- pipeline/neo4j_store.py +719 -0
- pipeline/onto_candidates.py +141 -0
- pipeline/ontology_builder.py +1192 -0
- pipeline/operating_points.py +103 -0
- pipeline/pack_schema.py +345 -0
- pipeline/pair_proposer.py +839 -0
- pipeline/profile_config.py +1106 -0
- pipeline/profiler.py +134 -0
- pipeline/promotion.py +261 -0
- pipeline/proposer.py +557 -0
- pipeline/query_stack.py +1330 -0
- pipeline/queue.py +256 -0
- pipeline/recalibrate.py +77 -0
- pipeline/reference_profile.py +67 -0
- pipeline/registry.py +284 -0
- pipeline/registry_store.py +121 -0
- pipeline/relation_map.py +92 -0
- pipeline/rer.py +379 -0
- pipeline/retrieval_eval.py +186 -0
- pipeline/review.py +233 -0
- pipeline/roster_pass.py +188 -0
- pipeline/search.py +1093 -0
- pipeline/setup_service.py +958 -0
- pipeline/threshold_stability.py +59 -0
- pipeline/turtle_export.py +103 -0
api/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The sample API package.
|
|
2
|
+
|
|
3
|
+
A reference HTTP API over the search surface and the
|
|
4
|
+
built graph — search, entity lookup, neighborhood, and
|
|
5
|
+
triple inspection with evidence. It
|
|
6
|
+
is the backend the hands-on guide and the demo app
|
|
7
|
+
run against.
|
|
8
|
+
|
|
9
|
+
The implementation lives in :mod:`api.server` (standard
|
|
10
|
+
library only); importing this package pulls nothing heavy.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__all__ = ["server"]
|
api/server.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""Sample API server: ``ontosmith serve``.
|
|
2
|
+
|
|
3
|
+
A reference HTTP API over the hybrid search
|
|
4
|
+
(:mod:`pipeline.search`) and the built graph:
|
|
5
|
+
|
|
6
|
+
GET /health store identity + inventory
|
|
7
|
+
GET /search hybrid search — the same result
|
|
8
|
+
dict the ``ontosmith search`` CLI
|
|
9
|
+
prints (``?q=...&k=...``)
|
|
10
|
+
GET /entity one entity and its facts
|
|
11
|
+
(``?id=...`` or ``?name=...``)
|
|
12
|
+
GET /neighborhood REL neighborhood of an entity
|
|
13
|
+
(``?id=...&hops=1..4``)
|
|
14
|
+
GET /triple one triple with its evidence spans
|
|
15
|
+
and provenance — the inspection report as
|
|
16
|
+
JSON (``?subject=&relation=&object=``
|
|
17
|
+
or ``?index=N``)
|
|
18
|
+
GET / the demo app: the
|
|
19
|
+
static page in ``app/`` — search box,
|
|
20
|
+
triple/evidence inspector, entity
|
|
21
|
+
report + neighborhood walker
|
|
22
|
+
GET /app/<file> the app's static assets
|
|
23
|
+
|
|
24
|
+
Framework: the Python standard library
|
|
25
|
+
(``http.server.ThreadingHTTPServer``) — a deliberate
|
|
26
|
+
choice for a *reference* server: the product's dependency
|
|
27
|
+
discipline keeps the core install to numpy +
|
|
28
|
+
requests, and a sample API should be readable end-to-end
|
|
29
|
+
in one file, not framework-shaped. Anything heavier
|
|
30
|
+
belongs to the deployment, not the sample.
|
|
31
|
+
|
|
32
|
+
Errors are JSON, named, and shaped like the product's
|
|
33
|
+
refusals: ``{"error": {"code": ..., "message": ...}}`` —
|
|
34
|
+
400 for bad input (missing/invalid parameters), 404 for
|
|
35
|
+
an unknown entity or unmatched triple, 500 only for an
|
|
36
|
+
unexpected server fault (message named, no traceback).
|
|
37
|
+
The server binds localhost by default and takes its store
|
|
38
|
+
from the same surfaces as the CLI (``--source`` /
|
|
39
|
+
``--batch`` / ``--store`` / ``NEO4J_*``); with no source
|
|
40
|
+
it serves the shipped example build, so a stranger can
|
|
41
|
+
start it and query the demo graph immediately.
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import argparse
|
|
46
|
+
import json
|
|
47
|
+
import sys
|
|
48
|
+
import sysconfig
|
|
49
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from urllib.parse import parse_qs, urlparse
|
|
52
|
+
|
|
53
|
+
from pipeline import connections as _connections
|
|
54
|
+
from pipeline import inspect as _inspect
|
|
55
|
+
from pipeline import search as _search
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ApiError(Exception):
|
|
59
|
+
"""A named, shaped API refusal: HTTP status + code."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, status: int, code: str, message: str):
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.status = status
|
|
64
|
+
self.code = code
|
|
65
|
+
self.message = message
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SearchService:
|
|
69
|
+
"""The endpoint logic over one opened serving store.
|
|
70
|
+
Transport-free: the handler below is a thin shell, so
|
|
71
|
+
the contract is testable without a socket."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, store, embedder=None):
|
|
74
|
+
self.store = store
|
|
75
|
+
self.embedder = embedder
|
|
76
|
+
|
|
77
|
+
# -- endpoints -------------------------------------------
|
|
78
|
+
def health(self) -> dict:
|
|
79
|
+
"""Store identity + inventory (the store's own
|
|
80
|
+
describe(), plus a status line)."""
|
|
81
|
+
return {"status": "ok", "store": self.store.describe()}
|
|
82
|
+
|
|
83
|
+
def search(self, params: dict) -> dict:
|
|
84
|
+
"""GET /search?q=...&k=... — the hybrid_search
|
|
85
|
+
result dict, unchanged."""
|
|
86
|
+
q = _required(params, "q")
|
|
87
|
+
k = _int_param(params, "k", 10, lo=1, hi=100)
|
|
88
|
+
try:
|
|
89
|
+
return _search.hybrid_search(
|
|
90
|
+
self.store, q, k=k, embedder=self.embedder)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
raise ApiError(400, "bad_query", str(e)) from e
|
|
93
|
+
|
|
94
|
+
def entity(self, params: dict) -> dict:
|
|
95
|
+
"""GET /entity?id=... | ?name=... — the entity
|
|
96
|
+
record plus every attested fact it endpoints."""
|
|
97
|
+
ent = self._resolve_entity(params)
|
|
98
|
+
facts = [t for t in self.store.triples()
|
|
99
|
+
if t["subject_id"] == ent["entity_id"]
|
|
100
|
+
or t["object_id"] == ent["entity_id"]]
|
|
101
|
+
return {
|
|
102
|
+
**ent,
|
|
103
|
+
"facts_as_subject": sum(
|
|
104
|
+
1 for t in facts
|
|
105
|
+
if t["subject_id"] == ent["entity_id"]),
|
|
106
|
+
"facts_as_object": sum(
|
|
107
|
+
1 for t in facts
|
|
108
|
+
if t["object_id"] == ent["entity_id"]),
|
|
109
|
+
"facts": facts,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def neighborhood(self, params: dict) -> dict:
|
|
113
|
+
"""GET /neighborhood?id=...&hops=1..4 — the
|
|
114
|
+
store's neighborhood contract, with the seed
|
|
115
|
+
entity echoed."""
|
|
116
|
+
ent = self._resolve_entity(params)
|
|
117
|
+
hops = _int_param(params, "hops", 1, lo=1, hi=4)
|
|
118
|
+
result = self.store.neighborhood(
|
|
119
|
+
[ent["entity_id"]], hops=hops)
|
|
120
|
+
return {"entity": ent, "hops": hops, **result}
|
|
121
|
+
|
|
122
|
+
def triple(self, params: dict) -> dict:
|
|
123
|
+
"""GET /triple?subject=&relation=&object=&index=
|
|
124
|
+
— one triple as the inspection report."""
|
|
125
|
+
records = self.store.triple_records()
|
|
126
|
+
index = params.get("index")
|
|
127
|
+
index = int(index) if index is not None else None
|
|
128
|
+
try:
|
|
129
|
+
rec, position, total = _inspect.select(
|
|
130
|
+
records, index=index,
|
|
131
|
+
subject=params.get("subject"),
|
|
132
|
+
relation=params.get("relation"),
|
|
133
|
+
object_=params.get("object"))
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
raise ApiError(404, "triple_not_found",
|
|
136
|
+
str(e)) from e
|
|
137
|
+
return _inspect.build_report(rec, "api", position,
|
|
138
|
+
total)
|
|
139
|
+
|
|
140
|
+
# -- helpers -----------------------------------------------
|
|
141
|
+
def _resolve_entity(self, params: dict) -> dict:
|
|
142
|
+
"""Entity by id (exact) or by name (the store's
|
|
143
|
+
deterministic link, best hit). 400 when neither
|
|
144
|
+
parameter is given, 404 when nothing resolves."""
|
|
145
|
+
entity_id = params.get("id")
|
|
146
|
+
name = params.get("name")
|
|
147
|
+
if entity_id:
|
|
148
|
+
ent = self.store.entity(entity_id)
|
|
149
|
+
if ent is None:
|
|
150
|
+
raise ApiError(
|
|
151
|
+
404, "entity_not_found",
|
|
152
|
+
f"no entity with id {entity_id!r}")
|
|
153
|
+
return ent
|
|
154
|
+
if name:
|
|
155
|
+
hits = self.store.find_entities(name, limit=1)
|
|
156
|
+
if not hits:
|
|
157
|
+
raise ApiError(
|
|
158
|
+
404, "entity_not_found",
|
|
159
|
+
f"no entity matches name {name!r}")
|
|
160
|
+
return hits[0]
|
|
161
|
+
raise ApiError(
|
|
162
|
+
400, "missing_parameter",
|
|
163
|
+
"entity lookup needs ?id= or ?name=")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
API_ROUTES = frozenset(
|
|
167
|
+
{"/health", "/search", "/entity", "/neighborhood",
|
|
168
|
+
"/triple"})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def api_route(service: SearchService, path: str,
|
|
172
|
+
params: dict):
|
|
173
|
+
"""The API route table: a zero-arg callable producing
|
|
174
|
+
the response payload for ``path``, or None when the
|
|
175
|
+
path is not an API route. ``API_ROUTES`` is exactly
|
|
176
|
+
this table's key set (pinned by test) — the demo app
|
|
177
|
+
pins the endpoints it calls against it."""
|
|
178
|
+
routes = {
|
|
179
|
+
"/health": lambda: service.health(),
|
|
180
|
+
"/search": lambda: service.search(params),
|
|
181
|
+
"/entity": lambda: service.entity(params),
|
|
182
|
+
"/neighborhood":
|
|
183
|
+
lambda: service.neighborhood(params),
|
|
184
|
+
"/triple": lambda: service.triple(params),
|
|
185
|
+
}
|
|
186
|
+
return routes.get(path)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _required(params: dict, name: str) -> str:
|
|
190
|
+
"""A required non-empty query parameter."""
|
|
191
|
+
val = params.get(name)
|
|
192
|
+
if val is None or not str(val).strip():
|
|
193
|
+
raise ApiError(400, "missing_parameter",
|
|
194
|
+
f"missing required parameter: {name}")
|
|
195
|
+
return str(val)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _int_param(params: dict, name: str, default: int,
|
|
199
|
+
lo: int, hi: int) -> int:
|
|
200
|
+
"""An optional integer parameter, range-checked; the
|
|
201
|
+
refusal names the parameter and the allowed range."""
|
|
202
|
+
raw = params.get(name)
|
|
203
|
+
if raw is None:
|
|
204
|
+
return default
|
|
205
|
+
try:
|
|
206
|
+
val = int(raw)
|
|
207
|
+
except (TypeError, ValueError):
|
|
208
|
+
raise ApiError(
|
|
209
|
+
400, "invalid_parameter",
|
|
210
|
+
f"parameter {name} must be an integer, "
|
|
211
|
+
f"got {raw!r}") from None
|
|
212
|
+
if not lo <= val <= hi:
|
|
213
|
+
raise ApiError(
|
|
214
|
+
400, "invalid_parameter",
|
|
215
|
+
f"parameter {name} must be {lo}..{hi}, "
|
|
216
|
+
f"got {val}")
|
|
217
|
+
return val
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ----------------------------------------------------------------------
|
|
221
|
+
# Demo app — static assets, served by the same shell
|
|
222
|
+
# ----------------------------------------------------------------------
|
|
223
|
+
APP_CONTENT_TYPES = {
|
|
224
|
+
".html": "text/html; charset=utf-8",
|
|
225
|
+
".css": "text/css; charset=utf-8",
|
|
226
|
+
".js": "text/javascript; charset=utf-8",
|
|
227
|
+
".svg": "image/svg+xml",
|
|
228
|
+
".png": "image/png",
|
|
229
|
+
".ico": "image/x-icon",
|
|
230
|
+
".json": "application/json; charset=utf-8",
|
|
231
|
+
".txt": "text/plain; charset=utf-8",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def app_candidates() -> list[Path]:
|
|
236
|
+
"""Where the demo app's static files may live, in
|
|
237
|
+
resolution order — the shipped example's candidate
|
|
238
|
+
discipline: the source tree, the pip
|
|
239
|
+
``--target`` layout (data files beside the packages),
|
|
240
|
+
the regular install layout (the interpreter's data
|
|
241
|
+
path)."""
|
|
242
|
+
pkg_parent = Path(__file__).resolve().parent.parent
|
|
243
|
+
return [
|
|
244
|
+
pkg_parent / "app",
|
|
245
|
+
pkg_parent / "share" / "ontosmith" / "app",
|
|
246
|
+
Path(sysconfig.get_path("data")) / "share"
|
|
247
|
+
/ "ontosmith" / "app",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def app_dir() -> Path | None:
|
|
252
|
+
"""The resolved demo-app directory (the first
|
|
253
|
+
candidate holding ``index.html``), or None when no
|
|
254
|
+
candidate resolves — the server then serves the JSON
|
|
255
|
+
API only and says so at startup."""
|
|
256
|
+
for cand in app_candidates():
|
|
257
|
+
if (cand / "index.html").is_file():
|
|
258
|
+
return cand
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def app_asset(name: str) -> tuple[bytes, str] | None:
|
|
263
|
+
"""One app asset by bare file name: ``(bytes,
|
|
264
|
+
content type)``, or None. The name must be a single
|
|
265
|
+
path segment — no directories, no traversal, no
|
|
266
|
+
dotfiles — with a known content type, and the file
|
|
267
|
+
must exist in the resolved app directory."""
|
|
268
|
+
if not name or name != Path(name).name \
|
|
269
|
+
or name.startswith("."):
|
|
270
|
+
return None
|
|
271
|
+
root = app_dir()
|
|
272
|
+
if root is None:
|
|
273
|
+
return None
|
|
274
|
+
path = root / name
|
|
275
|
+
ctype = APP_CONTENT_TYPES.get(path.suffix.lower())
|
|
276
|
+
if ctype is None or not path.is_file():
|
|
277
|
+
return None
|
|
278
|
+
return path.read_bytes(), ctype
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ----------------------------------------------------------------------
|
|
282
|
+
# HTTP transport — a thin shell over SearchService
|
|
283
|
+
# ----------------------------------------------------------------------
|
|
284
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
285
|
+
"""Routes GETs: ``/`` and ``/app/<file>`` serve the
|
|
286
|
+
demo app; the API routes answer JSON. No other
|
|
287
|
+
verbs: the sample API is read-only."""
|
|
288
|
+
|
|
289
|
+
service: SearchService # set by build_server
|
|
290
|
+
protocol_version = "HTTP/1.1"
|
|
291
|
+
|
|
292
|
+
def log_message(self, *args): # silence access logs
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
def do_GET(self):
|
|
296
|
+
parsed = urlparse(self.path)
|
|
297
|
+
if parsed.path == "/" \
|
|
298
|
+
or parsed.path.startswith("/app/"):
|
|
299
|
+
self._serve_app(parsed.path)
|
|
300
|
+
return
|
|
301
|
+
params = {k: v[0] for k, v in
|
|
302
|
+
parse_qs(parsed.query).items()}
|
|
303
|
+
route = api_route(self.service, parsed.path,
|
|
304
|
+
params)
|
|
305
|
+
if route is None:
|
|
306
|
+
self._send(404, {"error": {
|
|
307
|
+
"code": "not_found",
|
|
308
|
+
"message": f"no such endpoint: "
|
|
309
|
+
f"{parsed.path}"}})
|
|
310
|
+
return
|
|
311
|
+
try:
|
|
312
|
+
self._send(200, route())
|
|
313
|
+
except ApiError as e:
|
|
314
|
+
self._send(e.status, {"error": {
|
|
315
|
+
"code": e.code, "message": e.message}})
|
|
316
|
+
except Exception as e: # named, never a traceback
|
|
317
|
+
self._send(500, {"error": {
|
|
318
|
+
"code": "internal",
|
|
319
|
+
"message": f"{type(e).__name__}: {e}"}})
|
|
320
|
+
|
|
321
|
+
def _serve_app(self, path: str) -> None:
|
|
322
|
+
"""The demo app: ``/`` is its index page,
|
|
323
|
+
``/app/<file>`` one of its assets. A miss is the
|
|
324
|
+
API's JSON 404, never an HTML error page."""
|
|
325
|
+
name = "index.html" if path == "/" \
|
|
326
|
+
else path[len("/app/"):]
|
|
327
|
+
asset = app_asset(name)
|
|
328
|
+
if asset is None:
|
|
329
|
+
self._send(404, {"error": {
|
|
330
|
+
"code": "not_found",
|
|
331
|
+
"message": f"no such app asset: {path}"}})
|
|
332
|
+
return
|
|
333
|
+
body, ctype = asset
|
|
334
|
+
self.send_response(200)
|
|
335
|
+
self.send_header("Content-Type", ctype)
|
|
336
|
+
self.send_header("Content-Length", str(len(body)))
|
|
337
|
+
self.end_headers()
|
|
338
|
+
self.wfile.write(body)
|
|
339
|
+
|
|
340
|
+
def _send(self, status: int, payload: dict) -> None:
|
|
341
|
+
body = json.dumps(payload, ensure_ascii=False,
|
|
342
|
+
).encode("utf-8")
|
|
343
|
+
self.send_response(status)
|
|
344
|
+
self.send_header("Content-Type",
|
|
345
|
+
"application/json; charset=utf-8")
|
|
346
|
+
self.send_header("Content-Length", str(len(body)))
|
|
347
|
+
self.end_headers()
|
|
348
|
+
self.wfile.write(body)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_server(host: str, port: int,
|
|
352
|
+
service: SearchService) -> ThreadingHTTPServer:
|
|
353
|
+
"""A bound (not yet serving) server for ``service``.
|
|
354
|
+
Port 0 picks an ephemeral port — the test suite's
|
|
355
|
+
route, and a safe default for embedding."""
|
|
356
|
+
handler = type("BoundHandler", (_Handler,),
|
|
357
|
+
{"service": service})
|
|
358
|
+
return ThreadingHTTPServer((host, port), handler)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def main(argv=None) -> int:
|
|
362
|
+
"""``ontosmith serve [--source PATH] [--batch ID]
|
|
363
|
+
[--store auto|file|neo4j] [--host H] [--port N]
|
|
364
|
+
[--embed]`` — serve the sample API (default:
|
|
365
|
+
localhost:8765, the shipped example build). The
|
|
366
|
+
demo app is served at ``/`` on the same port
|
|
367
|
+
when its assets resolve."""
|
|
368
|
+
ap = argparse.ArgumentParser(
|
|
369
|
+
prog="ontosmith serve",
|
|
370
|
+
description="Sample API server over the "
|
|
371
|
+
"search/graph surface: "
|
|
372
|
+
"/search, /entity, /neighborhood, "
|
|
373
|
+
"/triple (with evidence)")
|
|
374
|
+
ap.add_argument("--source", default=None,
|
|
375
|
+
help="batch directory, records "
|
|
376
|
+
"directory, or records file "
|
|
377
|
+
"(default: the shipped example "
|
|
378
|
+
"build)")
|
|
379
|
+
ap.add_argument("--batch", default=None,
|
|
380
|
+
help="batch id under the state root's "
|
|
381
|
+
"batches/ (alternative to --source)")
|
|
382
|
+
ap.add_argument("--store", default="auto",
|
|
383
|
+
choices=("auto", "file", "neo4j"),
|
|
384
|
+
help="serving store: auto = Neo4j when "
|
|
385
|
+
"NEO4J_URL is configured, else the "
|
|
386
|
+
"file store (default: auto)")
|
|
387
|
+
ap.add_argument("--host", default="127.0.0.1",
|
|
388
|
+
help="bind host (default: 127.0.0.1)")
|
|
389
|
+
ap.add_argument("--port", type=int, default=8765,
|
|
390
|
+
help="bind port (default: 8765)")
|
|
391
|
+
ap.add_argument("--embed", action="store_true",
|
|
392
|
+
help="activate the search vector arm "
|
|
393
|
+
"via the connection layer's "
|
|
394
|
+
"default embedder (refuses if the "
|
|
395
|
+
"embedder seat's key is unset)")
|
|
396
|
+
args = ap.parse_args(argv)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
store = _search.open_store(source=args.source,
|
|
400
|
+
batch=args.batch,
|
|
401
|
+
store=args.store)
|
|
402
|
+
embedder = None
|
|
403
|
+
if args.embed:
|
|
404
|
+
from pipeline.embedder import default_embedder
|
|
405
|
+
embedder = default_embedder()
|
|
406
|
+
else:
|
|
407
|
+
# Construction only — no call is made here,
|
|
408
|
+
# and hybrid_search never embeds against a
|
|
409
|
+
# store that ships no chunk embeddings.
|
|
410
|
+
embedder = _search.try_embedder()
|
|
411
|
+
except (ValueError, _connections.BindingKeyError) as e:
|
|
412
|
+
print(f"serve refused: {e}", file=sys.stderr)
|
|
413
|
+
return 1
|
|
414
|
+
service = SearchService(store, embedder=embedder)
|
|
415
|
+
server = build_server(args.host, args.port, service)
|
|
416
|
+
host, port = server.server_address[:2]
|
|
417
|
+
desc = store.describe()
|
|
418
|
+
print(f"ontosmith serve: {desc['kind']} store "
|
|
419
|
+
f"({desc['triples']} triples, "
|
|
420
|
+
f"{desc['documents']} documents) at "
|
|
421
|
+
f"http://{host}:{port} — /search /entity "
|
|
422
|
+
f"/neighborhood /triple /health")
|
|
423
|
+
app = app_dir()
|
|
424
|
+
if app is not None:
|
|
425
|
+
print(f"ontosmith serve: demo app at "
|
|
426
|
+
f"http://{host}:{port}/ (assets: {app})")
|
|
427
|
+
else:
|
|
428
|
+
print("ontosmith serve: demo app assets not "
|
|
429
|
+
"found — JSON API only")
|
|
430
|
+
try:
|
|
431
|
+
server.serve_forever()
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
pass
|
|
434
|
+
finally:
|
|
435
|
+
server.server_close()
|
|
436
|
+
return 0
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
if __name__ == "__main__":
|
|
440
|
+
sys.exit(main())
|
harness/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""OntoSmith verification harness.
|
|
2
|
+
|
|
3
|
+
The harness is the measurement layer that ships with the
|
|
4
|
+
product in a supporting role: the tooling adopters
|
|
5
|
+
use to audit their own gold and score their own builds, and
|
|
6
|
+
the same tooling that produces the public quality record. The
|
|
7
|
+
reusable cores live in :mod:`harness.verification`; the
|
|
8
|
+
standalone evaluation scripts beside this package are the
|
|
9
|
+
retrieval-evaluation surface.
|
|
10
|
+
"""
|