@devilsdev/rag-pipeline-utils 1.0.1 → 2.0.1
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/.codecov.yml +21 -0
- package/.github/workflows/ci.yml +18 -0
- package/.github/workflows/docs-deploy.yml +36 -0
- package/.github/workflows/publish-release-blog.yml +51 -0
- package/.husky/pre-commit +6 -0
- package/CHANGELOG.md +40 -0
- package/README.md +101 -72
- package/USE_CASES.md +134 -0
- package/__tests__/fixtures/.ragrc.json +8 -0
- package/__tests__/fixtures/invalid-ragrc.json +4 -0
- package/__tests__/fixtures/src/mocks/openai-embedder.js +16 -0
- package/__tests__/fixtures/src/mocks/openai-llm.js +9 -0
- package/__tests__/fixtures/src/mocks/pdf-loader.js +17 -0
- package/__tests__/fixtures/src/mocks/pinecone-retriever.js +26 -0
- package/__tests__/fixtures/valid-ragrc.json +16 -0
- package/__tests__/integration/cli/config-flow.test.js +84 -15
- package/__tests__/integration/config/load-config.test.js +45 -30
- package/__tests__/unit/config/validate-schema.test.js +121 -0
- package/__tests__/unit/reranker/llm-reranker.test.js +9 -33
- package/__tests__/unit/reranker/reranker.enriched.test.js +19 -0
- package/__tests__/unit/reranker/reranker.fallback.test.js +15 -0
- package/__tests__/unit/reranker/reranker.snapshot.test.js +9 -11
- package/__tests__/unit/reranker/reranker.structured-output.test.js +17 -0
- package/bin/cli.js +49 -155
- package/docs-site/blog/2021-08-26-welcome/index.md +0 -1
- package/docs-site/blog/2025-04-17-release-2.0.md +67 -0
- package/docs-site/blog/2025-04-18-release-2.0.0.md +21 -0
- package/docs-site/blog/authors.yml +5 -23
- package/docs-site/blog/tags.yml +9 -0
- package/docs-site/docs/USE_CASES.md +0 -0
- package/docs-site/docusaurus.config.js +13 -0
- package/docs-site/sidebars.js +21 -9
- package/package.json +25 -2
- package/plugin.config.json +14 -0
- package/scripts/banner-injector.js +89 -0
- package/scripts/ci-runner.js +51 -0
- package/scripts/repair-fixtures.js +30 -0
- package/scripts/setup.js +26 -2
- package/scripts/validate-changelog-version.js +48 -0
- package/scripts/verify-fixture-mocks.js +45 -0
- package/scripts/verify-fixtures.js +36 -0
- package/src/config/load-config.js +19 -21
- package/src/config/load-plugin-config.js +56 -0
- package/src/config/validate-plugin-schema.js +28 -0
- package/src/config/validate-schema.js +88 -30
- package/src/core/create-pipeline.js +40 -26
- package/src/core/plugin-contracts.js +21 -0
- package/src/core/plugin-registry.js +4 -17
- package/src/mocks/openai-embedder.js +30 -0
- package/src/mocks/openai-llm.js +18 -0
- package/src/mocks/pdf-loader.js +17 -0
- package/src/mocks/pinecone-retriever.js +25 -0
- package/src/reranker/llm-reranker.js +28 -43
- package/src/utils/ci/diagnostic-reporter.js +36 -0
- package/src/utils/validate-plugin-contract.js +36 -0
- package/.github/workflows/release.yml +0 -20
- package/docs-site/blog/2019-05-28-first-blog-post.md +0 -12
- package/docs-site/blog/2019-05-29-long-blog-post.md +0 -44
- package/docs-site/blog/2021-08-01-mdx-blog-post.mdx +0 -24
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Auto-repairs mock plugin fixtures for integration tests
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, copyFileSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const MOCKS = [
|
|
11
|
+
'pdf-loader.js',
|
|
12
|
+
'openai-embedder.js',
|
|
13
|
+
'pinecone-retriever.js',
|
|
14
|
+
'openai-llm.js'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SRC_FIXTURES = resolve('__tests__/fixtures/src/mocks');
|
|
18
|
+
const DEST_FIXTURES = resolve('__tests__/__temp__/cli-config-fallback/src/mocks');
|
|
19
|
+
|
|
20
|
+
mkdirSync(DEST_FIXTURES, { recursive: true });
|
|
21
|
+
|
|
22
|
+
MOCKS.forEach((file) => {
|
|
23
|
+
const src = join(SRC_FIXTURES, file);
|
|
24
|
+
const dest = join(DEST_FIXTURES, file);
|
|
25
|
+
if (!existsSync(src)) throw new Error(`[FIXTURE MISSING] ${src}`);
|
|
26
|
+
copyFileSync(src, dest);
|
|
27
|
+
console.log(`[COPIED] ${file}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log('✅ Fixture mocks successfully repaired.');
|
package/scripts/setup.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Test setup script to ensure fixtures exist and validate mocks before running tests.
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import fs from 'fs';
|
|
2
|
-
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
// Resolve __dirname in ES module scope
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const fixturePath = path.resolve(__dirname, '../__tests__/fixtures/sample.pdf');
|
|
3
17
|
|
|
18
|
+
// Ensure PDF test fixture
|
|
4
19
|
if (!fs.existsSync(fixturePath)) {
|
|
5
|
-
fs.mkdirSync(
|
|
20
|
+
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
|
6
21
|
fs.writeFileSync(fixturePath, 'Dummy PDF content for test');
|
|
22
|
+
console.log('[setup] Created sample.pdf fixture');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate plugin fixture presence
|
|
26
|
+
try {
|
|
27
|
+
execSync('node ./scripts/verify-fixtures.js', { stdio: 'inherit' });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[setup] Fixture verification failed. Please check missing mocks.');
|
|
30
|
+
process.exit(1);
|
|
7
31
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Ensures CHANGELOG.md contains the current version and auto-corrects format if needed.
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json')));
|
|
16
|
+
const version = pkg.version;
|
|
17
|
+
const changelogPath = path.resolve(__dirname, '../CHANGELOG.md');
|
|
18
|
+
|
|
19
|
+
let changelog = fs.readFileSync(changelogPath, 'utf-8');
|
|
20
|
+
|
|
21
|
+
// Step 1: Normalize invalid headings like "# [2.0.0](...)"
|
|
22
|
+
const normalized = changelog.replace(/^# \[([0-9]+\.[0-9]+\.[0-9]+)\]\([^)]+\)/gm, (match, v) => {
|
|
23
|
+
const today = new Date().toISOString().split('T')[0];
|
|
24
|
+
return `## [${v}] - ${today}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Step 2: Update file if normalization occurred
|
|
28
|
+
if (normalized !== changelog) {
|
|
29
|
+
fs.writeFileSync(changelogPath, normalized);
|
|
30
|
+
console.log(`🛠️ CHANGELOG.md heading normalized for version ${version}`);
|
|
31
|
+
changelog = normalized;
|
|
32
|
+
|
|
33
|
+
// Auto-stage if in git project (Husky compatibility)
|
|
34
|
+
try {
|
|
35
|
+
execSync(`git add ${changelogPath}`);
|
|
36
|
+
console.log('CHANGELOG.md staged after correction');
|
|
37
|
+
} catch {
|
|
38
|
+
// Non-fatal
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Step 3: Validate presence of correct heading
|
|
43
|
+
const headingPattern = new RegExp(`^## \\[${version}\\]`, 'm');
|
|
44
|
+
if (!headingPattern.test(changelog)) {
|
|
45
|
+
throw new Error(`❌ CHANGELOG.md does not contain a properly formatted entry for version ${version}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`✅ CHANGELOG.md contains a valid entry for version ${version}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Verifies that plugin mock implementations expose required methods
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync } from 'fs';
|
|
8
|
+
import { resolve, join, extname } from 'path';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
|
|
11
|
+
const MOCKS_DIR = resolve('__tests__/fixtures/src/mocks');
|
|
12
|
+
|
|
13
|
+
const pluginContracts = {
|
|
14
|
+
'openai-embedder.js': ['embed', 'embedQuery'],
|
|
15
|
+
'openai-llm.js': ['ask'],
|
|
16
|
+
'pdf-loader.js': ['load'],
|
|
17
|
+
'pinecone-retriever.js': ['store', 'search'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
for (const file of readdirSync(MOCKS_DIR)) {
|
|
23
|
+
const fullPath = join(MOCKS_DIR, file);
|
|
24
|
+
const isFile = statSync(fullPath).isFile();
|
|
25
|
+
if (!isFile || extname(file) !== '.js') continue;
|
|
26
|
+
|
|
27
|
+
const expectedMethods = pluginContracts[file];
|
|
28
|
+
if (!expectedMethods) continue;
|
|
29
|
+
|
|
30
|
+
const mod = await import(pathToFileURL(fullPath).href);
|
|
31
|
+
const plugin = new mod.default();
|
|
32
|
+
|
|
33
|
+
for (const method of expectedMethods) {
|
|
34
|
+
if (typeof plugin[method] !== 'function') {
|
|
35
|
+
failures.push(`❌ ${file} failed: [${file}] missing method: ${method}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (failures.length > 0) {
|
|
41
|
+
failures.forEach(msg => console.error(msg));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
} else {
|
|
44
|
+
console.log('✅ All plugin mocks verified.');
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Script to verify presence of all expected fixture plugins before running integration tests.
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* File: scripts/verify-fixtures.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_FIXTURES = [
|
|
12
|
+
'__tests__/fixtures/sample.pdf',
|
|
13
|
+
'__tests__/fixtures/.ragrc.json',
|
|
14
|
+
'__tests__/fixtures/src/mocks/pdf-loader.js',
|
|
15
|
+
'__tests__/fixtures/src/mocks/openai-embedder.js',
|
|
16
|
+
'__tests__/fixtures/src/mocks/pinecone-retriever.js',
|
|
17
|
+
'__tests__/fixtures/src/mocks/openai-llm.js'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let missing = [];
|
|
21
|
+
for (const file of REQUIRED_FIXTURES) {
|
|
22
|
+
const fullPath = resolve(file);
|
|
23
|
+
if (!existsSync(fullPath)) {
|
|
24
|
+
missing.push(fullPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (missing.length) {
|
|
29
|
+
console.error('[ERROR] Missing test fixture files:');
|
|
30
|
+
for (const f of missing) {
|
|
31
|
+
console.error(' -', f);
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
34
|
+
} else {
|
|
35
|
+
console.log('[OK] All fixture files are present.');
|
|
36
|
+
}
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
*
|
|
4
|
-
* Description: Loader and validator for `.ragrc.json` configuration files
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Loads validated .ragrc.json config from disk
|
|
5
4
|
* Author: Ali Kahwaji
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import path from 'path';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
const DEFAULT_CONFIG_FILE = '.ragrc.json';
|
|
9
|
+
import { validateRagrcSchema } from './validate-schema.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
* @throws {Error} - If file not found or validation fails
|
|
13
|
+
* Loads and validates a RAG config file.
|
|
14
|
+
* @param {string} configPath - Absolute path to .ragrc.json
|
|
15
|
+
* @returns {object} Parsed config object
|
|
16
|
+
* @throws {Error} If file is missing or invalid
|
|
20
17
|
*/
|
|
21
|
-
export function loadRagConfig(configPath
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!fs.existsSync(fullPath)) {
|
|
25
|
-
throw new Error(`Config file not found: ${fullPath}`);
|
|
18
|
+
export function loadRagConfig(configPath) {
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
const raw = fs.readFileSync(
|
|
29
|
-
const
|
|
23
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
const result = validateRagrcSchema(parsed);
|
|
27
|
+
if (!result.valid) {
|
|
28
|
+
logger.error({ errors: result.errors }, 'Invalid .ragrc.json schema');
|
|
29
|
+
throw new Error('Config validation failed');
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
return
|
|
32
|
+
return parsed;
|
|
34
33
|
}
|
|
35
|
-
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.1
|
|
3
|
+
* Description: Dynamic plugin loader with direct type:name registry mapping
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
import registry from '../core/plugin-registry.js';
|
|
11
|
+
import { validatePluginSchema } from './validate-schema.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Loads plugins from validated config path and registers them.
|
|
16
|
+
* @param {string} configPath
|
|
17
|
+
*/
|
|
18
|
+
export async function loadPluginsFromJson(configPath) {
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(`Plugin config missing: ${configPath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
24
|
+
const config = JSON.parse(raw);
|
|
25
|
+
|
|
26
|
+
const validation = validatePluginSchema(config);
|
|
27
|
+
if (!validation.valid) {
|
|
28
|
+
logger.error({ errors: validation.errors }, 'Invalid plugin config');
|
|
29
|
+
throw new Error('Plugin config validation failed');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [type, namedPlugins] of Object.entries(config)) {
|
|
33
|
+
if (!['loader', 'embedder', 'retriever', 'llm'].includes(type)) continue;
|
|
34
|
+
|
|
35
|
+
for (const [name, relPath] of Object.entries(namedPlugins)) {
|
|
36
|
+
const absPath = path.resolve(path.dirname(configPath), relPath);
|
|
37
|
+
const fileUrl = pathToFileURL(absPath).href;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import(fileUrl);
|
|
41
|
+
const PluginClass = mod?.default;
|
|
42
|
+
if (typeof PluginClass !== 'function') {
|
|
43
|
+
throw new Error(`Invalid export in plugin [${type}:${name}] at ${absPath}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const instance = new PluginClass();
|
|
47
|
+
registry.register(type, name, instance);
|
|
48
|
+
|
|
49
|
+
logger.info({ type, name }, `Registered plugin: ${type}:${name}`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error({ type, name, err }, `Failed to register plugin [${type}:${name}]`);
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Description: Schema validator for plugin.config.json
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function validatePluginSchema(config) {
|
|
8
|
+
const validTypes = ['loader', 'embedder', 'retriever', 'llm'];
|
|
9
|
+
const errors = [];
|
|
10
|
+
|
|
11
|
+
for (const type of Object.keys(config)) {
|
|
12
|
+
if (!validTypes.includes(type)) {
|
|
13
|
+
errors.push(`Unknown plugin type: ${type}`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
for (const [name, modulePath] of Object.entries(config[type])) {
|
|
17
|
+
if (typeof name !== 'string' || typeof modulePath !== 'string') {
|
|
18
|
+
errors.push(`Invalid plugin definition for type "${type}" and name "${name}"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
valid: errors.length === 0,
|
|
25
|
+
errors
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -1,49 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
*
|
|
4
|
-
* Description: JSON schema validation for `.ragrc.json` config using AJV
|
|
2
|
+
* Version: 1.3.0
|
|
3
|
+
* Description: AJV-powered validators for .ragrc.json full config and plugin-only structure
|
|
5
4
|
* Author: Ali Kahwaji
|
|
5
|
+
* File: /src/config/validate-schema.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import Ajv from 'ajv';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Full .ragrc.json schema (used in load-config.js)
|
|
12
|
+
*/
|
|
13
|
+
const ragrcSchema = {
|
|
12
14
|
type: 'object',
|
|
15
|
+
required: ['loader', 'embedder', 'retriever', 'llm', 'namespace', 'pipeline'],
|
|
13
16
|
properties: {
|
|
14
|
-
loader: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
loader: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
minProperties: 1,
|
|
20
|
+
additionalProperties: { type: 'string' }
|
|
21
|
+
},
|
|
22
|
+
embedder: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
minProperties: 1,
|
|
25
|
+
additionalProperties: { type: 'string' }
|
|
26
|
+
},
|
|
27
|
+
retriever: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
minProperties: 1,
|
|
30
|
+
additionalProperties: { type: 'string' }
|
|
31
|
+
},
|
|
32
|
+
llm: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
minProperties: 1,
|
|
35
|
+
additionalProperties: { type: 'string' }
|
|
36
|
+
},
|
|
37
|
+
namespace: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
minLength: 1
|
|
40
|
+
},
|
|
41
|
+
pipeline: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
enum: ['loader', 'embedder', 'retriever']
|
|
23
46
|
},
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
minItems: 1,
|
|
48
|
+
uniqueItems: true
|
|
26
49
|
}
|
|
27
50
|
},
|
|
28
|
-
required: ['loader', 'embedder', 'retriever', 'llm'],
|
|
29
51
|
additionalProperties: false
|
|
30
52
|
};
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Minimal plugin-only schema (used in load-plugin-config.js)
|
|
56
|
+
*/
|
|
57
|
+
const pluginSchema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['loader', 'embedder', 'retriever', 'llm'],
|
|
60
|
+
properties: {
|
|
61
|
+
loader: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
minProperties: 1,
|
|
64
|
+
additionalProperties: { type: 'string' }
|
|
65
|
+
},
|
|
66
|
+
embedder: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
minProperties: 1,
|
|
69
|
+
additionalProperties: { type: 'string' }
|
|
70
|
+
},
|
|
71
|
+
retriever: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
minProperties: 1,
|
|
74
|
+
additionalProperties: { type: 'string' }
|
|
75
|
+
},
|
|
76
|
+
llm: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
minProperties: 1,
|
|
79
|
+
additionalProperties: { type: 'string' }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
additionalProperties: true // allow namespace, pipeline, etc.
|
|
83
|
+
};
|
|
34
84
|
|
|
35
85
|
/**
|
|
36
|
-
* Validates
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param {object} config - Configuration object
|
|
40
|
-
* @throws {Error} - Validation errors, if any
|
|
86
|
+
* Validates the full .ragrc.json config structure
|
|
87
|
+
* @param {object} config
|
|
88
|
+
* @returns {{ valid: boolean, errors?: any[] }}
|
|
41
89
|
*/
|
|
42
|
-
export function
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
90
|
+
export function validateRagrcSchema(config) {
|
|
91
|
+
const ajv = new Ajv({ allErrors: true });
|
|
92
|
+
const validate = ajv.compile(ragrcSchema);
|
|
93
|
+
const valid = validate(config);
|
|
94
|
+
return valid ? { valid: true } : { valid: false, errors: validate.errors };
|
|
48
95
|
}
|
|
49
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Validates a plugin-only structure (for runtime plugin loading)
|
|
99
|
+
* @param {object} config
|
|
100
|
+
* @returns {{ valid: boolean, errors?: any[] }}
|
|
101
|
+
*/
|
|
102
|
+
export function validatePluginSchema(config) {
|
|
103
|
+
const ajv = new Ajv({ allErrors: true });
|
|
104
|
+
const validate = ajv.compile(pluginSchema);
|
|
105
|
+
const valid = validate(config);
|
|
106
|
+
return valid ? { valid: true } : { valid: false, errors: validate.errors };
|
|
107
|
+
}
|
|
@@ -1,73 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version: 0.
|
|
2
|
+
* Version: 0.2.0
|
|
3
3
|
* Path: /src/core/create-pipeline.js
|
|
4
|
-
* Description: RAG pipeline factory with
|
|
4
|
+
* Description: RAG pipeline factory with modular retry, logging, and reranker middleware
|
|
5
5
|
* Author: Ali Kahwaji
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import registry from './plugin-registry.js';
|
|
9
9
|
import { logger } from '../utils/logger.js';
|
|
10
10
|
import { withRetry } from '../utils/retry.js';
|
|
11
11
|
import { LLMReranker } from '../reranker/llm-reranker.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Pipeline options to toggle middleware.
|
|
15
|
+
* @typedef {Object} PipelineOptions
|
|
16
|
+
* @property {boolean} [useRetry=true] - Enable retry logic on network/storage.
|
|
17
|
+
* @property {boolean} [useLogging=true] - Enable structured logging.
|
|
18
|
+
* @property {boolean} [useReranker=false] - Use reranker for query refinement.
|
|
19
|
+
*/
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {object} config
|
|
22
|
+
* Creates a RAG pipeline instance.
|
|
23
|
+
* @param {object} config - Pipeline configuration.
|
|
18
24
|
* @param {string} config.loader
|
|
19
25
|
* @param {string} config.embedder
|
|
20
26
|
* @param {string} config.retriever
|
|
21
27
|
* @param {string} config.llm
|
|
22
|
-
* @param {
|
|
28
|
+
* @param {PipelineOptions} [options]
|
|
23
29
|
* @returns {object} RAG pipeline instance
|
|
24
30
|
*/
|
|
25
|
-
export function createRagPipeline(
|
|
26
|
-
|
|
31
|
+
export function createRagPipeline(
|
|
32
|
+
{ loader, embedder, retriever, llm },
|
|
33
|
+
{ useRetry = true, useLogging = true, useReranker = false } = {}
|
|
34
|
+
) {
|
|
35
|
+
const loaderInstance = registry.get('loader', loader);
|
|
36
|
+
const embedderInstance = registry.get('embedder', embedder);
|
|
37
|
+
const retrieverInstance = registry.get('retriever', retriever);
|
|
38
|
+
const llmInstance = registry.get('llm', llm);
|
|
39
|
+
const rerankerInstance = useReranker ? new LLMReranker({ llm: llmInstance }) : null;
|
|
40
|
+
|
|
41
|
+
const log = (level, msg, data = {}) => {
|
|
42
|
+
if (useLogging) logger[level](data, msg);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const wrap = async (fn, context) => {
|
|
46
|
+
return useRetry ? withRetry(fn, context) : fn();
|
|
47
|
+
};
|
|
27
48
|
|
|
28
49
|
return {
|
|
29
|
-
async ingest(
|
|
30
|
-
|
|
31
|
-
const loaderInstance = registry.get('loader', loader);
|
|
32
|
-
const embedderInstance = registry.get('embedder', embedder);
|
|
33
|
-
const retrieverInstance = registry.get('retriever', retriever);
|
|
50
|
+
async ingest(docPath) {
|
|
51
|
+
log('info', 'Pipeline ingest start', { loader, embedder, retriever });
|
|
34
52
|
|
|
35
|
-
const documents = await loaderInstance.load(
|
|
53
|
+
const documents = await loaderInstance.load(docPath);
|
|
36
54
|
const chunks = documents.flatMap(doc => doc.chunk());
|
|
37
55
|
const vectors = await embedderInstance.embed(chunks);
|
|
38
56
|
|
|
39
|
-
await
|
|
57
|
+
await wrap(() => retrieverInstance.store(vectors), {
|
|
40
58
|
label: 'vector-store',
|
|
41
59
|
retries: 3,
|
|
42
60
|
initialDelay: 500
|
|
43
61
|
});
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
log('info', 'Ingestion pipeline completed');
|
|
46
64
|
},
|
|
47
65
|
|
|
48
66
|
async query(prompt) {
|
|
49
|
-
|
|
50
|
-
const embedderInstance = registry.get('embedder', embedder);
|
|
51
|
-
const retrieverInstance = registry.get('retriever', retriever);
|
|
52
|
-
const llmInstance = registry.get('llm', llm);
|
|
67
|
+
log('info', 'Pipeline query start', { prompt, embedder, retriever, llm, useReranker });
|
|
53
68
|
|
|
54
69
|
const queryVector = await embedderInstance.embedQuery(prompt);
|
|
55
|
-
let retrieved = await
|
|
70
|
+
let retrieved = await wrap(() => retrieverInstance.retrieve(queryVector), {
|
|
56
71
|
label: 'vector-retrieve',
|
|
57
72
|
retries: 3,
|
|
58
73
|
initialDelay: 500
|
|
59
74
|
});
|
|
60
75
|
|
|
61
76
|
if (rerankerInstance) {
|
|
62
|
-
|
|
63
|
-
retrieved = await
|
|
77
|
+
log('info', 'Applying LLM reranker to retrieved chunks');
|
|
78
|
+
retrieved = await wrap(() => rerankerInstance.rerank(prompt, retrieved), {
|
|
64
79
|
label: 'rerank',
|
|
65
80
|
retries: 2,
|
|
66
81
|
initialDelay: 400
|
|
67
82
|
});
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
const result = await
|
|
85
|
+
const result = await wrap(() => llmInstance.generate(prompt, retrieved), {
|
|
71
86
|
label: 'llm-generate',
|
|
72
87
|
retries: 3,
|
|
73
88
|
initialDelay: 500
|
|
@@ -79,4 +94,3 @@ export function createRagPipeline({ loader, embedder, retriever, llm, useReranke
|
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
export { registry };
|
|
82
|
-
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Shared plugin interface contracts for runtime + CI mock validation
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const pluginContracts = {
|
|
8
|
+
loader: {
|
|
9
|
+
requiredMethods: ['load'],
|
|
10
|
+
},
|
|
11
|
+
embedder: {
|
|
12
|
+
requiredMethods: ['embed', 'embedQuery'],
|
|
13
|
+
},
|
|
14
|
+
retriever: {
|
|
15
|
+
requiredMethods: ['store', 'search'],
|
|
16
|
+
},
|
|
17
|
+
llm: {
|
|
18
|
+
requiredMethods: ['ask'],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
@@ -14,12 +14,6 @@ export class PluginRegistry {
|
|
|
14
14
|
reranker: new Map(),
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Registers a plugin instance under a specific type and name.
|
|
19
|
-
* @param {'loader' | 'embedder' | 'retriever' | 'llm' | 'reranker'} type - Plugin type category.
|
|
20
|
-
* @param {string} name - Unique name/key for this plugin.
|
|
21
|
-
* @param {object} plugin - Plugin implementation (e.g., instance or factory).
|
|
22
|
-
*/
|
|
23
17
|
register(type, name, plugin) {
|
|
24
18
|
const group = this.#registry[type];
|
|
25
19
|
if (!group) {
|
|
@@ -28,12 +22,6 @@ export class PluginRegistry {
|
|
|
28
22
|
group.set(name, plugin);
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
* Retrieves a plugin by type and name.
|
|
33
|
-
* @param {'loader' | 'embedder' | 'retriever' | 'llm' | 'reranker'} type - Plugin type.
|
|
34
|
-
* @param {string} name - Plugin name/key.
|
|
35
|
-
* @returns {object} - The plugin instance.
|
|
36
|
-
*/
|
|
37
25
|
get(type, name) {
|
|
38
26
|
const group = this.#registry[type];
|
|
39
27
|
if (!group) {
|
|
@@ -46,11 +34,6 @@ export class PluginRegistry {
|
|
|
46
34
|
return plugin;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
/**
|
|
50
|
-
* Lists all registered plugin names for a type.
|
|
51
|
-
* @param {'loader' | 'embedder' | 'retriever' | 'llm' | 'reranker'} type - Plugin category.
|
|
52
|
-
* @returns {string[]} - Array of registered plugin names.
|
|
53
|
-
*/
|
|
54
37
|
list(type) {
|
|
55
38
|
const group = this.#registry[type];
|
|
56
39
|
if (!group) {
|
|
@@ -59,3 +42,7 @@ export class PluginRegistry {
|
|
|
59
42
|
return [...group.keys()];
|
|
60
43
|
}
|
|
61
44
|
}
|
|
45
|
+
|
|
46
|
+
const registry = new PluginRegistry();
|
|
47
|
+
|
|
48
|
+
export default registry;
|