@devilsdev/rag-pipeline-utils 1.0.0
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/.dockerignore +14 -0
- package/.github/workflows/ci.yml +42 -0
- package/.github/workflows/release.yml +21 -0
- package/Dockerfile +22 -0
- package/LICENSE +674 -0
- package/README.md +116 -0
- package/__tests__/fixtures/sample-eval-dataset.json +15 -0
- package/__tests__/integration/cli/config-flow.test.js +44 -0
- package/__tests__/integration/config/load-config.test.js +52 -0
- package/__tests__/unit/core/plugin-registry.test.js +55 -0
- package/__tests__/unit/reranker/__snapshots__/reranker.snapshot.test.js.snap +15 -0
- package/__tests__/unit/reranker/llm-reranker.test.js +42 -0
- package/__tests__/unit/reranker/reranker.snapshot.test.js +17 -0
- package/bin/cli.js +168 -0
- package/docs-site/README.md +41 -0
- package/docs-site/blog/2019-05-28-first-blog-post.md +12 -0
- package/docs-site/blog/2019-05-29-long-blog-post.md +44 -0
- package/docs-site/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs-site/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs-site/blog/2021-08-26-welcome/index.md +29 -0
- package/docs-site/blog/authors.yml +23 -0
- package/docs-site/blog/tags.yml +19 -0
- package/docs-site/docs/Architecture.md +76 -0
- package/docs-site/docs/CLI.md +69 -0
- package/docs-site/docs/Evaluation.md +61 -0
- package/docs-site/docs/FAQ.md +50 -0
- package/docs-site/docs/Introduction.md +58 -0
- package/docs-site/docs/Plugins.md +61 -0
- package/docs-site/docs/Usage.md +72 -0
- package/docs-site/docusaurus.config.js +57 -0
- package/docs-site/package-lock.json +17914 -0
- package/docs-site/package.json +44 -0
- package/docs-site/sidebars.js +22 -0
- package/docs-site/src/components/HomepageFeatures/index.js +59 -0
- package/docs-site/src/components/HomepageFeatures/styles.module.css +58 -0
- package/docs-site/src/css/custom.css +81 -0
- package/docs-site/src/pages/index.js +37 -0
- package/docs-site/src/pages/index.module.css +23 -0
- package/docs-site/src/pages/markdown-page.md +7 -0
- package/docs-site/static/.nojekyll +0 -0
- package/docs-site/static/img/favicon.ico +0 -0
- package/docs-site/static/img/logo.png +0 -0
- package/docs-site/static/img/logo.svg +1539 -0
- package/docs-site/static/img/social-card.jpg +0 -0
- package/jest.config.js +14 -0
- package/package.json +65 -0
- package/public/index.html +0 -0
- package/public/results/sample-eval.json +0 -0
- package/public/src/App.jsx +57 -0
- package/release.config.js +23 -0
- package/server.js +31 -0
- package/src/config/load-config.js +35 -0
- package/src/config/validate-schema.js +49 -0
- package/src/core/create-pipeline.js +82 -0
- package/src/core/plugin-registry.js +61 -0
- package/src/dag/dag-engine.js +70 -0
- package/src/evaluate/evaluator.js +44 -0
- package/src/evaluate/scoring.js +74 -0
- package/src/loader/csv-loader.js +30 -0
- package/src/loader/directory-loader.js +38 -0
- package/src/loader/html-loader.js +57 -0
- package/src/loader/markdown-loader.js +58 -0
- package/src/reranker/llm-reranker.js +49 -0
- package/src/utils/logger.js +29 -0
- package/src/utils/retry.js +42 -0
|
Binary file
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /jest.config.js
|
|
4
|
+
* Description: Jest configuration for native ESM test support
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
testEnvironment: 'node',
|
|
10
|
+
transform: {}, // Native ESM — no Babel used
|
|
11
|
+
roots: ['<rootDir>/__tests__'],
|
|
12
|
+
verbose: true
|
|
13
|
+
};
|
|
14
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devilsdev/rag-pipeline-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A modular toolkit for building RAG (Retrieval-Augmented Generation) pipelines in Node.js",
|
|
5
|
+
"main": "src/core/create-pipeline.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rag-pipeline": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
12
|
+
"test:watch": "jest --watch",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"prepare": "husky install"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/DevilsDev/rag-pipeline-utils.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"rag",
|
|
22
|
+
"retrieval-augmented-generation",
|
|
23
|
+
"vectorstore",
|
|
24
|
+
"llm",
|
|
25
|
+
"pipeline",
|
|
26
|
+
"cli",
|
|
27
|
+
"openai"
|
|
28
|
+
],
|
|
29
|
+
"author": "Ali Kahwaji",
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/DevilsDev/rag-pipeline-utils/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/DevilsDev/rag-pipeline-utils#readme",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
40
|
+
"@semantic-release/git": "^10.0.1",
|
|
41
|
+
"@semantic-release/github": "^9.2.6",
|
|
42
|
+
"@semantic-release/npm": "^11.0.3",
|
|
43
|
+
"@types/jest": "^29.5.14",
|
|
44
|
+
"cross-env": "^7.0.3",
|
|
45
|
+
"esbuild": "^0.25.2",
|
|
46
|
+
"eslint": "^8.57.1",
|
|
47
|
+
"husky": "^8.0.3",
|
|
48
|
+
"jest": "^29.7.0",
|
|
49
|
+
"prettier": "^3.5.3",
|
|
50
|
+
"rimraf": "^6.0.1",
|
|
51
|
+
"semantic-release": "^21.1.2",
|
|
52
|
+
"ts-jest": "^29.3.2"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"axios": "^1.8.4",
|
|
56
|
+
"chalk": "^5.4.1",
|
|
57
|
+
"commander": "^13.1.0",
|
|
58
|
+
"csv-parse": "^5.6.0",
|
|
59
|
+
"dotenv": "^16.5.0",
|
|
60
|
+
"fast-glob": "^3.3.3",
|
|
61
|
+
"jsdom": "^26.1.0",
|
|
62
|
+
"openai": "^4.93.0",
|
|
63
|
+
"pino": "^9.6.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /public/src/App.jsx
|
|
4
|
+
* Description: Evaluation results dashboard UI
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
export default function App() {
|
|
11
|
+
const [results, setResults] = useState([]);
|
|
12
|
+
const [summary, setSummary] = useState({ passRate: 0, avgBLEU: 0, avgROUGE: 0 });
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetch('/results/sample-eval.json')
|
|
16
|
+
.then(res => res.json())
|
|
17
|
+
.then(data => {
|
|
18
|
+
setResults(data);
|
|
19
|
+
|
|
20
|
+
const total = data.length;
|
|
21
|
+
const passCount = data.filter(r => r.success).length;
|
|
22
|
+
const avgBLEU = data.reduce((s, r) => s + r.scores.bleu, 0) / total;
|
|
23
|
+
const avgROUGE = data.reduce((s, r) => s + r.scores.rouge, 0) / total;
|
|
24
|
+
|
|
25
|
+
setSummary({
|
|
26
|
+
passRate: (passCount / total * 100).toFixed(1),
|
|
27
|
+
avgBLEU: avgBLEU.toFixed(2),
|
|
28
|
+
avgROUGE: avgROUGE.toFixed(2)
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<main className="max-w-6xl mx-auto p-6">
|
|
35
|
+
<h1 className="text-3xl font-bold mb-4">RAG Evaluation Dashboard</h1>
|
|
36
|
+
|
|
37
|
+
<div className="mb-6 p-4 border rounded shadow bg-white">
|
|
38
|
+
<h2 className="text-xl font-semibold mb-2">Summary</h2>
|
|
39
|
+
<p> Pass Rate: <strong>{summary.passRate}%</strong></p>
|
|
40
|
+
<p> Avg BLEU: <strong>{summary.avgBLEU}</strong></p>
|
|
41
|
+
<p> Avg ROUGE: <strong>{summary.avgROUGE}</strong></p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{results.map((res, i) => (
|
|
45
|
+
<div key={i} className="mb-4 p-4 border-l-4 rounded bg-white shadow border-l-gray-300">
|
|
46
|
+
<h3 className="font-bold text-lg">Prompt {i + 1}</h3>
|
|
47
|
+
<p className="mt-2"><strong>Prompt:</strong> {res.prompt}</p>
|
|
48
|
+
<p><strong>Expected:</strong> {res.expected}</p>
|
|
49
|
+
<p><strong>Actual:</strong> {res.actual}</p>
|
|
50
|
+
<p> BLEU: <strong>{res.scores.bleu.toFixed(2)}</strong></p>
|
|
51
|
+
<p> ROUGE: <strong>{res.scores.rouge.toFixed(2)}</strong></p>
|
|
52
|
+
<p>Pass: <strong className={res.success ? 'text-green-600' : 'text-red-600'}>{res.success ? 'Yes' : 'No'}</strong></p>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</main>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /release.config.js
|
|
4
|
+
* Description: Semantic release configuration for automated changelog, versioning, GitHub and npm publishing
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
branches: ['main'],
|
|
10
|
+
plugins: [
|
|
11
|
+
'@semantic-release/commit-analyzer',
|
|
12
|
+
'@semantic-release/release-notes-generator',
|
|
13
|
+
['@semantic-release/changelog', {
|
|
14
|
+
changelogFile: 'CHANGELOG.md'
|
|
15
|
+
}],
|
|
16
|
+
'@semantic-release/npm',
|
|
17
|
+
['@semantic-release/git', {
|
|
18
|
+
assets: ['CHANGELOG.md', 'package.json'],
|
|
19
|
+
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
|
|
20
|
+
}],
|
|
21
|
+
'@semantic-release/github'
|
|
22
|
+
]
|
|
23
|
+
};
|
package/server.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /server.js
|
|
4
|
+
* Description: Local static server for serving evaluation dashboard
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
const port = process.env.PORT || 3000;
|
|
17
|
+
|
|
18
|
+
// Serve evaluation results statically
|
|
19
|
+
app.use('/results', express.static(path.join(__dirname, '__tests__/fixtures')));
|
|
20
|
+
|
|
21
|
+
// Serve public frontend assets
|
|
22
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
23
|
+
|
|
24
|
+
// Fallback to index.html
|
|
25
|
+
app.get('*', (req, res) => {
|
|
26
|
+
res.sendFile(path.join(__dirname, 'public/index.html'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.listen(port, () => {
|
|
30
|
+
console.log(` Dashboard running at http://localhost:${port}`);
|
|
31
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/config/load-config.js
|
|
4
|
+
* Description: Loader and validator for `.ragrc.json` configuration files
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { validate } from './validate-schema.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG_FILE = '.ragrc.json';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load and validate the RAG configuration from a file.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} [configPath] - Optional custom config path
|
|
18
|
+
* @returns {object} - Validated config object
|
|
19
|
+
* @throws {Error} - If file not found or validation fails
|
|
20
|
+
*/
|
|
21
|
+
export function loadRagConfig(configPath = DEFAULT_CONFIG_FILE) {
|
|
22
|
+
const fullPath = path.resolve(process.cwd(), configPath);
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(fullPath)) {
|
|
25
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const raw = fs.readFileSync(fullPath, 'utf-8');
|
|
29
|
+
const config = JSON.parse(raw);
|
|
30
|
+
|
|
31
|
+
validate(config); // Throws if invalid
|
|
32
|
+
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/config/validate-schema.js
|
|
4
|
+
* Description: JSON schema validation for `.ragrc.json` config using AJV
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Ajv from 'ajv';
|
|
9
|
+
|
|
10
|
+
// Define allowed config structure
|
|
11
|
+
const schema = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
loader: { type: 'string' },
|
|
15
|
+
embedder: { type: 'string' },
|
|
16
|
+
retriever: { type: 'string' },
|
|
17
|
+
llm: { type: 'string' },
|
|
18
|
+
chunk: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
strategy: { type: 'string' },
|
|
22
|
+
size: { type: 'number' }
|
|
23
|
+
},
|
|
24
|
+
required: ['strategy', 'size'],
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
required: ['loader', 'embedder', 'retriever', 'llm'],
|
|
29
|
+
additionalProperties: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ajv = new Ajv();
|
|
33
|
+
const validateSchema = ajv.compile(schema);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validates a given config object against the `.ragrc.json` schema.
|
|
37
|
+
* Throws if the schema is invalid.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} config - Configuration object
|
|
40
|
+
* @throws {Error} - Validation errors, if any
|
|
41
|
+
*/
|
|
42
|
+
export function validate(config) {
|
|
43
|
+
const valid = validateSchema(config);
|
|
44
|
+
if (!valid) {
|
|
45
|
+
const messages = validateSchema.errors.map(e => `${e.instancePath} ${e.message}`).join(', ');
|
|
46
|
+
throw new Error(`Invalid config: ${messages}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.3
|
|
3
|
+
* Path: /src/core/create-pipeline.js
|
|
4
|
+
* Description: RAG pipeline factory with optional LLM reranker integration
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PluginRegistry } from './plugin-registry.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import { withRetry } from '../utils/retry.js';
|
|
11
|
+
import { LLMReranker } from '../reranker/llm-reranker.js';
|
|
12
|
+
|
|
13
|
+
const registry = new PluginRegistry();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a RAG pipeline instance using registered plugin components
|
|
17
|
+
* @param {object} config
|
|
18
|
+
* @param {string} config.loader
|
|
19
|
+
* @param {string} config.embedder
|
|
20
|
+
* @param {string} config.retriever
|
|
21
|
+
* @param {string} config.llm
|
|
22
|
+
* @param {boolean} [config.useReranker=false]
|
|
23
|
+
* @returns {object} RAG pipeline instance
|
|
24
|
+
*/
|
|
25
|
+
export function createRagPipeline({ loader, embedder, retriever, llm, useReranker = false }) {
|
|
26
|
+
const rerankerInstance = useReranker ? new LLMReranker({ llm: registry.get('llm', llm) }) : null;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
async ingest(path) {
|
|
30
|
+
logger.info({ loader, embedder, retriever }, 'Pipeline ingest start');
|
|
31
|
+
const loaderInstance = registry.get('loader', loader);
|
|
32
|
+
const embedderInstance = registry.get('embedder', embedder);
|
|
33
|
+
const retrieverInstance = registry.get('retriever', retriever);
|
|
34
|
+
|
|
35
|
+
const documents = await loaderInstance.load(path);
|
|
36
|
+
const chunks = documents.flatMap(doc => doc.chunk());
|
|
37
|
+
const vectors = await embedderInstance.embed(chunks);
|
|
38
|
+
|
|
39
|
+
await withRetry(() => retrieverInstance.store(vectors), {
|
|
40
|
+
label: 'vector-store',
|
|
41
|
+
retries: 3,
|
|
42
|
+
initialDelay: 500
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
logger.info('Ingestion pipeline completed');
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async query(prompt) {
|
|
49
|
+
logger.info({ prompt, embedder, retriever, llm, useReranker }, 'Pipeline query start');
|
|
50
|
+
const embedderInstance = registry.get('embedder', embedder);
|
|
51
|
+
const retrieverInstance = registry.get('retriever', retriever);
|
|
52
|
+
const llmInstance = registry.get('llm', llm);
|
|
53
|
+
|
|
54
|
+
const queryVector = await embedderInstance.embedQuery(prompt);
|
|
55
|
+
let retrieved = await withRetry(() => retrieverInstance.retrieve(queryVector), {
|
|
56
|
+
label: 'vector-retrieve',
|
|
57
|
+
retries: 3,
|
|
58
|
+
initialDelay: 500
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (rerankerInstance) {
|
|
62
|
+
logger.info('Applying LLM reranker to retrieved chunks');
|
|
63
|
+
retrieved = await withRetry(() => rerankerInstance.rerank(prompt, retrieved), {
|
|
64
|
+
label: 'rerank',
|
|
65
|
+
retries: 2,
|
|
66
|
+
initialDelay: 400
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result = await withRetry(() => llmInstance.generate(prompt, retrieved), {
|
|
71
|
+
label: 'llm-generate',
|
|
72
|
+
retries: 3,
|
|
73
|
+
initialDelay: 500
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { registry };
|
|
82
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Path: /src/core/plugin-registry.js
|
|
4
|
+
* Description: Registry for managing pluggable components (loader, embedder, retriever, llm)
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class PluginRegistry {
|
|
9
|
+
#registry = {
|
|
10
|
+
loader: new Map(),
|
|
11
|
+
embedder: new Map(),
|
|
12
|
+
retriever: new Map(),
|
|
13
|
+
llm: new Map(),
|
|
14
|
+
reranker: new Map(),
|
|
15
|
+
};
|
|
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
|
+
register(type, name, plugin) {
|
|
24
|
+
const group = this.#registry[type];
|
|
25
|
+
if (!group) {
|
|
26
|
+
throw new Error(`Unknown plugin type: '${type}'`);
|
|
27
|
+
}
|
|
28
|
+
group.set(name, plugin);
|
|
29
|
+
}
|
|
30
|
+
|
|
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
|
+
get(type, name) {
|
|
38
|
+
const group = this.#registry[type];
|
|
39
|
+
if (!group) {
|
|
40
|
+
throw new Error(`Unknown plugin type: '${type}'`);
|
|
41
|
+
}
|
|
42
|
+
const plugin = group.get(name);
|
|
43
|
+
if (!plugin) {
|
|
44
|
+
throw new Error(`Plugin not found: [${type}:${name}]`);
|
|
45
|
+
}
|
|
46
|
+
return plugin;
|
|
47
|
+
}
|
|
48
|
+
|
|
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
|
+
list(type) {
|
|
55
|
+
const group = this.#registry[type];
|
|
56
|
+
if (!group) {
|
|
57
|
+
throw new Error(`Unknown plugin type: '${type}'`);
|
|
58
|
+
}
|
|
59
|
+
return [...group.keys()];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/dag/dag-engine.js
|
|
4
|
+
* Description: DAG executor for chained RAG pipelines
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class DAGNode {
|
|
9
|
+
constructor(id, run) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.run = run; // async (input) => output
|
|
12
|
+
this.inputs = [];
|
|
13
|
+
this.outputs = [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class DAG {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.nodes = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addNode(id, fn) {
|
|
23
|
+
const node = new DAGNode(id, fn);
|
|
24
|
+
this.nodes.set(id, node);
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connect(fromId, toId) {
|
|
29
|
+
const from = this.nodes.get(fromId);
|
|
30
|
+
const to = this.nodes.get(toId);
|
|
31
|
+
if (!from || !to) throw new Error(`Invalid edge: ${fromId} → ${toId}`);
|
|
32
|
+
from.outputs.push(to);
|
|
33
|
+
to.inputs.push(from);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Executes DAG from source nodes to sinks using topological order
|
|
38
|
+
* @param {any} seed - Initial input to the graph
|
|
39
|
+
* @returns {Promise<any>} - Final output from sink(s)
|
|
40
|
+
*/
|
|
41
|
+
async execute(seed) {
|
|
42
|
+
const visited = new Set();
|
|
43
|
+
const order = this.topoSort();
|
|
44
|
+
const results = new Map();
|
|
45
|
+
|
|
46
|
+
for (const node of order) {
|
|
47
|
+
const inputValues = node.inputs.map(n => results.get(n.id));
|
|
48
|
+
const input = inputValues.length === 1 ? inputValues[0] : inputValues;
|
|
49
|
+
const output = await node.run(input ?? seed);
|
|
50
|
+
results.set(node.id, output);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const last = order.at(-1);
|
|
54
|
+
return results.get(last.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
topoSort() {
|
|
58
|
+
const order = [];
|
|
59
|
+
const visited = new Set();
|
|
60
|
+
const visit = (node) => {
|
|
61
|
+
if (visited.has(node.id)) return;
|
|
62
|
+
visited.add(node.id);
|
|
63
|
+
node.inputs.forEach(visit);
|
|
64
|
+
order.push(node);
|
|
65
|
+
};
|
|
66
|
+
for (const node of this.nodes.values()) visit(node);
|
|
67
|
+
return order;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.1
|
|
3
|
+
* Path: /src/evaluate/evaluator.js
|
|
4
|
+
* Description: Evaluation runner with scoring metrics for batch RAG QA
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRagPipeline } from '../core/create-pipeline.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { scoreAnswer } from './scoring.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate a list of prompt/answer pairs using the RAG pipeline and compute scores
|
|
16
|
+
* @param {string} datasetPath - Path to JSON file with { prompt, expected }[]
|
|
17
|
+
* @param {object} config - RAG plugin config (loader optional)
|
|
18
|
+
* @returns {Promise<Array<{ prompt: string, expected: string, actual: string, success: boolean, scores: object }>>}
|
|
19
|
+
*/
|
|
20
|
+
export async function evaluateRagDataset(datasetPath, config) {
|
|
21
|
+
const file = await fs.readFile(path.resolve(datasetPath), 'utf-8');
|
|
22
|
+
const cases = JSON.parse(file);
|
|
23
|
+
const pipeline = createRagPipeline(config);
|
|
24
|
+
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const { prompt, expected } of cases) {
|
|
27
|
+
try {
|
|
28
|
+
logger.info({ prompt }, 'Evaluating case');
|
|
29
|
+
const actual = await pipeline.query(prompt);
|
|
30
|
+
const success = normalizeText(actual) === normalizeText(expected);
|
|
31
|
+
const scores = scoreAnswer(actual, expected);
|
|
32
|
+
results.push({ prompt, expected, actual, success, scores });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.error({ prompt, error: err.message }, 'Evaluation failed');
|
|
35
|
+
results.push({ prompt, expected, actual: '', success: false, scores: { bleu: 0, rouge: 0 } });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeText(txt) {
|
|
42
|
+
return txt.trim().toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ');
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/evaluate/scoring.js
|
|
4
|
+
* Description: Basic scoring metrics (BLEU, ROUGE-L) for RAG output evaluation
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tokenize a string by words
|
|
10
|
+
* @param {string} text
|
|
11
|
+
* @returns {string[]}
|
|
12
|
+
*/
|
|
13
|
+
function tokenize(text) {
|
|
14
|
+
return text.toLowerCase().replace(/[^a-z0-9 ]/g, '').split(/\s+/).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute BLEU-1 score: unigram precision
|
|
19
|
+
* @param {string[]} candidate - Generated tokens
|
|
20
|
+
* @param {string[]} reference - Reference tokens
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
export function computeBLEU(candidate, reference) {
|
|
24
|
+
const candCounts = candidate.reduce((acc, t) => (acc[t] = (acc[t] || 0) + 1, acc), {});
|
|
25
|
+
const refCounts = reference.reduce((acc, t) => (acc[t] = (acc[t] || 0) + 1, acc), {});
|
|
26
|
+
|
|
27
|
+
let match = 0;
|
|
28
|
+
for (const token of Object.keys(candCounts)) {
|
|
29
|
+
match += Math.min(candCounts[token], refCounts[token] || 0);
|
|
30
|
+
}
|
|
31
|
+
return candidate.length ? match / candidate.length : 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute ROUGE-L (Longest Common Subsequence) recall
|
|
36
|
+
* @param {string[]} candidate
|
|
37
|
+
* @param {string[]} reference
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
export function computeROUGE(candidate, reference) {
|
|
41
|
+
const m = candidate.length;
|
|
42
|
+
const n = reference.length;
|
|
43
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i <= m; i++) {
|
|
46
|
+
for (let j = 1; j <= n; j++) {
|
|
47
|
+
if (candidate[i - 1] === reference[j - 1]) {
|
|
48
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
49
|
+
} else {
|
|
50
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lcs = dp[m][n];
|
|
56
|
+
return reference.length ? lcs / reference.length : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Score a prompt-answer pair using multiple metrics
|
|
61
|
+
* @param {string} actual - LLM response
|
|
62
|
+
* @param {string} expected - Reference answer
|
|
63
|
+
* @returns {{ bleu: number, rouge: number }}
|
|
64
|
+
*/
|
|
65
|
+
export function scoreAnswer(actual, expected) {
|
|
66
|
+
const candTokens = tokenize(actual);
|
|
67
|
+
const refTokens = tokenize(expected);
|
|
68
|
+
return {
|
|
69
|
+
bleu: computeBLEU(candTokens, refTokens),
|
|
70
|
+
rouge: computeROUGE(candTokens, refTokens)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/loader/csv-loader.js
|
|
4
|
+
* Description: CSV document loader with row-wise chunking
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { parse } from 'csv-parse/sync';
|
|
11
|
+
|
|
12
|
+
export class CSVLoader {
|
|
13
|
+
/**
|
|
14
|
+
* Load and parse a CSV file into chunks
|
|
15
|
+
* @param {string} filePath
|
|
16
|
+
* @returns {Promise<Array<{ chunk(): string[] }>>}
|
|
17
|
+
*/
|
|
18
|
+
async load(filePath) {
|
|
19
|
+
const absPath = path.resolve(filePath);
|
|
20
|
+
const raw = await fs.readFile(absPath, 'utf-8');
|
|
21
|
+
const records = parse(raw, { columns: true });
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
chunk: () => records.map(r => Object.values(r).join(' '))
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* Path: /src/loader/directory-loader.js
|
|
4
|
+
* Description: Recursive directory loader with file type routing
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fg from 'fast-glob';
|
|
9
|
+
import { MarkdownLoader } from './markdown-loader.js';
|
|
10
|
+
import { HTMLLoader } from './html-loader.js';
|
|
11
|
+
import { CSVLoader } from './csv-loader.js';
|
|
12
|
+
|
|
13
|
+
const loaders = {
|
|
14
|
+
'.md': new MarkdownLoader(),
|
|
15
|
+
'.html': new HTMLLoader(),
|
|
16
|
+
'.csv': new CSVLoader()
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class DirectoryLoader {
|
|
20
|
+
/**
|
|
21
|
+
* Load and route supported files from a directory
|
|
22
|
+
* @param {string} dirPath
|
|
23
|
+
* @returns {Promise<Array<{ chunk(): string[] }>>}
|
|
24
|
+
*/
|
|
25
|
+
async load(dirPath) {
|
|
26
|
+
const matches = await fg([`${dirPath}/**/*.{md,html,csv}`]);
|
|
27
|
+
const all = [];
|
|
28
|
+
for (const file of matches) {
|
|
29
|
+
const ext = file.slice(file.lastIndexOf('.'));
|
|
30
|
+
const loader = loaders[ext];
|
|
31
|
+
if (loader) {
|
|
32
|
+
const docs = await loader.load(file);
|
|
33
|
+
all.push(...docs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return all;
|
|
37
|
+
}
|
|
38
|
+
}
|