Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat docs #6

Merged
merged 4 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions src/components/KnowledgeStoreUploader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createUploaderComponent } from 'quasar';
import { computed, ref } from 'vue';

import { chatTag } from 'src/utils/chat';

// State
import { useKnowledgeStore } from 'src/stores/knowledge-store';

Expand All @@ -9,15 +11,23 @@ const pdfjsLib = window.pdfjsLib;

export default createUploaderComponent({
name: 'KnowledgeStoreUploader',
props: {},
emits: [],
injectPlugin({ _props, _emit, helpers }) {
props: {
chatRef: {
type: Object,
required: true,
},
},
emits: ['attachment-added'],
injectPlugin({ props, emit, helpers }) {
const loading = ref(false);
const chatId = props.chatRef.id;

// Map of file objects to their status as either 'queued', 'uploading', 'embedding', 'uploaded', or 'failed'
const fileStatus = ref({});

// Upload Logic
// TODO: we should be feeding the chat id through here, not through props
// We're gonna need a way to add more custom tags to the documents
async function upload(_args) {
// Set the loading state
loading.value = true;
Expand All @@ -40,12 +50,26 @@ export default createUploaderComponent({
try {
fileStatus.value[file.name] = 'uploading';
helpers.updateFileStatus(file, 'uploading');
let { title, text } = await processFile(file);
let { title, text, type } = await processFile(file);
// Check how big the file is
// If the text is less than 4 KiB, then just inline it
if (text.length < 4 * 1024) {
fileStatus.value[file.name] = 'uploaded';
helpers.updateFileStatus(file, 'uploaded');
// If you don't embed the doucment, make sure to set the content
emit('attachment-added', { title, type, content: text });
return;
}

// Embed the document
fileStatus.value[file.name] = 'embedding';
helpers.updateFileStatus(file, 'embedding');
await knowledgeStore.addDocument(title, text);
let tag = chatTag(chatId);
let { id } = await knowledgeStore.addDocument(title, text, [tag]);
let documentId = id;
fileStatus.value[file.name] = 'uploaded';
helpers.updateFileStatus(file, 'uploaded');
emit('attachment-added', { title, documentId, type });
} catch (error) {
console.error(error);
fileStatus.value[file.name] = 'failed';
Expand Down Expand Up @@ -84,6 +108,7 @@ export default createUploaderComponent({
async function processFile(file) {
const title = file.name;
let extractedText = '';
let type = file.type;

try {
switch (file.type) {
Expand All @@ -98,6 +123,14 @@ export default createUploaderComponent({
reader.readAsText(file);
});
break;
case 'text/markdown':
extractedText = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.onerror = (error) => reject(error);
reader.readAsText(file);
});
break;
default:
throw new Error(`Unsupported file type: ${file.type}`);
}
Expand All @@ -106,7 +139,7 @@ export default createUploaderComponent({
throw error;
}

return { title, text: extractedText };
return { title, text: extractedText, type };
}

/**
Expand Down
108 changes: 96 additions & 12 deletions src/pages/Chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
<q-item-label class="text-semibold">
{{ message.role.replace(chatRef.username, 'You').replace('assistant', 'Libertai') }}
</q-item-label>
<!-- Display any attachments -->
<q-item-label v-if="message.attachments && message.attachments.length > 0">
<q-chip
v-for="attachment in message.attachments"
:key="attachment.id"
class="q-mr-xs bg-primary text-white"
>
{{ attachment.title }}
</q-chip>
</q-item-label>
<!-- Display the content of the message -->
<q-item-label style="display: block">
<MarkdownRenderer :content="message.content" breaks />
Expand Down Expand Up @@ -80,7 +90,14 @@
style="margin-left: 16px"
/>

<!-- Input for sending messages -->
<q-chip
v-for="attachment in attachmentsRef"
:key="attachment.id"
removable
@remove="removeAttachment(attachment)"
>
{{ attachment.title }}
</q-chip>
<message-input
:isLoading="isLoadingRef"
@sendMessage="sendMessage"
Expand All @@ -100,8 +117,11 @@
</q-checkbox>
</div>
</div>
<!-- This should really not pass the ref, but it's a quick fix for now -->
<q-dialog v-model="showKnowledgeUploaderRef" position="bottom">
<KnowledgeStoreUploader
@attachment-added="addAttachment"
:chatRef="chatRef"
label="Auto KnowledgeStoreUploader"
auto-upload
url="http://localhost:4444/upload"
Expand All @@ -123,14 +143,14 @@ import { inferChatTopic, defaultChatTopic } from 'src/utils/chat';
import { LlamaCppApiEngine } from '@libertai/libertai-js';

// Local state
import { useChatsStore } from '../stores/chats-store';
import { useModelsStore } from '../stores/models-store';
import { useKnowledgeStore } from '../stores/knowledge-store';
import { useChatsStore } from 'src/stores/chats-store';
import { useModelsStore } from 'src/stores/models-store';
import { useKnowledgeStore } from 'src/stores/knowledge-store';
import { useAccount } from 'src/stores/account';

// Components
import MarkdownRenderer from '../components/MarkdownRenderer.vue';
import MessageInput from '../components/MessageInput.vue';
import MarkdownRenderer from 'src/components/MarkdownRenderer.vue';
import MessageInput from 'src/components/MessageInput.vue';
import axios from 'axios';

console.log(nextTick);
Expand Down Expand Up @@ -161,6 +181,7 @@ export default defineComponent({
const enableEditRef = ref(false);
const enableKnowledgeRef = ref(true);
const showKnowledgeUploaderRef = ref(false);
const attachmentsRef = ref([]);

// Chat specific state
const chatRef = ref();
Expand Down Expand Up @@ -220,6 +241,18 @@ export default defineComponent({

/* Helper functions */

function addAttachment(attachmentEvent) {
let attachment = JSON.parse(JSON.stringify(attachmentEvent));
attachmentsRef.value.push(attachment);
}

async function removeAttachment(attachment) {
// Remove the attachment from the knowledge store
await knowledgeStore.removeDocument(attachment.documentId);
let index = attachmentsRef.value.indexOf(attachment);
attachmentsRef.value.splice(index, 1);
}

// Set the name of the chat based on the first sentence
async function setChatName(first_sentence) {
// Get our chat id
Expand All @@ -245,6 +278,7 @@ export default defineComponent({
// Generate a new response from the AI
async function generatePersonaMessage() {
let chatId = chatRef.value.id;
let chatTags = chatRef.value.tags;
let messages = JSON.parse(JSON.stringify(messagesRef.value));
let persona = personaRef.value;
let username = usernameRef.value;
Expand All @@ -270,18 +304,62 @@ export default defineComponent({
// NOTE: assuming last message is gauranteed to be non-empty and the user's last message
// Get the last message from the user
let lastMessage = messages[messages.length - 1];
let searchResults = await knowledgeStore.searchDocuments(lastMessage.content);
let searchResultMessages = [];
let searchResults = await knowledgeStore.searchDocuments(lastMessage.content, chatTags);
searchResults.forEach((result) => {
console.log('pages::Chat.vue::generatePersonaMessage - embedding search result', result);
messages.push({
searchResultMessages.push({
role: 'search-result',
content: result.content,
});
});

// Expand all the messages to inline any compatible attachments
const exapndedMessages = messages
.map((message) => {
let ret = [];
// Push any attachments ahead of the message
if (message.attachments) {
message.attachments.forEach((attachment) => {
if (attachment.content) {
ret.push({
role: 'attachment',
content: `[${attachment.title}](${attachment.content})`,
});
} else if (attachment.documentId) {
ret.push({
role: 'attachment',
content: `[${attachment.title}](document-id-${attachment.documentId})`,
});
}
});
}

// Push what search results we found based on the message
// TODO: this should prabably be a more generic tool-call or llm-chain-link
// TODO: this should probably link back to the document id
// TODO: I should probably write these below messages in the log
// Really these search results should get attached to the message that
// lead to them being queried
if (message.searchResults) {
message.searchResults.forEach((result) => {
ret.push({
role: 'search-result',
content: result.content,
});
});
}
// Push the message itself
ret.push(message);
return ret;
})
.flat();

// Append the search results to the messages
const allMessages = [...exapndedMessages, ...searchResultMessages];

// Generate a stream of responses from the AI
for await (const output of inferenceEngine.generateAnswer(
messages,
allMessages,
model,
persona,
// Set the target to the user
Expand All @@ -300,7 +378,7 @@ export default defineComponent({
messagesRef.value = [...messagesRef.value];
}
// A successful response! Append the chat to long term storage.
await chatsStore.appendModelResponse(chatId, response.content);
await chatsStore.appendModelResponse(chatId, response.content, searchResults);
} catch (error) {
console.error('pages::Chat.vue::generatePersonaMessage - error', error);
response.error = error;
Expand Down Expand Up @@ -333,9 +411,12 @@ export default defineComponent({
console.log('pages::Chat.vue::sendMessage');
let chatId = chatRef.value.id;
let inputText = inputTextRef.value;
const attachments = JSON.parse(JSON.stringify(attachmentsRef.value));

// Wipe the input text
inputTextRef.value = '';
// Wipe the attachments
attachmentsRef.value = [];

nextTick(scrollBottom);

Expand All @@ -344,7 +425,7 @@ export default defineComponent({
if (content.trim() === '') return;

// Append the new message to the chat history and push to local state
let newMessage = await chatsStore.appendUserMessage(chatId, inputText);
let newMessage = await chatsStore.appendUserMessage(chatId, inputText, attachments);
messagesRef.value.push({ ...newMessage, stopped: true, error: null });
chatRef.value.messages = messagesRef.value;
await generatePersonaMessage();
Expand Down Expand Up @@ -445,6 +526,9 @@ export default defineComponent({
isLoadingRef,
inputRef,
inputTextRef,
attachmentsRef,
addAttachment,
removeAttachment,
sendMessage,
showKnowledgeUploaderRef,
openKnowledgeUploader,
Expand Down
Loading
Loading