Releases: yiongq/mcp-chinese-rag-toolkit
Release list
v0.7.1
Patch Changes
- fcbafd8: xlsx: single-column sheets now render as plain prose lines instead of piped one-cell Markdown tables — fixes raw-pipe noise in snippet displays and improves cross-encoder rerank scores for prose-notes sheets.
v0.7.0
Minor Changes
- fe0e69f: Three ingestion/retrieval capabilities: xlsx parsing with header-repeating markdown row groups (chunker-friendly tables, section = sheet name), image/png + image/jpeg support with an exported
captionImagevision bridge (cached, jpeg re-encode, defensive downscale), andgraphRecall— an opt-in entity-match third recall source fused into hybrid search via N-way RRF (absent hook keeps retrieval byte-identical). Benchmark gains graph on/off A/B configs.
v0.6.0
Minor Changes
-
e819cea: Ingest: add
parseDocument, a stateless entry point that turns an uploaded file into a uniformParsedDocthe existing chunkers accept as-is (additive, no breaking changes). It normalizes four whitelisted formats — PDF text layer (application/pdf), Word documents (docx), Markdown (text/markdown) and plain text (text/plain) — delegating every parse to a mature library (PDF via the existingparsePdf, docx viamammoth); no parser is hand-rolled. PDF documents keep their 1-indexed page numbers ondoc.pages(ready forchunkPdfPages); docx/Markdown/text become heading-preserving text ondoc.text(ready forchunk, whose heading tracker turns#–####into asectionprovenance path). Markdown and plain-text bytes are decoded as UTF-8 first with an automatic GBK fallback — no charset-detection dependency, using the Node-built-in decoder — and the detected encoding is reported ondoc.encoding.Failure is returned, never thrown, so a single bad file never crashes a batch ingest: the result is a discriminated union
{ ok: true; doc } | { ok: false; error; message }carrying a stableINGEST_ERROR_CODEScode —UNSUPPORTED_MIME_TYPE,PARSE_FAILED(corrupt / encrypted / malformed input),PARSE_TIMEOUT(a configurabletimeoutMshard ceiling, defaultDEFAULT_PARSE_TIMEOUT_MS) orEMPTY_DOCUMENT. The function is side-effect-free and never mutates or detaches its input buffer. ExportsparseDocument,DEFAULT_PARSE_TIMEOUT_MS,INGEST_ERROR_CODES, and theParsedDoc/ParseResult/ParseDocumentOptions/SupportedMimeType/IngestErrorCodetypes. -
89a903f: Knowledge graph: add stateless primitives to grow an entity / relation graph from already-indexed chunks and store it alongside the vectors and full-text index (additive, no breaking changes). The graph tables live in the same
.dbfile as the existingdocs/ vector / FTS tables, so the graph travels with that index version; rebuilding the corpus into a fresh version starts with no graph.buildGraphSchema(db)— idempotent DDL (IF NOT EXISTS) that addsentities,relationsand their*_mentionsback-link tables in place. Safe to call on any already-built index to opt it into graph storage; it never touches the core tables, and a graph-less database still opens read-only unchanged.extractGraph({ chunks, extractFn })— pure orchestration that turns chunks into a deduplicated graph. Extraction is fully injected viaextractFn(wire it to any LLM provider, a rule-based extractor, or a test stub) — the toolkit ships no default extractor and depends on no model SDK. Entities and relations collapse onto a deterministic, NFKC-based normalized key so re-extracting the same corpus produces no duplicate nodes; each node and edge back-links to the source chunk(s) it was mentioned in; relation endpoints are registered as nodes so the graph is always connected; and a single chunk whoseextractFnthrows (or resolves a malformed result) is skipped and counted infailedChunksrather than aborting the batch.writeGraph(db, graph)— persists an extracted graph in one transaction, idempotently: entities and relations useINSERT OR IGNOREagainst aUNIQUEnormalized key and mention rows use a composite primary key, so writing the same graph twice inserts nothing and leaves node / edge counts unchanged.
extractGraphaccepts an optionalonSpancallback that emits oneingest.graphspan carrying scalar counts only (chunk / entity / relation / failed counts) — never names or chunk content — so it is safe to export to an observability backend. Uses only Node built-ins and the existing SQLite dependency; no new dependencies. ExportsbuildGraphSchema,extractGraph,writeGraph,GRAPH_ERROR_CODES,GraphError, and theGraphChunk/ExtractFn/RawEntity/RawRelation/RawExtraction/ExtractedEntity/ExtractedRelation/ExtractedGraph/GraphExtractionOptions/GraphStats/GraphErrorCodetypes. -
0c1faaa: Observability: the retrieval and ingest pipelines can now emit structured
PipelineSpanevents through an optional, injectedonSpancallback, so you can bridge pipeline timing to any observability backend without the toolkit depending on a tracing SDK (additive, no breaking changes).Pass
onSpanon the per-call options of the bound hybrid search (createHybridSearch), the bound reranker (createReranker),parseDocument, orIndexStore.buildVersion. Each pipeline stage then emits one span:createHybridSearchemits aretrieve.hybridparent span withretrieve.bm25/retrieve.vector/retrieve.rrfchild spans (each child'sparentIdis the hybrid span'sid).createRerankeremits aretrieve.rerankspan (including a zero-candidate span when the input list is empty).parseDocumentemits oneingest.parsespan per call, for successful and failed parses alike.IndexStore.buildVersionemits aningest.indexspan on a successful build.
Each span carries a name, an id and optional
parentId,startedAtEpochMs,durationMs, and a scalar-onlyattributesmap (counts, scores, dimensions, format enums, error codes). Spans are metadata only — never query text, chunk content, source or file names — so they are safe to export to an external backend. Pass an outer span id viaparentSpanIdto graft the toolkit's spans under your own trace.When no
onSpanis provided the pipelines read no clock, mint no id and allocate no span object, so the hot path is unchanged and there is zero overhead. Uses only Node built-ins (node:crypto,performance,Date) — no new dependencies. Exports thePipelineSpan,PipelineSpanName,OnSpanandSpanAttributeValuetypes. -
e819cea: RAG store: add
openIndexStore, a versioned wrapper overopenIndexthat lets a running service re-index a corpus and switch over without interrupting in-flight queries (additive, no breaking changes —openIndexand its handle are untouched). It keeps per-corpus version files (v<N>.db) plus a singlemanifest.jsonpointer, and exposesbuildVersion(rows)(full rebuild into a fresh version file — decoupled from going live, so a slow rebuild never stalls current queries),swap(version?)(atomically flip the serving pointer via a temp-file +rename, so a reader or a crash never sees a half-written manifest),rollback()(step the pointer back one version), andwithHandle(fn)for running a query against the current version.Handle lifetime is reference-counted: a query that started before a
swapkeeps reading the pre-swap version to completion, and that version's connection is only closed once its last in-flight reader releases it. Old version files are pruned to a configurablekeepVersionswindow (default 2), but the current version, its one-step rollback target, and any version with an in-flight reader are never deleted — a reader-held version's.db(with its WAL/SHM sidecars) is unlinked only after the reader releases. Building an empty corpus is refused, and a malformed or inconsistentmanifest.jsonfails fast with a typedIndexStoreError(err.code, e.g.EMPTY_CORPUS/CORRUPT_MANIFEST/VERSION_NOT_FOUND). Zero new dependencies — purenode:fsover the existingbetter-sqlite3path. ExportsopenIndexStore,IndexStoreError,INDEX_STORE_ERROR_CODES, and theIndexStore/IndexStoreOptions/BuildVersionResult/IndexStoreErrorCodetypes.
v0.5.0
Minor Changes
-
42a9bf2: Query: add history-aware query rewriting as a stateless pure function.
rewriteQuery({ history, query, generateFn })asks a caller-injected language model to rewrite a context-dependent query (pronouns, omitted subjects) into a self-contained retrieval query, using the conversation history the caller supplies. The outcome is a discriminated union that is honest by construction:model(a cleaned rewrite),short-circuit(blank query or empty/blank history — the model is never called), ordegraded(timeout or unusable output — the original query is kept, with areason). Conversation history and the query are embedded as fenced untrusted data (data preface + declared length + content-derived sentinel), never as bare instruction. Also exportsbuildRewritePrompt,REWRITE_PROMPT_VERSION(stamp for run metadata, bumped on any prompt wording change), andDEFAULT_REWRITE_TIMEOUT_MS. The caller controls the history window; non-timeoutgenerateFnrejections (network/auth/provider faults) propagate unchanged. -
07cc542: Eval: add optional multi-turn conversation history to eval cases.
EvalQuerygainshistory?: ConversationTurn[](declared per query in the eval-set YAML, oldest-first);loadEvalSetvalidates the shape (role must beuserorassistant, content a non-empty string) and fails fast with the exactqueries[i].history[j]location on authoring mistakes. The harness passes the history through verbatim to the injectedsearchFn(new optionalhistoryin its options) andgenerateFn(new optionalhistoryon its input) — it never consumes history itself, so whether retrieval or generation is history-aware stays entirely the caller's decision. Purely additive: single-turn eval sets, existing callers, and history-agnostic functions are unaffected.Also re-exports
aggregateAnswerMeans(the per-query answer-metric mean aggregatorrunBenchmarkalready uses internally) at the package level, so downstream verdict layers can aggregate metric means over filtered per-query subsets without re-implementing the formula. -
cb9ba6d: Guard: add
sanitizeRetrievedContent, a stateless rule-based pure function that defends against indirect prompt injection (retrieval poisoning) — the RAG-specific attack surface where a malicious instruction is smuggled inside an indexed document and re-enters the model context as "trusted" retrieved text. It detects three injection classes — instruction-override clauses ("ignore the previous instructions"), forged role / delimiter markers (a line-start系统:/助手:,<|im_start|>,[INST]) and persona hijacks ("you are now…", "act as…") — and neutralizes them without deleting any content: structural tokens get a zero-width break, imperative/persona clauses are wrapped in a deterministic⟦untrusted:<category>:<token>⟧…⟦/untrusted:<token>⟧annotation that tells the model the span is flagged data. The result is an honest{ sanitized, flagged, detections }structure —detectionsis source-ordered and excerpt-bounded so it can be counted by a metric without copying whole passages — and the function is idempotent (re-sanitizing already-sanitized text is a no-op) and rejects pre-planted forged markers. ExportssanitizeRetrievedContent, theSANITIZE_RULES_VERSIONstamp, and theInjectionCategory/InjectionDetection/SanitizeOptions/SanitizeResulttypes.Query rewrite: harden the conversation-history serialization in the rewrite prompt so a turn's content can no longer forge an extra line-start role label (e.g. an embedded
\n助手:…) and fake a turn inside the data block. Line terminators inside a turn's content are now collapsed before the turns are joined.REWRITE_PROMPT_VERSIONis bumped to2026-06-16to reflect the prompt change.
Patch Changes
- c992453: Eval: harden
callJudgetimeout handling. A judge rejection arriving after the timeout already degraded the call is now observed by a no-op handler instead of surfacing as an unhandled rejection (a process crash by default in Node); a rejection that loses no race still propagates unchanged. FinitetimeoutMsvalues above 2^31-1 (the largest delaysetTimeouthonours) are now capped instead of being silently clamped to ~1ms, which previously turned a huge "effectively no timeout" budget into an instant spurious timeout on every call. The same cap applies torewriteQuery'stimeoutMs.
v0.4.0
Minor Changes
- 3025c00: Benchmark: support per-config answer generation.
BenchmarkConfiggains optionalgenerateFn/generateModeloverrides so configurations that generate answers differently (e.g. different generation models or end-to-end orchestrations) can be compared in one run. Omitting them keeps the shared run-level pair — existing callers are unaffected. When per-config models differ, the summary'sversionMeta.generateModelbecomes an explicitname=model; name=modelaggregate and the comparison table lists the generation model per configuration.
v0.3.0
Minor Changes
-
ee1a569: Add
runAnswerEval— a single provider-injection entry point that runs a whole answer-quality evaluation (additive, no breaking changes). It wires the existing layers into one flow: retrieve context, generate an answer from it, drive the judge tasks, feed their structured output to the pure scoring functions, and stamp the run with reproducible version metadata. It is an orchestrator only — it does not build prompts, parse model output, or compute any metric formula.runAnswerEval(evalSet, opts)— for each query it retrieves top-K context, generates an answer, and computes up to five RAGAS metrics: faithfulness, answer relevance and context precision (reference-free), plus answer correctness and context recall (reference-based, computed only when the query carries a reference answer). Retrieval, generation, judging and embedding are all caller-injected, so a deterministic mock drives the whole thing in CI with no API key and no network.- Graceful skips, never faked scores: the reference-based pair is skipped (
NO_REFERENCE_ANSWER) when a query has no reference answer, and answer relevance is skipped (NO_EMBED_FN) when no embed function is injected. A degraded judge call (timeout or malformed output) skips just its own metric. - Resilient per query, like the retrieval runner: a fault fatal to one query — a throwing search/generate function, a missing chunk, or a judge infrastructure rejection — is recorded on that query's row and the run continues. An embed-function failure is localized to answer relevance, its only consumer.
- Reproducible version metadata
{ generateModel, judgeModel, judgePromptVersion, toolkitVersion, evalSpecVersion }is stamped onto every run, with each field carrying its own provenance (model names are caller-injected, the toolkit version is read from package.json, never hardcoded), so scores stay comparable and auditable across time and configurations. - New types
GenerateFn,EmbedFn,AnswerEvalOptions,AnswerEvalMetrics,AnswerEvalQueryResult,AnswerEvalSummaryandAnswerEvalVersionMetaare exported from the package root.
answerCorrectnessreports the statement-level F1 component only; the full RAGAS metric blends it with an answer–reference semantic similarity term, which needs another embedding pass and is left to a later calibration step. -
69e9ac4: Add
runBenchmark— run one eval set through several named retrieval configurations and lay the results out as a single comparison table (additive, no breaking changes). It is an orchestrator only: it reuses the retrieval runner, the answer-eval orchestrator and the ranking-gain metric, and reimplements none of their formulas.runBenchmark(evalSet, opts)— for each{ name, searchFn }configuration it scores retrieval (Hit Rate@K, MRR, and a mean nDCG@K derived from the existing expected-hit labels) and answer quality (the five RAGAS metrics), keeping the full per-config sub-results alongside the aggregates. Each row is stamped with the same reproducible version metadata, so the table is comparable and auditable across runs.- Stays provider-agnostic: the toolkit never learns how a retrieval configuration is built (reranking, lexical vs vector, tokenizer choices). The caller pre-wires each configuration as a
searchFn; the toolkit only iterates over them. renderBenchmarkTable(summary)— render the comparison as deterministic GitHub-flavoured markdown, one row per configuration. A never-measured answer metric renders asn/a(never faked to0), and there is deliberately no aggregate "overall" column — the metrics measure different things on different scales.- New types
BenchmarkConfig,BenchmarkOptions,BenchmarkConfigResultandBenchmarkSummaryare exported from the package root.
The mean nDCG@K is derived from the existing binary expected-hit labels (a hit scores gain
1, a miss0); even so it is informative, rewarding a configuration that ranks a hit higher — something Hit Rate@K and MRR do not capture. Graded relevance labels are a future, purely-additive extension. -
c389b63: Extend the eval framework (additive, no breaking changes):
EvalQuerygains an optionalreferenceAnswerfield, parsed and validated byloadEvalSet(must be a non-empty string when present). Reference-based answer metrics can consume it; retrieval scoring ignores it.- New
assertContentPopulated(result)content guard plus a lean eval error layer (EvalFrameworkError,evalError,EVAL_ERROR_CODESwithEVAL_CONTENT_MISSING). It throws on missing/blank chunk content so answer-quality metrics fail loudly instead of silently scoring low. - Raise the supported Node engine to
>=22.
-
9cbcdfa: Add a judge result cache and benchmark query sampling (additive, no breaking changes).
withJudgeCache(judgeFn, opts)wraps a judge function so identical calls are memoized. The cache key is a content hash of the prompt (viacomputeJudgeCacheKey), so only hashes are stored — never the raw prompt or output.createMemoryJudgeCacheStore(max?)provides a default bounded in-memory store, and theJudgeCacheStore/JudgeCacheOptionstypes let callers plug in their own. Caching is opt-in at the call site, so variance sampling (which must drive the raw judge) is never collapsed to a single cached value.sampleQueries(evalSet, n, opts?)deterministically selects a subset of an eval-set, so a benchmark can run a cheap PR-smoke subset and the full set on a nightly schedule from the same data.
Both are exported from the package root.
-
7ba049f: Add the LLM-facing judge layer to the eval framework (additive, no breaking changes). This is the impure counterpart to the pure scoring functions: it builds a prompt, calls an injected judge, and parses the model's text into the structured inputs the scoring layer consumes — keeping that scoring layer pure.
callJudge(judgeFn, prompt, parse, opts?)— drives a judge function, racing it against a wall-clock timeout and parsing the result. It NEVER throws for the two judge-output failure modes: a timeout degrades toEVAL_JUDGE_TIMEOUT(retryable) and unparseable / wrong-shape output degrades toEVAL_JUDGE_MALFORMED_OUTPUT(not retryable), both returned as a discriminatedJudgeOutcome. A non-timeout rejection from the judge itself (e.g. a provider error) propagates unchanged.- Five judge tasks —
judgeClaimSupport,judgeReverseQuestions,judgeContextUsefulness,judgeStatementClassification,judgeContextAttribution— each build a prompt and parse the judge's JSON (tolerant of code fences and surrounding prose) into the structured input one scoring metric consumes. Output is validated against the target shape (required fields and value types are enforced; unrecognized extra keys are ignored). - New types
JudgeFn((prompt: string) => Promise<string>),JudgeCallOptions,JudgeOutcome<T>, and the lean degrade coreEvalErrorCore, plus theJUDGE_PROMPT_VERSIONandDEFAULT_JUDGE_TIMEOUT_MSconstants and the two new error codes, are exported from the package root.
The judge signature is provider-agnostic and free of any business / envelope fields, so the layer can be exercised in CI with a deterministic mock judge — no API keys and no network.
Also reject negative graded labels in
ndcg(alongside the existing non-finite guard), since a negative gain would push the score outside the documented[0, 1]range. -
219adc1: Add reference-based answer-quality scoring and a graded-relevance ranking metric to the eval framework (additive, no breaking changes):
answerCorrectness(statements)— statement-level F1 between an answer and a gold reference, from per-statement TP/FP/FN classifications (2·tp / (2·tp + fp + fn)). Returns precision, recall, and the raw counts for auditing. Implements the factual F1 component only; the optional semantic-similarity term is left to the caller so the function stays embedding-free.contextRecall(attributedFlags)— fraction of reference sentences attributable to the retrieved context. A sentence counts only when its flag is strictlytrue.ndcg(gains, opts?)— Normalized Discounted Cumulative Gain over a ranked list of graded relevance labels, complementing the existing binary Hit Rate@K / MRR@K. Uses the standard linear gain with a log2 position discount (DCG@k / IDCG@k);opts.ktruncates the ranking. Returns the rawdcg/idcg/ effectivekfor auditing.
All three are deterministic pure functions (no model calls, no embedding calls, no I/O), so they can be regression-tested offline with no API keys. New result/input types (
AnswerCorrectnessStatement,AnswerCorrectnessResult,ContextRecallResult,NdcgResult,CorrectnessLabel) are exported from the package root. Structurally malformed input (a non-array, a non-finite gain, or a badk) throws the existing codedEvalFrameworkError(EVAL_INVALID_METRIC_INPUT). -
e115b92: Add reference-free answer-quality scoring to the eval framework (additive, no breaking changes):
faithfulness(verdicts)— fraction of an answer's atomic claims supported by the retrieved context (0/0 → 0).answerRelevance({ queryEmbedding, generatedQuestionEmbeddings })— mean cosine similarity between the original query and reverse questions generated from the answer (unclamped, ∈ [-1, 1]).contextPrecision(usefulFlags)— order-sensitive average precision over a ranked list of useful/not-useful chunks.cosineSimilarity(a, b)helper — zero-norm vectors yield0; length mismatch or non-finite values throw a codedEvalFrameworkError.
All four are deterministic pure functions (no model calls, no embedding calls, no I/O), so they can be regression-tested offline with no API keys. New result/input types (
ClaimVerdict,FaithfulnessResult,AnswerRelevanceInput,AnswerRelevanceResult,ContextPrecisionResult) and a new `EVAL_INVALID_MET...
v0.2.1
Patch Changes
- f3dad22: Docs hygiene: rewrite the README, source comments and generated API docs in
plain user-facing language. Strips internal development-process references
(story/epic numbers, requirement IDs, private downstream-package names, private
planning-doc paths) that have no meaning to external users of a public package,
and adds acheck-public-hygieneCI gate that fails the build if such jargon is
reintroduced. Documentation-only — no API, type, or runtime behaviour change.
v0.2.0
Minor Changes
-
a08b181: Add the
withPageCaptionRAG plugin and a Streamable HTTP CORS whitelist.withPageCaption— a new page-level multimodal captioning plugin
(PageCaptionOptions) that renders each PDF page and captions it through a
pluggable vision provider. It shares a singlecaption-enginewith
withVisionCaption, so the retry/backoff policy has one source of truth.
Exported from the package root.- Streamable HTTP CORS whitelist —
createMcpServernow accepts acors
option with anoriginswhitelist (exact origin orscheme://*wildcard).
Matched origins are echoed back,OPTIONSpreflight is answered, and no
Access-Control-*headers are emitted when the option is omitted. This lets
browser MCP clients (e.g. a Chrome extension) connect over HTTP.
Patch Changes
-
a08b181: CI: OIDC trusted publishing + provenance + size gate. Adds Changesets-driven
versioning/CHANGELOG, a GitHub Actionsrelease.ymlusing npm Trusted Publishing
(OIDC, tokenless) withprovenance: true, and anpm packsize guard (<100MB).
Replaces the manual webauthnnpm publishflow. -
a08b181: Mark
special_tokens_map.jsonas optional in model manifests. Some Hugging Face
model repos (e.g. certain reranker exports) ship without this file; the loader no
longer fails manifest verification when an entry flaggedoptional: trueis
absent. -
a08b181: Fix BM25 keyword recall and harden the caption pipeline.
- BM25 recall — multi-token FTS5 queries are now joined with
ORinstead of
being matched as a single quoted phrase, restoring keyword recall that an
earlier phrase-match regression had silently narrowed. - Vision caption buffer — each per-page
extractImagescall now receives its
own.slice()of the PDF bytes, becauseunpdf/pdf.js detaches the input
ArrayBufferon each call; sharing it caused "detached ArrayBuffer" failures. - Network-error retry — the shared caption engine now treats transient
network errors (ECONNRESETand friends, including nestedcausechains) as
retryable, so captioning rides out flaky vision-provider connections.
- BM25 recall — multi-token FTS5 queries are now joined with