Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/18973-ragflow-reads-declared-adapter-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@objectstack/knowledge-ragflow": patch
---

The RAGFlow adapter now reads the declared key: a source's RAGFlow binding comes from `adapterConfig.datasetId`, not `options.datasetId`.

`KnowledgeSourceSchema` declares `adapterConfig` for adapter-specific configuration and is a plain `z.object` — it carries no `.passthrough()`, so any path that parses a source drops `options` before an adapter ever sees it. The adapter read `options` through a cast, which worked only because no path parses a source today. The cast is gone; there is no fallback that also reads `options` (Prime Directive #12 — one strict contract, no lenient consumer).

Migration, `FROM` → `TO`, one line per source:

```ts
// FROM
{ id: 'product_docs', adapter: 'ragflow', options: { datasetId: 'rgf_…' } }
// TO
{ id: 'product_docs', adapter: 'ragflow', adapterConfig: { datasetId: 'rgf_…' } }
```

The same move applies to `rerankModel`, `similarityThreshold` and `vectorSimilarityWeight`, which the adapter reads from the same bag. A source left on the old spelling is refused by name — `RAGFlow adapter requires source.adapterConfig.datasetId on source '<id>'` — rather than silently retrieving nothing, so the upgrade is self-describing at the first call. Nothing an author could declare is removed: `options` was never a key `KnowledgeSourceSchema` accepted, which is why this carries no ADR-0087 conversion.

The package's published `README.md` moves with the adapter and now compiles against it — it was the one block of the 44 that #18915 could not repair, because correcting the spelling alone would have compiled and stopped working.

Clause-②: no
4 changes: 2 additions & 2 deletions packages/plugins/knowledge-ragflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ kernel.use(new KnowledgeServicePlugin({
label: 'Product documentation',
adapter: 'ragflow',
source: { kind: 'http', urls: ['https://docs.example.com/sitemap.xml'] },
options: { datasetId: 'rgf_doc_dataset_id' }, // RAGFlow dataset to bind
adapterConfig: { datasetId: 'rgf_doc_dataset_id' }, // RAGFlow dataset to bind
}],
}));
kernel.use(new KnowledgeRagflowPlugin({
Expand All @@ -36,7 +36,7 @@ kernel.use(new KnowledgeRagflowPlugin({

## Source binding

Each `KnowledgeSource` must include `options.datasetId` pointing to a pre-created RAGFlow dataset. The adapter doesn't create datasets — operators do that once in the RAGFlow UI, where they pick the chunking method, embedding model, and rerank policy.
Each `KnowledgeSource` must include `adapterConfig.datasetId` pointing to a pre-created RAGFlow dataset. `adapterConfig` is the key `KnowledgeSourceSchema` declares for adapter-specific configuration; a source that spells it anything else loses it on any parsing path, and the adapter refuses it by name. The adapter doesn't create datasets — operators do that once in the RAGFlow UI, where they pick the chunking method, embedding model, and rerank policy.

## What the adapter does

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const source: KnowledgeSource = {
label: 'Docs',
adapter: 'ragflow',
source: { kind: 'http', urls: ['https://docs.example.com'] } as KnowledgeSource['source'],
options: { datasetId: 'ds_42' },
adapterConfig: { datasetId: 'ds_42' },
};

function fakeFetch(handler: (url: string, init?: any) => unknown): { fetch: FetchLike; calls: Array<{ url: string; init: any }> } {
Expand All @@ -30,11 +30,30 @@ function fakeFetch(handler: (url: string, init?: any) => unknown): { fetch: Fetc
}

describe('KnowledgeRagflowAdapter', () => {
it('rejects sources without datasetId', async () => {
it('rejects sources without datasetId, naming the declared key', async () => {
const { fetch } = fakeFetch(() => ({}));
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://x', apiKey: 'k', fetch });
const bad: KnowledgeSource = { ...source, options: {} as Record<string, unknown> };
await expect(a.search('q', { source: bad, topK: 1 })).rejects.toThrow(/datasetId/);
const bad: KnowledgeSource = { ...source, adapterConfig: {} };
// The refusal text is the migration notice a host reads, so it is pinned:
// it must name `adapterConfig.datasetId`, the key the schema declares.
await expect(a.search('q', { source: bad, topK: 1 })).rejects.toThrow(
/source\.adapterConfig\.datasetId/,
);
});

it('does not read the undeclared `options` spelling', async () => {
// `KnowledgeSourceSchema` is a plain `z.object`: it declares `adapterConfig`
// and drops `options` on any parsing path. The adapter reads the declared
// key only — no lenient fallback (Prime Directive #12). A host still on the
// old spelling is refused loudly rather than served with silence.
const { fetch, calls } = fakeFetch(() => ({}));
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://x', apiKey: 'k', fetch });
const { adapterConfig: _dropped, ...rest } = source;
const legacy = { ...rest, options: { datasetId: 'ds_42' } } as unknown as KnowledgeSource;
await expect(a.search('q', { source: legacy, topK: 1 })).rejects.toThrow(
/source\.adapterConfig\.datasetId/,
);
expect(calls).toHaveLength(0);
});

it('upsert deletes-then-creates chunks and stamps objectstack metadata', async () => {
Expand Down Expand Up @@ -106,7 +125,7 @@ describe('KnowledgeRagflowAdapter', () => {
const a = new KnowledgeRagflowAdapter({ endpoint: 'http://r', apiKey: 'k', fetch });
const s: KnowledgeSource = {
...source,
options: { datasetId: 'ds_42', rerankModel: 'bge-reranker', similarityThreshold: 0.6 },
adapterConfig: { datasetId: 'ds_42', rerankModel: 'bge-reranker', similarityThreshold: 0.6 },
};
await a.search('q', { source: s, topK: 3, filter: { tag: 'a' } });
const body = JSON.parse(calls[0].init.body);
Expand Down
22 changes: 14 additions & 8 deletions packages/plugins/knowledge-ragflow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,29 @@ interface RagflowSourceOptions {
vectorSimilarityWeight?: number;
}

/**
* Reads this source's RAGFlow binding from `adapterConfig` — the key
* `KnowledgeSourceSchema` declares for adapter-specific configuration.
* That schema is a plain `z.object`, so any parsing path drops keys it
* does not declare; the adapter therefore reads the declared key and no
* other spelling (Prime Directive #12 — no lenient consumer).
*/
function extractRagflowOptions(source: KnowledgeSource): RagflowSourceOptions {
const opts = ((source as unknown as { options?: Record<string, unknown> }).options ?? {}) as
Record<string, unknown>;
const datasetId = opts.datasetId;
const cfg = source.adapterConfig ?? {};
const datasetId = cfg.datasetId;
if (typeof datasetId !== 'string' || !datasetId) {
throw new Error(
`RAGFlow adapter requires source.options.datasetId on source '${source.id}'`,
`RAGFlow adapter requires source.adapterConfig.datasetId on source '${source.id}'`,
);
}
return {
datasetId,
rerankModel: typeof opts.rerankModel === 'string' ? opts.rerankModel : undefined,
rerankModel: typeof cfg.rerankModel === 'string' ? cfg.rerankModel : undefined,
similarityThreshold:
typeof opts.similarityThreshold === 'number' ? opts.similarityThreshold : undefined,
typeof cfg.similarityThreshold === 'number' ? cfg.similarityThreshold : undefined,
vectorSimilarityWeight:
typeof opts.vectorSimilarityWeight === 'number'
? opts.vectorSimilarityWeight
typeof cfg.vectorSimilarityWeight === 'number'
? cfg.vectorSimilarityWeight
: undefined,
};
}
Expand Down
8 changes: 2 additions & 6 deletions packages/plugins/knowledge-ragflow/test-typecheck-debt.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{
"_comment": "Per-file tsc error debt of the @objectstack/knowledge-ragflow TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. Each entry maps a file to its per-SIGNATURE error counts, never to a bare total (#13470): a signature is the TS code plus the diagnostic message with structural type blobs collapsed, and it carries NO line or column — so the pin survives edits that move code around, and only stops matching when the error itself becomes a different error. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, a signature that ARRIVES or VANISHES is red even when the file total is unchanged, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/knowledge-ragflow gen:test-typecheck-debt",
"_note": "STARTING LEDGER, opened by #14062 under the director ruling of 2026-09-01 (maintainer verbatim: 「同意」), which carries the #5286 maintainer authority for it. 3 errors in 1 file, all PRE-EXISTING — and this package was silent for a DIFFERENT reason than its siblings: its `tsconfig.json` never excluded tests, so a tsc program would have read them, but the package declared NO `typecheck` script at all, and `turbo run typecheck` cannot run a script that does not exist. #14062 added one naming this gate. ⛔ That is not the repo-wide 'packages missing a `typecheck` script' carry-over, which the same ruling holds separate (item 5): this is the one invocation path #14062's own instrument needs in order to run here at all.",
"entries": {
"src/__tests__/ragflow-adapter.test.ts": {
"TS2353: Object literal may only specify known properties, and 'options' does not exist in type '…'.": 3
}
}
"_note": "STARTING LEDGER, opened by #14062 under the director ruling of 2026-09-01 (maintainer verbatim: 「同意」), which carries the #5286 maintainer authority for it. 3 errors in 1 file, all PRE-EXISTING — and this package was silent for a DIFFERENT reason than its siblings: its `tsconfig.json` never excluded tests, so a tsc program would have read them, but the package declared NO `typecheck` script at all, and `turbo run typecheck` cannot run a script that does not exist. #14062 added one naming this gate. ⛔ That is not the repo-wide 'packages missing a `typecheck` script' carry-over, which the same ruling holds separate (item 5): this is the one invocation path #14062's own instrument needs in order to run here at all. GRADUATED — the ledger is now empty. All 3 errors were one defect: the test wrote `options` on a `KnowledgeSource` literal, a key `KnowledgeSourceSchema` does not declare, because the adapter read that spelling. The adapter now reads the declared `adapterConfig` (ruled: batch #160 item 2, letter A) and the literals moved with it, so tsc reports none. ⛔ This file is kept, not deleted: it is the per-file ledger `check:test-typecheck` reads for this package, and an empty `entries` is the pin that the test layer owes nothing.",
"entries": {}
}
Loading