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
This commit is contained in:
Logan Yang 2023-06-15 06:39:14 -07:00 committed by GitHub
parent 4a6e427eda
commit 2dc2ae2113
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 655 additions and 176 deletions

View file

@ -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<void> {
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<string>(langChainParams.model);
const [currentChain, setCurrentChain] = useState<string>(langChainParams.chainType);
const [currentModel, setCurrentModel] = useState<string>(langChainParams.modelDisplayName);
const [currentChain, setCurrentChain] = useState<ChainType>(langChainParams.chainType);
const [, setChatMemory] = useState<BufferWindowMemory | null>(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);
};

View file

@ -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<string, BaseChain> = new Map();
public static vectorStoreMap: Map<string, VectorStore> = 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;
}

View file

@ -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<ChatProps> = ({
}
};
const useActiveNoteAsContext = async () => {
const forceRebuildActiveNoteContext = async () => {
if (!app) {
console.error('App instance is not available.');
return;
@ -141,26 +141,16 @@ const Chat: React.FC<ChatProps> = ({
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<ChatProps> = ({
}
}
onSaveAsNote={handleSaveAsNote}
onUseActiveNoteAsContext={useActiveNoteAsContext}
onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext}
addMessage={addMessage}
/>
<ChatInput

View file

@ -1,5 +1,8 @@
import { SetChainOptions } from '@/aiState';
import { AI_SENDER } from '@/constants';
import {
AI_SENDER,
ChatModelDisplayNames,
} from '@/constants';
import {
ChatMessage
} from '@/sharedState';
@ -13,23 +16,24 @@ import {
useState,
} from 'react';
import { RETRIEVAL_QA_CHAIN } from '@/chainFactory';
import { ChainType } from '@/chainFactory';
import {
RefreshIcon, SaveAsNoteIcon,
StopIcon,
UseActiveNoteAsContextIcon
} from '@/components/Icons';
import { stringToChainType } from '@/utils';
import React from 'react';
interface ChatIconsProps {
currentModel: string;
setCurrentModel: (model: string) => 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<ChatIconsProps> = ({
onStopGenerating,
onNewChat,
onSaveAsNote,
onUseActiveNoteAsContext,
onForceRebuildActiveNoteContext,
addMessage,
}) => {
const [selectedChain, setSelectedChain] = useState<string>(currentChain);
const [selectedChain, setSelectedChain] = useState<ChainType>(currentChain);
const handleModelChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setCurrentModel(event.target.value);
};
const handleChainChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
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<ChatIconsProps> = ({
isVisible: true,
};
addMessage(activeNoteOnMessage);
setCurrentChain(selectedChain, { noteContent });
if (noteContent) {
setCurrentChain(selectedChain, { noteContent });
}
};
handleRetrievalQAChain();
@ -98,8 +103,17 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
value={currentModel}
onChange={handleModelChange}
>
<option value='gpt-3.5-turbo'>GPT-3.5</option>
<option value='gpt-4'>GPT-4</option>
<option value={ChatModelDisplayNames.GPT_35_TURBO}>{ChatModelDisplayNames.GPT_35_TURBO}</option>
<option value={ChatModelDisplayNames.GPT_35_TURBO_16K}>{ChatModelDisplayNames.GPT_35_TURBO_16K}</option>
<option value={ChatModelDisplayNames.GPT_4}>{ChatModelDisplayNames.GPT_4}</option>
<option value={ChatModelDisplayNames.GPT_4_32K}>{ChatModelDisplayNames.GPT_4_32K}</option>
{/* <option value={ChatModelDisplayNames.CLAUDE_1}>{ChatModelDisplayNames.CLAUDE_1}</option>
<option value={ChatModelDisplayNames.CLAUDE_1_100K}>{ChatModelDisplayNames.CLAUDE_1_100K}</option>
<option value={ChatModelDisplayNames.CLAUDE_INSTANT_1}>{ChatModelDisplayNames.CLAUDE_INSTANT_1}</option>
<option value={ChatModelDisplayNames.CLAUDE_INSTANT_1_100K}>{ChatModelDisplayNames.CLAUDE_INSTANT_1_100K}</option> */}
<option value={ChatModelDisplayNames.AZURE_GPT_35_TURBO}>{ChatModelDisplayNames.AZURE_GPT_35_TURBO}</option>
<option value={ChatModelDisplayNames.AZURE_GPT_4}>{ChatModelDisplayNames.AZURE_GPT_4}</option>
<option value={ChatModelDisplayNames.AZURE_GPT_4_32K}>{ChatModelDisplayNames.AZURE_GPT_4_32K}</option>
</select>
<span className="tooltip-text">Model Selection</span>
</div>
@ -130,7 +144,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
<span className="tooltip-text">Mode Selection</span>
</div>
</div>
<button className='chat-icon-button' onClick={onUseActiveNoteAsContext}>
<button className='chat-icon-button' onClick={onForceRebuildActiveNoteContext}>
<UseActiveNoteAsContextIcon className='icon-scaler' />
<span className="tooltip-text">Rebuild Index for Active Note</span>
</button>

View file

@ -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<string, string> = {
[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<string, Set<string>> = {
[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,

View file

@ -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);
}
};

View file

@ -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,
};
}
}

View file

@ -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',

View file

@ -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<string, string> => {
const model_to_vendor: Record<string, string> = {};
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) => {

View file

@ -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 {