update dependencies, add script to create RAG of a github repo, add API endpoint to query it, add chatbot page to interact with it

This commit is contained in:
silentsilas 2024-09-21 22:01:29 -04:00
parent dae681d2c2
commit 8f3d476a38
Signed by: silentsilas
GPG Key ID: 113DFB380F724A81
11 changed files with 2608 additions and 785 deletions

2
.gitignore vendored
View File

@ -5,7 +5,9 @@ node_modules
/package /package
.env .env
.env.* .env.*
.envrc
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
.vercel .vercel
vectorstore

3028
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,49 +12,54 @@
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write .", "format": "prettier --write .",
"model-pipeline:run": "node scripts/model-pipeline.js", "model-pipeline:run": "node scripts/model-pipeline.js",
"generate-embeddings": "node scripts/generate-embeddings.js" "generate-embeddings": "node scripts/generate-embeddings.js",
"finetune": "node scripts/finetune.js"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.2.4",
"@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-node": "^5.2.2",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.5.22",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.14",
"@theatre/core": "^0.7.1", "@theatre/core": "^0.7.2",
"@theatre/studio": "^0.7.1", "@theatre/studio": "^0.7.2",
"@threlte/theatre": "^2.1.7", "@threlte/theatre": "^2.1.8",
"@types/eslint": "^8.56.0", "@types/eslint": "^8.56.11",
"@types/three": "^0.159.0", "@types/three": "^0.159.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"daisyui": "^4.11.1", "daisyui": "^4.12.10",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.43.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"postcss": "^8.4.38", "postcss": "^8.4.41",
"prettier": "^3.1.1", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.7", "svelte": "^4.2.18",
"svelte-check": "^3.6.0", "svelte-check": "^3.8.5",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.10",
"tslib": "^2.4.1", "tslib": "^2.6.3",
"typescript": "^5.0.0", "typescript": "^5.5.4",
"vite": "^5.0.3", "vite": "^5.4.1",
"vitest": "^1.2.0" "vitest": "^1.6.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dimforge/rapier3d-compat": "^0.11.2", "@dimforge/rapier3d-compat": "^0.11.2",
"@langchain/anthropic": "^0.3.1",
"@langchain/community": "^0.3.1",
"@langchain/openai": "^0.3.0",
"@langchain/textsplitters": "^0.1.0",
"@tensorflow-models/universal-sentence-encoder": "^1.3.3", "@tensorflow-models/universal-sentence-encoder": "^1.3.3",
"@tensorflow/tfjs-node": "^4.20.0", "@tensorflow/tfjs-node": "^4.20.0",
"@threlte/core": "^7.3.0", "@threlte/core": "^7.3.1",
"@threlte/extras": "^8.11.2", "@threlte/extras": "^8.11.5",
"@threlte/rapier": "^2.0.0", "@threlte/rapier": "^2.0.1",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"marked": "^12.0.2", "marked": "^12.0.2",
"mdsvex": "^0.11.0", "mdsvex": "^0.11.2",
"three": "^0.159.0" "three": "^0.159.0"
} }
} }

35
scripts/finetune.js Normal file
View File

@ -0,0 +1,35 @@
// scripts/finetune.js
import { GithubRepoLoader } from '@langchain/community/document_loaders/web/github';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import { OpenAIEmbeddings } from '@langchain/openai';
import { FaissStore } from '@langchain/community/vectorstores/faiss';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
async function finetune() {
console.log('Fine-tuning model...');
const loader = new GithubRepoLoader('https://github.com/silentsilas/Authenticator', {
branch: 'develop',
recursive: true,
unknown: 'warn',
maxConcurrency: 5
});
const docs = await loader.load();
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
const splitDocs = await textSplitter.splitDocuments(docs);
const embeddings = new OpenAIEmbeddings();
const vectorStore = await FaissStore.fromDocuments(splitDocs, embeddings);
const directory = join(__dirname, '..', 'vectorstore');
await vectorStore.save(directory);
console.log('Fine-tuning complete. Vector store saved.');
}
finetune();

