Skip to content

Commit 906a39c

Browse files
authored
Merge pull request #24 from glorat/qna
feat: multidoc qna
2 parents 7b4336b + 5eb9f6d commit 906a39c

19 files changed

+370
-115
lines changed

quasar.config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@
1010

1111

1212
const { configure } = require('quasar/wrappers');
13+
const {defaultUnstructuredUrl} = require(__dirname + '/src/staticConfig');
14+
const {reduce} = require("lodash");
1315

16+
const pathsToProxy = ['/general']; // unstructured endpoint
17+
const proxyTarget = defaultUnstructuredUrl
18+
19+
const proxyProxyConfig = reduce(
20+
pathsToProxy,
21+
(obj, path) => ({ ...obj, [path]: { target: proxyTarget, changeOrigin:true, secure:false } }),
22+
{}
23+
);
1424

1525
module.exports = configure(function (/* ctx */) {
1626
return {
@@ -88,7 +98,8 @@ module.exports = configure(function (/* ctx */) {
8898
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
8999
devServer: {
90100
// https: true
91-
open: true // opens browser window automatically
101+
open: false, // opens browser window automatically
102+
proxy: proxyProxyConfig
92103
},
93104

94105
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

src/components/ChatEntry.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<q-toolbar class="bg-grey-3 text-black row">
3+
<q-btn @click="onReset" flat class="q-ml-sm" icon="refresh" color="primary"/>
4+
<q-input rounded outlined dense class="WAL__field q-mr-sm" bg-color="white"
5+
:model-value="modelValue"
6+
@update:model-value="onModelValueUpdate"
7+
@keydown.enter="sendMessage"
8+
placeholder="Type your message"/>
9+
<audio-transcriber @message="onAudioMessage"></audio-transcriber>
10+
<q-btn disable round flat :icon="isVolumeOn ? matVolumeUp : matVolumeOff"/>
11+
<q-btn @click="sendMessage" flat class="q-ml-sm" icon="send" color="primary"/>
12+
</q-toolbar>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import {ref} from 'vue';
17+
import {matVolumeOff, matVolumeUp} from '@quasar/extras/material-icons';
18+
import AudioTranscriber from 'components/AudioTranscriber.vue';
19+
20+
const isVolumeOn = ref(true);
21+
const emit = defineEmits(['message', 'update:modelValue', 'reset']);
22+
23+
defineProps({
24+
modelValue: {
25+
type: String,
26+
},
27+
});
28+
29+
const onReset = () => {
30+
emit('reset')
31+
}
32+
33+
const sendMessage = async () => {
34+
emit('message');
35+
};
36+
37+
const onAudioMessage = (msg: string) => {
38+
emit('update:modelValue', msg)
39+
};
40+
41+
const onModelValueUpdate = (msg:string) => {
42+
emit('update:modelValue', msg)
43+
}
44+
</script>
45+
46+
<style scoped>
47+
/* Component styles */
48+
</style>

src/components/MultiFileManager.vue

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {createVectorStoreFromLargeContent} from 'src/lib/ai/largeDocQna'
3333
import {fileToText} from 'src/lib/ai/unstructured'
3434
import {DocumentInfo, useMultiFileStore} from 'stores/multiFileStore'
3535
import {matCloudUpload} from '@quasar/extras/material-icons'
36+
import {RecursiveCharacterTextSplitter} from "langchain/text_splitter";
3637
3738
defineProps({
3839
loading:Boolean
@@ -90,42 +91,14 @@ const onFileChange = (newFiles: File[]) => {
9091
// Clear the files variable after populating uploadedFiles
9192
files.value = [];
9293
93-
processNextDocument()
94+
multiFileStore.processNextDocument()
9495
};
9596
9697
const processingDocuments = computed(() => {
9798
return multiFileStore.processing
9899
// return uploadedFiles.value.some(file => includes([ 'processing', 'parsing'], file.status));
99100
});
100101
101-
const processNextDocument = async () => {
102-
const pendingDocument = multiFileStore.documentInfo.find(file => file.status === 'pending');
103-
104-
if (pendingDocument) {
105-
// Set the status to 'parsing'
106-
pendingDocument.status = 'parsing';
107-
108-
try {
109-
// Simulating asynchronous processing with a timeout
110-
await new Promise(resolve => setTimeout(resolve, 2000));
111-
const text = await fileToText(pendingDocument.file)
112-
pendingDocument.status = 'processing'
113-
const vectorStore = await createVectorStoreFromLargeContent(text, (p)=>{pendingDocument.progress=p})
114-
// Important to markRaw to avoid proxying the insides
115-
pendingDocument.vectors = markRaw(vectorStore)
116-
// Update the status to 'ready' on successful processing
117-
pendingDocument.status = 'ready';
118-
} catch (error) {
119-
// Set the status to 'error' on processing failure
120-
pendingDocument.status = 'error';
121-
console.error('Error occurred during document processing:', error);
122-
}
123-
124-
// Call the processNextDocument function recursively to process the next document
125-
await processNextDocument();
126-
}
127-
};
128-
129102
</script>
130103

131104

src/components/SettingsConfigEditor.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {ref, onMounted} from 'vue'
8181
import {Dialog, Notify} from 'quasar'
8282
import {applyAiUserSettings, Config, getOpenAIAPI, getSettingsFromLocalStorage} from 'src/lib/ai/config'
8383
import {matRefresh, matSave, matShare} from '@quasar/extras/material-icons';
84+
import {merge} from "lodash";
8485
8586
const settings = ref({
8687
server: 'openai',
@@ -98,7 +99,7 @@ onMounted(() => {
9899
function loadSettingsFromLocalStorage() {
99100
const savedSettings = getSettingsFromLocalStorage()
100101
if (savedSettings) {
101-
settings.value = savedSettings
102+
settings.value = merge(settings.value, savedSettings)
102103
}
103104
}
104105

src/lib/ai/answer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {logger} from './logger'
44
import {QnaStorage} from 'src/lib/ai/largeDocQna'
55
import {answerMe, createEmbedding} from 'src/lib/ai/openaiFacade'
66
import {VectorStore} from 'langchain/vectorstores';
7+
import {MemoryVectorStore} from "langchain/vectorstores/memory";
78

89

9-
export async function performQna2(question:string, db: VectorStore, prompt?: string): Promise<string|undefined> {
10-
const similarities = await db.similaritySearch(question, 10)
10+
export async function performQna2(question:string, db: VectorStore, filter:(d:any)=>boolean = ()=>true, prompt?: string): Promise<string|undefined> {
11+
const similarities = await db.similaritySearch(question, 10, filter)
1112

1213
const contexts = []
1314
let contextLength = 0

src/lib/ai/config.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {Configuration, ConfigurationParameters, OpenAIApi} from 'openai'
2-
2+
import {OpenAI, OpenAIChat} from 'langchain/llms/openai'
33
import FormData from 'form-data'
44
import ChatGPTClient from 'src/lib/ai/ChatGPTClient'
5+
import {defaultUnstructuredUrl} from '../myfirebase'
56

6-
type OpenAIEngine = 'text-ada-001' | 'text-davinci-003'
7+
type OpenAIEngine = 'text-ada-001' | 'text-davinci-003'
78

89
const engineId: OpenAIEngine = 'text-davinci-003'
910
export const Config = {
@@ -14,24 +15,25 @@ export const Config = {
1415

1516
class CustomFormData extends FormData {
1617
getHeaders(): Record<string, string> {
17-
return {};
18+
return {}
1819
}
1920
}
2021

2122

2223
let aiUserSettings: Partial<AiUserSettings> = {
2324
server: 'openai',
24-
openaiSettings: {apiKey: process.env.OPENAPI_KEY??''},
25+
openaiSettings: {apiKey: process.env.OPENAPI_KEY ?? ''},
2526
unstructuredSettings: {endpoint: process.env.UNSTRUCTURED_URL ?? ''},
2627
}
28+
2729
export interface AiUserSettings {
2830
server: 'openai' | 'azure' | 'hosted'
29-
azureSettings: {apiKey:string, basePath:string}, // only applicable if server is 'azure
30-
openaiSettings: {apiKey: string}, // only applicable if server is 'openai'
31-
unstructuredSettings: {apiKey?: string, endpoint: string},
31+
azureSettings: { apiKey: string, basePath: string }, // only applicable if server is 'azure
32+
openaiSettings: { apiKey: string }, // only applicable if server is 'openai'
33+
unstructuredSettings: { apiKey?: string, endpoint: string },
3234
}
3335

34-
export function applyAiUserSettings(settings:AiUserSettings) {
36+
export function applyAiUserSettings(settings: AiUserSettings) {
3537
aiUserSettings = {...settings}
3638
}
3739

@@ -60,8 +62,9 @@ export function getLangchainConfig() {
6062
return {
6163
azureOpenAIApiKey: aiUserSettings.azureSettings?.apiKey,
6264
azureOpenAIApiInstanceName: 'kevin-test-openai-1', // FIXME: configurable
63-
azureOpenAIApiDeploymentName: Config.embedModel,
64-
azureOpenAIApiVersion: '2023-03-15-preview'
65+
azureOpenAIApiVersion: '2023-03-15-preview',
66+
azureOpenAIApiDeploymentName: Config.chatModel.replaceAll(/\./g, ''),
67+
azureOpenAIApiEmbeddingsDeploymentName: Config.embedModel.replaceAll(/\./g, ''),
6568
}
6669
} else {
6770
throw new Error('unsupported')
@@ -73,7 +76,7 @@ export function getOpenAIConfig(deployment?: string) {
7376
const params = {...getOpenAIParams()}
7477
if (params.basePath) {
7578
if (deployment) {
76-
const d2 = deployment.replaceAll(/\./g,'')
79+
const d2 = deployment.replaceAll(/\./g, '')
7780
params.basePath = `${params.basePath}/openai/deployments/${d2}`
7881
params.baseOptions = {
7982
headers: {'api-key': params.apiKey},
@@ -91,7 +94,7 @@ export function getOpenAIConfig(deployment?: string) {
9194

9295
export const getOpenAIAPI = (deployment?: string) => {
9396
const config = getOpenAIConfig(deployment)
94-
return new OpenAIApi(config);
97+
return new OpenAIApi(config)
9598
}
9699

97100
export function getSettingsFromLocalStorage() {
@@ -105,27 +108,36 @@ export function getSettingsFromLocalStorage() {
105108
}
106109

107110

108-
109-
function getChatGPTClientOptions(clientOptions?:any) {
111+
function getChatGPTClientOptions(clientOptions?: any) {
110112
const cfg = getOpenAIConfig(Config.chatModel)
111113
if (cfg.basePath) {
112114
// Set up azure mode
113115
// https://kevin-test-openai-1.openai.azure.com//openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-03-15-preview
114116
const opts = {azure: true, reverseProxyUrl: `${cfg.basePath}/chat/completions?api-version=2023-03-15-preview`}
115-
return {...(clientOptions??{}), ...opts}
117+
return {...(clientOptions ?? {}), ...opts}
116118
} else {
117119
return clientOptions ?? {}
118120
}
119121

120122
}
121123

122-
export function getChatGPTClient(cache:any) {
124+
export function getChatGPTClient(cache: any) {
123125
const clientOptions = getChatGPTClientOptions()
124126
const client = new ChatGPTClient(getOpenAIParams().apiKey, clientOptions, cache)
125127
return client
126128
}
127129

128130
export function getUnstructuredEndpoint() {
129-
const endpoint = aiUserSettings?.unstructuredSettings?.endpoint ?? ''
131+
let endpoint = aiUserSettings?.unstructuredSettings?.endpoint
132+
if (!endpoint) {
133+
endpoint = process.env.UNSTRUCTURED_URL
134+
}
135+
if (!endpoint) {
136+
endpoint = defaultUnstructuredUrl
137+
}
130138
return `${endpoint}/general/v0/general`
131139
}
140+
141+
export function getOpenAIChat() {
142+
return new OpenAIChat(getLangchainConfig())
143+
}

src/lib/ai/langchainWrapper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {useQnaStore} from '../../stores/qnaStore'
2+
3+
4+
export const performVectorStoreQnaDirect = async (args: {question:string}) => {
5+
return useQnaStore().performVectorStoreQnaDirect(args)
6+
}

src/lib/ai/openaiFacade.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from 'src/lib/ai/openaiWrapper'
1010
import {CreateImageRequest, ImagesResponse} from 'openai'
1111
import {getOpenAIAPI} from 'src/lib/ai/config'
12+
import {performVectorStoreQnaDirect} from "src/lib/ai/langchainWrapper";
1213

1314
const FUNCTIONS_REGION = 'asia-northeast1'
1415

src/lib/ai/unstructured.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from 'axios'
22
import {getUnstructuredEndpoint} from 'src/lib/ai/config'
33

4+
// const FormData = require('form-data');
45
export interface UnstructuredMetadata {
56
filetype: string
67
page_number: number
@@ -19,29 +20,54 @@ export interface UnstructuredElement {
1920

2021
export async function fileToText(file:File) {
2122
const elems = await fileToPartitions(file)
23+
return partitionsToText(elems)
24+
}
25+
26+
export async function anyBufferToText(buffer: any, name:string) {
27+
const form = new FormData();
28+
form.append('files', buffer, name);
29+
30+
const response = await fetch(getUnstructuredEndpoint(),
31+
{method: 'POST', body: form})
32+
33+
// const response = await axios.post(
34+
// getUnstructuredEndpoint(),
35+
// form
36+
// );
37+
if (response.ok) {
38+
const json = await response.json()
39+
40+
return partitionsToText(json)
41+
} else {
42+
const text = await response.json()
43+
throw new Error(text)
44+
}
45+
46+
}
47+
48+
49+
function partitionsToText(elemsOrig: Partial<UnstructuredElement>[]) {
50+
const elems = elemsOrig.filter((el:any) => typeof el?.text === 'string')
51+
if (!Array.isArray(elems)) {
52+
throw new Error(
53+
`Expected partitioning request to return an array, but got ${elems}`
54+
);
55+
}
2256
const texts:string[] = elems.map(x => x.text!)
2357
// TODO: Consider determine elem type to use '\n' or '\n\n' instead of ' '
2458
return texts.join(' ')
2559
}
2660

2761
export async function fileToPartitions(file:File):Promise<Partial<UnstructuredElement>[]> {
28-
const url = getUnstructuredEndpoint()
29-
3062
const formData = new FormData();
3163
formData.append('files', file, file.name);
64+
const url = getUnstructuredEndpoint()
3265
const response = await axios.post(url, formData, {
3366
headers: {
3467
'Accept': 'application/json',
3568
'Content-Type': 'multipart/form-data',
3669
},
3770
});
38-
39-
const elems = response.data.filter((el:any) => typeof el?.text === 'string')
40-
if (!Array.isArray(elems)) {
41-
throw new Error(
42-
`Expected partitioning request to return an array, but got ${elems}`
43-
);
44-
}
45-
return elems
71+
return response.data
4672

4773
}

0 commit comments

Comments
 (0)