diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index 4a3b6ab..62232df 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -27,7 +27,7 @@ import { LLMBaseUrlNotSetException, LLMModelNotSetException, } from '../../core/llm/exception' -import { regexSearchFiles } from '../../core/services/ripgrep' +import { regexSearchFiles } from '../../core/ripgrep' import { useChatHistory } from '../../hooks/use-chat-history' import { ApplyStatus, ToolArgs } from '../../types/apply' import { ChatMessage, ChatUserMessage } from '../../types/chat' @@ -527,10 +527,9 @@ const Chat = forwardRef((props, ref) => { } } else if (toolArgs.type === 'regex_search_files') { const baseVaultPath = app.vault.adapter.getBasePath() + const ripgrepPath = settings.ripgrepPath const absolutePath = path.join(baseVaultPath, toolArgs.filepath) - console.log("absolutePath", absolutePath) - const results = await regexSearchFiles(absolutePath, toolArgs.regex) - console.log("results", results) + const results = await regexSearchFiles(absolutePath, toolArgs.regex, ripgrepPath) const formattedContent = `[regex_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; return { type: 'regex_search_files', diff --git a/src/components/chat-view/MarkdownApplyDiffBlock.tsx b/src/components/chat-view/MarkdownApplyDiffBlock.tsx index efff169..cc9ac5f 100644 --- a/src/components/chat-view/MarkdownApplyDiffBlock.tsx +++ b/src/components/chat-view/MarkdownApplyDiffBlock.tsx @@ -1,4 +1,4 @@ -import { Check, Edit, Loader2, X, Diff } from 'lucide-react' +import { Check, Diff, Loader2, X } from 'lucide-react' import { PropsWithChildren, useState } from 'react' import { useDarkModeContext } from '../../contexts/DarkModeContext' diff --git a/src/components/chat-view/MarkdownWithIcon.tsx b/src/components/chat-view/MarkdownWithIcon.tsx index 669982a..01fb562 100644 --- a/src/components/chat-view/MarkdownWithIcon.tsx +++ b/src/components/chat-view/MarkdownWithIcon.tsx @@ -137,7 +137,7 @@ const MarkdownWithIcons = ({ {markdownContent} - {markdownContent && finish && + {markdownContent && finish && iconName === "attempt_completion" &&
diff --git a/src/core/llm/manager.ts b/src/core/llm/manager.ts index 15f6152..76aa57f 100644 --- a/src/core/llm/manager.ts +++ b/src/core/llm/manager.ts @@ -17,7 +17,7 @@ import { GroqProvider } from './groq' import { InfioProvider } from './infio' import { OllamaProvider } from './ollama' import { OpenAIAuthenticatedProvider } from './openai' -import { OpenAICompatibleProvider } from './openai-compatible-provider' +import { OpenAICompatibleProvider } from './openai-compatible' export type LLMManagerInterface = { diff --git a/src/core/llm/openai-compatible-provider.ts b/src/core/llm/openai-compatible.ts similarity index 100% rename from src/core/llm/openai-compatible-provider.ts rename to src/core/llm/openai-compatible.ts diff --git a/src/core/rag/embedding.ts b/src/core/rag/embedding.ts index b1015da..fab85ee 100644 --- a/src/core/rag/embedding.ts +++ b/src/core/rag/embedding.ts @@ -1,3 +1,6 @@ +import https from 'https' +import { URL } from 'url' + import { GoogleGenerativeAI } from '@google/generative-ai' import { OpenAI } from 'openai' @@ -176,6 +179,42 @@ export const getEmbeddingModel = ( }, } } + case ApiProvider.OpenAICompatible: { + const openai = new OpenAI({ + apiKey: settings.openaicompatibleProvider.apiKey, + baseURL: settings.openaicompatibleProvider.baseUrl, + dangerouslyAllowBrowser: true, + }); + return { + id: settings.embeddingModelId, + dimension: 0, + getEmbedding: async (text: string) => { + try { + if (!openai.apiKey) { + throw new LLMAPIKeyNotSetException( + 'OpenAI Compatible API key is missing. Please set it in settings menu.', + ) + } + const embedding = await openai.embeddings.create({ + model: settings.embeddingModelId, + input: text, + encoding_format: "float", + }) + return embedding.data[0].embedding + } catch (error) { + if ( + error.status === 429 && + error.message.toLowerCase().includes('rate limit') + ) { + throw new LLMRateLimitExceededException( + 'OpenAI Compatible API rate limit exceeded. Please try again later.', + ) + } + throw error + } + }, + } + } default: throw new Error('Invalid embedding model') } diff --git a/src/core/rag/rag-engine.ts b/src/core/rag/rag-engine.ts index a54eb74..7cc6ce1 100644 --- a/src/core/rag/rag-engine.ts +++ b/src/core/rag/rag-engine.ts @@ -34,7 +34,8 @@ export class RAGEngine { } async initializeDimension(): Promise { - if (this.embeddingModel.dimension === 0 && this.settings.embeddingModelProvider === ApiProvider.Ollama) { + if (this.embeddingModel.dimension === 0 && + (this.settings.embeddingModelProvider === ApiProvider.Ollama || this.settings.embeddingModelProvider === ApiProvider.OpenAICompatible)) { this.embeddingModel.dimension = (await this.embeddingModel.getEmbedding("hello world")).length } } diff --git a/src/core/services/ripgrep/index.ts b/src/core/ripgrep/index.ts similarity index 94% rename from src/core/services/ripgrep/index.ts rename to src/core/ripgrep/index.ts index f9939e3..434b6da 100644 --- a/src/core/services/ripgrep/index.ts +++ b/src/core/ripgrep/index.ts @@ -30,8 +30,8 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } -async function getBinPath(): Promise { - const binPath = path.join("/opt/homebrew/bin/", binName) +async function getBinPath(ripgrepPath: string): Promise { + const binPath = path.join(ripgrepPath, binName) return (await pathExists(binPath)) ? binPath : undefined } @@ -86,20 +86,21 @@ async function execRipgrep(bin: string, args: string[]): Promise { export async function regexSearchFiles( directoryPath: string, regex: string, + ripgrepPath: string, ): Promise { - const rgPath = await getBinPath() + const rgPath = await getBinPath(ripgrepPath) if (!rgPath) { throw new Error("Could not find ripgrep binary") } - // 使用--glob参数排除.obsidian目录 + // use --glob param to exclude .obsidian directory const args = [ "--json", "-e", regex, "--glob", - "!.obsidian/**", // 排除.obsidian目录及其所有子目录 + "!.obsidian/**", // exclude .obsidian directory and all its subdirectories "--glob", "!.git/**", "--context", diff --git a/src/core/services/semantic/index.ts b/src/core/services/semantic/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/database/modules/conversation/conversation-repository.ts b/src/database/modules/conversation/conversation-repository.ts index c47c7ef..a2d7ea7 100644 --- a/src/database/modules/conversation/conversation-repository.ts +++ b/src/database/modules/conversation/conversation-repository.ts @@ -62,7 +62,6 @@ export class ConversationRepository { message.createdAt || new Date() ] ) - console.log('createMessage: ', message.id, result) return result.rows[0] } @@ -129,11 +128,10 @@ export class ConversationRepository { } async deleteAllMessagesFromConversation(conversationId: string, tx?: Transaction): Promise { - const result = await (tx ?? this.db).query( + await (tx ?? this.db).query( `DELETE FROM messages WHERE conversation_id = $1`, [conversationId] ) - console.log('deleteAllMessagesFromConversation', conversationId, result) return } } diff --git a/src/main.ts b/src/main.ts index 41ce227..d02dc91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -142,8 +142,8 @@ export default class InfioPlugin extends Plugin { this.app.metadataCache.on("changed", (file: TFile) => { if (file) { eventListener.handleFileChange(file); - console.log("file changed: filename: ", file.name); - this.ragEngine?.updateFileIndex(file); + // is not worth it to update the file index on every file change + // this.ragEngine?.updateFileIndex(file); } }) ); @@ -151,7 +151,6 @@ export default class InfioPlugin extends Plugin { this.registerEvent( this.app.metadataCache.on("deleted", (file: TFile) => { if (file) { - console.log("file deleted: filename: ", file.name) this.ragEngine?.deleteFileIndex(file); } }) diff --git a/src/settings/SettingTab.tsx b/src/settings/SettingTab.tsx index 6dde387..2b759f1 100644 --- a/src/settings/SettingTab.tsx +++ b/src/settings/SettingTab.tsx @@ -37,8 +37,8 @@ export class InfioSettingTab extends PluginSettingTab { const { containerEl } = this containerEl.empty() this.renderModelsSection(containerEl) - this.renderDeepResearchSection(containerEl) this.renderFilesSearchSection(containerEl) + this.renderDeepResearchSection(containerEl) this.renderRAGSection(containerEl) this.renderAutoCompleteSection(containerEl) } @@ -60,15 +60,15 @@ export class InfioSettingTab extends PluginSettingTab { } private renderFilesSearchSection(containerEl: HTMLElement): void { + new Setting(containerEl).setHeading().setName('File search') new Setting(containerEl) - .setHeading() - .setName('Files Search Method') + .setName('Files search method') .setDesc('Choose the method to search for files.') .addDropdown((dropdown) => dropdown .addOption('auto', 'Auto') - .addOption('regex', 'Regex') .addOption('semantic', 'Semantic') + .addOption('regex', 'Regex') .setValue(this.plugin.settings.filesSearchMethod) .onChange(async (value) => { await this.plugin.setSettings({ @@ -77,6 +77,20 @@ export class InfioSettingTab extends PluginSettingTab { }) }), ) + new Setting(containerEl) + .setName('ripgrep path') + .setDesc('Path to the ripgrep binary. When using regex search, this is required.') + .addText((text) => + text + .setPlaceholder('/opt/homebrew/bin/') + .setValue(this.plugin.settings.ripgrepPath) + .onChange(async (value) => { + await this.plugin.setSettings({ + ...this.plugin.settings, + ripgrepPath: value, + }) + }), + ) } renderModelsSection(containerEl: HTMLElement): void { @@ -88,10 +102,10 @@ export class InfioSettingTab extends PluginSettingTab { renderDeepResearchSection(containerEl: HTMLElement): void { new Setting(containerEl) .setHeading() - .setName('Deep Research') + .setName('Deep research') new Setting(containerEl) - .setName('Serper Api Key') + .setName('Serper API key') .setDesc(createFragment(el => { el.appendText('API key for web search functionality. Serper allows the plugin to search the internet for information, similar to a search engine. Get your key from '); const a = el.createEl('a', { @@ -114,7 +128,7 @@ export class InfioSettingTab extends PluginSettingTab { ) new Setting(containerEl) - .setName('Jina Api Key (Optional)') + .setName('Jina API key (Optional)') .setDesc(createFragment(el => { el.appendText('API key for parsing web pages into markdown format. If not provided, local parsing will be used. Get your key from '); const a = el.createEl('a', { diff --git a/src/settings/components/ModelProviderSettings.tsx b/src/settings/components/ModelProviderSettings.tsx index 9cc4633..0819bca 100644 --- a/src/settings/components/ModelProviderSettings.tsx +++ b/src/settings/components/ModelProviderSettings.tsx @@ -50,7 +50,7 @@ const CustomProviderSettings: React.FC = ({ plugin, const handleSettingsUpdate = async (newSettings: InfioSettings) => { await plugin.setSettings(newSettings); - // 使用父组件传入的回调函数来刷新整个容器 + // Use the callback function passed from the parent component to refresh the entire container onSettingsUpdate?.(); }; diff --git a/src/types/settings.ts b/src/types/settings.ts index 4ef4407..640528e 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -237,6 +237,7 @@ export const InfioSettingsSchema = z.object({ // Files Search filesSearchMethod: z.enum(['regex', 'semantic', 'auto']).catch('auto'), + ripgrepPath: z.string().catch(''), /// [compatible] // activeModels [compatible] diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index 88a4ae5..742a356 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -467,7 +467,14 @@ export class PromptGenerator { } private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise { - const systemPrompt = await SYSTEM_PROMPT(this.app.vault.getRoot().path, false, mode, filesSearchMethod, preferredLanguage, this.diffStrategy) + const systemPrompt = await SYSTEM_PROMPT( + this.app.vault.getRoot().path, + false, + mode, + filesSearchMethod, + preferredLanguage, + this.diffStrategy + ) return { role: 'system',