Quickstart
Point CiteNexus at your evidence, bring your own models, and get answers that cite their source — or abstain. Pick your language in the first tab below; the tabs stay in sync, so you can read your own column top to bottom.
-
Install the package.
Terminal window go get github.com/muthuishere/citenexus/golang@v0.12.0Terminal window npm install @muthuishere/citenexusTerminal window pip install citenexus -
Set up the evidence and your models. Nothing is bundled — every model is an injected OpenAI-compatible endpoint (Ollama, OpenAI, a local vLLM, Gemini’s compat endpoint).
The Go port has no config file and no facade object: the corpus is the setup. You hand
Askdocuments in memory.import ("fmt""github.com/muthuishere/citenexus/golang/answer""github.com/muthuishere/citenexus/golang/models""github.com/muthuishere/citenexus/golang/result")corpus := []answer.Doc{{DocumentID: "employee-nda", Text: "The employee shall not disclose confidential information."},}// Optional: models. Omit this and the flow runs on deterministic in-process// fakes, so the quickstart needs no endpoint at all.transport := models.NewHTTPClient(nil, 0)providers := answer.Providers{Embedding: models.NewOpenAIEmbedding("http://localhost:11434/v1", "bge-m3", transport.Do),Generator: models.NewOpenAIChatGenerator("http://localhost:11434/v1", "qwen2.5", 0, nil, transport.Do),}Every client posts through an injected
models.Transport, and auth lives in headers as${ENV}templates expanded at call time — a key is never held as a value. See Bring your own model.The JavaScript port has no config file and no facade object: the corpus is the setup. You hand
askdocuments in memory.import {ask, askWith, Decision, HttpClient, OpenAIEmbedder, OpenAIChatGenerator,} from "@muthuishere/citenexus"const corpus = [{ document_id: "employee-nda", text: "The employee shall not disclose confidential information." },]// Optional: models. Omit them and the flow runs on deterministic in-process// fakes, so the quickstart needs no endpoint at all.const http = new HttpClient()const transport = (url, body, headers) => http.send(url, body, headers)const embedding = new OpenAIEmbedder({ base_url: "http://localhost:11434/v1", model: "bge-m3" }, transport)const generator = new OpenAIChatGenerator({ base_url: "http://localhost:11434/v1", model: "qwen2.5" }, transport)Every client posts through an injected
Transport, and auth lives in headers as${ENV}templates expanded at call time — a key is never held as a value. See Bring your own model.Python is the batteries-included facade: one
CiteNexusobject over a local directory (or an S3 bucket), with the models injected. Every model client is importable from the top-level package.from citenexus import (CiteNexus,OpenAICompatibleEmbedding,OpenAICompatibleGenerator,)rag = CiteNexus("./citenexus-data", # a local directory (or an S3 bucket)embedder=OpenAICompatibleEmbedding(base_url="http://localhost:11434/v1", model="bge-m3"),generator=OpenAICompatibleGenerator(base_url="http://localhost:11434/v1", model="qwen2.5"),)Auth lives in headers as
${ENV}templates expanded at call time — a key is never held as a value. See Bring your own model. -
Ask, and read the cited answer — or the refusal.
res := answer.Ask(corpus, "Can the employee disclose confidential information?", answer.DefaultTopK)fmt.Println(res.Answer) // the verbatim quote — or the pinned refusal// A refusal carries NO sources, so branch on the decision first —// res.Sources[0] would panic.if res.Evidence.Decision == result.DecisionAnswered {src := res.Sources[0]fmt.Println(src.Document, src.Passage)} else {fmt.Println("refused:", res.MissingEvidence)}To run the same flow on the models from step 2, swap in
answer.AskWith(corpus, question, answer.DefaultTopK, providers)— it returns(result.Result, error), because a model failure is an error, never a refusal.Reading files (PDF/DOCX/HTML/…) is a separate, opt-in path in Go:
ingest.Ingest(...)sits behind thecitenexus_ffibuild tag and needs the Rust cdylib built first — see Install. Everything above runs on a plaingo get.const res = ask(corpus, "Can the employee disclose confidential information?", 5)console.log(res.answer) // the verbatim quote — or the pinned refusal// A refusal carries NO sources, so branch on the decision first —// res.sources[0] would be undefined.if (res.evidence.decision === Decision.answered) {const src = res.sources[0]console.log(src.document, src.passage)} else {console.log("refused:", res.missing_evidence)}To run the same flow on the models from step 2,
await askWith(corpus, question, { embedding, generator, topK: 5 })— notetopKlives inside the providers object here, where Go takes it positionally.Reading files (PDF/DOCX/HTML/…) is a separate, opt-in path in JavaScript: it lives on the
/ffisubpath and needs the Rust cdylib built first — see Install. Everything above runs on a plainnpm install.rag.ingest("employee-nda.pdf") # a file on diskresponse = rag.ask("Can the employee disclose confidential information?")print(response.answer) # the verbatim quote — or a refusal# A refusal carries NO sources, so always branch on the decision first —# response.sources[0] would raise IndexError.from citenexus.answer.result import Decisionif response.evidence.decision is Decision.answered:src = response.sources[0]print(src.document, src.page)else:print("refused:", response.missing_evidence)Python is the only port with file ingest on the default install:
ingest()takes a file, raw text, an image, or a URL — see Ingest anything.
- How it works — what each of those calls actually did, stage by stage.
- The deterministic core — the shared cite-or-abstain flow every port runs, byte-for-byte identical.
- Store your corpus on S3 / MinIO instead of a local folder.
- Models & endpoints — embedding, LLM, reranker, vision.
- Bring your own model — a model that isn’t an HTTP endpoint? Keep the same client, swap the transport.
- Ingest anything — a file, raw text, an image, or a URL,
one source per call (there is no “ingest the whole bucket” call);
crawl()walks a whole site. - Prove it with
evaluate()against a golden set.