@retrivora-ai/rag-engine 0.1.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/.env.example +77 -0
- package/README.md +149 -0
- package/dist/DocumentChunker-BUrIrcPk.d.mts +43 -0
- package/dist/DocumentChunker-BUrIrcPk.d.ts +43 -0
- package/dist/RAGPipeline-BmkIv1HD.d.mts +298 -0
- package/dist/RAGPipeline-BmkIv1HD.d.ts +298 -0
- package/dist/chunk-NCG2JKXB.mjs +1254 -0
- package/dist/chunk-ZPXLQR5Q.mjs +67 -0
- package/dist/handlers/index.d.mts +68 -0
- package/dist/handlers/index.d.ts +68 -0
- package/dist/handlers/index.js +1319 -0
- package/dist/handlers/index.mjs +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +612 -0
- package/dist/index.mjs +551 -0
- package/dist/server.d.mts +211 -0
- package/dist/server.d.ts +211 -0
- package/dist/server.js +1457 -0
- package/dist/server.mjs +148 -0
- package/package.json +90 -0
- package/src/app/api/chat/route.ts +4 -0
- package/src/app/api/health/route.ts +4 -0
- package/src/app/api/ingest/route.ts +26 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +163 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +506 -0
- package/src/components/ChatWidget.tsx +91 -0
- package/src/components/ChatWindow.tsx +248 -0
- package/src/components/ConfigProvider.tsx +56 -0
- package/src/components/MessageBubble.tsx +99 -0
- package/src/components/SourceCard.tsx +66 -0
- package/src/components/ThemeProvider.tsx +8 -0
- package/src/components/ThemeToggle.tsx +29 -0
- package/src/config/RagConfig.ts +159 -0
- package/src/config/UniversalProfiles.ts +83 -0
- package/src/config/serverConfig.ts +142 -0
- package/src/handlers/index.ts +202 -0
- package/src/hooks/useHydrated.ts +13 -0
- package/src/hooks/useRagChat.ts +167 -0
- package/src/hooks/useStoredMessages.ts +53 -0
- package/src/index.ts +27 -0
- package/src/llm/ILLMProvider.ts +60 -0
- package/src/llm/LLMFactory.ts +54 -0
- package/src/llm/providers/AnthropicProvider.ts +87 -0
- package/src/llm/providers/OllamaProvider.ts +102 -0
- package/src/llm/providers/OpenAIProvider.ts +92 -0
- package/src/llm/providers/UniversalLLMAdapter.ts +154 -0
- package/src/rag/DocumentChunker.ts +105 -0
- package/src/rag/RAGPipeline.ts +196 -0
- package/src/server.ts +30 -0
- package/src/types/pdf-parse.d.ts +13 -0
- package/src/utils/DocumentParser.ts +75 -0
- package/src/utils/templateUtils.ts +78 -0
- package/src/vectordb/IVectorDB.ts +75 -0
- package/src/vectordb/VectorDBFactory.ts +41 -0
- package/src/vectordb/adapters/MongoDbAdapter.ts +175 -0
- package/src/vectordb/adapters/PgVectorAdapter.ts +159 -0
- package/src/vectordb/adapters/PineconeAdapter.ts +115 -0
- package/src/vectordb/adapters/UniversalVectorDBAdapter.ts +177 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-configured templates for popular AI and Vector services.
|
|
3
|
+
* Use these as defaults in the Universal Adapters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const OPENAI_BASE = {
|
|
7
|
+
chatPath: '/chat/completions',
|
|
8
|
+
embedPath: '/embeddings',
|
|
9
|
+
responseExtractPath: 'choices[0].message.content',
|
|
10
|
+
embedExtractPath: 'data[0].embedding',
|
|
11
|
+
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}},"temperature":{{temperature}}}',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const LLM_PROFILES = {
|
|
15
|
+
'openai-compatible': OPENAI_BASE,
|
|
16
|
+
'litellm': {
|
|
17
|
+
...OPENAI_BASE,
|
|
18
|
+
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}},"temperature":{{temperature}},"stream":false}'
|
|
19
|
+
},
|
|
20
|
+
'anthropic-claude': {
|
|
21
|
+
chatPath: '/v1/messages',
|
|
22
|
+
responseExtractPath: 'content[0].text',
|
|
23
|
+
headers: { 'anthropic-version': '2023-06-01' },
|
|
24
|
+
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}}}',
|
|
25
|
+
},
|
|
26
|
+
'google-gemini': {
|
|
27
|
+
chatPath: '/v1beta/models/{{model}}:generateContent',
|
|
28
|
+
responseExtractPath: 'candidates[0].content.parts[0].text',
|
|
29
|
+
chatPayloadTemplate: '{"contents":[{"parts":[{"text":"{{messages}}"}]}]}',
|
|
30
|
+
},
|
|
31
|
+
'github-copilot': {
|
|
32
|
+
...OPENAI_BASE,
|
|
33
|
+
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"temperature":{{temperature}}}',
|
|
34
|
+
},
|
|
35
|
+
'ollama-standard': {
|
|
36
|
+
chatPath: '/api/chat',
|
|
37
|
+
embedPath: '/api/embeddings',
|
|
38
|
+
responseExtractPath: 'message.content',
|
|
39
|
+
embedExtractPath: 'embedding',
|
|
40
|
+
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"stream":false}',
|
|
41
|
+
embedPayloadTemplate: '{"model":"{{model}}","prompt":"{{input}}"}',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const VECTOR_PROFILES = {
|
|
46
|
+
'pinecone-rest': {
|
|
47
|
+
queryPath: '/query',
|
|
48
|
+
upsertPath: '/vectors/upsert',
|
|
49
|
+
responseExtractPath: 'matches',
|
|
50
|
+
idPath: 'id',
|
|
51
|
+
scorePath: 'score',
|
|
52
|
+
contentPath: 'metadata.content',
|
|
53
|
+
metadataPath: 'metadata',
|
|
54
|
+
queryPayloadTemplate: '{"vector":{{vector}},"topK":{{topK}},"includeMetadata":true,"namespace":"{{namespace}}"}',
|
|
55
|
+
},
|
|
56
|
+
'mongodb-atlas': {
|
|
57
|
+
queryPath: '/action/aggregate',
|
|
58
|
+
responseExtractPath: 'documents',
|
|
59
|
+
idPath: '_id',
|
|
60
|
+
scorePath: 'score',
|
|
61
|
+
contentPath: 'content',
|
|
62
|
+
metadataPath: 'metadata',
|
|
63
|
+
queryPayloadTemplate: '{"collection":"{{index}}","database":"ai_db","dataSource":"Cluster0","pipeline":[{"$vectorSearch":{"index":"vector_index","path":"embedding","queryVector":{{vector}},"numCandidates":100,"limit":{{topK}}}},{"$project":{"_id":1,"content":1,"metadata":1,"score":{"$meta":"vectorSearchScore"}}}]}',
|
|
64
|
+
},
|
|
65
|
+
'chromadb': {
|
|
66
|
+
queryPath: '/api/v1/collections/{{index}}/query',
|
|
67
|
+
upsertPath: '/api/v1/collections/{{index}}/add',
|
|
68
|
+
responseExtractPath: 'matches',
|
|
69
|
+
idPath: 'id',
|
|
70
|
+
scorePath: 'distance',
|
|
71
|
+
contentPath: 'document',
|
|
72
|
+
metadataPath: 'metadata',
|
|
73
|
+
},
|
|
74
|
+
'qdrant': {
|
|
75
|
+
queryPath: '/collections/{{index}}/points/search',
|
|
76
|
+
upsertPath: '/collections/{{index}}/points',
|
|
77
|
+
responseExtractPath: 'result',
|
|
78
|
+
idPath: 'id',
|
|
79
|
+
scorePath: 'score',
|
|
80
|
+
contentPath: 'payload.content',
|
|
81
|
+
metadataPath: 'payload',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serverConfig.ts — reads RagConfig from environment variables at runtime.
|
|
3
|
+
*
|
|
4
|
+
* This keeps secrets server-side and never exposes them to the browser.
|
|
5
|
+
* Consumer projects set these env vars in their .env.local file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RagConfig } from './RagConfig';
|
|
9
|
+
|
|
10
|
+
const VECTOR_DB_PROVIDERS = ['pinecone', 'pgvector', 'mongodb', 'rest', 'universal_rest', 'custom'] as const;
|
|
11
|
+
const LLM_PROVIDERS = ['openai', 'anthropic', 'ollama', 'rest', 'universal_rest', 'custom'] as const;
|
|
12
|
+
const EMBEDDING_PROVIDERS = ['openai', 'ollama', 'rest', 'universal_rest', 'custom'] as const;
|
|
13
|
+
|
|
14
|
+
function readString(env: Record<string, string | undefined>, name: string): string | undefined {
|
|
15
|
+
const value = env[name]?.trim();
|
|
16
|
+
return value ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readNumber(env: Record<string, string | undefined>, name: string, fallback: number): number {
|
|
20
|
+
const value = env[name];
|
|
21
|
+
if (!value) return fallback;
|
|
22
|
+
|
|
23
|
+
const parsed = Number(value);
|
|
24
|
+
if (!Number.isFinite(parsed)) {
|
|
25
|
+
throw new Error(`[getRagConfig] ${name} must be a valid number`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readEnum<T extends readonly string[]>(
|
|
32
|
+
env: Record<string, string | undefined>,
|
|
33
|
+
name: string,
|
|
34
|
+
fallback: T[number],
|
|
35
|
+
allowed: T
|
|
36
|
+
): T[number] {
|
|
37
|
+
const value = readString(env, name) ?? fallback;
|
|
38
|
+
if ((allowed as readonly string[]).includes(value)) {
|
|
39
|
+
return value as T[number];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getRagConfig(env: Record<string, string | undefined> = process.env): RagConfig {
|
|
46
|
+
const projectId = readString(env, 'RAG_PROJECT_ID') ?? readString(env, 'NEXT_PUBLIC_PROJECT_ID') ?? 'default';
|
|
47
|
+
const vectorProvider = readEnum(env, 'VECTOR_DB_PROVIDER', 'pinecone', VECTOR_DB_PROVIDERS);
|
|
48
|
+
const llmProvider = readEnum(env, 'LLM_PROVIDER', 'openai', LLM_PROVIDERS);
|
|
49
|
+
const embeddingProvider = readEnum(env, 'EMBEDDING_PROVIDER', 'openai', EMBEDDING_PROVIDERS);
|
|
50
|
+
const embeddingDimensions = readNumber(env, 'EMBEDDING_DIMENSIONS', 1536);
|
|
51
|
+
|
|
52
|
+
// Build provider-specific vectorDb options
|
|
53
|
+
const vectorDbOptions: Record<string, unknown> = {};
|
|
54
|
+
if (vectorProvider === 'pinecone') {
|
|
55
|
+
vectorDbOptions.apiKey = readString(env, 'PINECONE_API_KEY') ?? '';
|
|
56
|
+
vectorDbOptions.environment = readString(env, 'PINECONE_ENVIRONMENT') ?? '';
|
|
57
|
+
} else if (vectorProvider === 'pgvector') {
|
|
58
|
+
vectorDbOptions.connectionString = readString(env, 'PGVECTOR_CONNECTION_STRING') ?? '';
|
|
59
|
+
vectorDbOptions.dimensions = embeddingDimensions;
|
|
60
|
+
} else if (vectorProvider === 'rest') {
|
|
61
|
+
vectorDbOptions.baseUrl = readString(env, 'VECTOR_DB_REST_URL') ?? '';
|
|
62
|
+
vectorDbOptions.headers = readString(env, 'VECTOR_DB_REST_API_KEY')
|
|
63
|
+
? { 'api-key': readString(env, 'VECTOR_DB_REST_API_KEY') }
|
|
64
|
+
: {};
|
|
65
|
+
} else if (vectorProvider === 'universal_rest') {
|
|
66
|
+
vectorDbOptions.baseUrl = readString(env, 'VECTOR_BASE_URL') ?? readString(env, 'VECTOR_DB_REST_URL') ?? '';
|
|
67
|
+
vectorDbOptions.profile = readString(env, 'VECTOR_UNIVERSAL_PROFILE');
|
|
68
|
+
vectorDbOptions.headers = readString(env, 'VECTOR_DB_REST_API_KEY')
|
|
69
|
+
? { Authorization: `Bearer ${readString(env, 'VECTOR_DB_REST_API_KEY')}` }
|
|
70
|
+
: {};
|
|
71
|
+
} else if (vectorProvider === 'mongodb') {
|
|
72
|
+
vectorDbOptions.uri = readString(env, 'MONGODB_URI') ?? '';
|
|
73
|
+
vectorDbOptions.database = readString(env, 'MONGODB_DB') ?? '';
|
|
74
|
+
vectorDbOptions.collection = readString(env, 'MONGODB_COLLECTION') ?? '';
|
|
75
|
+
vectorDbOptions.indexName = readString(env, 'MONGODB_INDEX_NAME') ?? 'vector_index';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const llmApiKeyByProvider: Record<string, string | undefined> = {
|
|
79
|
+
openai: readString(env, 'OPENAI_API_KEY'),
|
|
80
|
+
anthropic: readString(env, 'ANTHROPIC_API_KEY'),
|
|
81
|
+
ollama: undefined,
|
|
82
|
+
universal_rest: readString(env, 'LLM_API_KEY'),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const embeddingApiKeyByProvider: Record<string, string | undefined> = {
|
|
86
|
+
openai: readString(env, 'OPENAI_API_KEY'),
|
|
87
|
+
ollama: undefined,
|
|
88
|
+
custom: readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'OPENAI_API_KEY'),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
projectId,
|
|
93
|
+
vectorDb: {
|
|
94
|
+
provider: vectorProvider,
|
|
95
|
+
indexName: readString(env, 'VECTOR_DB_INDEX') ?? 'rag-index',
|
|
96
|
+
options: vectorDbOptions,
|
|
97
|
+
},
|
|
98
|
+
llm: {
|
|
99
|
+
provider: llmProvider,
|
|
100
|
+
model: readString(env, 'LLM_MODEL') ?? 'gpt-4o',
|
|
101
|
+
apiKey: llmApiKeyByProvider[llmProvider] ?? '',
|
|
102
|
+
baseUrl: readString(env, 'LLM_BASE_URL'),
|
|
103
|
+
systemPrompt: readString(env, 'LLM_SYSTEM_PROMPT'),
|
|
104
|
+
maxTokens: readNumber(env, 'LLM_MAX_TOKENS', 1024),
|
|
105
|
+
temperature: readNumber(env, 'LLM_TEMPERATURE', 0.7),
|
|
106
|
+
options: {
|
|
107
|
+
profile: readString(env, 'LLM_UNIVERSAL_PROFILE'),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
embedding: {
|
|
111
|
+
provider: embeddingProvider,
|
|
112
|
+
model: readString(env, 'EMBEDDING_MODEL') ?? 'text-embedding-3-small',
|
|
113
|
+
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
114
|
+
baseUrl: readString(env, 'EMBEDDING_BASE_URL'),
|
|
115
|
+
dimensions: embeddingDimensions,
|
|
116
|
+
options: {
|
|
117
|
+
profile: readString(env, 'EMBEDDING_UNIVERSAL_PROFILE'),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
ui: {
|
|
121
|
+
title: readString(env, 'NEXT_PUBLIC_UI_TITLE') ?? readString(env, 'UI_TITLE') ?? 'AI Assistant',
|
|
122
|
+
subtitle: readString(env, 'NEXT_PUBLIC_UI_SUBTITLE') ?? readString(env, 'UI_SUBTITLE') ?? 'Powered by RAG',
|
|
123
|
+
primaryColor: readString(env, 'NEXT_PUBLIC_PRIMARY_COLOR') ?? readString(env, 'UI_PRIMARY_COLOR') ?? '#10b981',
|
|
124
|
+
accentColor: readString(env, 'NEXT_PUBLIC_ACCENT_COLOR') ?? readString(env, 'UI_ACCENT_COLOR') ?? '#3b82f6',
|
|
125
|
+
logoUrl: readString(env, 'NEXT_PUBLIC_LOGO_URL') ?? readString(env, 'UI_LOGO_URL'),
|
|
126
|
+
placeholder: readString(env, 'NEXT_PUBLIC_PLACEHOLDER') ?? readString(env, 'UI_PLACEHOLDER') ?? 'Ask me anything…',
|
|
127
|
+
showSources: (readString(env, 'NEXT_PUBLIC_SHOW_SOURCES') ?? readString(env, 'UI_SHOW_SOURCES') ?? 'true') !== 'false',
|
|
128
|
+
welcomeMessage:
|
|
129
|
+
readString(env, 'NEXT_PUBLIC_WELCOME_MESSAGE') ??
|
|
130
|
+
readString(env, 'UI_WELCOME_MESSAGE') ??
|
|
131
|
+
'Hello! I\'m your AI assistant. Ask me anything about your documents.',
|
|
132
|
+
visualStyle: (readString(env, 'NEXT_PUBLIC_UI_VISUAL_STYLE') ?? readString(env, 'UI_VISUAL_STYLE') ?? 'glass') as 'glass' | 'solid',
|
|
133
|
+
borderRadius: (readString(env, 'NEXT_PUBLIC_UI_BORDER_RADIUS') ?? readString(env, 'UI_BORDER_RADIUS') ?? 'xl') as 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full',
|
|
134
|
+
},
|
|
135
|
+
rag: {
|
|
136
|
+
topK: readNumber(env, 'RAG_TOP_K', 5),
|
|
137
|
+
scoreThreshold: readNumber(env, 'RAG_SCORE_THRESHOLD', 0),
|
|
138
|
+
chunkSize: readNumber(env, 'RAG_CHUNK_SIZE', 1000),
|
|
139
|
+
chunkOverlap: readNumber(env, 'RAG_CHUNK_OVERLAP', 200),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createChatHandler — factory that returns a Next.js App Router POST handler
|
|
3
|
+
* for the RAG chat endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Use this when you want to mount the RAG chat API inside your OWN Next.js app
|
|
6
|
+
* (i.e. you installed @rag/ai-accelerator as a package and want the API route
|
|
7
|
+
* to live at your project's /api/chat).
|
|
8
|
+
*
|
|
9
|
+
* Usage — in your project's `src/app/api/chat/route.ts`:
|
|
10
|
+
*
|
|
11
|
+
* import { createChatHandler } from '@rag/ai-accelerator/handlers';
|
|
12
|
+
* import myConfig from '@/config/ragConfig';
|
|
13
|
+
* export const POST = createChatHandler(myConfig);
|
|
14
|
+
*
|
|
15
|
+
* Or with env-based config:
|
|
16
|
+
*
|
|
17
|
+
* import { createChatHandler } from '@rag/ai-accelerator/handlers';
|
|
18
|
+
* import { getRagConfig } from '@rag/ai-accelerator';
|
|
19
|
+
* export const POST = createChatHandler(getRagConfig());
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
23
|
+
import { RAGPipeline } from '../rag/RAGPipeline';
|
|
24
|
+
import { RagConfig } from '../config/RagConfig';
|
|
25
|
+
import { ChatMessage, ILLMProvider } from '../llm/ILLMProvider';
|
|
26
|
+
import { IVectorDB } from '../vectordb/IVectorDB';
|
|
27
|
+
import { DocumentParser } from '../utils/DocumentParser';
|
|
28
|
+
|
|
29
|
+
export interface CustomAdapters {
|
|
30
|
+
vectorDB?: IVectorDB;
|
|
31
|
+
llmProvider?: ILLMProvider;
|
|
32
|
+
embeddingProvider?: ILLMProvider;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createChatHandler(config: RagConfig, adapters?: CustomAdapters) {
|
|
36
|
+
let pipeline: RAGPipeline | null = null;
|
|
37
|
+
|
|
38
|
+
function getPipeline(): RAGPipeline {
|
|
39
|
+
if (!pipeline) pipeline = new RAGPipeline(config, adapters);
|
|
40
|
+
return pipeline;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return async function POST(req: NextRequest) {
|
|
44
|
+
try {
|
|
45
|
+
const body = await req.json();
|
|
46
|
+
const { message, history = [], namespace } = body as {
|
|
47
|
+
message: string;
|
|
48
|
+
history?: ChatMessage[];
|
|
49
|
+
namespace?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!message?.trim()) {
|
|
53
|
+
return NextResponse.json({ error: 'message is required' }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rag = getPipeline();
|
|
57
|
+
const result = await rag.ask(message, history, namespace);
|
|
58
|
+
return NextResponse.json(result);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
61
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* createIngestHandler — factory for the document ingestion endpoint.
|
|
68
|
+
*
|
|
69
|
+
* Usage — in your project's `src/app/api/ingest/route.ts`:
|
|
70
|
+
*
|
|
71
|
+
* import { createIngestHandler } from '@rag/ai-accelerator/handlers';
|
|
72
|
+
* export const POST = createIngestHandler(myConfig, { vectorDB: new MyCustomDB() });
|
|
73
|
+
*/
|
|
74
|
+
export function createIngestHandler(config: RagConfig, adapters?: CustomAdapters) {
|
|
75
|
+
let pipeline: RAGPipeline | null = null;
|
|
76
|
+
|
|
77
|
+
function getPipeline(): RAGPipeline {
|
|
78
|
+
if (!pipeline) pipeline = new RAGPipeline(config, adapters);
|
|
79
|
+
return pipeline;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return async function POST(req: NextRequest) {
|
|
83
|
+
try {
|
|
84
|
+
const body = await req.json();
|
|
85
|
+
const { documents, namespace } = body as {
|
|
86
|
+
documents: Array<{ docId: string; content: string; metadata?: Record<string, unknown> }>;
|
|
87
|
+
namespace?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(documents) || documents.length === 0) {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: 'documents array is required and must not be empty' },
|
|
93
|
+
{ status: 400 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rag = getPipeline();
|
|
98
|
+
const results = await rag.ingest(documents, namespace);
|
|
99
|
+
const totalChunks = results.reduce((sum, r) => sum + r.chunksIngested, 0);
|
|
100
|
+
return NextResponse.json({ results, totalChunks });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
103
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* createHealthHandler — factory for the health-check endpoint.
|
|
110
|
+
*
|
|
111
|
+
* Usage — in your project's `src/app/api/health/route.ts`:
|
|
112
|
+
*
|
|
113
|
+
* import { createHealthHandler } from '@rag/ai-accelerator/handlers';
|
|
114
|
+
* export const GET = createHealthHandler(myConfig, { vectorDB: new MyCustomDB() });
|
|
115
|
+
*/
|
|
116
|
+
export function createHealthHandler(config: RagConfig, adapters?: CustomAdapters) {
|
|
117
|
+
let pipeline: RAGPipeline | null = null;
|
|
118
|
+
|
|
119
|
+
function getPipeline(): RAGPipeline {
|
|
120
|
+
if (!pipeline) pipeline = new RAGPipeline(config, adapters);
|
|
121
|
+
return pipeline;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return async function GET() {
|
|
125
|
+
try {
|
|
126
|
+
const rag = getPipeline();
|
|
127
|
+
const health = await rag.health();
|
|
128
|
+
const status = health.vectorDB && health.llm ? 'ok' : 'degraded';
|
|
129
|
+
return NextResponse.json({
|
|
130
|
+
status,
|
|
131
|
+
...health,
|
|
132
|
+
project: config.projectId,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{ status: 'error', error: message, timestamp: new Date().toISOString() },
|
|
139
|
+
{ status: 500 }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* createUploadHandler — factory for the file upload ingestion endpoint.
|
|
147
|
+
*
|
|
148
|
+
* Usage — in your project's `src/app/api/upload/route.ts`:
|
|
149
|
+
*
|
|
150
|
+
* import { createUploadHandler } from '@rag/ai-accelerator/handlers';
|
|
151
|
+
* export const POST = createUploadHandler(myConfig);
|
|
152
|
+
*/
|
|
153
|
+
export function createUploadHandler(config: RagConfig, adapters?: CustomAdapters) {
|
|
154
|
+
let pipeline: RAGPipeline | null = null;
|
|
155
|
+
|
|
156
|
+
function getPipeline(): RAGPipeline {
|
|
157
|
+
if (!pipeline) pipeline = new RAGPipeline(config, adapters);
|
|
158
|
+
return pipeline;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return async function POST(req: NextRequest) {
|
|
162
|
+
try {
|
|
163
|
+
const formData = await req.formData();
|
|
164
|
+
const files = formData.getAll('files') as File[];
|
|
165
|
+
const namespace = (formData.get('namespace') as string) || undefined;
|
|
166
|
+
|
|
167
|
+
if (!files || files.length === 0) {
|
|
168
|
+
return NextResponse.json({ error: 'No files provided' }, { status: 400 });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const documents = await Promise.all(
|
|
172
|
+
files.map(async (file) => {
|
|
173
|
+
const content = await DocumentParser.parse(file, file.name, file.type);
|
|
174
|
+
return {
|
|
175
|
+
docId: file.name,
|
|
176
|
+
content,
|
|
177
|
+
metadata: {
|
|
178
|
+
fileName: file.name,
|
|
179
|
+
fileSize: file.size,
|
|
180
|
+
fileType: file.type,
|
|
181
|
+
uploadedAt: new Date().toISOString(),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const rag = getPipeline();
|
|
188
|
+
const results = await rag.ingest(documents, namespace);
|
|
189
|
+
const totalChunks = results.reduce((sum, r) => sum + r.chunksIngested, 0);
|
|
190
|
+
|
|
191
|
+
return NextResponse.json({
|
|
192
|
+
message: 'Upload successful',
|
|
193
|
+
filesProcessed: files.length,
|
|
194
|
+
totalChunks,
|
|
195
|
+
results,
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const message = err instanceof Error ? err.message : 'Upload failed';
|
|
199
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useRagChat — headless React hook for the RAG chat pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Use this when you want to build a COMPLETELY CUSTOM UI but still leverage
|
|
7
|
+
* the RAG backend. The hook handles all state: messages, loading, errors,
|
|
8
|
+
* localStorage persistence, and the /api/chat round-trip via Axios.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { messages, send, clear, isLoading, error } = useRagChat('my-project');
|
|
12
|
+
*
|
|
13
|
+
* Then build any UI on top of the returned state.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
17
|
+
import axios from 'axios';
|
|
18
|
+
import { VectorMatch } from '@/vectordb/IVectorDB';
|
|
19
|
+
import { useStoredMessages } from './useStoredMessages';
|
|
20
|
+
|
|
21
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export type MessageRole = 'user' | 'assistant';
|
|
24
|
+
|
|
25
|
+
export interface RagMessage {
|
|
26
|
+
id: string;
|
|
27
|
+
role: MessageRole;
|
|
28
|
+
content: string;
|
|
29
|
+
/** Retrieved source chunks (assistant messages only) */
|
|
30
|
+
sources?: VectorMatch[];
|
|
31
|
+
/** ISO timestamp */
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseRagChatOptions {
|
|
36
|
+
/** Override the chat API endpoint (default: /api/chat) */
|
|
37
|
+
apiUrl?: string;
|
|
38
|
+
/** Override project namespace */
|
|
39
|
+
namespace?: string;
|
|
40
|
+
/** Persist chat history to localStorage (default: true) */
|
|
41
|
+
persist?: boolean;
|
|
42
|
+
/** Called after each successful assistant reply */
|
|
43
|
+
onReply?: (message: RagMessage) => void;
|
|
44
|
+
/** Called on error */
|
|
45
|
+
onError?: (error: string) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface UseRagChatReturn {
|
|
49
|
+
/** All messages in the current conversation */
|
|
50
|
+
messages: RagMessage[];
|
|
51
|
+
/** Whether a response is in flight */
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
/** Current error message, or null */
|
|
54
|
+
error: string | null;
|
|
55
|
+
/** Send a user message */
|
|
56
|
+
send: (text: string) => Promise<void>;
|
|
57
|
+
/** Clear the conversation (and localStorage if persist=true) */
|
|
58
|
+
clear: () => void;
|
|
59
|
+
/** Retry the last failed send */
|
|
60
|
+
retry: () => Promise<void>;
|
|
61
|
+
/** Programmatically set the conversation (e.g. to restore from a DB) */
|
|
62
|
+
setMessages: React.Dispatch<React.SetStateAction<RagMessage[]>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Hook ─────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function useRagChat(
|
|
68
|
+
projectId: string,
|
|
69
|
+
options: UseRagChatOptions = {}
|
|
70
|
+
): UseRagChatReturn {
|
|
71
|
+
const {
|
|
72
|
+
apiUrl = '/api/chat',
|
|
73
|
+
namespace,
|
|
74
|
+
persist = true,
|
|
75
|
+
onReply,
|
|
76
|
+
onError,
|
|
77
|
+
} = options;
|
|
78
|
+
const storageKey = `rag_chat_${projectId}`;
|
|
79
|
+
|
|
80
|
+
const [messages, setMessages] = useStoredMessages<RagMessage>(storageKey, persist);
|
|
81
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
82
|
+
const [error, setError] = useState<string | null>(null);
|
|
83
|
+
const lastInputRef = useRef<string>('');
|
|
84
|
+
const messagesRef = useRef<RagMessage[]>(messages);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
messagesRef.current = messages;
|
|
88
|
+
}, [messages]);
|
|
89
|
+
|
|
90
|
+
// ── Core send function ────────────────────────────────────────────────────
|
|
91
|
+
const sendMessage = useCallback(
|
|
92
|
+
async (text: string, options?: { skipUserMessage?: boolean }) => {
|
|
93
|
+
const trimmed = text.trim();
|
|
94
|
+
if (!trimmed || isLoading) return;
|
|
95
|
+
|
|
96
|
+
lastInputRef.current = trimmed;
|
|
97
|
+
|
|
98
|
+
if (!options?.skipUserMessage) {
|
|
99
|
+
const userMessage: RagMessage = {
|
|
100
|
+
id: `user_${Date.now()}`,
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: trimmed,
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
107
|
+
}
|
|
108
|
+
setError(null);
|
|
109
|
+
setIsLoading(true);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const history = messagesRef.current.map(({ role, content }) => ({ role, content }));
|
|
113
|
+
|
|
114
|
+
const { data } = await axios.post(apiUrl, {
|
|
115
|
+
message: trimmed,
|
|
116
|
+
history,
|
|
117
|
+
namespace: namespace ?? projectId,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const assistantMessage: RagMessage = {
|
|
121
|
+
id: `assistant_${Date.now()}`,
|
|
122
|
+
role: 'assistant',
|
|
123
|
+
content: data.reply,
|
|
124
|
+
sources: data.sources,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
129
|
+
onReply?.(assistantMessage);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const msg = axios.isAxiosError(err)
|
|
132
|
+
? (err.response?.data?.error ?? err.message)
|
|
133
|
+
: 'Something went wrong. Please try again.';
|
|
134
|
+
setError(msg);
|
|
135
|
+
onError?.(msg);
|
|
136
|
+
} finally {
|
|
137
|
+
setIsLoading(false);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const clear = useCallback(() => {
|
|
144
|
+
setMessages([]);
|
|
145
|
+
setError(null);
|
|
146
|
+
if (persist) localStorage.removeItem(storageKey);
|
|
147
|
+
}, [persist, setMessages, storageKey]);
|
|
148
|
+
|
|
149
|
+
const retry = useCallback(async () => {
|
|
150
|
+
if (lastInputRef.current) {
|
|
151
|
+
const latestMessage = messagesRef.current[messagesRef.current.length - 1];
|
|
152
|
+
await sendMessage(lastInputRef.current, {
|
|
153
|
+
skipUserMessage: latestMessage?.role === 'user' && latestMessage.content === lastInputRef.current,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}, [sendMessage]);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
messages,
|
|
160
|
+
isLoading,
|
|
161
|
+
error,
|
|
162
|
+
send: sendMessage,
|
|
163
|
+
clear,
|
|
164
|
+
retry,
|
|
165
|
+
setMessages,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface StoredMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
role: 'user' | 'assistant';
|
|
8
|
+
content: string;
|
|
9
|
+
createdAt?: string;
|
|
10
|
+
sources?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readStoredMessages(storageKey: string): StoredMessage[] {
|
|
14
|
+
if (typeof window === 'undefined') {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const saved = window.localStorage.getItem(storageKey);
|
|
19
|
+
if (!saved) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(saved);
|
|
25
|
+
return Array.isArray(parsed) ? (parsed as StoredMessage[]) : [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useStoredMessages<T extends StoredMessage>(
|
|
32
|
+
storageKey: string,
|
|
33
|
+
persist: boolean
|
|
34
|
+
): [T[], React.Dispatch<React.SetStateAction<T[]>>] {
|
|
35
|
+
const [messages, setMessages] = React.useState<T[]>(() => (
|
|
36
|
+
persist ? (readStoredMessages(storageKey) as T[]) : []
|
|
37
|
+
));
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
if (!persist || typeof window === 'undefined') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (messages.length === 0) {
|
|
45
|
+
window.localStorage.removeItem(storageKey);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
window.localStorage.setItem(storageKey, JSON.stringify(messages));
|
|
50
|
+
}, [messages, persist, storageKey]);
|
|
51
|
+
|
|
52
|
+
return [messages, setMessages];
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package entry point — public API exported to consumers of this npm package.
|
|
3
|
+
* This entry point is CLIENT-SAFE and can be imported in React Client Components.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { ChatWidget, ConfigProvider, useRagChat } from '@rag/ai-accelerator';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── React Components ─────────────────────────────────────────
|
|
10
|
+
export { ChatWidget } from './components/ChatWidget';
|
|
11
|
+
export { ChatWindow } from './components/ChatWindow';
|
|
12
|
+
export { MessageBubble } from './components/MessageBubble';
|
|
13
|
+
export { SourceCard } from './components/SourceCard';
|
|
14
|
+
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
15
|
+
|
|
16
|
+
// ── Headless Hooks ────────────────────────────────────────────
|
|
17
|
+
export { useRagChat } from './hooks/useRagChat';
|
|
18
|
+
|
|
19
|
+
// ── Types (Interfaces/Types only, no Node.js deps) ─────────────
|
|
20
|
+
export type { RagConfig, VectorDBConfig, LLMConfig, EmbeddingConfig, UIConfig, RAGConfig } from './config/RagConfig';
|
|
21
|
+
export type { IVectorDB, VectorMatch, UpsertDocument } from './vectordb/IVectorDB';
|
|
22
|
+
export type { ILLMProvider, ChatMessage, ChatOptions, EmbedOptions } from './llm/ILLMProvider';
|
|
23
|
+
export type { IngestDocument, ChatResponse } from './rag/RAGPipeline';
|
|
24
|
+
export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
|
|
25
|
+
export type { ClientConfig } from './components/ConfigProvider';
|
|
26
|
+
export type { RagMessage, UseRagChatOptions, UseRagChatReturn } from './hooks/useRagChat';
|
|
27
|
+
|