From b119349dd9162fb930d9a6b010485a2b082b525d Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Mon, 4 Mar 2024 22:00:32 -0800 Subject: [PATCH] Add claude 3 and proxy server (#329) --- package-lock.json | 37 ++++ package.json | 1 + src/LLMProviders/chatModelManager.ts | 11 ++ src/aiParams.ts | 2 + src/components/ChatComponents/ChatIcons.tsx | 181 +++++++++++++------- src/constants.ts | 9 + src/main.ts | 2 + src/proxyServer.ts | 65 +++++++ src/settings/SettingsPage.tsx | 1 + src/settings/components/ApiSettings.tsx | 35 ++++ src/settings/components/SettingsMain.tsx | 9 + 11 files changed, 289 insertions(+), 64 deletions(-) create mode 100644 src/proxyServer.ts diff --git a/package-lock.json b/package-lock.json index 401be9e8..0eaeafd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@huggingface/inference": "^2.6.4", "@koa/cors": "^5.0.0", + "@langchain/anthropic": "^0.1.1", "@langchain/cohere": "^0.0.2", "@langchain/community": "^0.0.16", "@langchain/google-genai": "^0.0.7", @@ -1863,6 +1864,42 @@ "node": ">= 14.0.0" } }, + "node_modules/@langchain/anthropic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.1.1.tgz", + "integrity": "sha512-xt66bysyOp+Y3gWog/j6kTwi8GkLnEp7oacF+Z124mKZAO0+lKMxG4xGv4rNwCPjbY3ziYR01E6UV+TVgCFcbQ==", + "dependencies": { + "@anthropic-ai/sdk": "^0.15.0", + "@langchain/core": "~0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/anthropic/node_modules/@anthropic-ai/sdk": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.15.0.tgz", + "integrity": "sha512-QMNEFcwGGB64oEIL+U9b+mxSbat5TCdNxvQVV0qCNGQvg/nlnbOmq2/x/0mKhuKD0n5bioL75oCkTbQaAgyYtw==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/@langchain/anthropic/node_modules/@types/node": { + "version": "18.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.21.tgz", + "integrity": "sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@langchain/cohere": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/@langchain/cohere/-/cohere-0.0.2.tgz", diff --git a/package.json b/package.json index e3e93eb4..424fe258 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "@huggingface/inference": "^2.6.4", "@koa/cors": "^5.0.0", + "@langchain/anthropic": "^0.1.1", "@langchain/cohere": "^0.0.2", "@langchain/community": "^0.0.16", "@langchain/google-genai": "^0.0.7", diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 6f6725b5..9e27200e 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -1,5 +1,6 @@ import { LangChainParams, ModelConfig } from '@/aiParams'; import { + ANTHROPIC_MODELS, AZURE_MODELS, GOOGLE_MODELS, LM_STUDIO_MODELS, @@ -7,10 +8,12 @@ import { OLLAMA_MODELS, OPENAI_MODELS, OPENROUTERAI_MODELS, + PROXY_SERVER_PORT, } from '@/constants'; import EncryptionService from '@/encryptionService'; import { ProxyChatOpenAI } from '@/langchainWrappers'; import { getModelName } from '@/utils'; +import { ChatAnthropic } from "@langchain/anthropic"; import { ChatOllama } from "@langchain/community/chat_models/ollama"; import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; import { BaseChatModel } from 'langchain/chat_models/base'; @@ -69,7 +72,9 @@ export default class ChatModelManager { openAIProxyBaseUrl: params.openAIProxyBaseUrl, }, [ModelProviders.ANTHROPIC]: { + anthropicApiUrl: `http://localhost:${PROXY_SERVER_PORT}`, anthropicApiKey: decrypt(params.anthropicApiKey), + modelName: params.anthropicModel, }, [ModelProviders.AZURE_OPENAI]: { maxTokens: params.maxTokens, @@ -125,6 +130,12 @@ export default class ChatModelManager { constructor: ChatGoogleGenerativeAI, vendor: ModelProviders.GOOGLE, }, + { + models: ANTHROPIC_MODELS, + apiKey: this.langChainParams.anthropicApiKey, + constructor: ChatAnthropic, + vendor: ModelProviders.ANTHROPIC, + }, { models: OPENROUTERAI_MODELS, apiKey: this.langChainParams.openRouterAiApiKey, diff --git a/src/aiParams.ts b/src/aiParams.ts index 7b4eee95..58f43b20 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -11,6 +11,7 @@ export interface ModelConfig { maxTokens?: number, openAIApiKey?: string, anthropicApiKey?: string, + anthropicModel?: string, azureOpenAIApiKey?: string, azureOpenAIApiInstanceName?: string, azureOpenAIApiDeploymentName?: string, @@ -30,6 +31,7 @@ export interface LangChainParams { huggingfaceApiKey: string, cohereApiKey: string, anthropicApiKey: string, + anthropicModel: string, azureOpenAIApiKey: string, azureOpenAIApiInstanceName: string, azureOpenAIApiDeploymentName: string, diff --git a/src/components/ChatComponents/ChatIcons.tsx b/src/components/ChatComponents/ChatIcons.tsx index 31d76db3..7d4ff278 100644 --- a/src/components/ChatComponents/ChatIcons.tsx +++ b/src/components/ChatComponents/ChatIcons.tsx @@ -1,31 +1,26 @@ -import { SetChainOptions } from '@/aiParams'; +import { SetChainOptions } from "@/aiParams"; import { AI_SENDER, ChatModelDisplayNames, + PROXY_SERVER_PORT, VAULT_VECTOR_STORE_STRATEGY, -} from '@/constants'; -import { - ChatMessage -} from '@/sharedState'; -import { - getFileContent, - getFileName, -} from '@/utils'; -import { Notice, Vault } from 'obsidian'; -import { - useEffect, - useState, -} from 'react'; +} from "@/constants"; +import { ProxyServer } from "@/proxyServer"; +import { ChatMessage } from "@/sharedState"; +import { getFileContent, getFileName } from "@/utils"; +import { Notice, Vault } from "obsidian"; +import { useEffect, useState } from "react"; -import { ChainType } from '@/chainFactory'; +import { ChainType } from "@/chainFactory"; import { - RefreshIcon, SaveAsNoteIcon, + RefreshIcon, + SaveAsNoteIcon, SendActiveNoteToPromptIcon, StopIcon, UseActiveNoteAsContextIcon, -} from '@/components/Icons'; -import { stringToChainType } from '@/utils'; -import React from 'react'; +} from "@/components/Icons"; +import { stringToChainType } from "@/utils"; +import React from "react"; interface ChatIconsProps { currentModel: string; @@ -59,32 +54,44 @@ const ChatIcons: React.FC = ({ vault_qa_strategy, }) => { const [selectedChain, setSelectedChain] = useState(currentChain); + const proxyServer = new ProxyServer(PROXY_SERVER_PORT); - const handleModelChange = (event: React.ChangeEvent) => { + const handleModelChange = async (event: React.ChangeEvent) => { + const selectedModel = event.target.value; setCurrentModel(event.target.value); + + if (selectedModel === ChatModelDisplayNames.CLAUDE) { + // Start the proxy server when CLAUDE is selected + await proxyServer.startProxyServer('https://api.anthropic.com/'); + } else { + // Stop the proxy server when another model is selected + await proxyServer.stopProxyServer(); + } }; - const handleChainChange = async (event: React.ChangeEvent) => { + const handleChainChange = async ( + event: React.ChangeEvent + ) => { setSelectedChain(stringToChainType(event.target.value)); - } + }; useEffect(() => { - const handleChainSelection = async () => { + const handleChainSelection = async () => { if (!app) { - console.error('App instance is not available.'); + console.error("App instance is not available."); return; } if (selectedChain === ChainType.LONG_NOTE_QA_CHAIN) { const file = app.workspace.getActiveFile(); if (!file) { - new Notice('No active note found.'); - console.error('No active note found.'); + new Notice("No active note found."); + console.error("No active note found."); return; } const noteContent = await getFileContent(file, vault); - const fileMetadata = app.metadataCache.getFileCache(file) + const fileMetadata = app.metadataCache.getFileCache(file); const noteFile = { path: file.path, basename: file.basename, @@ -118,78 +125,124 @@ const ChatIcons: React.FC = ({ } setCurrentChain(selectedChain); - }; + }; - handleChainSelection(); + handleChainSelection(); }, [selectedChain]); return ( -
+
Model Selection
- - -
Mode Selection
- {selectedChain === 'llm_chain' && ( - )} - {selectedChain === 'long_note_qa' && ( - )} - {selectedChain === 'vault_qa' && ( - )}
diff --git a/src/constants.ts b/src/constants.ts index dd5b720b..2bee99c1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,6 +22,7 @@ export enum ChatModelDisplayNames { GPT_4_TURBO = 'GPT-4 TURBO', GPT_4_32K = 'GPT-4 32K', AZURE_OPENAI = 'AZURE OPENAI', + CLAUDE = 'CLAUDE 3', GEMINI_PRO = 'GEMINI PRO', OPENROUTERAI = 'OPENROUTER.AI', OLLAMA = 'OLLAMA (LOCAL)', @@ -45,6 +46,10 @@ export const GOOGLE_MODELS = new Set([ ChatModelDisplayNames.GEMINI_PRO, ]); +export const ANTHROPIC_MODELS = new Set([ + ChatModelDisplayNames.CLAUDE, +]); + export const OPENROUTERAI_MODELS = new Set([ ChatModelDisplayNames.OPENROUTERAI, ]) @@ -84,6 +89,7 @@ export const VENDOR_MODELS: Record> = { [ModelProviders.OPENAI]: OPENAI_MODELS, [ModelProviders.AZURE_OPENAI]: AZURE_MODELS, [ModelProviders.GOOGLE]: GOOGLE_MODELS, + [ModelProviders.ANTHROPIC]: ANTHROPIC_MODELS, [ModelProviders.OPENROUTERAI]: OPENROUTERAI_MODELS, [ModelProviders.OLLAMA]: OLLAMA_MODELS, [ModelProviders.LM_STUDIO]: LM_STUDIO_MODELS, @@ -133,11 +139,14 @@ export const VAULT_VECTOR_STORE_STRATEGIES = [ VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH, ]; +export const PROXY_SERVER_PORT = 53001; + export const DEFAULT_SETTINGS: CopilotSettings = { openAIApiKey: '', huggingfaceApiKey: '', cohereApiKey: '', anthropicApiKey: '', + anthropicModel: 'claude-3-sonnet-20240229', azureOpenAIApiKey: '', azureOpenAIApiInstanceName: '', azureOpenAIApiDeploymentName: '', diff --git a/src/main.ts b/src/main.ts index a879208b..c8b831a8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -650,6 +650,7 @@ export default class CopilotPlugin extends Plugin { huggingfaceApiKey, cohereApiKey, anthropicApiKey, + anthropicModel, azureOpenAIApiKey, azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, @@ -671,6 +672,7 @@ export default class CopilotPlugin extends Plugin { huggingfaceApiKey, cohereApiKey, anthropicApiKey, + anthropicModel: anthropicModel || DEFAULT_SETTINGS.anthropicModel, azureOpenAIApiKey, azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, diff --git a/src/proxyServer.ts b/src/proxyServer.ts new file mode 100644 index 00000000..e3d11e5c --- /dev/null +++ b/src/proxyServer.ts @@ -0,0 +1,65 @@ +import cors from '@koa/cors'; +import Koa from 'koa'; +import proxy from 'koa-proxies'; +import net from 'net'; + +export class ProxyServer { + private port: number; + private server?: net.Server; + + constructor(port: number) { + this.port = port; + } + + async startProxyServer(proxyBaseUrl: string) { + console.log('loading plugin'); + // check if the port is already in use + const inUse = await this.checkPortInUse(this.port); + + if (!inUse) { + // Create a new Koa application + const app = new Koa(); + + app.use(cors()); + + // Create and apply the proxy middleware + app.use(proxy('/', { + // your target API, e.g. https://api.anthropic.com/ + target: proxyBaseUrl, + changeOrigin: true, + })); + + // Start the server on the specified port + this.server = app.listen(this.port); + console.log(`Proxy server running on http://localhost:${this.port}`); + } else { + console.error(`Port ${this.port} is in use`); + } + } + + async stopProxyServer() { + if (this.server) { + this.server.close(); + } + } + + checkPortInUse(port: number) { + return new Promise((resolve, reject) => { + const server = net.createServer() + .once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + resolve(true); // Port is in use + } else { + reject(err); + } + }) + .once('listening', () => { + server.once('close', () => { + resolve(false); // Port is not in use + }) + .close(); + }) + .listen(port); + }); + } +} \ No newline at end of file diff --git a/src/settings/SettingsPage.tsx b/src/settings/SettingsPage.tsx index b56b9c0f..72524d18 100644 --- a/src/settings/SettingsPage.tsx +++ b/src/settings/SettingsPage.tsx @@ -9,6 +9,7 @@ export interface CopilotSettings { huggingfaceApiKey: string; cohereApiKey: string; anthropicApiKey: string; + anthropicModel: string; azureOpenAIApiKey: string; azureOpenAIApiInstanceName: string; azureOpenAIApiDeploymentName: string; diff --git a/src/settings/components/ApiSettings.tsx b/src/settings/components/ApiSettings.tsx index 7bbf9c46..53e1ed95 100644 --- a/src/settings/components/ApiSettings.tsx +++ b/src/settings/components/ApiSettings.tsx @@ -8,6 +8,10 @@ interface ApiSettingsProps { setOpenAIApiKey: (value: string) => void; googleApiKey: string; setGoogleApiKey: (value: string) => void; + anthropicApiKey: string; + setAnthropicApiKey: (value: string) => void; + anthropicModel: string; + setAnthropicModel: (value: string) => void; openRouterAiApiKey: string; setOpenRouterAiApiKey: (value: string) => void; openRouterModel: string; @@ -29,6 +33,10 @@ const ApiSettings: React.FC = ({ setOpenAIApiKey, googleApiKey, setGoogleApiKey, + anthropicApiKey, + setAnthropicApiKey, + anthropicModel, + setAnthropicModel, openRouterAiApiKey, setOpenRouterAiApiKey, openRouterModel, @@ -100,6 +108,33 @@ const ApiSettings: React.FC = ({
+ +
+ + +

+ If you have Anthropic API access, you can get the API key {' '} + + here + . +
+ Your API key is stored locally and is only used to make requests to Anthropic's services. +

+
+
+