Backend-agnostic, one-shot GraphRAG. Fire several retrieval strategies at once — vector/lexical passages + graph label-linking + class enumeration + relation expansion — and fuse them with MMR diversity into a single LLM synthesis. No iterative ReAct tool loop. Zero infra, zero lock-in: the full algorithm runs on a pure-Python in-memory backend (dict + BM25), and swaps to Fuseki / Qdrant / any LLM by passing objects that match three small protocols.
from omnifuse import from_triples
of = from_triples( # nodes are inferred; no DB, no API key
[("담보", "instanceOf", "규정"), ("담보", "한도", "5억")],
chunks=[("c1", "담보 한도는 5억원이다", ["담보"])],
)
print(of.search("담보 한도").answer)Load however you have the data — all zero-dep, same search():
from omnifuse import from_jsonl, from_csv, from_fuseki, build_inmemory
of = from_jsonl(triples="t.jsonl", chunks="c.jsonl")
of = from_csv(triples="triples.csv", chunks="chunks.csv")
of = from_fuseki("http://localhost:3030/ds/query", graph_uri="urn:g", user="admin", password="…")
of = build_inmemory(nodes, triples, chunks) # explicit Node/Triple/ChunkBuild the index once, start warm afterwards (stdlib pickle, zero deps):
from omnifuse import save_index, load_index
save_index(of, "idx.pkl")
of = load_index("idx.pkl") # ~29x faster than rebuilding; pass embedder=/llm= herejinsoo96/js-omnifuse is the personal source of
truth. PlateerLab/xgen-omnifuse is the
organization mirror and keeps the published Python package name xgen-omnifuse.
Changes land on js-omnifuse:main first. The organization repository runs
sync-from-js-omnifuse.yml every 15 minutes
and on manual dispatch. It accepts only a fast-forward from the personal source; it never
force-pushes or silently overwrites an independent organization commit.
Pure vector RAG answers from the top-k passages it happens to embed near the query. A graph store also gives you operations cosine similarity can't:
- Complete enumeration — all instances of a class ("list every regulation"), exact counts.
- Relations / multi-hop — what an entity is connected to, 1-hop neighbors, paths.
- Minority evidence survives — MMR diversity keeps the decisive exception/warning that near-duplicate passages would otherwise crowd out of a fixed top-k.
OmniFuse fuses both: the vector seed for content, the graph seeds for structure.
The algorithm only talks to three typing.Protocols, never to a database:
class GraphStore(Protocol):
def search_labels(self, query, *, limit=30) -> list[tuple[Node, float]]: ... # full-text label search
def class_instances(self, class_id, *, limit=1000) -> list[Node]: ... # enumeration
def neighbors(self, node_id, *, hops=1, limit=100) -> list[tuple[str,str,str]]: ... # traversal
def count_class(self, class_id) -> int: ...
def get_node(self, node_id) -> Node | None: ...
class VectorStore(Protocol):
def search(self, query, *, limit=20) -> list[tuple[Chunk, float]]: ...
def fetch(self, ids) -> list[Chunk]: ...
class LLM(Protocol):
def generate(self, prompt, *, system="", timeout=None) -> str: ...- Zero-infra default —
InMemoryGraphindexes node labels with BM25 (CJK character n-grams, so Korean/CJK search works with no morphological analyzer).InMemoryVectorpicks its mode from what the chunks carry: hybrid (dense cosine and lexical BM25 min-max normalized per query, then weighted) when embeddings and text are present, dense cosine with embeddings only, else field-weighted BM25 (BM25F) that scores a chunk's shorttitleabove its body. dependencies = []— the core needs nothing but the standard library. Real backends are optional extras (pip install "xgen-omnifuse[fuseki,qdrant]").- Bring your own LLM — pass anything with
generate(...); the bundledEchoLLMreturns the fused evidence so the pipeline runs end-to-end with no API key.
- vector/lexical seed + 1-hop graph fusion → adaptive top-k (score-distribution cut, not fixed k)
- graph label-linking → 1-hop relations
- class enumeration (complete list/count)
- HippoRAG — entities of the retrieved chunks → 1-hop expansion
- evidence assembled with MMR diversity (Jaccard, no embeddings needed)
- one LLM synthesis over the fused evidence
- honest
evidence_nodes— only the nodes the answer actually cites
retrieve(question) returns the ranked (chunk, score) list with no LLM call —
use it directly for search/eval. On top of the vector seed it does graph-companion
fusion: a passage that a strong seed references/links to is surfaced beside it
(companion score = fusion_alpha × seed), so multi-hop evidence that shares no query
vocabulary lands in one shot — no agent, no LLM. search() builds its chunks and
evidence on retrieve(). Opt out with graph_fusion=False.
For a static text-only corpus, build_inmemory(..., auto_link_titles=True) derives
directed references edges when a passage names another passage's unambiguous title:
of = build_inmemory([], [], chunks, auto_link_titles=True)The linker uses a token trie, ignores ambiguous and single-token aliases, and recognizes
conservative name forms such as Philip V for Philip V of Spain. It is opt-in because
an incrementally mutable corpus needs an equally mutable graph; combining
auto_link_titles=True with mutable=True is rejected instead of leaving stale edges.
pip install xgen-omnifuse # core (zero deps)
pip install "xgen-omnifuse[dev]" # + pytest, ruffRun the demo with no install:
python examples/quickstart.pysrc/omnifuse/
protocols.py # GraphStore / VectorStore / LLM (the swap points)
models.py # Node, Triple, Chunk (+ optional title), SearchResult
text.py # tokenizer + BM25 + BM25F (field-weighted, CJK n-grams)
lexical_rerank.py # bounded phrase/surface reranking + Korean zero-hit fallback
linking.py # deterministic title-mention graph edges for static text corpora
fusion.py # MMR, adaptive top-k, relation ranking
oneshot.py # OmniFuse.search / retrieve — the fusion algorithm
backends/memory.py # InMemoryGraph + InMemoryVector (hybrid/dense/lexical, zero infra)
llm.py # EchoLLM, CallableLLM
feedback.py # Feedback — memory as a BM25F evidence field
facade.py # build_inmemory(...), save_index / load_index
examples/ tests/ eval/ # eval/ = head-to-head benchmark vs synaptic-memory
# (a) self-contained — zero infra
from omnifuse import build_inmemory
of = build_inmemory(nodes, triples, chunks)
# (b) backed by Apache Jena Fuseki (or any SPARQL endpoint) — graph-only or with a vector store
from omnifuse import OmniFuse, InMemoryVector
from omnifuse.backends.fuseki import FusekiGraph
graph = FusekiGraph("http://localhost:3030/ds/query", graph_uri="urn:my-graph", user="admin", password="…")
of = OmniFuse(graph, InMemoryVector([])) # search() unchangedFusekiGraph is stdlib-only (urllib) and uses portable FILTER(CONTAINS(...)), so it
works on any SPARQL 1.1 store — not just jena-text.
The current retrieval path is one general algorithm. It contains no benchmark name, query id, qrel, expected answer or document-specific exception.
- Lossless index, query-only cleanup. Documents keep every token. At query time, Korean request endings and English closed-class grammar words are removed so subject terms carry the rank. If cleanup would remove everything, OmniFuse falls back to the original query tokens.
- BM25F candidate admission. Title and body fields produce a configured bounded
frontier (
max(limit, pool), default pool 40); title matches remain more informative than the same token buried in a long passage. - Complete-word coordination. When Korean word-boundary evidence exists, candidates containing the complete subject word are preferred over substring-only matches. At most one excluded candidate is restored, and only when its original BM25F score exceeds the weakest retained complete-word hit. The final top-K naturally decides whether it stays.
- Korean zero-hit recovery. If ordinary lexical search returns no candidate, a character-evidence BM25 fallback scans that query's corpus once. It shares the configured BM25 constants, preserves mutable slot IDs, adds no persistent character index, and never runs on a normal hit path.
- Phrase evidence. Ordered query bigrams vote inside the bounded frontier. Only matched query pairs are retained for each candidate.
- Personal-memory surface fusion. First-person memory questions also receive an
independent raw phrase/coverage ranking. Normalized lexical rank and surface rank are
combined as
0.4 / lexical_rank + 0.6 / surface_rank, an inverse-rank fusion inspired by Reciprocal Rank Fusion. It only reorders the already admitted candidate frontier and never scans the corpus. - Question title anchors. In graph mode, exact title mentions and conservative one-edit multi-token typos become graph seeds. A single-token alias is accepted only when it is the unambiguous base of a parenthetically disambiguated title. Earlier mentions receive slightly higher priority, then the usual directed companion expansion runs.
- Lazy owned memory. Feedback is copied on construction and BM25F evidence materializes
on first use.
remember()andforget()remain exact incremental updates, while an unused store does not pay eager indexing cost.
This separates complementary signals instead of mixing incomparable raw score scales. The candidate bound, generic linguistic classes and title-edit rule are product contracts, not per-dataset switches.
The candidate-local policy lives in omnifuse.lexical_rerank, separate from index storage
and persistence. Its thresholds and weights are named once rather than scattered through
backend branches, and pool is the only configured candidate floor—there is no hidden
minimum that overrides a caller's smaller pool.
The complete iteration history, rejected variants and immutable artifacts are kept outside
this product overview in eval/README.md and eval/results.
The current canonical comparison uses the official synaptic-memory v0.27.0 tag at
836d53640e520c88910dd57e098167a4defe50d2. Comparable retrieval tracks use the same
corpus, queries, relevance judgments, K and byte-identical scorer. Each artifact binds the
source, inputs, Python environment, isolated workers and postflight state.
| track | OmniFuse | synaptic-memory | verdict |
|---|---|---|---|
| Direct14, 2,269 queries | MRR@10 0.7049, nDCG@10 0.6783 | MRR@10 0.6537, nDCG@10 0.6100 | five metrics 14/0/0, Recall 13/0/1 by dataset |
| HotPotQA retrieval, 24 questions | Recall 0.9792, nDCG 0.9483, mean 3.83 ms | Recall 0.7292, nDCG 0.6908, mean 60.43 ms | 11/0 aggregate; zero per-question quality losses |
| LongMemEval-S, 48 questions | MRR 0.8392, nDCG 0.8643, mean 60.22 ms | MRR 0.6990, nDCG 0.6898, mean 241.61 ms | 8/0 quality and efficiency aggregate |
| Enterprise full-native | MRR 0.7689, nDCG 0.7637, mean 0.791 ms | MRR 0.7467, nDCG 0.6649, mean 5.799 ms | quality, search and build win |
| local-Qwen answer E2E | correctness 0.7938, mean generation 6,323 ms | correctness 0.5450, mean generation 6,752 ms | 9/1 aggregate; stochastic p95 generation loses |
The scope is precise:
- Direct14 has no dataset-level loss, but 301/2,269 queries have at least one lower quality metric. Synaptic already reaches a metric ceiling on many queries, so “all datasets win” is not rewritten as “every query wins.”
- HotPotQA and LongMemEval have zero individual quality losses. LongMemEval still records five local efficiency losses even though all aggregate efficiency metrics win.
- Enterprise
full_nativepreserves each product's native graph and memory semantics. The asymmetricdocs_onlydiagnostic is excluded from the equal-input verdict. - Answer generation is stochastic and is reported separately from deterministic retrieval.
Full protocols, every historical run, rejected variants and reproduction commands are in
eval/README.md. Machine-readable, write-once evidence is in
eval/results; the current artifacts are:
The deepest difference between OmniFuse and synaptic-memory was never the ranking: it
is that synaptic is stateful and learns. Synaptic's own suite already tests reinforcement
and consolidation contracts; our narrower question was whether those updates improve
held-out graph.search retrieval. The first version told us we had won when we had not,
which is why the placebo controls are now part of the harness.
A confirmed query becomes evidence about a chunk: indexed as a BM25F evidence field whose terms score it but never enter document frequency, and which is not length-normalized.
from omnifuse import Feedback, build_inmemory
fb = Feedback()
fb.remember("statin side effects", ["doc7"]) # a user confirmed doc7 answered it
of = build_inmemory(nodes, triples, chunks, feedback=fb)The evidence field scores only the chunk that owns it. It does not enter document frequency or length normalization, so a cold feedback-enabled store ranks identically to a store with no feedback and unrelated chunks keep their content IDF.
remember(query, doc_ids) and forget(query, doc_ids) update only the affected evidence
postings. Both directions are bit-identical to a full rebuild; the update cost follows the
changed memory rather than the corpus size.
of = build_inmemory(nodes, triples, chunks, feedback=Feedback())
of.remember("statin side effects", ["doc7"])
of.forget("statin side effects", ["doc7"])Held-out paraphrase, shuffled/random placebo, consolidation and incremental-cost experiments
are archived with their controls and retraction history in
eval/README.md, adaptive_memory.json
and incremental_memory.json.
backends/qdrant.pyvector adapter; jena-text fast path forFusekiGraph- async pipeline (parallel seeds via
asyncio.gather) - cross-encoder reranker hook, query expansion
- configurable ISA predicates and prompt templates (per domain/language)
A growing knowledge store with two omnifuse-specific dynamics, not a generic remember/recall: fuse-on-write (facts deduped & merged by entity) and salience (frequently fused/surfaced nodes rank higher). Zero infra; notes auto-link to known entities; persists to JSONL.
from omnifuse import Vault
v = Vault()
v.fuse(facts=[("담보", "instanceOf", "규정")])
v.fuse("담보 한도는 5억원이다", facts=[("담보", "한도", "5억")])
print(v.surface("담보 한도").answer) # fusion search over everything fused, salience-ranked
v.save("vault.jsonl"); v2 = Vault.load("vault.jsonl")ci.yml— runs pytest (3.10–3.12) +python -m build+twine checkon every push/PR.publish.yml— on a GitHub Release, builds and uploads to PyPI via Trusted Publishing (no token in the repo). One-time PyPI setup: project → Publishing → add pending publisherPlateerLab / xgen-omnifuse / publish.yml / pypi. (Token mode: addsecrets.PYPI_API_TOKEN.)
Build locally:
pip install build && python -m build # dist/*.tar.gz + *.whlTBD.