hyphal 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +60 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +24 -0
- package/dist/factStore.d.ts +24 -0
- package/dist/factStore.js +88 -0
- package/dist/facts.d.ts +12 -0
- package/dist/facts.js +49 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/ontology.d.ts +12 -0
- package/dist/ontology.js +38 -0
- package/dist/query.d.ts +9 -0
- package/dist/query.js +22 -0
- package/dist/render.d.ts +16 -0
- package/dist/render.js +52 -0
- package/dist/schemas.d.ts +111 -0
- package/dist/schemas.js +29 -0
- package/dist/search.d.ts +13 -0
- package/dist/search.js +18 -0
- package/dist/store.d.ts +40 -0
- package/dist/store.js +89 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atharva Deshpande
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# hyphal
|
|
2
|
+
|
|
3
|
+
**Turn your unstructured documents into something your agents can actually reason over.**
|
|
4
|
+
|
|
5
|
+
Hand hyphal a pile of documents. It reads them once, builds an RDF ontology and a validated fact graph as it goes, and gives your agents a way to ask precise questions against everything it's read — instead of re-reading raw text every turn, or drowning in a context window that grows with your document count.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export ANTHROPIC_API_KEY=sk-...
|
|
9
|
+
npx hyphal extract document1.txt document2.txt
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Runs entirely on your infrastructure, on your own Anthropic key. No server, no hosted service, nothing leaves your machine.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What your agents get
|
|
17
|
+
|
|
18
|
+
Not the documents. Not a data dump. Two things, both cheap to hand an agent on every call:
|
|
19
|
+
|
|
20
|
+
- **A schema** — stable, versioned, safe to keep in context indefinitely — that tells the agent what's knowable about your document set.
|
|
21
|
+
- **A query interface** — SPARQL, run locally against the extracted graph — the agent asks exactly what it needs and gets back exactly that, at effectively zero marginal cost per question, regardless of the size.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## How it stays accurate as your document set grows
|
|
25
|
+
|
|
26
|
+
Every new document is processed against the ontology's current serialized state, shown to the model as context — not extracted from scratch. Whatever the model proposes is diffed against exactly that snapshot, an exact structural comparison, not fuzzy or embedding-based. Anything it restates that already exists is dropped before it's ever written; only the genuine delta gets committed, as a new content-hashed version chained to the one before it.
|
|
27
|
+
|
|
28
|
+
The same discipline applies to entities: before extracting facts from a new document, the model is shown a summary of what's already known, so "Acme" in document forty resolves to the same record "Acme Robotics Inc." created in document one — not a duplicate. Facts live in a separate namespace from the schema, so the two can never collide.
|
|
29
|
+
|
|
30
|
+
Run two real documents through the same store and the effect is directly observable: the first contributes a substantial ontology delta, the second contributes noticeably less, because the concepts it needs mostly already exist. That shrinking delta is the mechanism working
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart TD
|
|
36
|
+
A[Unstructured documents] --> B[Render + diff loop]
|
|
37
|
+
B --> C[Ontology store]
|
|
38
|
+
B --> D[Facts store]
|
|
39
|
+
C --> E[Query layer — SPARQL via Comunica]
|
|
40
|
+
D --> E
|
|
41
|
+
E --> F[Your agents]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What gets exposed to your agents
|
|
45
|
+
|
|
46
|
+
Three functions, deliberately small in count — this is the whole tool surface an agent framework needs to wrap:
|
|
47
|
+
|
|
48
|
+
- **`toTurtle()`** — the current schema, serialized. Cheap enough to include as context on every single call, so the agent always knows what's askable before it asks anything.
|
|
49
|
+
- **`queryGraph(sparql)`** — the agent writes its own SPARQL, informed by the schema above, and gets back exactly the matching rows. Runs locally, no LLM in the path, effectively free per call.
|
|
50
|
+
- **`searchEntities(name)`** — resolves a name the agent knows ("Acme") to the exact identifier it doesn't (`cd:acme_robotics_inc`), so the query above can be precise instead of guessed.
|
|
51
|
+
|
|
52
|
+
An agent gets the schema once per turn, calls `searchEntities` when it needs an identifier, and `queryGraph` when it needs a fact — nothing more ever needs to enter its context.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## For engineers looking at the code
|
|
56
|
+
|
|
57
|
+
The entire reuse-not-duplicate mechanism lives in dependency-free function: `complementInserts` in `src/diff.ts` — an exact subject/predicate/object(+datatype) key comparison against the snapshot the model was shown, roughly twenty lines. `test/diff.test.ts` proves it deterministically, no API key required, if you'd rather verify the claim above than take it on faith.
|
|
58
|
+
|
|
59
|
+
Querying runs on Comunica against N3-backed RDF stores, entirely locally — real SPARQL, real joins, no LLM anywhere in the query path. Only the extraction step costs anything. Entity resolution (`searchEntities`) is a plain label substring match today, not embeddings — deliberately simple, resolves a name an agent knows to an IRI it doesn't, ahead of a precise query.
|
|
60
|
+
|
|
61
|
+
This is an early, honest-scope build: single process, sequential, in-memory per run, no persistence across invocations yet, no document-update or retraction handling. Each is a deliberate next step, not an oversight — the priority so far has been proving the core mechanism before hardening what's around it. Issues and PRs welcome, especially on persistence and entity resolution.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { OntologyStore } from "./store.js";
|
|
3
|
+
import { FactsStore } from "./factStore.js";
|
|
4
|
+
import { evolveOntology } from "./ontology.js";
|
|
5
|
+
import { extractFacts } from "./facts.js";
|
|
6
|
+
const DEFAULT_PREFIXES = {
|
|
7
|
+
ex: "https://example.org/ontology#",
|
|
8
|
+
cd: "https://example.org/facts#",
|
|
9
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
10
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
11
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
12
|
+
};
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = [...argv];
|
|
15
|
+
let apiKey = process.env.ANTHROPIC_API_KEY;
|
|
16
|
+
const files = [];
|
|
17
|
+
while (args.length) {
|
|
18
|
+
const arg = args.shift();
|
|
19
|
+
if (arg === "--api-key") {
|
|
20
|
+
apiKey = args.shift();
|
|
21
|
+
}
|
|
22
|
+
else if (arg === "extract") {
|
|
23
|
+
// command word, ignore
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
files.push(arg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { apiKey, files };
|
|
30
|
+
}
|
|
31
|
+
async function main() {
|
|
32
|
+
const { apiKey, files } = parseArgs(process.argv.slice(2));
|
|
33
|
+
if (!apiKey) {
|
|
34
|
+
console.error("Error: no API key found. Set ANTHROPIC_API_KEY in your environment, or pass --api-key sk-...\n" +
|
|
35
|
+
"This tool never ships or stores a key of its own — it only uses yours, for this run.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (files.length === 0) {
|
|
39
|
+
console.error("Usage: hyphal extract <file.txt> [file2.txt ...]");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const ontologyStore = new OntologyStore(DEFAULT_PREFIXES);
|
|
43
|
+
const factsStore = new FactsStore(DEFAULT_PREFIXES);
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const text = await readFile(file, "utf-8");
|
|
46
|
+
console.error(`\n--- Processing ${file} ---`);
|
|
47
|
+
const ontologyResult = await evolveOntology(ontologyStore, text, apiKey);
|
|
48
|
+
console.error(`Ontology: +${ontologyResult.added.length} new triple(s), version ${ontologyResult.version.hash} (parent: ${ontologyResult.version.parentHash ?? "none"})`);
|
|
49
|
+
const factsResult = await extractFacts(ontologyStore, factsStore, text, apiKey);
|
|
50
|
+
console.error(`Facts: +${factsResult.facts.length} new triple(s) (total: ${factsStore.size})`);
|
|
51
|
+
}
|
|
52
|
+
console.log("\n=== Final ontology (Turtle) ===");
|
|
53
|
+
console.log(await ontologyStore.toTurtle());
|
|
54
|
+
console.log("\n=== Final facts (Turtle) ===");
|
|
55
|
+
console.log(await factsStore.toTurtle());
|
|
56
|
+
}
|
|
57
|
+
main().catch((err) => {
|
|
58
|
+
console.error("Fatal error:", err.message ?? err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Triple } from "./schemas.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return only the triples in `proposed` that are NOT already present in
|
|
4
|
+
* `snapshotKeys` — the exact ontology state that was shown to the LLM as
|
|
5
|
+
* context for this render call.
|
|
6
|
+
*
|
|
7
|
+
* This is the single most important function in the system: it's what lets
|
|
8
|
+
* the LLM freely restate existing schema (which it will do constantly,
|
|
9
|
+
* since it's reasoning over the whole snapshot) without that restatement
|
|
10
|
+
* being treated as new information.
|
|
11
|
+
*/
|
|
12
|
+
export declare function complementInserts(proposed: Triple[], snapshotKeys: Set<string>, prefixes: Record<string, string>): Triple[];
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { tripleToQuad } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Return only the triples in `proposed` that are NOT already present in
|
|
4
|
+
* `snapshotKeys` — the exact ontology state that was shown to the LLM as
|
|
5
|
+
* context for this render call.
|
|
6
|
+
*
|
|
7
|
+
* This is the single most important function in the system: it's what lets
|
|
8
|
+
* the LLM freely restate existing schema (which it will do constantly,
|
|
9
|
+
* since it's reasoning over the whole snapshot) without that restatement
|
|
10
|
+
* being treated as new information.
|
|
11
|
+
*/
|
|
12
|
+
export function complementInserts(proposed, snapshotKeys, prefixes) {
|
|
13
|
+
const seenInThisBatch = new Set();
|
|
14
|
+
const result = [];
|
|
15
|
+
for (const triple of proposed) {
|
|
16
|
+
const quad = tripleToQuad(triple, prefixes);
|
|
17
|
+
const key = `${quad.subject.value}\u0000${quad.predicate.value}\u0000${quad.object.value}\u0000${quad.object.termType === "Literal" ? quad.object.datatype?.value ?? "" : ""}`;
|
|
18
|
+
if (!snapshotKeys.has(key) && !seenInThisBatch.has(key)) {
|
|
19
|
+
seenInThisBatch.add(key);
|
|
20
|
+
result.push(triple);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Store } from "n3";
|
|
2
|
+
import type { Triple } from "./schemas.js";
|
|
3
|
+
/**
|
|
4
|
+
* Store for extracted instance facts, kept separate from the ontology's
|
|
5
|
+
* own store. Adds getEntitySummary(), which lets extractFacts() show the
|
|
6
|
+
* LLM which entities already exist — the facts-side equivalent of showing
|
|
7
|
+
* the ontology snapshot for schema reuse.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FactsStore {
|
|
10
|
+
readonly prefixes: Record<string, string>;
|
|
11
|
+
private store;
|
|
12
|
+
constructor(prefixes: Record<string, string>);
|
|
13
|
+
addNew(triples: Triple[]): Triple[];
|
|
14
|
+
toTurtle(): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Compact text summary of known entities (IRI, type, label), shown to
|
|
17
|
+
* the LLM so it can reuse an existing entity's IRI instead of minting
|
|
18
|
+
* a duplicate under a different name.
|
|
19
|
+
*/
|
|
20
|
+
getEntitySummary(): string;
|
|
21
|
+
/** Raw RDF/JS-compatible store, for Comunica SPARQL queries. */
|
|
22
|
+
get rdfSource(): Store;
|
|
23
|
+
get size(): number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Store, Writer } from "n3";
|
|
2
|
+
import { tripleToQuad } from "./store.js";
|
|
3
|
+
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
|
4
|
+
const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
|
|
5
|
+
/** Turn a full IRI back into its prefixed form, e.g. "https://.../Company" -> "ex:Company". */
|
|
6
|
+
function compactIri(iri, prefixes) {
|
|
7
|
+
for (const [prefix, namespace] of Object.entries(prefixes)) {
|
|
8
|
+
if (iri.startsWith(namespace)) {
|
|
9
|
+
return `${prefix}:${iri.slice(namespace.length)}`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return `<${iri}>`; // fallback: full IRI, unrecognized namespace
|
|
13
|
+
}
|
|
14
|
+
function quadKey(subject, predicate, object) {
|
|
15
|
+
return `${subject}\u0000${predicate}\u0000${object}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Store for extracted instance facts, kept separate from the ontology's
|
|
19
|
+
* own store. Adds getEntitySummary(), which lets extractFacts() show the
|
|
20
|
+
* LLM which entities already exist — the facts-side equivalent of showing
|
|
21
|
+
* the ontology snapshot for schema reuse.
|
|
22
|
+
*/
|
|
23
|
+
export class FactsStore {
|
|
24
|
+
prefixes;
|
|
25
|
+
store = new Store();
|
|
26
|
+
constructor(prefixes) {
|
|
27
|
+
this.prefixes = prefixes;
|
|
28
|
+
}
|
|
29
|
+
addNew(triples) {
|
|
30
|
+
const existing = new Set(this.store
|
|
31
|
+
.getQuads(null, null, null, null)
|
|
32
|
+
.map((q) => quadKey(q.subject.value, q.predicate.value, q.object.value)));
|
|
33
|
+
const added = [];
|
|
34
|
+
for (const t of triples) {
|
|
35
|
+
const quad = tripleToQuad(t, this.prefixes);
|
|
36
|
+
const key = quadKey(quad.subject.value, quad.predicate.value, quad.object.value);
|
|
37
|
+
if (!existing.has(key)) {
|
|
38
|
+
this.store.addQuad(quad);
|
|
39
|
+
existing.add(key);
|
|
40
|
+
added.push(t);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return added;
|
|
44
|
+
}
|
|
45
|
+
toTurtle() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const writer = new Writer({ prefixes: this.prefixes });
|
|
48
|
+
for (const quad of this.store.getQuads(null, null, null, null)) {
|
|
49
|
+
writer.addQuad(quad);
|
|
50
|
+
}
|
|
51
|
+
writer.end((err, result) => (err ? reject(err) : resolve(result)));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compact text summary of known entities (IRI, type, label), shown to
|
|
56
|
+
* the LLM so it can reuse an existing entity's IRI instead of minting
|
|
57
|
+
* a duplicate under a different name.
|
|
58
|
+
*/
|
|
59
|
+
getEntitySummary() {
|
|
60
|
+
const types = new Map(); // subject IRI -> type IRI
|
|
61
|
+
const labels = new Map(); // subject IRI -> label
|
|
62
|
+
for (const quad of this.store.getQuads(null, null, null, null)) {
|
|
63
|
+
if (quad.predicate.value === RDF_TYPE) {
|
|
64
|
+
types.set(quad.subject.value, quad.object.value);
|
|
65
|
+
}
|
|
66
|
+
else if (quad.predicate.value === RDFS_LABEL) {
|
|
67
|
+
labels.set(quad.subject.value, quad.object.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (types.size === 0)
|
|
71
|
+
return "(no entities extracted yet)";
|
|
72
|
+
const lines = [];
|
|
73
|
+
for (const [subjectIri, typeIri] of types) {
|
|
74
|
+
const subject = compactIri(subjectIri, this.prefixes);
|
|
75
|
+
const type = compactIri(typeIri, this.prefixes);
|
|
76
|
+
const label = labels.get(subjectIri);
|
|
77
|
+
lines.push(label ? `${subject} (${type}, label: "${label}")` : `${subject} (${type})`);
|
|
78
|
+
}
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
/** Raw RDF/JS-compatible store, for Comunica SPARQL queries. */
|
|
82
|
+
get rdfSource() {
|
|
83
|
+
return this.store;
|
|
84
|
+
}
|
|
85
|
+
get size() {
|
|
86
|
+
return this.store.getQuads(null, null, null, null).length;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OntologyStore } from "./store.js";
|
|
2
|
+
import type { FactsStore } from "./factStore.js";
|
|
3
|
+
import type { Triple } from "./schemas.js";
|
|
4
|
+
export interface FactsResult {
|
|
5
|
+
facts: Triple[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract instance-level facts from a text chunk against the CURRENT state
|
|
9
|
+
* of the ontology store, AND the current state of the facts store — the
|
|
10
|
+
* latter is what lets entity reuse across documents actually happen.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractFacts(ontologyStore: OntologyStore, factsStore: FactsStore, text: string, apiKey: string): Promise<FactsResult>;
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render } from "./render.js";
|
|
2
|
+
const FACTS_INSTRUCTIONS = `You are extracting FACTS (instance data) from the text below, typed against
|
|
3
|
+
the domain ontology shown to you.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Check the "Known entities" list below FIRST. If the text refers to an
|
|
7
|
+
entity already listed there (even under a different name, abbreviation,
|
|
8
|
+
or phrasing — e.g. "Acme" vs "Acme Robotics Inc."), reuse its EXACT
|
|
9
|
+
existing IRI. Do not mint a new IRI for something that already exists.
|
|
10
|
+
- Only mint a new "cd:" IRI for entities that are genuinely not in the
|
|
11
|
+
known entities list.
|
|
12
|
+
- Type each new entity using rdf:type against an EXISTING class from the
|
|
13
|
+
ontology shown to you. Do not invent new classes here.
|
|
14
|
+
- Reuse ontology properties (ex:...) for relationships between cd: entities.
|
|
15
|
+
- Use lowercase_snake_case or lowerCamelCase local names under cd:.
|
|
16
|
+
- Emit insert operations only.`;
|
|
17
|
+
/**
|
|
18
|
+
* Extract instance-level facts from a text chunk against the CURRENT state
|
|
19
|
+
* of the ontology store, AND the current state of the facts store — the
|
|
20
|
+
* latter is what lets entity reuse across documents actually happen.
|
|
21
|
+
*/
|
|
22
|
+
export async function extractFacts(ontologyStore, factsStore, text, apiKey) {
|
|
23
|
+
const ontologyTurtle = await ontologyStore.toTurtle();
|
|
24
|
+
const knownEntities = factsStore.getEntitySummary();
|
|
25
|
+
const combinedContext = [
|
|
26
|
+
ontologyTurtle,
|
|
27
|
+
"",
|
|
28
|
+
"# Known entities (reuse these IRIs if the text refers to them):",
|
|
29
|
+
knownEntities,
|
|
30
|
+
].join("\n");
|
|
31
|
+
const update = await render({
|
|
32
|
+
apiKey,
|
|
33
|
+
instructions: FACTS_INSTRUCTIONS,
|
|
34
|
+
ontologySnapshotTurtle: combinedContext,
|
|
35
|
+
text,
|
|
36
|
+
});
|
|
37
|
+
const facts = update.triple_operations
|
|
38
|
+
.filter((op) => op.type === "insert")
|
|
39
|
+
.flatMap((op) => op.triples);
|
|
40
|
+
const safe = facts.filter((t) => {
|
|
41
|
+
const violatesNamespace = t.subject.startsWith("ex:");
|
|
42
|
+
if (violatesNamespace) {
|
|
43
|
+
console.warn(`Dropped fact minted directly under ontology namespace: ${JSON.stringify(t)}`);
|
|
44
|
+
}
|
|
45
|
+
return !violatesNamespace;
|
|
46
|
+
});
|
|
47
|
+
const added = factsStore.addNew(safe);
|
|
48
|
+
return { facts: added };
|
|
49
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OntologyStore, type OntologyVersion } from "./store.js";
|
|
2
|
+
export { FactsStore } from "./factStore.js";
|
|
3
|
+
export { complementInserts } from "./diff.js";
|
|
4
|
+
export { evolveOntology, type OntologyUpdateResult } from "./ontology.js";
|
|
5
|
+
export { extractFacts, type FactsResult } from "./facts.js";
|
|
6
|
+
export { queryGraph } from "./query.js";
|
|
7
|
+
export { searchEntities, type SearchResult } from "./search.js";
|
|
8
|
+
export { TripleSchema, TripleOpSchema, GraphUpdateSchema, type Triple, type TripleOp, type GraphUpdate, } from "./schemas.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OntologyStore } from "./store.js";
|
|
2
|
+
export { FactsStore } from "./factStore.js";
|
|
3
|
+
export { complementInserts } from "./diff.js";
|
|
4
|
+
export { evolveOntology } from "./ontology.js";
|
|
5
|
+
export { extractFacts } from "./facts.js";
|
|
6
|
+
export { queryGraph } from "./query.js";
|
|
7
|
+
export { searchEntities } from "./search.js";
|
|
8
|
+
export { TripleSchema, TripleOpSchema, GraphUpdateSchema, } from "./schemas.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OntologyStore, type OntologyVersion } from "./store.js";
|
|
2
|
+
import type { Triple } from "./schemas.js";
|
|
3
|
+
export interface OntologyUpdateResult {
|
|
4
|
+
added: Triple[];
|
|
5
|
+
version: OntologyVersion;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run one document chunk through the ontology-proposal loop against a
|
|
9
|
+
* live OntologyStore, mutating it in place with only the genuinely new
|
|
10
|
+
* triples.
|
|
11
|
+
*/
|
|
12
|
+
export declare function evolveOntology(store: OntologyStore, text: string, apiKey: string): Promise<OntologyUpdateResult>;
|
package/dist/ontology.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render } from "./render.js";
|
|
2
|
+
import { complementInserts } from "./diff.js";
|
|
3
|
+
const ONTOLOGY_INSTRUCTIONS = `You are proposing additions to a domain ontology (RDFS/OWL schema: classes and
|
|
4
|
+
properties only — no instance data) so that it can represent the concepts in
|
|
5
|
+
the text below. You will be shown the ontology as it currently exists.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- Reuse any class or property already in the ontology if it fits. Do not
|
|
9
|
+
re-propose triples that already exist in the snapshot shown to you.
|
|
10
|
+
- Only propose NEW classes/properties for concepts the current ontology
|
|
11
|
+
cannot represent.
|
|
12
|
+
- Use rdfs:Class, rdf:Property, rdfs:subClassOf, rdfs:domain, rdfs:range,
|
|
13
|
+
rdfs:label, rdfs:comment as needed. Prefix new terms with "ex:".
|
|
14
|
+
- Emit insert operations only — this pass never deletes.
|
|
15
|
+
- If the ontology already fully covers this text, return an empty
|
|
16
|
+
triple_operations list.`;
|
|
17
|
+
/**
|
|
18
|
+
* Run one document chunk through the ontology-proposal loop against a
|
|
19
|
+
* live OntologyStore, mutating it in place with only the genuinely new
|
|
20
|
+
* triples.
|
|
21
|
+
*/
|
|
22
|
+
export async function evolveOntology(store, text, apiKey) {
|
|
23
|
+
const snapshotTurtle = await store.toTurtle();
|
|
24
|
+
const snapshotKeys = store.snapshotKeys();
|
|
25
|
+
const update = await render({
|
|
26
|
+
apiKey,
|
|
27
|
+
instructions: ONTOLOGY_INSTRUCTIONS,
|
|
28
|
+
ontologySnapshotTurtle: snapshotTurtle,
|
|
29
|
+
text,
|
|
30
|
+
});
|
|
31
|
+
const proposedInserts = update.triple_operations
|
|
32
|
+
.filter((op) => op.type === "insert")
|
|
33
|
+
.flatMap((op) => op.triples);
|
|
34
|
+
const genuinelyNew = complementInserts(proposedInserts, snapshotKeys, store.prefixes);
|
|
35
|
+
const added = store.addNew(genuinelyNew);
|
|
36
|
+
const version = await store.commitVersion();
|
|
37
|
+
return { added, version };
|
|
38
|
+
}
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type * as RDF from "@rdfjs/types";
|
|
2
|
+
/**
|
|
3
|
+
* Run a SPARQL SELECT query across one or more RDF sources (e.g. an
|
|
4
|
+
* OntologyStore's and a FactsStore's rdfSource) and return plain,
|
|
5
|
+
* JSON-friendly rows. This is the query_graph() half of the exposure
|
|
6
|
+
* layer — the agent writes the SPARQL, this just executes it locally,
|
|
7
|
+
* no LLM call involved.
|
|
8
|
+
*/
|
|
9
|
+
export declare function queryGraph(sparql: string, sources: RDF.Source[]): Promise<Record<string, string>[]>;
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { QueryEngine } from "@comunica/query-sparql-rdfjs";
|
|
2
|
+
const engine = new QueryEngine();
|
|
3
|
+
/**
|
|
4
|
+
* Run a SPARQL SELECT query across one or more RDF sources (e.g. an
|
|
5
|
+
* OntologyStore's and a FactsStore's rdfSource) and return plain,
|
|
6
|
+
* JSON-friendly rows. This is the query_graph() half of the exposure
|
|
7
|
+
* layer — the agent writes the SPARQL, this just executes it locally,
|
|
8
|
+
* no LLM call involved.
|
|
9
|
+
*/
|
|
10
|
+
export async function queryGraph(sparql, sources) {
|
|
11
|
+
const bindingsStream = await engine.queryBindings(sparql, {
|
|
12
|
+
sources: sources,
|
|
13
|
+
});
|
|
14
|
+
const bindings = await bindingsStream.toArray();
|
|
15
|
+
return bindings.map((binding) => {
|
|
16
|
+
const row = {};
|
|
17
|
+
for (const [variable, term] of binding) {
|
|
18
|
+
row[variable.value] = term.value;
|
|
19
|
+
}
|
|
20
|
+
return row;
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type GraphUpdate } from "./schemas.js";
|
|
2
|
+
export interface RenderOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
/** What this render call is trying to produce — ontology proposal vs. facts. */
|
|
6
|
+
instructions: string;
|
|
7
|
+
/** Current ontology, serialized as Turtle, given as context. */
|
|
8
|
+
ontologySnapshotTurtle: string;
|
|
9
|
+
/** The document chunk being processed. */
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One LLM call: given instructions, an ontology snapshot, and a text chunk,
|
|
14
|
+
* get back a validated GraphUpdate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function render(opts: RenderOptions): Promise<GraphUpdate>;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3
|
+
import { GraphUpdateSchema } from "./schemas.js";
|
|
4
|
+
const TOOL_NAME = "emit_graph_update";
|
|
5
|
+
// Anthropic's tool input_schema wants a plain JSON Schema object, not a $ref
|
|
6
|
+
// wrapper — zod-to-json-schema produces the latter by default, so unwrap it.
|
|
7
|
+
function graphUpdateJsonSchema() {
|
|
8
|
+
const schema = zodToJsonSchema(GraphUpdateSchema, "GraphUpdate");
|
|
9
|
+
return schema.definitions?.GraphUpdate ?? schema;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One LLM call: given instructions, an ontology snapshot, and a text chunk,
|
|
13
|
+
* get back a validated GraphUpdate.
|
|
14
|
+
*/
|
|
15
|
+
export async function render(opts) {
|
|
16
|
+
const client = new Anthropic({ apiKey: opts.apiKey });
|
|
17
|
+
const response = await client.messages.create({
|
|
18
|
+
model: opts.model ?? "claude-sonnet-4-6",
|
|
19
|
+
max_tokens: 4096,
|
|
20
|
+
tools: [
|
|
21
|
+
{
|
|
22
|
+
name: TOOL_NAME,
|
|
23
|
+
description: "Emit RDF triple insert/delete operations representing your proposed change, as structured data.",
|
|
24
|
+
input_schema: graphUpdateJsonSchema(),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
28
|
+
messages: [
|
|
29
|
+
{
|
|
30
|
+
role: "user",
|
|
31
|
+
content: [
|
|
32
|
+
opts.instructions,
|
|
33
|
+
"",
|
|
34
|
+
"Current ontology (Turtle):",
|
|
35
|
+
"```turtle",
|
|
36
|
+
opts.ontologySnapshotTurtle || "# (empty — no ontology yet)",
|
|
37
|
+
"```",
|
|
38
|
+
"",
|
|
39
|
+
"Text to process:",
|
|
40
|
+
"```",
|
|
41
|
+
opts.text,
|
|
42
|
+
"```",
|
|
43
|
+
].join("\n"),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
const toolUse = response.content.find((block) => block.type === "tool_use");
|
|
48
|
+
if (!toolUse) {
|
|
49
|
+
throw new Error("Model did not return a tool_use block — no structured output produced.");
|
|
50
|
+
}
|
|
51
|
+
return GraphUpdateSchema.parse(toolUse.input);
|
|
52
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A single triple, expressed as three IRIs/literals in string form.
|
|
4
|
+
* Kept flat (not N3 Quad objects) because this is the shape the LLM
|
|
5
|
+
* actually produces and validates against — conversion to N3 terms
|
|
6
|
+
* happens after parsing, in store.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const TripleSchema: z.ZodObject<{
|
|
9
|
+
subject: z.ZodString;
|
|
10
|
+
predicate: z.ZodString;
|
|
11
|
+
object: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
object: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
predicate: string;
|
|
16
|
+
}, {
|
|
17
|
+
object: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
predicate: string;
|
|
20
|
+
}>;
|
|
21
|
+
export type Triple = z.infer<typeof TripleSchema>;
|
|
22
|
+
/**
|
|
23
|
+
* One insert or delete operation over a set of triples.
|
|
24
|
+
* An ordered list of these forms a GraphUpdate.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TripleOpSchema: z.ZodObject<{
|
|
27
|
+
type: z.ZodEnum<["insert", "delete"]>;
|
|
28
|
+
triples: z.ZodArray<z.ZodObject<{
|
|
29
|
+
subject: z.ZodString;
|
|
30
|
+
predicate: z.ZodString;
|
|
31
|
+
object: z.ZodString;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
object: string;
|
|
34
|
+
subject: string;
|
|
35
|
+
predicate: string;
|
|
36
|
+
}, {
|
|
37
|
+
object: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
predicate: string;
|
|
40
|
+
}>, "many">;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
type: "insert" | "delete";
|
|
43
|
+
triples: {
|
|
44
|
+
object: string;
|
|
45
|
+
subject: string;
|
|
46
|
+
predicate: string;
|
|
47
|
+
}[];
|
|
48
|
+
}, {
|
|
49
|
+
type: "insert" | "delete";
|
|
50
|
+
triples: {
|
|
51
|
+
object: string;
|
|
52
|
+
subject: string;
|
|
53
|
+
predicate: string;
|
|
54
|
+
}[];
|
|
55
|
+
}>;
|
|
56
|
+
export type TripleOp = z.infer<typeof TripleOpSchema>;
|
|
57
|
+
/**
|
|
58
|
+
* The full structured output the LLM must return for a render step —
|
|
59
|
+
* ontology proposal or facts extraction alike.
|
|
60
|
+
*/
|
|
61
|
+
export declare const GraphUpdateSchema: z.ZodObject<{
|
|
62
|
+
triple_operations: z.ZodArray<z.ZodObject<{
|
|
63
|
+
type: z.ZodEnum<["insert", "delete"]>;
|
|
64
|
+
triples: z.ZodArray<z.ZodObject<{
|
|
65
|
+
subject: z.ZodString;
|
|
66
|
+
predicate: z.ZodString;
|
|
67
|
+
object: z.ZodString;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
object: string;
|
|
70
|
+
subject: string;
|
|
71
|
+
predicate: string;
|
|
72
|
+
}, {
|
|
73
|
+
object: string;
|
|
74
|
+
subject: string;
|
|
75
|
+
predicate: string;
|
|
76
|
+
}>, "many">;
|
|
77
|
+
}, "strip", z.ZodTypeAny, {
|
|
78
|
+
type: "insert" | "delete";
|
|
79
|
+
triples: {
|
|
80
|
+
object: string;
|
|
81
|
+
subject: string;
|
|
82
|
+
predicate: string;
|
|
83
|
+
}[];
|
|
84
|
+
}, {
|
|
85
|
+
type: "insert" | "delete";
|
|
86
|
+
triples: {
|
|
87
|
+
object: string;
|
|
88
|
+
subject: string;
|
|
89
|
+
predicate: string;
|
|
90
|
+
}[];
|
|
91
|
+
}>, "many">;
|
|
92
|
+
}, "strip", z.ZodTypeAny, {
|
|
93
|
+
triple_operations: {
|
|
94
|
+
type: "insert" | "delete";
|
|
95
|
+
triples: {
|
|
96
|
+
object: string;
|
|
97
|
+
subject: string;
|
|
98
|
+
predicate: string;
|
|
99
|
+
}[];
|
|
100
|
+
}[];
|
|
101
|
+
}, {
|
|
102
|
+
triple_operations: {
|
|
103
|
+
type: "insert" | "delete";
|
|
104
|
+
triples: {
|
|
105
|
+
object: string;
|
|
106
|
+
subject: string;
|
|
107
|
+
predicate: string;
|
|
108
|
+
}[];
|
|
109
|
+
}[];
|
|
110
|
+
}>;
|
|
111
|
+
export type GraphUpdate = z.infer<typeof GraphUpdateSchema>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A single triple, expressed as three IRIs/literals in string form.
|
|
4
|
+
* Kept flat (not N3 Quad objects) because this is the shape the LLM
|
|
5
|
+
* actually produces and validates against — conversion to N3 terms
|
|
6
|
+
* happens after parsing, in store.ts.
|
|
7
|
+
*/
|
|
8
|
+
export const TripleSchema = z.object({
|
|
9
|
+
subject: z.string().describe("Subject IRI, e.g. 'ex:AcmeRobotics'"),
|
|
10
|
+
predicate: z.string().describe("Predicate IRI, e.g. 'rdf:type' or 'ex:hasIssuer'"),
|
|
11
|
+
object: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Object IRI or literal. Literals should be quoted, e.g. '\"340000000\"^^xsd:decimal'. IRIs are unquoted, e.g. 'ex:Company'."),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* One insert or delete operation over a set of triples.
|
|
17
|
+
* An ordered list of these forms a GraphUpdate.
|
|
18
|
+
*/
|
|
19
|
+
export const TripleOpSchema = z.object({
|
|
20
|
+
type: z.enum(["insert", "delete"]),
|
|
21
|
+
triples: z.array(TripleSchema),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* The full structured output the LLM must return for a render step —
|
|
25
|
+
* ontology proposal or facts extraction alike.
|
|
26
|
+
*/
|
|
27
|
+
export const GraphUpdateSchema = z.object({
|
|
28
|
+
triple_operations: z.array(TripleOpSchema),
|
|
29
|
+
});
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FactsStore } from "./factStore.js";
|
|
2
|
+
export interface SearchResult {
|
|
3
|
+
iri: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find entities in the facts store whose rdfs:label loosely matches the
|
|
8
|
+
* query text. Deliberately simple (case-insensitive substring match) for
|
|
9
|
+
* this first pass — an agent that knows roughly what it's looking for
|
|
10
|
+
* ("Acme") but not the exact IRI ("cd:acme_robotics_inc") uses this to
|
|
11
|
+
* resolve one before it, not both.
|
|
12
|
+
*/
|
|
13
|
+
export declare function searchEntities(query: string, factsStore: FactsStore): SearchResult[];
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
|
|
2
|
+
/**
|
|
3
|
+
* Find entities in the facts store whose rdfs:label loosely matches the
|
|
4
|
+
* query text. Deliberately simple (case-insensitive substring match) for
|
|
5
|
+
* this first pass — an agent that knows roughly what it's looking for
|
|
6
|
+
* ("Acme") but not the exact IRI ("cd:acme_robotics_inc") uses this to
|
|
7
|
+
* resolve one before it, not both.
|
|
8
|
+
*/
|
|
9
|
+
export function searchEntities(query, factsStore) {
|
|
10
|
+
const needle = query.toLowerCase();
|
|
11
|
+
const results = [];
|
|
12
|
+
for (const quad of factsStore.rdfSource.getQuads(null, null, null, null)) {
|
|
13
|
+
if (quad.predicate.value === RDFS_LABEL && quad.object.value.toLowerCase().includes(needle)) {
|
|
14
|
+
results.push({ iri: quad.subject.value, label: quad.object.value });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return results;
|
|
18
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Store, type Quad } from "n3";
|
|
2
|
+
import type { Triple } from "./schemas.js";
|
|
3
|
+
/**
|
|
4
|
+
* Convert one flat Triple (subject/predicate/object as strings, possibly
|
|
5
|
+
* using prefixed names or quoted literals) into an N3 Quad, by parsing it
|
|
6
|
+
* as a one-line Turtle snippet. Reuses N3's parser instead of hand-rolling
|
|
7
|
+
* IRI/literal handling.
|
|
8
|
+
*/
|
|
9
|
+
export declare function tripleToQuad(triple: Triple, prefixes: Record<string, string>): Quad;
|
|
10
|
+
export interface OntologyVersion {
|
|
11
|
+
hash: string;
|
|
12
|
+
parentHash: string | null;
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Versioned in-memory ontology graph. One instance = one ontology's
|
|
17
|
+
* current state plus enough history to explain how it got there.
|
|
18
|
+
*/
|
|
19
|
+
export declare class OntologyStore {
|
|
20
|
+
readonly prefixes: Record<string, string>;
|
|
21
|
+
private store;
|
|
22
|
+
private hash;
|
|
23
|
+
private parentHash;
|
|
24
|
+
constructor(prefixes: Record<string, string>);
|
|
25
|
+
/** All quads currently in the store, as canonical keys — the "snapshot" for diffing. */
|
|
26
|
+
snapshotKeys(): Set<string>;
|
|
27
|
+
/** Serialize the current store to Turtle text — what gets shown to the LLM as context. */
|
|
28
|
+
toTurtle(): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Add triples that are NOT already present (by exact subject/predicate/object
|
|
31
|
+
* match). Returns only the triples actually added — i.e. the genuine delta.
|
|
32
|
+
*/
|
|
33
|
+
addNew(triples: Triple[]): Triple[];
|
|
34
|
+
get size(): number;
|
|
35
|
+
/** Raw RDF/JS-compatible store, for Comunica SPARQL queries. */
|
|
36
|
+
get rdfSource(): Store;
|
|
37
|
+
/** Recompute content hash and bump version lineage (parentHash = previous hash). */
|
|
38
|
+
commitVersion(): Promise<OntologyVersion>;
|
|
39
|
+
get currentVersion(): OntologyVersion;
|
|
40
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Parser, Store, Writer } from "n3";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
/** Build "@prefix x: <...> ." declaration lines for a prefix map. */
|
|
4
|
+
function prefixDecls(prefixes) {
|
|
5
|
+
return Object.entries(prefixes)
|
|
6
|
+
.map(([p, ns]) => `@prefix ${p}: <${ns}> .`)
|
|
7
|
+
.join("\n");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert one flat Triple (subject/predicate/object as strings, possibly
|
|
11
|
+
* using prefixed names or quoted literals) into an N3 Quad, by parsing it
|
|
12
|
+
* as a one-line Turtle snippet. Reuses N3's parser instead of hand-rolling
|
|
13
|
+
* IRI/literal handling.
|
|
14
|
+
*/
|
|
15
|
+
export function tripleToQuad(triple, prefixes) {
|
|
16
|
+
const turtle = `${prefixDecls(prefixes)}\n${triple.subject} ${triple.predicate} ${triple.object} .`;
|
|
17
|
+
const quads = new Parser().parse(turtle);
|
|
18
|
+
if (quads.length !== 1) {
|
|
19
|
+
throw new Error(`Expected exactly one quad from triple ${JSON.stringify(triple)}, got ${quads.length}. Check IRI/literal syntax.`);
|
|
20
|
+
}
|
|
21
|
+
return quads[0];
|
|
22
|
+
}
|
|
23
|
+
/** Canonical string key for a quad, used for exact-match diffing. */
|
|
24
|
+
function quadKey(q) {
|
|
25
|
+
return `${q.subject.value}\u0000${q.predicate.value}\u0000${q.object.value}\u0000${q.object.termType === "Literal" ? q.object.datatype?.value ?? "" : ""}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Versioned in-memory ontology graph. One instance = one ontology's
|
|
29
|
+
* current state plus enough history to explain how it got there.
|
|
30
|
+
*/
|
|
31
|
+
export class OntologyStore {
|
|
32
|
+
prefixes;
|
|
33
|
+
store = new Store();
|
|
34
|
+
hash = null;
|
|
35
|
+
parentHash = null;
|
|
36
|
+
constructor(prefixes) {
|
|
37
|
+
this.prefixes = prefixes;
|
|
38
|
+
}
|
|
39
|
+
/** All quads currently in the store, as canonical keys — the "snapshot" for diffing. */
|
|
40
|
+
snapshotKeys() {
|
|
41
|
+
return new Set(this.store.getQuads(null, null, null, null).map(quadKey));
|
|
42
|
+
}
|
|
43
|
+
/** Serialize the current store to Turtle text — what gets shown to the LLM as context. */
|
|
44
|
+
toTurtle() {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const writer = new Writer({ prefixes: this.prefixes });
|
|
47
|
+
for (const quad of this.store.getQuads(null, null, null, null)) {
|
|
48
|
+
writer.addQuad(quad);
|
|
49
|
+
}
|
|
50
|
+
writer.end((err, result) => (err ? reject(err) : resolve(result)));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add triples that are NOT already present (by exact subject/predicate/object
|
|
55
|
+
* match). Returns only the triples actually added — i.e. the genuine delta.
|
|
56
|
+
*/
|
|
57
|
+
addNew(triples) {
|
|
58
|
+
const existing = this.snapshotKeys();
|
|
59
|
+
const added = [];
|
|
60
|
+
for (const t of triples) {
|
|
61
|
+
const quad = tripleToQuad(t, this.prefixes);
|
|
62
|
+
if (!existing.has(quadKey(quad))) {
|
|
63
|
+
this.store.addQuad(quad);
|
|
64
|
+
existing.add(quadKey(quad));
|
|
65
|
+
added.push(t);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return added;
|
|
69
|
+
}
|
|
70
|
+
get size() {
|
|
71
|
+
return this.store.getQuads(null, null, null, null).length;
|
|
72
|
+
}
|
|
73
|
+
/** Raw RDF/JS-compatible store, for Comunica SPARQL queries. */
|
|
74
|
+
get rdfSource() {
|
|
75
|
+
return this.store;
|
|
76
|
+
}
|
|
77
|
+
/** Recompute content hash and bump version lineage (parentHash = previous hash). */
|
|
78
|
+
async commitVersion() {
|
|
79
|
+
const turtle = await this.toTurtle();
|
|
80
|
+
const canonical = turtle.split("\n").filter(Boolean).sort().join("\n");
|
|
81
|
+
const newHash = createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
82
|
+
this.parentHash = this.hash;
|
|
83
|
+
this.hash = newHash;
|
|
84
|
+
return { hash: this.hash, parentHash: this.parentHash, size: this.size };
|
|
85
|
+
}
|
|
86
|
+
get currentVersion() {
|
|
87
|
+
return { hash: this.hash ?? "unversioned", parentHash: this.parentHash, size: this.size };
|
|
88
|
+
}
|
|
89
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hyphal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "vitest run",
|
|
7
|
+
"build": "tsc -p tsconfig.json",
|
|
8
|
+
"dev": "tsx --env-file=.env src/cli.ts",
|
|
9
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"rdf",
|
|
13
|
+
"ontology",
|
|
14
|
+
"knowledge-graph",
|
|
15
|
+
"llm",
|
|
16
|
+
"agent",
|
|
17
|
+
"sparql"
|
|
18
|
+
],
|
|
19
|
+
"author": "Atharva Deshpande",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"description": "Ontology-co-evolving RDF extraction and querying for agents",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@anthropic-ai/sdk": "^0.122.0",
|
|
24
|
+
"@comunica/query-sparql-rdfjs": "^5.3.0",
|
|
25
|
+
"n3": "^1.26.0",
|
|
26
|
+
"zod": "^3.25.76",
|
|
27
|
+
"zod-to-json-schema": "^3.25.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/n3": "^1.26.3",
|
|
31
|
+
"@types/node": "^26.4.0",
|
|
32
|
+
"tsx": "^4.23.12",
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"vitest": "^4.1.11"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"types": "./dist/index.d.ts"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"bin": {
|
|
51
|
+
"hyphal": "./dist/cli.js"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/Atharva91/hyphal.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/Atharva91/hyphal/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/Atharva91/hyphal#readme"
|
|
61
|
+
}
|