@devilsdev/rag-pipeline-utils 1.0.1 → 2.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/.codecov.yml +21 -0
- package/.github/workflows/ci.yml +18 -0
- package/.husky/pre-commit +5 -0
- package/CHANGELOG.md +33 -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/package.json +17 -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/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
|
@@ -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;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* File: /src/mocks/openai-embedder.js
|
|
4
|
+
* Description: Mock implementation of an OpenAI embedder
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default class OpenAIEmbedder {
|
|
9
|
+
/**
|
|
10
|
+
* Returns static vector embeddings for input chunks
|
|
11
|
+
* @param {string[]} chunks
|
|
12
|
+
* @returns {Promise<Array<{ id: string, values: number[], metadata: { text: string } }>>}
|
|
13
|
+
*/
|
|
14
|
+
async embed(chunks) {
|
|
15
|
+
return chunks.map((text, i) => ({
|
|
16
|
+
id: `v${i}`,
|
|
17
|
+
values: [0.1, 0.2, 0.3],
|
|
18
|
+
metadata: { text },
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns a static vector for the given query prompt
|
|
24
|
+
* @param {string} prompt
|
|
25
|
+
* @returns {Promise<number[]>}
|
|
26
|
+
*/
|
|
27
|
+
async embedQuery(prompt) {
|
|
28
|
+
return [0.1, 0.2, 0.3];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* File: /src/mocks/openai-llm.js
|
|
4
|
+
* Description: Mock implementation of an OpenAI LLM
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default class OpenAILLM {
|
|
9
|
+
/**
|
|
10
|
+
* Generates a static answer from input prompt and context
|
|
11
|
+
* @param {string} prompt
|
|
12
|
+
* @param {Array<{ text: string }>} context
|
|
13
|
+
* @returns {Promise<string>}
|
|
14
|
+
*/
|
|
15
|
+
async generate(prompt, context) {
|
|
16
|
+
return `Generated answer using: ${context.map(d => d.text).join(', ')}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* File: /src/mocks/pdf-loader.js
|
|
4
|
+
* Description: Mock implementation of a PDF document loader
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default class PDFLoader {
|
|
9
|
+
/**
|
|
10
|
+
* Loads a mock PDF file and returns an array of chunked content.
|
|
11
|
+
* @param {string} filePath - Path to the PDF file
|
|
12
|
+
* @returns {Promise<Array<{ chunk: () => string[] }>>}
|
|
13
|
+
*/
|
|
14
|
+
async load(filePath) {
|
|
15
|
+
return [{ chunk: () => [`Test loaded: ${filePath}`] }];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.1.0
|
|
3
|
+
* File: /src/mocks/pinecone-retriever.js
|
|
4
|
+
* Description: Mock implementation of a Pinecone retriever
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default class PineconeRetriever {
|
|
9
|
+
/**
|
|
10
|
+
* Mocks vector storage (no-op).
|
|
11
|
+
*/
|
|
12
|
+
async store() {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns mock retrieved documents
|
|
16
|
+
* @returns {Promise<Array<{ id: string, text: string, metadata: object }>>}
|
|
17
|
+
*/
|
|
18
|
+
async retrieve() {
|
|
19
|
+
return [
|
|
20
|
+
{ id: 'a', text: 'Chunk about pine trees', metadata: {} },
|
|
21
|
+
{ id: 'b', text: 'Chunk about vectors', metadata: {} },
|
|
22
|
+
{ id: 'c', text: 'Chunk about databases', metadata: {} },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,49 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Version: 1.0.4
|
|
3
|
+
* Description: LLM reranker using LLM to reorder documents based on relevance
|
|
4
|
+
* Author: devilsdev
|
|
5
|
+
* File: src/reranker/llm-reranker.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* LLMReranker ranks context chunks by relevance to a given prompt.
|
|
10
|
-
* @interface
|
|
11
|
-
*/
|
|
12
8
|
export class LLMReranker {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let indexOrder = [];
|
|
38
|
-
try {
|
|
39
|
-
indexOrder = JSON.parse(result.match(/\[.*\]/s)?.[0] || '[]');
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.warn('Failed to parse reranker output:', result);
|
|
42
|
-
return documents.slice(0, topK); // fallback
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const ranked = indexOrder.map(i => documents[i]).filter(Boolean);
|
|
46
|
-
return ranked.slice(0, topK);
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} options
|
|
11
|
+
* @param {{ generate(prompt: string): Promise<string> }} options.llm - An LLM client with a `generate` method
|
|
12
|
+
*/
|
|
13
|
+
constructor({ llm }) {
|
|
14
|
+
this.llm = llm;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rerank documents based on the LLM output
|
|
19
|
+
* @param {string} query
|
|
20
|
+
* @param {Array<{ text: string }>} docs
|
|
21
|
+
* @returns {Promise<Array<{ text: string }>>}
|
|
22
|
+
*/
|
|
23
|
+
async rerank(query, docs) {
|
|
24
|
+
const prompt = `Given the query: "${query}", rank the following documents by relevance: \n${docs.map((d, i) => `[${i}]: ${d.text}`).join('\n')}\nReturn an array of indices like [2, 0, 1]`;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const json = await this.llm.generate(prompt);
|
|
28
|
+
const indices = JSON.parse(json);
|
|
29
|
+
return indices.map(i => docs[i]).filter(Boolean);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return docs; // Fallback if LLM response is not parsable
|
|
47
32
|
}
|
|
48
33
|
}
|
|
49
|
-
|
|
34
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Enhanced CI diagnostics for plugin mock verification
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pretty prints CI validation errors and guidance
|
|
12
|
+
* @param {Array<{ file: string, missing: string[] }>} failures
|
|
13
|
+
*/
|
|
14
|
+
export function reportPluginValidationFailures(failures) {
|
|
15
|
+
if (!failures.length) return;
|
|
16
|
+
|
|
17
|
+
console.log(`\n${chalk.redBright.bold('❌ Plugin Mock Validation Failed')}`);
|
|
18
|
+
|
|
19
|
+
failures.forEach(({ file, missing }) => {
|
|
20
|
+
const fileName = path.basename(file);
|
|
21
|
+
console.log(
|
|
22
|
+
`${chalk.red(` ✖ ${fileName}`)} ${chalk.gray(`missing: ${missing.join(', ')}`)}`
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Add method-level fix hints
|
|
26
|
+
missing.forEach(method => {
|
|
27
|
+
const pluginType = fileName.split('.')[0].split('-')[1] || 'unknown';
|
|
28
|
+
console.error(` → Fix: implement method ${chalk.cyan(`${method}()`)} for plugin type ${chalk.yellow(pluginType)}`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(`\n${chalk.yellowBright('Recommended Fixes:')}`);
|
|
33
|
+
console.log(' • Implement all required methods defined in /src/core/plugin-contracts.js');
|
|
34
|
+
console.log(` • Re-run: ${chalk.cyan('npm run ci')} to confirm validation`);
|
|
35
|
+
console.log(` • Optionally use: ${chalk.cyan('scripts/repair-fixtures.js')} to auto-repair mocks\n`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Type-safe plugin contract validator for runtime safety
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* File: /src/utils/validate-plugin-contract.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pluginContracts } from '../core/plugin-contracts.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a plugin instance implements all required methods.
|
|
12
|
+
* @param {string} type - Plugin type (e.g. 'loader', 'embedder')
|
|
13
|
+
* @param {object} instance - Instantiated plugin module
|
|
14
|
+
* @param {string} filePath - Path to plugin (used in error reporting)
|
|
15
|
+
* @throws {Error} if any required method is missing or not a function
|
|
16
|
+
*/
|
|
17
|
+
export function validatePluginContract(type, instance, filePath) {
|
|
18
|
+
const expectedMethods = pluginContracts[type];
|
|
19
|
+
if (!expectedMethods) {
|
|
20
|
+
throw new Error(`[validatePluginContract] Unknown plugin type: ${type}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const missing = expectedMethods.filter(method =>
|
|
24
|
+
typeof instance[method] !== 'function'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[validatePluginContract] Plugin '${filePath}' is missing required methods for '${type}': ${missing.join(', ')}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Optionally wraps plugin instance with a Proxy for method usage auditing (see enforcePluginProxy)
|
|
36
|
+
*/
|