From ec5d775da5ed43189c411057ba960fb018787c18 Mon Sep 17 00:00:00 2001 From: Saurav Panda Date: Sat, 4 Jan 2025 17:10:12 -0800 Subject: [PATCH] fix: updated the ai search version --- docs/src/app/aiSearch/page.tsx | 316 +++++++++--------- docs/src/components/aiSearch/AIResponse.tsx | 44 ++- docs/src/lib/aisearch/embeddings.worker.ts | 21 +- packages/akiradocs/src/app/aiSearch/page.tsx | 316 +++++++++--------- .../src/components/aiSearch/AIResponse.tsx | 44 ++- .../src/lib/aisearch/embeddings.worker.ts | 21 +- packages/create-app/package.json | 2 +- 7 files changed, 439 insertions(+), 325 deletions(-) diff --git a/docs/src/app/aiSearch/page.tsx b/docs/src/app/aiSearch/page.tsx index 4261df2..a30a34e 100644 --- a/docs/src/app/aiSearch/page.tsx +++ b/docs/src/app/aiSearch/page.tsx @@ -21,23 +21,23 @@ import { generateEmbedding } from '@/lib/aisearch/embeddings' import { getDbWorker } from '@/lib/aisearch/dbWorker' function cosineSimilarity(a: number[], b: number[]): number { - if (a.some(isNaN) || b.some(isNaN)) { - console.error("NaN values detected in vectors:"); - console.error("Vector A NaN indices:", a.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); - console.error("Vector B NaN indices:", b.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); - throw new Error("Invalid vectors containing NaN values"); - } - - const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); - const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); - const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); - return dotProduct / (magnitudeA * magnitudeB); + if (a.some(isNaN) || b.some(isNaN)) { + console.error("NaN values detected in vectors:"); + console.error("Vector A NaN indices:", a.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); + console.error("Vector B NaN indices:", b.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); + throw new Error("Invalid vectors containing NaN values"); + } + + const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); + const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); + const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); + return dotProduct / (magnitudeA * magnitudeB); } export default function Home() { const [query, setQuery] = useState('') const [aiResponse, setAiResponse] = useState('') - const [loaderText, setLoaderText] = useState('Loading database ...') + const [loaderText, setLoaderText] = useState('Loading database ...') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const recommendedArticles = getRecommendedArticles() @@ -45,20 +45,20 @@ export default function Home() { const headerConfig = getHeaderConfig() const config = getAkiradocsConfig() const [sources, setSources] = useState([]) - const handleGenerateEmbedding = useCallback(async (text: string) => { - try { - setIsLoading(true); - // console.log("Loading model for embedding"); - const embedding = await generateEmbedding(text, (progress) => {}); - return embedding; - } catch (error) { - console.error('Error generating embedding:', error); - throw error; - } - finally { - setLoaderText('Searching database for relevant information ...') - } - }, []); + const handleGenerateEmbedding = useCallback(async (text: string) => { + try { + setIsLoading(true); + // console.log("Loading model for embedding"); + const embedding = await generateEmbedding(text, (progress) => { }); + return embedding; + } catch (error) { + console.error('Error generating embedding:', error); + throw error; + } + finally { + setLoaderText('Searching database for relevant information ...') + } + }, []); // If AI Search is disabled, show the disabled message if (!config.navigation.header.items.find((item: any) => item.href === '/aiSearch')?.show) { @@ -104,96 +104,98 @@ export default function Home() { setIsLoading(true) setError(null) setSources([]) - - const startTime = performance.now() - - try { - // Generate embedding for the query - const queryEmbedding = await handleGenerateEmbedding(query); - // console.log("Query embedding:", queryEmbedding) - // Get database worker - const worker = await getDbWorker(); - - // Get all documents - const allDocs = await worker.db.query(` + + const startTime = performance.now() + + try { + // Generate embedding for the query + const queryEmbedding = await handleGenerateEmbedding(query); + // console.log("Query embedding:", queryEmbedding) + // Get database worker + const worker = await getDbWorker(); + + // Get all documents + const allDocs = await worker.db.query(` SELECT path, content, embedding FROM documents WHERE embedding IS NOT NULL `); - // Calculate similarity scores and filter results - const similarityThreshold = 0.5; - const scoredDocs = allDocs - .map((doc: any) => { - // Clean the embedding string and parse it - const cleanEmbeddingStr = doc.embedding.replace(/[\[\]]/g, ''); // Remove square brackets - const embeddingArray = cleanEmbeddingStr - .split(',') - .map((val: string) => { - const parsed = parseFloat(val.trim()); - if (isNaN(parsed)) { - console.error(`Invalid embedding value found: "${val}"`); - } - return parsed; - }); - - return { - ...doc, - similarity_score: cosineSimilarity(queryEmbedding, embeddingArray) - }; - }) - .filter((doc: any) => doc.similarity_score > similarityThreshold) - .sort((a: any, b: any) => b.similarity_score - a.similarity_score) - .slice(0, 5); - - console.log("RAG top 5 results:", scoredDocs); - - // If no relevant documents found, return early - if (scoredDocs.length === 0) { - setAiResponse("I cannot answer this question from the given documentation. The available content doesn't seem relevant enough to provide a accurate answer."); - setIsLoading(false); - return; - } - - setLoaderText('Loading the AI model ...') - - // Combine relevant documents into context - const docsContext = scoredDocs - .map((doc: any) => ` + // Calculate similarity scores and filter results + const similarityThreshold = 0.5; + const scoredDocs = allDocs + .map((doc: any) => { + // Clean the embedding string and parse it + const cleanEmbeddingStr = doc.embedding.replace(/[\[\]]/g, ''); // Remove square brackets + const embeddingArray = cleanEmbeddingStr + .split(',') + .map((val: string) => { + const parsed = parseFloat(val.trim()); + if (isNaN(parsed)) { + console.error(`Invalid embedding value found: "${val}"`); + } + return parsed; + }); + + return { + ...doc, + similarity_score: cosineSimilarity(queryEmbedding, embeddingArray) + }; + }) + .filter((doc: any) => doc.similarity_score > similarityThreshold) + .sort((a: any, b: any) => b.similarity_score - a.similarity_score) + .slice(0, 5); + + console.log("RAG top 5 results:", scoredDocs); + + // If no relevant documents found, return early + if (scoredDocs.length === 0) { + setAiResponse("I cannot answer this question from the given documentation. The available content doesn't seem relevant enough to provide a accurate answer."); + setIsLoading(false); + return; + } + + setLoaderText('Loading the AI model ...') + + // Combine relevant documents into context + const docsContext = scoredDocs + .map((doc: any) => ` Source: ${doc.path} --- Content --- ${doc.content} --- End of Content --- `) - .join('\n'); + .join('\n'); const engine = await CreateMLCEngine( "Llama-3.2-1B-Instruct-q4f16_1-MLC", - { initProgressCallback: (progress: any) => { - console.log(progress) - setLoaderText(`Loading the AI model ${Math.round(progress.progress * 100)}% ...`) - } }, { - context_window_size: 20000, + initProgressCallback: (progress: any) => { + console.log(progress) + setLoaderText(`Loading the AI model ${Math.round(progress.progress * 100)}% ...`) + } + }, + { + context_window_size: 20000, } ); - const engineLoadTime = performance.now() // Track engine load time - console.log(`Time taken for engine initialization: ${(engineLoadTime - startTime) / 1000}s`) - setLoaderText('Processing information and generating AI response ...') + const engineLoadTime = performance.now() // Track engine load time + console.log(`Time taken for engine initialization: ${(engineLoadTime - startTime) / 1000}s`) + setLoaderText('Processing information and generating AI response ...') const messages = [ - { - role: "system", - content: `You are a technical documentation assistant for AkiraDocs. Your purpose is to: + { + role: "system", + content: `You are a technical documentation assistant for AkiraDocs. Your purpose is to: 1. Provide accurate, helpful answers using ONLY the provided documentation 2. Stay positive and factual based on the documentation provided. 3. Make sure the markdown answer is pretty, clean and easy to read.` }, - { - role: "user", - content: ` + { + role: "user", + content: ` Please provide a helpful answer which is short and concise to the following question using only the provided documentation. Question: ${query} @@ -221,41 +223,43 @@ export default function Home() { } ]; - // console.log("Messages:", messages) + // console.log("Messages:", messages) - const chunks = await engine.chat.completions.create({ + const chunks = await engine.chat.completions.create({ messages: messages as ChatCompletionMessageParam[], stream: true, - stream_options: { include_usage: true }, - max_tokens: 500, - temperature: 0.7, - top_p: 0.95, - frequency_penalty: 0.5, - presence_penalty: 0.5, + stream_options: { include_usage: false }, + max_tokens: 300, + temperature: 0.5, + top_p: 0.8, + frequency_penalty: 0.3, + presence_penalty: 0.3, }); let aiContent = ""; for await (const chunk of chunks) { const newContent = chunk.choices[0]?.delta.content || ""; aiContent += newContent; - - // Process partial content for streaming - const { cleanResponse } = extractSources(aiContent); - setAiResponse(cleanResponse); - } - - // Only extract and set sources after streaming is complete - const { sources } = extractSources(aiContent); + + // Process partial content for streaming + const { cleanResponse } = extractSources(aiContent); + setAiResponse(cleanResponse); + setIsLoading(false) + } + + + // Only extract and set sources after streaming is complete + const { sources } = extractSources(aiContent); setSources(sources); - - const endTime = performance.now() // Track total time - console.log(`Total time taken for AI search: ${(endTime - startTime) / 1000}s`) - } catch (error) { - console.error('Search error:', error); - setError(error instanceof Error ? error.message : 'An error occurred'); + const endTime = performance.now() // Track total time + console.log(`Total time taken for AI search: ${(endTime - startTime) / 1000}s`) + + } catch (error) { + console.error('Search error:', error); + setError(error instanceof Error ? error.message : 'An error occurred'); } finally { - setIsLoading(false); + setIsLoading(false); } } @@ -265,49 +269,49 @@ export default function Home() { return (
-
-
- -
- -
- +
+ +
+ - -
- - - {isLoading ? ( -
- -

- {loaderText} -

-
- ) : error ? ( -
-

Error

-

{error}

-
- ) : aiResponse ? ( - + - ) : recommendedArticles && ( - - )} -
+ +
+ + + {isLoading ? ( +
+ +

+ {loaderText} +

+
+ ) : error ? ( +
+

Error

+

{error}

+
+ ) : aiResponse ? ( + + ) : recommendedArticles && ( + + )} +
+
-
) -} +} \ No newline at end of file diff --git a/docs/src/components/aiSearch/AIResponse.tsx b/docs/src/components/aiSearch/AIResponse.tsx index e78f31f..b4be488 100644 --- a/docs/src/components/aiSearch/AIResponse.tsx +++ b/docs/src/components/aiSearch/AIResponse.tsx @@ -20,8 +20,46 @@ interface AIResponseProps { const MemoizedMarkdown = memo(({ content }: { content: string }) => ( {children} + pre({ children, ...props }) { + return ( +
+            {children}
+          
+ ) + }, + p: ({ children }) =>

{children}

, + ol: ({ node, ...props }) => ( +
    + ), + ul: ({ node, ...props }) => ( +
      + ), + br: ({ node, ...props }) => ( +
      + ), + code({ node, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || '') + return ( +
      + + {children} + +
      + ) } }} > @@ -49,7 +87,7 @@ export function AIResponse({ response, sources, onBack }: AIResponseProps) { Back - +
      isNaN(val) ? i : null).filter(x => x !== null)); - console.error("Vector B NaN indices:", b.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); - throw new Error("Invalid vectors containing NaN values"); - } - - const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); - const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); - const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); - return dotProduct / (magnitudeA * magnitudeB); + if (a.some(isNaN) || b.some(isNaN)) { + console.error("NaN values detected in vectors:"); + console.error("Vector A NaN indices:", a.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); + console.error("Vector B NaN indices:", b.map((val, i) => isNaN(val) ? i : null).filter(x => x !== null)); + throw new Error("Invalid vectors containing NaN values"); + } + + const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); + const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); + const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); + return dotProduct / (magnitudeA * magnitudeB); } export default function Home() { const [query, setQuery] = useState('') const [aiResponse, setAiResponse] = useState('') - const [loaderText, setLoaderText] = useState('Loading database ...') + const [loaderText, setLoaderText] = useState('Loading database ...') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const recommendedArticles = getRecommendedArticles() @@ -45,20 +45,20 @@ export default function Home() { const headerConfig = getHeaderConfig() const config = getAkiradocsConfig() const [sources, setSources] = useState([]) - const handleGenerateEmbedding = useCallback(async (text: string) => { - try { - setIsLoading(true); - // console.log("Loading model for embedding"); - const embedding = await generateEmbedding(text, (progress) => {}); - return embedding; - } catch (error) { - console.error('Error generating embedding:', error); - throw error; - } - finally { - setLoaderText('Searching database for relevant information ...') - } - }, []); + const handleGenerateEmbedding = useCallback(async (text: string) => { + try { + setIsLoading(true); + // console.log("Loading model for embedding"); + const embedding = await generateEmbedding(text, (progress) => { }); + return embedding; + } catch (error) { + console.error('Error generating embedding:', error); + throw error; + } + finally { + setLoaderText('Searching database for relevant information ...') + } + }, []); // If AI Search is disabled, show the disabled message if (!config.navigation.header.items.find((item: any) => item.href === '/aiSearch')?.show) { @@ -104,96 +104,98 @@ export default function Home() { setIsLoading(true) setError(null) setSources([]) - - const startTime = performance.now() - - try { - // Generate embedding for the query - const queryEmbedding = await handleGenerateEmbedding(query); - // console.log("Query embedding:", queryEmbedding) - // Get database worker - const worker = await getDbWorker(); - - // Get all documents - const allDocs = await worker.db.query(` + + const startTime = performance.now() + + try { + // Generate embedding for the query + const queryEmbedding = await handleGenerateEmbedding(query); + // console.log("Query embedding:", queryEmbedding) + // Get database worker + const worker = await getDbWorker(); + + // Get all documents + const allDocs = await worker.db.query(` SELECT path, content, embedding FROM documents WHERE embedding IS NOT NULL `); - // Calculate similarity scores and filter results - const similarityThreshold = 0.5; - const scoredDocs = allDocs - .map((doc: any) => { - // Clean the embedding string and parse it - const cleanEmbeddingStr = doc.embedding.replace(/[\[\]]/g, ''); // Remove square brackets - const embeddingArray = cleanEmbeddingStr - .split(',') - .map((val: string) => { - const parsed = parseFloat(val.trim()); - if (isNaN(parsed)) { - console.error(`Invalid embedding value found: "${val}"`); - } - return parsed; - }); - - return { - ...doc, - similarity_score: cosineSimilarity(queryEmbedding, embeddingArray) - }; - }) - .filter((doc: any) => doc.similarity_score > similarityThreshold) - .sort((a: any, b: any) => b.similarity_score - a.similarity_score) - .slice(0, 5); - - console.log("RAG top 5 results:", scoredDocs); - - // If no relevant documents found, return early - if (scoredDocs.length === 0) { - setAiResponse("I cannot answer this question from the given documentation. The available content doesn't seem relevant enough to provide a accurate answer."); - setIsLoading(false); - return; - } - - setLoaderText('Loading the AI model ...') - - // Combine relevant documents into context - const docsContext = scoredDocs - .map((doc: any) => ` + // Calculate similarity scores and filter results + const similarityThreshold = 0.5; + const scoredDocs = allDocs + .map((doc: any) => { + // Clean the embedding string and parse it + const cleanEmbeddingStr = doc.embedding.replace(/[\[\]]/g, ''); // Remove square brackets + const embeddingArray = cleanEmbeddingStr + .split(',') + .map((val: string) => { + const parsed = parseFloat(val.trim()); + if (isNaN(parsed)) { + console.error(`Invalid embedding value found: "${val}"`); + } + return parsed; + }); + + return { + ...doc, + similarity_score: cosineSimilarity(queryEmbedding, embeddingArray) + }; + }) + .filter((doc: any) => doc.similarity_score > similarityThreshold) + .sort((a: any, b: any) => b.similarity_score - a.similarity_score) + .slice(0, 5); + + console.log("RAG top 5 results:", scoredDocs); + + // If no relevant documents found, return early + if (scoredDocs.length === 0) { + setAiResponse("I cannot answer this question from the given documentation. The available content doesn't seem relevant enough to provide a accurate answer."); + setIsLoading(false); + return; + } + + setLoaderText('Loading the AI model ...') + + // Combine relevant documents into context + const docsContext = scoredDocs + .map((doc: any) => ` Source: ${doc.path} --- Content --- ${doc.content} --- End of Content --- `) - .join('\n'); + .join('\n'); const engine = await CreateMLCEngine( "Llama-3.2-1B-Instruct-q4f16_1-MLC", - { initProgressCallback: (progress: any) => { - console.log(progress) - setLoaderText(`Loading the AI model ${Math.round(progress.progress * 100)}% ...`) - } }, { - context_window_size: 20000, + initProgressCallback: (progress: any) => { + console.log(progress) + setLoaderText(`Loading the AI model ${Math.round(progress.progress * 100)}% ...`) + } + }, + { + context_window_size: 20000, } ); - const engineLoadTime = performance.now() // Track engine load time - console.log(`Time taken for engine initialization: ${(engineLoadTime - startTime) / 1000}s`) - setLoaderText('Processing information and generating AI response ...') + const engineLoadTime = performance.now() // Track engine load time + console.log(`Time taken for engine initialization: ${(engineLoadTime - startTime) / 1000}s`) + setLoaderText('Processing information and generating AI response ...') const messages = [ - { - role: "system", - content: `You are a technical documentation assistant for AkiraDocs. Your purpose is to: + { + role: "system", + content: `You are a technical documentation assistant for AkiraDocs. Your purpose is to: 1. Provide accurate, helpful answers using ONLY the provided documentation 2. Stay positive and factual based on the documentation provided. 3. Make sure the markdown answer is pretty, clean and easy to read.` }, - { - role: "user", - content: ` + { + role: "user", + content: ` Please provide a helpful answer which is short and concise to the following question using only the provided documentation. Question: ${query} @@ -221,41 +223,43 @@ export default function Home() { } ]; - // console.log("Messages:", messages) + // console.log("Messages:", messages) - const chunks = await engine.chat.completions.create({ + const chunks = await engine.chat.completions.create({ messages: messages as ChatCompletionMessageParam[], stream: true, - stream_options: { include_usage: true }, - max_tokens: 500, - temperature: 0.7, - top_p: 0.95, - frequency_penalty: 0.5, - presence_penalty: 0.5, + stream_options: { include_usage: false }, + max_tokens: 300, + temperature: 0.5, + top_p: 0.8, + frequency_penalty: 0.3, + presence_penalty: 0.3, }); let aiContent = ""; for await (const chunk of chunks) { const newContent = chunk.choices[0]?.delta.content || ""; aiContent += newContent; - - // Process partial content for streaming - const { cleanResponse } = extractSources(aiContent); - setAiResponse(cleanResponse); - } - - // Only extract and set sources after streaming is complete - const { sources } = extractSources(aiContent); + + // Process partial content for streaming + const { cleanResponse } = extractSources(aiContent); + setAiResponse(cleanResponse); + setIsLoading(false) + } + + + // Only extract and set sources after streaming is complete + const { sources } = extractSources(aiContent); setSources(sources); - - const endTime = performance.now() // Track total time - console.log(`Total time taken for AI search: ${(endTime - startTime) / 1000}s`) - } catch (error) { - console.error('Search error:', error); - setError(error instanceof Error ? error.message : 'An error occurred'); + const endTime = performance.now() // Track total time + console.log(`Total time taken for AI search: ${(endTime - startTime) / 1000}s`) + + } catch (error) { + console.error('Search error:', error); + setError(error instanceof Error ? error.message : 'An error occurred'); } finally { - setIsLoading(false); + setIsLoading(false); } } @@ -265,49 +269,49 @@ export default function Home() { return (
      -
      -
      - -
      - -
      - +
      + +
      + - -
      - - - {isLoading ? ( -
      - -

      - {loaderText} -

      -
      - ) : error ? ( -
      -

      Error

      -

      {error}

      -
      - ) : aiResponse ? ( - + - ) : recommendedArticles && ( - - )} -
      + +
      + + + {isLoading ? ( +
      + +

      + {loaderText} +

      +
      + ) : error ? ( +
      +

      Error

      +

      {error}

      +
      + ) : aiResponse ? ( + + ) : recommendedArticles && ( + + )} +
      +
      -
      ) -} +} \ No newline at end of file diff --git a/packages/akiradocs/src/components/aiSearch/AIResponse.tsx b/packages/akiradocs/src/components/aiSearch/AIResponse.tsx index e78f31f..b4be488 100644 --- a/packages/akiradocs/src/components/aiSearch/AIResponse.tsx +++ b/packages/akiradocs/src/components/aiSearch/AIResponse.tsx @@ -20,8 +20,46 @@ interface AIResponseProps { const MemoizedMarkdown = memo(({ content }: { content: string }) => ( {children}
      + pre({ children, ...props }) { + return ( +
      +            {children}
      +          
      + ) + }, + p: ({ children }) =>

      {children}

      , + ol: ({ node, ...props }) => ( +
        + ), + ul: ({ node, ...props }) => ( +
          + ), + br: ({ node, ...props }) => ( +
          + ), + code({ node, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || '') + return ( +
          + + {children} + +
          + ) } }} > @@ -49,7 +87,7 @@ export function AIResponse({ response, sources, onBack }: AIResponseProps) { Back - +