From 2dc2ae2113ea345569203dfa7c644f7a3e069d60 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Thu, 15 Jun 2023 06:39:14 -0700 Subject: [PATCH] Add new models (#75) * Add notice for vector store creation * Add new models to dropdown in settings and chat UI * Implement new set model logic * Disable claude models for now because of CORS * Add working version of AIState with more models * Make the button force rebuild index * Wrap the buttons when the view is narrow --- src/aiState.ts | 369 +++++++++++++++----- src/chainFactory.ts | 33 +- src/components/Chat.tsx | 32 +- src/components/ChatComponents/ChatIcons.tsx | 42 ++- src/constants.ts | 80 ++++- src/langchainStream.ts | 30 +- src/main.ts | 34 +- src/settings.ts | 148 ++++++-- src/utils.ts | 62 +++- styles.css | 1 + 10 files changed, 655 insertions(+), 176 deletions(-) diff --git a/src/aiState.ts b/src/aiState.ts index cb8905b4..12e3f5dc 100644 --- a/src/aiState.ts +++ b/src/aiState.ts @@ -1,22 +1,29 @@ import ChainFactory, { - LLM_CHAIN, - RETRIEVAL_QA_CHAIN + ChainType } from '@/chainFactory'; import { AI_SENDER, + ANTHROPIC, + AZURE_MODELS, + AZURE_OPENAI, + CLAUDE_MODELS, COHEREAI, DEFAULT_SYSTEM_PROMPT, HUGGINGFACE, OPENAI, + OPENAI_MODELS, USER_SENDER } from '@/constants'; import { ChatMessage } from '@/sharedState'; +import { getModelName, isSupportedChain } from '@/utils'; import { BaseChain, ConversationChain, ConversationalRetrievalQAChain, - RetrievalQAChain, + RetrievalQAChain } from "langchain/chains"; +import { ChatAnthropic } from 'langchain/chat_models/anthropic'; +import { BaseChatModel } from 'langchain/chat_models/base'; import { ChatOpenAI } from 'langchain/chat_models/openai'; import { VectorStore } from 'langchain/dist/vectorstores/base'; import { Embeddings } from "langchain/embeddings/base"; @@ -38,42 +45,93 @@ import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { Notice } from 'obsidian'; import { useState } from 'react'; + +interface ModelConfig { + modelName: string, + temperature: number, + streaming: boolean, + maxRetries: number, + maxConcurrency: number, + maxTokens?: number, + openAIApiKey?: string, + anthropicApiKey?: string, + azureOpenAIApiKey?: string, + azureOpenAIApiInstanceName?: string, + azureOpenAIApiDeploymentName?: string, + azureOpenAIApiVersion?: string, +} + export interface LangChainParams { - openAiApiKey: string, + openAIApiKey: string, huggingfaceApiKey: string, cohereApiKey: string, + anthropicApiKey: string, + azureOpenAIApiKey: string, + azureOpenAIApiInstanceName: string, + azureOpenAIApiDeploymentName: string, + azureOpenAIApiVersion: string, model: string, + modelDisplayName: string, temperature: number, maxTokens: number, systemMessage: string, chatContextTurns: number, embeddingProvider: string, - chainType: string, + chainType: ChainType, // Default ChainType is set in main.ts getAIStateParams + options: SetChainOptions, } export interface SetChainOptions { prompt?: ChatPromptTemplate; - noteContent?: string | null; + noteContent?: string; + forceNewCreation?: boolean; } class AIState { - static chatOpenAI: ChatOpenAI; - static chain: BaseChain; - static retrievalChain: RetrievalQAChain; - static conversationalRetrievalChain: ConversationalRetrievalQAChain; + private static chatModel: BaseChatModel; + private static chatOpenAI: ChatOpenAI; + private static chatAnthropic: ChatAnthropic; + private static azureChatOpenAI: ChatOpenAI; + private static chain: BaseChain; + private static retrievalChain: RetrievalQAChain; + private static conversationalRetrievalChain: ConversationalRetrievalQAChain; + + private chatPrompt: ChatPromptTemplate; + private vectorStore: VectorStore; + memory: BufferWindowMemory; langChainParams: LangChainParams; - chatPrompt: ChatPromptTemplate; - vectorStore: VectorStore + modelMap: Record< + string, + { + hasApiKey: boolean; + AIConstructor: new (config: ModelConfig) => BaseChatModel; + vendor: string; + }>; constructor(langChainParams: LangChainParams) { this.langChainParams = langChainParams; + this.buildModelMap(); + this.initMemory(); + this.initChatPrompt(); + // This takes care of chain creation as well + this.setModel(this.langChainParams.modelDisplayName); + } + + private initMemory(): void { this.memory = new BufferWindowMemory({ k: this.langChainParams.chatContextTurns * 2, memoryKey: 'history', inputKey: 'input', returnMessages: true, }); + } + + private setNoteContent(noteContent: string): void { + this.langChainParams.options.noteContent = noteContent; + } + + private initChatPrompt(): void { this.chatPrompt = ChatPromptTemplate.fromPromptMessages([ SystemMessagePromptTemplate.fromTemplate( this.langChainParams.systemMessage @@ -81,23 +139,108 @@ class AIState { new MessagesPlaceholder("history"), HumanMessagePromptTemplate.fromTemplate("{input}"), ]); - this.createNewChain(LLM_CHAIN); } - clearChatMemory(): void { - console.log('clearing chat memory'); - this.memory.clear(); + private validateChainType(chainType: ChainType): void { + if (chainType === undefined || chainType === null) throw new Error('No chain type set'); } - setModel(newModel: string): void { - console.log('setting model to', newModel); - this.langChainParams.model = newModel; - AIState.chatOpenAI.modelName = newModel; + private validateChatModel(chatModel: BaseChatModel): void { + if (chatModel === undefined || chatModel === null) throw new Error('No chat model set'); + } + + private getModelConfig(chatModelProvider: string): ModelConfig { + const { + openAIApiKey, + anthropicApiKey, + azureOpenAIApiKey, + azureOpenAIApiInstanceName, + azureOpenAIApiDeploymentName, + azureOpenAIApiVersion, + model, + temperature, + maxTokens, + } = this.langChainParams; + + // Create a base configuration that applies to all models + let config: ModelConfig = { + modelName: model, + temperature: temperature, + streaming: true, + maxRetries: 3, + maxConcurrency: 3 + }; + + switch(chatModelProvider) { + case OPENAI: + config = { + ...config, + openAIApiKey, + maxTokens, + }; + break; + case ANTHROPIC: + config = { + ...config, + anthropicApiKey, + }; + break; + case AZURE_OPENAI: + config = { + ...config, + maxTokens, + azureOpenAIApiKey: azureOpenAIApiKey, + azureOpenAIApiInstanceName: azureOpenAIApiInstanceName, + azureOpenAIApiDeploymentName: azureOpenAIApiDeploymentName, + azureOpenAIApiVersion: azureOpenAIApiVersion, + }; + break; + } + + return config; + } + + private buildModelMap() { + const modelMap: Record< + string, + { + hasApiKey: boolean; + AIConstructor: new (config: ModelConfig) => BaseChatModel; + vendor: string; + } + > = {}; + + // Build modelMap + for (const modelDisplayNameKey of OPENAI_MODELS) { + modelMap[modelDisplayNameKey] = { + hasApiKey: Boolean(this.langChainParams.openAIApiKey), + AIConstructor: ChatOpenAI, + vendor: OPENAI, + }; + } + + for (const modelDisplayNameKey of CLAUDE_MODELS) { + modelMap[modelDisplayNameKey] = { + hasApiKey: Boolean(this.langChainParams.anthropicApiKey), + AIConstructor: ChatAnthropic, + vendor: ANTHROPIC, + }; + } + + for (const modelDisplayNameKey of AZURE_MODELS) { + modelMap[modelDisplayNameKey] = { + hasApiKey: Boolean(this.langChainParams.azureOpenAIApiKey), + AIConstructor: ChatOpenAI, + vendor: AZURE_OPENAI, + }; + } + + this.modelMap = modelMap; } getEmbeddingsAPI(): Embeddings { const OpenAIEmbeddingsAPI = new OpenAIEmbeddings({ - openAIApiKey: this.langChainParams.openAiApiKey, + openAIApiKey: this.langChainParams.openAIApiKey, maxRetries: 3, maxConcurrency: 3, timeout: 10000, @@ -127,61 +270,118 @@ class AIState { } } - /* Create a new chain, or update chain with new model */ - createNewChain( - chainType: string, - options?: SetChainOptions, - ): void { - const { - openAiApiKey, model, temperature, maxTokens, - } = this.langChainParams; + clearChatMemory(): void { + console.log('clearing chat memory'); + this.memory.clear(); + } - if (!openAiApiKey) { - new Notice( - 'No OpenAI API key provided. Please set it in Copilot settings, and restart the plugin.' - ); - return; + private setChatModel(modelDisplayName: string): void { + if (!this.modelMap.hasOwnProperty(modelDisplayName)) { + throw new Error(`No model found for: ${modelDisplayName}`); } - AIState.chatOpenAI = new ChatOpenAI({ - openAIApiKey: openAiApiKey, - modelName: model, - temperature: temperature, - maxTokens: maxTokens, - streaming: true, - maxRetries: 3, - maxConcurrency: 3, - }); + // Create and return the appropriate model + const selectedModel = this.modelMap[modelDisplayName]; + if (!selectedModel.hasApiKey) { + new Notice(`API key is not provided for the model: ${modelDisplayName}`); + console.error(`API key is not provided for the model: ${modelDisplayName}`); + } - this.setChain(chainType, options); + const modelConfig = this.getModelConfig(selectedModel.vendor); + + try { + const newModelInstance = new selectedModel.AIConstructor({ + ...modelConfig, + }); + + switch(selectedModel.vendor) { + case OPENAI: + AIState.chatOpenAI = newModelInstance as ChatOpenAI; + break; + case ANTHROPIC: + AIState.chatAnthropic = newModelInstance as ChatAnthropic; + break; + case AZURE_OPENAI: + AIState.azureChatOpenAI = newModelInstance as ChatOpenAI; + break; + } + + AIState.chatModel = newModelInstance; + } catch (error) { + console.error(error); + new Notice(`Error creating model: ${modelDisplayName}`); + } + } + + setModel(newModelDisplayName: string): void { + const newModel = getModelName(newModelDisplayName); + // model and model display name must be update at the same time! + this.langChainParams.model = newModel; + this.langChainParams.modelDisplayName = newModelDisplayName; + this.setChatModel(newModelDisplayName); + // Must update the chatModel for chain because ChainFactory always + // retrieves the old chain without the chatModel change if it exists! + // Create a new chain with the new chatModel + this.createChain( + this.langChainParams.chainType, + {...this.langChainParams.options, forceNewCreation: true}, + ) + console.log(`Setting model to ${newModelDisplayName}: ${newModel}`); + } + + /* Create a new chain, or update chain with new model */ + createChain( + chainType: ChainType, + options?: SetChainOptions, + ): void { + this.validateChainType(chainType); + try { + this.setChain(chainType, options); + } catch (error) { + new Notice('Error creating chain:', error); + console.error('Error creating chain:', error); + } } async setChain( - chainType: string, + chainType: ChainType, options: SetChainOptions = {}, ): Promise { + this.validateChatModel(AIState.chatModel); + this.validateChainType(chainType); + switch (chainType) { - case LLM_CHAIN: { - AIState.chain = ChainFactory.getLLMChain({ - llm: AIState.chatOpenAI, - memory: this.memory, - prompt: options.prompt || this.chatPrompt, - }) as ConversationChain; - this.langChainParams.chainType = LLM_CHAIN; - console.log('Set chain:', LLM_CHAIN); - break; - } - case RETRIEVAL_QA_CHAIN: { - if (!options.noteContent) { - console.error('No note content provided.'); - return; + case ChainType.LLM_CHAIN: { + if (options.forceNewCreation) { + AIState.chain = ChainFactory.createNewLLMChain({ + llm: AIState.chatModel, + memory: this.memory, + prompt: options.prompt || this.chatPrompt, + }) as ConversationChain; + } else { + AIState.chain = ChainFactory.getLLMChainFromMap({ + llm: AIState.chatModel, + memory: this.memory, + prompt: options.prompt || this.chatPrompt, + }) as ConversationChain; } + this.langChainParams.chainType = ChainType.LLM_CHAIN; + console.log('Set chain:', ChainType.LLM_CHAIN); + break; + } + case ChainType.RETRIEVAL_QA_CHAIN: { + if (!options.noteContent) { + new Notice('No note content provided'); + throw new Error('No note content provided'); + } + + this.setNoteContent(options.noteContent); const docHash = ChainFactory.getDocumentHash(options.noteContent); const vectorStore = ChainFactory.vectorStoreMap.get(docHash); if (vectorStore) { AIState.retrievalChain = RetrievalQAChain.fromLLM( - AIState.chatOpenAI, + AIState.chatModel, vectorStore.asRetriever(), ); console.log('Existing vector store for document hash: ', docHash); @@ -192,13 +392,13 @@ class AIState { return; } - const baseCompressor = LLMChainExtractor.fromLLM(AIState.chatOpenAI); + const baseCompressor = LLMChainExtractor.fromLLM(AIState.chatModel); const retriever = new ContextualCompressionRetriever({ baseCompressor, baseRetriever: this.vectorStore.asRetriever(), }); AIState.retrievalChain = RetrievalQAChain.fromLLM( - AIState.chatOpenAI, + AIState.chatModel, retriever, ); console.log( @@ -207,12 +407,12 @@ class AIState { ); } - this.langChainParams.chainType = RETRIEVAL_QA_CHAIN; - console.log('Set chain:', RETRIEVAL_QA_CHAIN); + this.langChainParams.chainType = ChainType.RETRIEVAL_QA_CHAIN; + console.log('Set chain:', ChainType.RETRIEVAL_QA_CHAIN); break; } default: - console.error('No chain type set.'); + this.validateChainType(chainType); break; } } @@ -231,9 +431,10 @@ class AIState { ); ChainFactory.setVectorStore(this.vectorStore, docHash); console.log('Vector store created successfully.'); + new Notice('Vector store created successfully.'); } catch (error) { new Notice('Failed to create vector store, please try again:', error); - console.log('Failed to create vector store, please try again.:', error); + console.error('Failed to create vector store, please try again.:', error); } } @@ -241,7 +442,7 @@ class AIState { return AIState.chatOpenAI.getNumTokens(inputStr); } - async runChatOpenAI( + async runChatModel( userMessage: ChatMessage, chatContext: ChatMessage[], abortController: AbortController, @@ -250,6 +451,7 @@ class AIState { debug = false, ) { if (debug) { + console.log('chatModel:', AIState.chatModel); for (const [i, chatMessage] of chatContext.entries()) { console.log( `chat message ${i}:\nsender: ${chatMessage.sender}\n${chatMessage.message}` @@ -269,7 +471,7 @@ class AIState { ]; let fullAIResponse = ''; - await AIState.chatOpenAI.call( + await AIState.chatModel.call( messages, { signal: abortController.signal }, [ @@ -293,17 +495,26 @@ class AIState { async runChain( userMessage: string, - chatContext: ChatMessage[], abortController: AbortController, updateCurrentAiMessage: (message: string) => void, addMessage: (message: ChatMessage) => void, debug = false, ) { + // Check if chain is initialized properly + if (!isSupportedChain(AIState.chain)) { + console.error( + 'Chain is not initialized properly, re-initializing chain: ', + this.langChainParams.chainType + ); + this.setChain(this.langChainParams.chainType, this.langChainParams.options); + } + let fullAIResponse = ''; try { switch(this.langChainParams.chainType) { - case LLM_CHAIN: + case ChainType.LLM_CHAIN: if (debug) { + console.log('chain:', AIState.chain); console.log('Chat memory:', this.memory); } await AIState.chain.call( @@ -321,7 +532,7 @@ class AIState { ] ); break; - case RETRIEVAL_QA_CHAIN: + case ChainType.RETRIEVAL_QA_CHAIN: if (debug) { console.log('embedding provider:', this.langChainParams.embeddingProvider); } @@ -367,13 +578,13 @@ export function useAIState( ): [ string, (model: string) => void, - string, - (chain: string, options?: SetChainOptions) => void, + ChainType, + (chain: ChainType, options?: SetChainOptions) => void, () => void, ] { const { langChainParams } = aiState; - const [currentModel, setCurrentModel] = useState(langChainParams.model); - const [currentChain, setCurrentChain] = useState(langChainParams.chainType); + const [currentModel, setCurrentModel] = useState(langChainParams.modelDisplayName); + const [currentChain, setCurrentChain] = useState(langChainParams.chainType); const [, setChatMemory] = useState(aiState.memory); const clearChatMemory = () => { @@ -381,12 +592,12 @@ export function useAIState( setChatMemory(aiState.memory); }; - const setModel = (newModel: string) => { - aiState.setModel(newModel); - setCurrentModel(newModel); + const setModel = (newModelDisplayName: string) => { + aiState.setModel(newModelDisplayName); + setCurrentModel(newModelDisplayName); }; - const setChain = (newChain: string, options?: SetChainOptions) => { + const setChain = (newChain: ChainType, options?: SetChainOptions) => { aiState.setChain(newChain, options); setCurrentChain(newChain); }; diff --git a/src/chainFactory.ts b/src/chainFactory.ts index dccc66cd..d06b735a 100644 --- a/src/chainFactory.ts +++ b/src/chainFactory.ts @@ -28,30 +28,33 @@ export interface ConversationalRetrievalChainParams { } } -// Add new chain types here -export const LLM_CHAIN = 'llm_chain'; // Issue where conversational retrieval chain gives rephrased question // when streaming: https://github.com/hwchase17/langchainjs/issues/754#issuecomment-1540257078 // Temp workaround triggers CORS issue 'refused to set header user-agent' -// TODO: Wait for official fix and use conversational retrieval chain instead of retrieval qa. -export const CONVERSATIONAL_RETRIEVAL_QA_CHAIN = 'conversational_retrieval_chain'; -export const RETRIEVAL_QA_CHAIN = 'retrieval_qa'; -export const SUPPORTED_CHAIN_TYPES = new Set([ - LLM_CHAIN, - RETRIEVAL_QA_CHAIN, - CONVERSATIONAL_RETRIEVAL_QA_CHAIN, -]); + +// Add new chain types here +export enum ChainType { + LLM_CHAIN = 'llm_chain', + RETRIEVAL_QA_CHAIN = 'retrieval_qa', + // TODO: Wait for official fix and use conversational retrieval chain instead of retrieval qa. + CONVERSATIONAL_RETRIEVAL_QA_CHAIN = 'conversational_retrieval_chain', +} class ChainFactory { public static instances: Map = new Map(); public static vectorStoreMap: Map = new Map(); - public static getLLMChain(args: LLMChainInput): BaseChain { - let instance = ChainFactory.instances.get(LLM_CHAIN); + public static createNewLLMChain(args: LLMChainInput): BaseChain { + const instance = new ConversationChain(args as LLMChainInput); + console.log('New chain created: ', instance._chainType()); + ChainFactory.instances.set(ChainType.LLM_CHAIN, instance); + return instance; + } + + public static getLLMChainFromMap(args: LLMChainInput): BaseChain { + let instance = ChainFactory.instances.get(ChainType.LLM_CHAIN); if (!instance) { - instance = new ConversationChain(args as LLMChainInput); - console.log('New chain created: ', instance._chainType()); - ChainFactory.instances.set(LLM_CHAIN, instance); + instance = ChainFactory.createNewLLMChain(args); } return instance; } diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index bce19229..2e1591db 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -1,5 +1,5 @@ import AIState, { useAIState } from '@/aiState'; -import ChainFactory, { RETRIEVAL_QA_CHAIN } from '@/chainFactory'; +import ChainFactory, { ChainType } from '@/chainFactory'; import ChatIcons from '@/components/ChatComponents/ChatIcons'; import ChatInput from '@/components/ChatComponents/ChatInput'; import ChatMessages from '@/components/ChatComponents/ChatMessages'; @@ -121,7 +121,7 @@ const Chat: React.FC = ({ } }; - const useActiveNoteAsContext = async () => { + const forceRebuildActiveNoteContext = async () => { if (!app) { console.error('App instance is not available.'); return; @@ -141,26 +141,16 @@ const Chat: React.FC = ({ return; } - let activeNoteOnMessage: ChatMessage; const docHash = ChainFactory.getDocumentHash(noteContent); - const vectorStore = ChainFactory.vectorStoreMap.get(docHash); - if (vectorStore) { - activeNoteOnMessage = { - sender: AI_SENDER, - message: `I have read [[${noteName}]].\n\n Please switch to "QA: Active Note" in Mode Selection to ask questions about it.`, - isVisible: true, - }; - } else { - await aiState.buildIndex(noteContent, docHash); - activeNoteOnMessage = { - sender: AI_SENDER, - message: `Reading [[${noteName}]]...\n\n Please switch to "QA: Active Note" in Mode Selection to ask questions about it.`, - isVisible: true, - }; - } + await aiState.buildIndex(noteContent, docHash); + const activeNoteOnMessage: ChatMessage = { + sender: AI_SENDER, + message: `Reading [[${noteName}]]...\n\n Please switch to "QA: Active Note" in Mode Selection to ask questions about it.`, + isVisible: true, + }; - if (currentChain === RETRIEVAL_QA_CHAIN) { - setChain(RETRIEVAL_QA_CHAIN, { noteContent }); + if (currentChain === ChainType.RETRIEVAL_QA_CHAIN) { + setChain(ChainType.RETRIEVAL_QA_CHAIN, { noteContent }); } addMessage(activeNoteOnMessage); @@ -312,7 +302,7 @@ const Chat: React.FC = ({ } } onSaveAsNote={handleSaveAsNote} - onUseActiveNoteAsContext={useActiveNoteAsContext} + onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext} addMessage={addMessage} /> void; - currentChain: string; - setCurrentChain: (chain: string, options?: SetChainOptions) => void; + currentChain: ChainType; + setCurrentChain: (chain: ChainType, options?: SetChainOptions) => void; onStopGenerating: () => void; onNewChat: () => void; onSaveAsNote: () => void; - onUseActiveNoteAsContext: () => void; + onForceRebuildActiveNoteContext: () => void; addMessage: (message: ChatMessage) => void; } @@ -41,22 +45,22 @@ const ChatIcons: React.FC = ({ onStopGenerating, onNewChat, onSaveAsNote, - onUseActiveNoteAsContext, + onForceRebuildActiveNoteContext, addMessage, }) => { - const [selectedChain, setSelectedChain] = useState(currentChain); + const [selectedChain, setSelectedChain] = useState(currentChain); const handleModelChange = (event: React.ChangeEvent) => { setCurrentModel(event.target.value); }; const handleChainChange = async (event: React.ChangeEvent) => { - setSelectedChain(event.target.value); + setSelectedChain(stringToChainType(event.target.value)); } useEffect(() => { const handleRetrievalQAChain = async () => { - if (selectedChain !== RETRIEVAL_QA_CHAIN) { + if (selectedChain !== ChainType.RETRIEVAL_QA_CHAIN) { setCurrentChain(selectedChain); return; } @@ -81,8 +85,9 @@ const ChatIcons: React.FC = ({ isVisible: true, }; addMessage(activeNoteOnMessage); - - setCurrentChain(selectedChain, { noteContent }); + if (noteContent) { + setCurrentChain(selectedChain, { noteContent }); + } }; handleRetrievalQAChain(); @@ -98,8 +103,17 @@ const ChatIcons: React.FC = ({ value={currentModel} onChange={handleModelChange} > - - + + + + + {/* + + + */} + + + Model Selection @@ -130,7 +144,7 @@ const ChatIcons: React.FC = ({ Mode Selection - diff --git a/src/constants.ts b/src/constants.ts index 2ec264c6..d2d742c0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,10 +5,78 @@ export const USER_SENDER = 'user'; export const AI_SENDER = 'ai'; export const DEFAULT_SYSTEM_PROMPT = 'You are Obsidian Copilot, a helpful assistant that integrates AI to Obsidian note-taking.'; -// Embedding Providers +export enum ChatModels { + GPT_35_TURBO = 'gpt-3.5-turbo', + GPT_35_TURBO_16K = 'gpt-3.5-turbo-16k', + GPT_4 = 'gpt-4', + GPT_4_32K = 'gpt-4-32k', + CLAUDE_1 = 'claude-1', + CLAUDE_1_100K = 'claude-1-100k', + CLAUDE_INSTANT_1 = 'claude-instant-1', + CLAUDE_INSTANT_1_100K = 'claude-instant-1-100k', + AZURE_GPT_35_TURBO = 'gpt-35-turbo', +} + +export enum ChatModelDisplayNames { + GPT_35_TURBO = 'GPT-3.5', + GPT_35_TURBO_16K = 'GPT-3.5 16K', + GPT_4 = 'GPT-4', + GPT_4_32K = 'GPT-4 32K', + CLAUDE_1 = 'CLAUDE-1', + CLAUDE_1_100K = 'CLAUDE-1-100K', + CLAUDE_INSTANT_1 = 'CLAUDE-INSTANT', + CLAUDE_INSTANT_1_100K = 'CLAUDE-INSTANT-100K', + AZURE_GPT_35_TURBO = 'AZURE GPT-3.5', + AZURE_GPT_4 = 'AZURE GPT-4', + AZURE_GPT_4_32K = 'AZURE GPT-4 32K', +} + +export const OPENAI_MODELS = new Set([ + ChatModelDisplayNames.GPT_35_TURBO, + ChatModelDisplayNames.GPT_35_TURBO_16K, + ChatModelDisplayNames.GPT_4, + ChatModelDisplayNames.GPT_4_32K, +]); + +export const AZURE_MODELS = new Set([ + ChatModelDisplayNames.AZURE_GPT_35_TURBO, + ChatModelDisplayNames.AZURE_GPT_4, + ChatModelDisplayNames.AZURE_GPT_4_32K, +]); + +export const CLAUDE_MODELS = new Set([ + ChatModelDisplayNames.CLAUDE_1, + ChatModelDisplayNames.CLAUDE_1_100K, + ChatModelDisplayNames.CLAUDE_INSTANT_1, + ChatModelDisplayNames.CLAUDE_INSTANT_1_100K, +]); + +export const DISPLAY_NAME_TO_MODEL: Record = { + [ChatModelDisplayNames.GPT_35_TURBO]: ChatModels.GPT_35_TURBO, + [ChatModelDisplayNames.GPT_35_TURBO_16K]: ChatModels.GPT_35_TURBO_16K, + [ChatModelDisplayNames.GPT_4]: ChatModels.GPT_4, + [ChatModelDisplayNames.GPT_4_32K]: ChatModels.GPT_4_32K, + [ChatModelDisplayNames.CLAUDE_1]: ChatModels.CLAUDE_1, + [ChatModelDisplayNames.CLAUDE_1_100K]: ChatModels.CLAUDE_1_100K, + [ChatModelDisplayNames.CLAUDE_INSTANT_1]: ChatModels.CLAUDE_INSTANT_1, + [ChatModelDisplayNames.CLAUDE_INSTANT_1_100K]: ChatModels.CLAUDE_INSTANT_1_100K, + [ChatModelDisplayNames.AZURE_GPT_35_TURBO]: ChatModels.AZURE_GPT_35_TURBO, + [ChatModelDisplayNames.AZURE_GPT_4]: ChatModels.GPT_4, + [ChatModelDisplayNames.AZURE_GPT_4_32K]: ChatModels.GPT_4_32K, +}; + +// Model Providers export const OPENAI = 'openai'; export const HUGGINGFACE = 'huggingface'; export const COHEREAI = 'cohereai'; +export const AZURE_OPENAI = 'azure_openai'; +export const ANTHROPIC = 'anthropic'; + +export const VENDOR_MODELS: Record> = { + [OPENAI]: OPENAI_MODELS, + [AZURE_OPENAI]: AZURE_MODELS, + [ANTHROPIC]: CLAUDE_MODELS, +}; // Embedding Models export const DISTILBERT_NLI = 'sentence-transformers/distilbert-base-nli-mean-tokens'; @@ -16,10 +84,16 @@ export const INSTRUCTOR_XL = 'hkunlp/instructor-xl'; // Inference API is off for export const MPNET_V2 = 'sentence-transformers/all-mpnet-base-v2'; // Inference API returns 400 export const DEFAULT_SETTINGS: CopilotSettings = { - openAiApiKey: '', + openAIApiKey: '', huggingfaceApiKey: '', cohereApiKey: '', - defaultModel: 'gpt-3.5-turbo', + anthropicApiKey: '', + azureOpenAIApiKey: '', + azureOpenAIApiInstanceName: '', + azureOpenAIApiDeploymentName: '', + azureOpenAIApiVersion: '', + defaultModel: ChatModels.GPT_35_TURBO_16K, + defaultModelDisplayName: ChatModelDisplayNames.GPT_35_TURBO_16K, temperature: 0.7, maxTokens: 1000, contextTurns: 3, diff --git a/src/langchainStream.ts b/src/langchainStream.ts index d1f0044a..84eb57c5 100644 --- a/src/langchainStream.ts +++ b/src/langchainStream.ts @@ -14,19 +14,12 @@ export const getAIResponse = async ( debug = false, ) => { const { - openAiApiKey, model, temperature, maxTokens, systemMessage, chatContextTurns, } = aiState.langChainParams; - if (!openAiApiKey) { - new Notice( - 'No OpenAI API key provided. Please set it in Copilot settings, and restart the plugin.' - ); - return; - } const abortController = new AbortController(); @@ -42,12 +35,19 @@ export const getAIResponse = async ( ); } - await aiState.runChain( - userMessage.message, - chatContext, - abortController, - updateCurrentAiMessage, - addMessage, - debug, - ); + try { + // TODO: Need to run certain models without langchain + // it will mean no retrieval qa mode for those models! + + await aiState.runChain( + userMessage.message, + abortController, + updateCurrentAiMessage, + addMessage, + debug, + ); + } catch (error) { + console.error('Model request failed:', error); + new Notice('Model request failed:', error); + } }; diff --git a/src/main.ts b/src/main.ts index fd72b4f4..7a19ac1e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ -import AIState, { LangChainParams } from '@/aiState'; -import { LLM_CHAIN } from '@/chainFactory'; +import AIState, { LangChainParams, SetChainOptions } from '@/aiState'; +import { ChainType } from '@/chainFactory'; import { AddPromptModal } from "@/components/AddPromptModal"; import CopilotView from '@/components/CopilotView'; import { LanguageModal } from "@/components/LanguageModal"; @@ -16,10 +16,16 @@ import PouchDB from 'pouchdb'; export interface CopilotSettings { - openAiApiKey: string; + openAIApiKey: string; huggingfaceApiKey: string; cohereApiKey: string; + anthropicApiKey: string; + azureOpenAIApiKey: string; + azureOpenAIApiInstanceName: string; + azureOpenAIApiDeploymentName: string; + azureOpenAIApiVersion: string; defaultModel: string; + defaultModelDisplayName: string; temperature: number; maxTokens: number; contextTurns: number; @@ -407,25 +413,37 @@ export default class CopilotPlugin extends Plugin { getAIStateParams(): LangChainParams { const { - openAiApiKey, + openAIApiKey, huggingfaceApiKey, cohereApiKey, + anthropicApiKey, + azureOpenAIApiKey, + azureOpenAIApiInstanceName, + azureOpenAIApiDeploymentName, + azureOpenAIApiVersion, temperature, maxTokens, contextTurns, embeddingProvider, } = sanitizeSettings(this.settings); return { - openAiApiKey: openAiApiKey, - huggingfaceApiKey: huggingfaceApiKey, - cohereApiKey: cohereApiKey, + openAIApiKey, + huggingfaceApiKey, + cohereApiKey, + anthropicApiKey, + azureOpenAIApiKey, + azureOpenAIApiInstanceName, + azureOpenAIApiDeploymentName, + azureOpenAIApiVersion, model: this.settings.defaultModel, + modelDisplayName: this.settings.defaultModelDisplayName, temperature: Number(temperature), maxTokens: Number(maxTokens), systemMessage: DEFAULT_SYSTEM_PROMPT || this.settings.userSystemPrompt, chatContextTurns: Number(contextTurns), embeddingProvider: embeddingProvider, - chainType: LLM_CHAIN, + chainType: ChainType.LLM_CHAIN, // Set LLM_CHAIN as default ChainType + options: { forceNewCreation: true } as SetChainOptions, }; } } diff --git a/src/settings.ts b/src/settings.ts index 5b5f2341..32641e45 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,11 @@ -import { COHEREAI, DEFAULT_SETTINGS, HUGGINGFACE, OPENAI } from "@/constants"; +import { + COHEREAI, + ChatModelDisplayNames, + ChatModels, + DEFAULT_SETTINGS, + HUGGINGFACE, + OPENAI +} from "@/constants"; import CopilotPlugin from "@/main"; import { App, DropdownComponent, Notice, PluginSettingTab, Setting } from "obsidian"; @@ -30,7 +37,37 @@ export class CopilotSettingTab extends PluginSettingTab { { text: 'Please reload the plugin when you change any setting below.' } ); - containerEl.createEl('h4', { text: 'OpenAI API Settings' }); + new Setting(containerEl) + .setName("Default Model") + .setDesc( + createFragment((frag) => { + frag.appendText("The default model to use, only takes effect when you "); + frag.createEl('strong', { text: "restart the plugin" }); + }) + ) + .addDropdown((dropdown: DropdownComponent) => { + dropdown + .addOption(ChatModels.GPT_35_TURBO, ChatModelDisplayNames.GPT_35_TURBO) + .addOption(ChatModels.GPT_35_TURBO_16K, ChatModelDisplayNames.GPT_35_TURBO_16K) + .addOption(ChatModels.GPT_4, ChatModelDisplayNames.GPT_4) + .addOption(ChatModels.GPT_4_32K, ChatModelDisplayNames.GPT_4_32K) + // .addOption(ChatModels.CLAUDE_1, ChatModelDisplayNames.CLAUDE_1) + // .addOption(ChatModels.CLAUDE_1_100K, ChatModelDisplayNames.CLAUDE_1_100K) + // .addOption(ChatModels.CLAUDE_INSTANT_1, ChatModelDisplayNames.CLAUDE_INSTANT_1) + // .addOption(ChatModels.CLAUDE_INSTANT_1_100K, ChatModelDisplayNames.CLAUDE_INSTANT_1_100K) + .addOption(ChatModels.AZURE_GPT_35_TURBO, ChatModelDisplayNames.AZURE_GPT_35_TURBO) + .addOption(ChatModels.GPT_4, ChatModelDisplayNames.AZURE_GPT_4) + .addOption(ChatModels.GPT_4_32K, ChatModelDisplayNames.AZURE_GPT_4_32K) + .setValue(this.plugin.settings.defaultModel) + .onChange(async (value: string) => { + this.plugin.settings.defaultModel = value; + this.plugin.settings.defaultModelDisplayName = ChatModelDisplayNames[value as keyof typeof ChatModels]; + await this.plugin.saveSettings(); + }); + }); + + containerEl.createEl('h4', { text: 'API Settings' }); + containerEl.createEl('h6', { text: 'OpenAI API' }); new Setting(containerEl) .setName("Your OpenAI API key") @@ -57,32 +94,103 @@ export class CopilotSettingTab extends PluginSettingTab { text.inputEl.style.width = "100%"; text .setPlaceholder("OpenAI API key") - .setValue(this.plugin.settings.openAiApiKey) + .setValue(this.plugin.settings.openAIApiKey) .onChange(async (value) => { - this.plugin.settings.openAiApiKey = value; + this.plugin.settings.openAIApiKey = value; + await this.plugin.saveSettings(); + }) + } + ); + + // containerEl.createEl('h6', { text: 'Anthropic' }); + + // new Setting(containerEl) + // .setName("Your Anthropic API key") + // .setDesc( + // createFragment((frag) => { + // frag.appendText("This is for Claude models. Sign up on their waitlist if you don't have access."); + // frag.createEl('a', { + // text: "https://docs.anthropic.com/claude/docs/getting-access-to-claude", + // href: "https://docs.anthropic.com/claude/docs/getting-access-to-claude" + // }); + // }) + // ) + // .addText((text) => { + // text.inputEl.type = "password"; + // text.inputEl.style.width = "100%"; + // text + // .setPlaceholder("Anthropic API key") + // .setValue(this.plugin.settings.anthropicApiKey) + // .onChange(async (value) => { + // this.plugin.settings.anthropicApiKey = value; + // await this.plugin.saveSettings(); + // }) + // } + // ); + + containerEl.createEl('h6', { text: 'Azure OpenAI API' }); + + new Setting(containerEl) + .setName("Your Azure OpenAI API key") + .setDesc( + createFragment((frag) => { + frag.appendText("This is for Azure OpenAI APIs. Sign up on their waitlist if you don't have access."); + }) + ) + .addText((text) => { + text.inputEl.type = "password"; + text.inputEl.style.width = "100%"; + text + .setPlaceholder("Azure OpenAI API key") + .setValue(this.plugin.settings.azureOpenAIApiKey) + .onChange(async (value) => { + this.plugin.settings.azureOpenAIApiKey = value; await this.plugin.saveSettings(); }) } ); new Setting(containerEl) - .setName("Default Model") - .setDesc( - createFragment((frag) => { - frag.appendText("The default model to use, only takes effect when you "); - frag.createEl('strong', { text: "restart the plugin" }); - }) - ) - .addDropdown((dropdown: DropdownComponent) => { - dropdown - .addOption('gpt-3.5-turbo', 'GPT-3.5') - .addOption('gpt-4', 'GPT-4') - .setValue(this.plugin.settings.defaultModel) - .onChange(async (value: string) => { - this.plugin.settings.defaultModel = value; + .setName("Your Azure OpenAI instance name") + .addText((text) => { + text.inputEl.style.width = "100%"; + text + .setPlaceholder("Azure OpenAI instance name") + .setValue(this.plugin.settings.azureOpenAIApiInstanceName) + .onChange(async (value) => { + this.plugin.settings.azureOpenAIApiInstanceName = value; await this.plugin.saveSettings(); - }); - }); + }) + } + ); + + new Setting(containerEl) + .setName("Your Azure OpenAI deployment name") + .addText((text) => { + text.inputEl.style.width = "100%"; + text + .setPlaceholder("Azure OpenAI deployment name") + .setValue(this.plugin.settings.azureOpenAIApiDeploymentName) + .onChange(async (value) => { + this.plugin.settings.azureOpenAIApiDeploymentName = value; + await this.plugin.saveSettings(); + }) + } + ); + + new Setting(containerEl) + .setName("Your Azure OpenAI API version") + .addText((text) => { + text.inputEl.style.width = "100%"; + text + .setPlaceholder("Azure OpenAI API version") + .setValue(this.plugin.settings.azureOpenAIApiVersion) + .onChange(async (value) => { + this.plugin.settings.azureOpenAIApiVersion = value; + await this.plugin.saveSettings(); + }) + } + ); containerEl.createEl( 'h6', diff --git a/src/utils.ts b/src/utils.ts index d98b18fa..55b55ff9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,69 @@ -import { DEFAULT_SETTINGS, USER_SENDER } from '@/constants'; +import { ChainType } from '@/chainFactory'; +import { + ANTHROPIC, + AZURE_MODELS, + AZURE_OPENAI, + CLAUDE_MODELS, + DEFAULT_SETTINGS, + DISPLAY_NAME_TO_MODEL, + OPENAI, + OPENAI_MODELS, + USER_SENDER, +} from '@/constants'; import { CopilotSettings } from '@/main'; import { ChatMessage } from '@/sharedState'; +import { + BaseChain, + LLMChain, + RetrievalQAChain +} from "langchain/chains"; import moment from 'moment'; import { TFile } from 'obsidian'; +export const stringToChainType = (chain: string): ChainType => { + switch(chain) { + case 'llm_chain': + return ChainType.LLM_CHAIN; + case 'retrieval_qa': + return ChainType.RETRIEVAL_QA_CHAIN; + default: + throw new Error(`Unknown chain type: ${chain}`); + } +} + +export const isLLMChain = (chain: BaseChain): chain is LLMChain => { + return 'llm' in chain && chain.llm !== undefined; + } + +export const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => { + return 'retriever' in chain && chain.retriever !== undefined; + } + +export const isSupportedChain = (chain: BaseChain): chain is BaseChain => { + return isLLMChain(chain) || isRetrievalQAChain(chain); + } + +export const getModelName = (modelDisplayName: string): string => { + return DISPLAY_NAME_TO_MODEL[modelDisplayName]; +} + +export const getModelVendorMap = (): Record => { + const model_to_vendor: Record = {}; + + for (const model of OPENAI_MODELS) { + model_to_vendor[model] = OPENAI; + } + + for (const model of AZURE_MODELS) { + model_to_vendor[model] = AZURE_OPENAI; + } + + for (const model of CLAUDE_MODELS) { + model_to_vendor[model] = ANTHROPIC; + } + return model_to_vendor; +} + // Returns the last N messages from the chat history, // last one being the newest ai message export const getChatContext = (chatHistory: ChatMessage[], contextSize: number) => { diff --git a/styles.css b/styles.css index 12d39c4e..2923f0e0 100644 --- a/styles.css +++ b/styles.css @@ -33,6 +33,7 @@ If your plugin does not need CSS, delete this file. padding: 8px 8px 0; order: 1; gap: 8px; + flex-wrap: wrap; } .icon-scaler {