View File

@ -1,5 +1,7 @@
import { getModel } from '$lib/utils/search'; import { getModel } from '$lib/utils/search';
import { building } from '$app/environment'; import { building } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { v4 as uuidv4 } from 'uuid';
if (!building) { if (!building) {
getModel().catch((error) => { getModel().catch((error) => {
@ -8,3 +10,18 @@ if (!building) {
console.log('Model loaded successfully!'); console.log('Model loaded successfully!');
} }
export const handle: Handle = async ({ event, resolve }) => {
// Check for existing session
let sessionId = event.cookies.get('sessionId');
// If no session exists, create a new one
if (!sessionId) {
sessionId = uuidv4();
event.cookies.set('sessionId', sessionId, { path: '/', httpOnly: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 }); // 1 week
}
// Add sessionId to locals for easy access in routes
event.locals = { ...event.locals, sessionId };
return resolve(event);
};

View File

@ -0,0 +1,9 @@
<script lang="ts">
import SearchResults from '$lib/components/SearchResults.svelte';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
</script>
<AppContainer>
<slot />
<SearchResults />
</AppContainer>

2
src/routes/ai/+layout.ts Normal file
View File

@ -0,0 +1,2 @@
export const prerender = false;
export const ssr = false;

141
src/routes/ai/+page.svelte Normal file
View File

@ -0,0 +1,141 @@
<script lang="ts">
import '../../app.css';
import type { SearchResult } from '$lib/utils/search';
import { searchResults } from '$lib/store';
import { onMount } from 'svelte';
import { marked } from 'marked';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import type { ChatHistory } from '../api/ai/+server';
import { PUBLIC_LOAD_DUMMY_HISTORY } from '$env/static/public';
let searchResultsValue: SearchResult[] = [];
let query = '';
let loading = false;
function generateDummyHistory() {
return [
JSON.parse(JSON.stringify(new HumanMessage({ content: 'Hello, AI!' }))),
JSON.parse(JSON.stringify(new AIMessage({ content: 'Hello! How can I assist you today?' }))),
JSON.parse(JSON.stringify(new HumanMessage({ content: "What's the weather like?" }))),
JSON.parse(
JSON.stringify(
new AIMessage({
content:
"I'm sorry, but I don't have access to real-time weather information. You might want to check a weather app or website for the most up-to-date forecast."
})
)
)
];
}
let chatHistory: ChatHistory = [];
onMount(async () => {
try {
if (PUBLIC_LOAD_DUMMY_HISTORY === 'true') {
chatHistory = generateDummyHistory();
return;
}
const response = await fetch('/api/ai');
const data = await response.json();
chatHistory = data.chatHistory || [];
} catch (error) {
console.error('Error fetching chat history:', error);
}
});
searchResults.subscribe((value: SearchResult[]) => {
searchResultsValue = value ? value : [];
});
function getRoleAndContent(message: any): { role: string; content: string } {
if (message.type === 'constructor') {
const messageType = message.id[2];
return {
role: messageType.replace('Message', '').toLowerCase(),
content: message.kwargs.content
};
}
return {
role: 'unknown',
content: JSON.stringify(message)
};
}
// safely render markdown
function renderMarkdown(content: string) {
return marked(content);
}
async function handleSubmit() {
loading = true;
try {
const response = await fetch('/api/ai', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const data = await response.json();
chatHistory = data.chatHistory || [];
query = '';
} catch (error) {
console.error('Error fetching AI response:', error);
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>silentsilas - AI</title>
</svelte:head>
{#if searchResultsValue.length === 0}
<div class="flex-grow flex-col overflow-auto p-4">
<div class="space-y-4">
{#each chatHistory as message}
{@const { role, content } = getRoleAndContent(message)}
{#if role === 'human'}
<div class="chat chat-end">
<div class="chat-bubble chat-bubble-primary">
{@html renderMarkdown(content)}
</div>
</div>
{:else}
<div class="p-4 rounded-lg bg-base-300 prose md:container">
{@html renderMarkdown(content)}
</div>
{/if}
{/each}
</div>
{#if loading}
<div class="mt-4">
<span class="loading loading-dots loading-lg"></span>
</div>
{/if}
<form on:submit|preventDefault={handleSubmit} class="mt-4 flex-col">
<label class="form-control">
<div class="label">
<span class="label-text">
Querying the Authenticator <a
href="https://git.silentsilas.com/silentsilas/Authenticator"
target="_blank"
class="link-primary">repository</a
>
</span>
</div>
<textarea
bind:value={query}
class="textarea textarea-bordered h-24"
placeholder="Type your message here..."
></textarea>
</label>
<button type="submit" class="btn btn-block btn-primary mt-2" disabled={loading}>
Send
</button>
</form>
</div>
{/if}

View File

@ -0,0 +1,82 @@
import { json } from '@sveltejs/kit';
import { FaissStore } from '@langchain/community/vectorstores/faiss';
import { OpenAIEmbeddings } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { join } from 'path';
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages';
import type { RequestEvent } from '@sveltejs/kit';
const chatHistories: Record<string, ChatHistory> = {};
type VectorDocument = {
pageContent: string;
};
const formatDocumentsAsString = (documents: VectorDocument[]) => {
return documents.map((doc) => doc.pageContent).join('\n\n');
};
export type ChatHistory = HumanMessage[] | AIMessage[] | SystemMessage[];
export async function POST({ request, locals }: RequestEvent): Promise<Response> {
const { query } = await request.json();
const sessionId = locals.sessionId;
if (!chatHistories[sessionId]) {
chatHistories[sessionId] = [];
}
const chatHistory = chatHistories[sessionId];
const directory = join(process.cwd(), 'vectorstore');
const embeddings = new OpenAIEmbeddings();
const vectorStore = await FaissStore.load(directory, embeddings);
const vectorStoreRetriever = vectorStore.asRetriever();
const model = new ChatAnthropic({
modelName: 'claude-3-5-sonnet-20240620',
anthropicApiKey: process.env.ANTHROPIC_API_KEY
});
const SYSTEM_TEMPLATE = `Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
----------------
Context:
{context}
Question:
{question}`;
const prompt = ChatPromptTemplate.fromMessages([
['system', SYSTEM_TEMPLATE],
...chatHistory,
['user', '{question}']
]);
const chain = RunnableSequence.from([
{
context: vectorStoreRetriever.pipe(formatDocumentsAsString),
question: new RunnablePassthrough(),
},
prompt,
model,
new StringOutputParser()
]);
const answer = await chain.invoke(query);
chatHistory.push(new HumanMessage({ content: query }));
chatHistory.push(new AIMessage({ content: answer }));
return json({ response: answer, chatHistory });
}
export async function GET({ locals }): Promise<Response> {
const sessionId = locals.sessionId;
const chatHistory = chatHistories[sessionId] || [];
return json({ chatHistory });
}

View File

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import Footer from '$lib/components/Footer.svelte';
import NavBar from '$lib/components/NavBar.svelte';
import SearchResults from '$lib/components/SearchResults.svelte'; import SearchResults from '$lib/components/SearchResults.svelte';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte'; import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
</script> </script>

View File

@ -6,10 +6,12 @@ import { mdsvex } from 'mdsvex';
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: [vitePreprocess(), mdsvex({ extensions: ['.md'], layout: { preprocess: [vitePreprocess(), mdsvex({
poetry: './src/lib/utils/PoetryLayout.svelte', extensions: ['.md'], layout: {
thoughts: './src/lib/utils/ThoughtsLayout.svelte', poetry: './src/lib/utils/PoetryLayout.svelte',
}})], thoughts: './src/lib/utils/ThoughtsLayout.svelte',
}
})],
kit: { kit: {